---
title: "Self-hosting: initial setup"
slug: /self-hosting-1-initial-setup/
date: 2025-02-23
tags: ["projects", "self-hosting"]
---

With the way the Internet (and the world) is going, the benefits of self-hosting
the services you rely on is becoming increasingly apparent.

I have been using AWS to run a limited set microservices on the Free Tier but
doing anything interesting with an actual server and database becomes expensive
quickly. While I hope to eventually run my own physical server, for now I have
opted to maintain a remote virtual private server. The skills I gain here will
obviously transfer to the physical context.

I intend to run a series of third-party FOSS applications on different
subdomains via Docker containers. I will also be deploying my own software using
Docker.

This post goes over the initial setup and configuration of the server.

## Choosing a provider

Jurisdiction, location and price were the main considerations. I want the server
to be based in a country with good privacy laws but not so far away that latency
would be noticeable. I also don't want to pay that much initially given that I
am just starting out.

![The Hetzner Cloud Console](./img/hetzner-console.png)

I opted for the German company, Hetzner. Being in the EU means their digital
privacy laws are better than the UK and Germany's recent history has created
more of a culture of resistance to surveillance than other EU states.

I chose their CX22 Cloud package which provides the following for just under €4
a month:

- 2 virtual CPUs (Intel)
- 4 GB RAM
- 40 GB SSD
- IPv4 and free IPv6

This is modest but given that it's virtual, I can scale up later as necessary.

![My server](./img/hetzner-server.png)

## Creating a server

Once you've paid you get access to the Hetzner Cloud Console and you just click
"Add server" and specify the location, hostname and OS. I opted for Nuremberg as
it's nearest and decided on Debian as my Linux distro.

I set up a basic firewall using the Hetzner console, allowing incoming traffic
on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). This will apply at the Network
Layer. In a moment I'll explain how I also created a firewall at the Application
Layer, over SSH.

## Connecting

Once the server was spun up the obvious next step was to connect to it and start
configuring from the inside. To do this I needed SSH access. This was
straightforward, I just added my local machine's public key via the Hetzner
console and then connected with:

```sh
ssh root@<server_ip4_address>
```

## Create non-root user

Once I had access as root the first thing I did was update my packages:

```sh
apt update
apt upgrade
```

Next I created a non-root user with `sudo` privileges. This is the account I
will use for administrating the server which is obviously safer than doing stuff
as root:

```sh
apt install sudo
add user <my_username>
usermod -aG sudo <my_username>
```

I then switched to this user:

```sh
su - <my_username>
```

I'm connecting to the server over SSH but I also want to login as `my_username`
using SSH keys instead of a password as this is less secure.

I set up the necessary SSH directory and permissions for `my_username`:

```sh
mkdir /home/<my_username>/.ssh
chown <my_username>:<my_username> /home/<my_username>/.ssh
chmod 700 /home/<my_username>/.ssh
```

Then I transferred my local machine's public key, that I added to the server via
the Hetzner Console earlier, to the known hosts for `my_username`.

```sh
cp /root/.ssh/authorized_keys /home/<my_username>/.ssh/
chown <my_username>:<my_username> /home/<my_username>/.ssh/authorized_keys
chmod 600 /home/<my_username>/.ssh
```

To confirm, I closed the connection and then connected to the server again but
this time as `my_username`:

```sh
ssh <my_username>@<server_ip4_address>
```

Having confirmed SSH login I installed a lightweight version of vim
([vim-tiny](https://www.baeldung.com/linux/vim-tiny-properties)) to make
text-editing easier.

```sh
sudo apt install vim-tiny
```

## Disable root login and access

An important server-hardening routine is to prevent password logins as root.

To do this I edited the root SSH config:

```sh
sudo vim /etc/sshd_config
```

And added the following:

```
PasswordAuthentication no
```

Then I restarted the SSH daemon:

```
sudo systemctl restart sshd
```

Then when I attempted `ssh root@<server_ip4_address>` I was met with "Permission
denied", as intended.

As I side note, it's actually not possible for me to login as root at all, which
is best practice. I haven't set a root password, so if I attempt to login as
root when logged in as `my_username` (with `su -`), all attempts will fail.

When I need to run processes as root, I will do this exclusively by assuming
`sudo` and to do this, I need to enter the password for `my_username`.

## Firewall

Earlier, I set up a firewall from outside of the server, using the Hetzner
console. I'm now going to do the same thing but from within the server as
`my_username`. It will share the same rules. This means I have a firewall at the
Network Layer (Hetzner) blocking requests before they reach the server and a
firewall operating at the Application Layer for those requests that get to the
server.

This doesn't add much in terms of security, especially as the rules are
identical, but having a peripheral firewall on the Network and a firewall on the
server provides a degree of "defence in depth". It will also potentially reduce
the load on the server if blocked requests are deflected at the Network Layer
before reaching the server.

It would be better to block by IP, and only open the SSH port to requests from
my local network (HTTP/HTTPS is obviously public and open to all). This isn't an
option because my my broadband package doesn't include a static IP. One way
around this would be to deploy another Hetzner server and set it up as a VPN. I
would then restrict port 22 to requests from that server's IP and connect to the
VPN server first. This might be a project further down the line.

To set up the host firewall I used `ufw` (Uncomplicated Firewall):

```sh
sudo apt install ufw
```

This is a wrapper for the venerable `iptables` that simplifies the process.

I applied the same basic rules as earlier:

```sh
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```

To confirm:

```sh
sudo ufw status

```

```
To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)

```

(This automatically applies the rules for IPv6 as well as IPv4.)

## Fail2Ban

As an additional security measure I installed `fail2ban`. This software is
designed to work alongside your firewall to detect and block brute-force login
attempts. You apply it to services running on specific ports at the Application
Layer.

Debian does not come with the `rsyslog` package installed and `fail2ban` works
best with this logging program rather than `systemd` and its `journalctl` logs.
I tried to get it working with `systemd` but kept facing errors I couldn't
resolve, so I just installed `rsyslog` along with `fail2ban`:

```sh
sudo apt install fail2ban
sudo apt install rsyslog

sudo systemctl start rsyslog
sudo systemctl enable rsyslog
sudo systemctl start fail2ban
sudo systemctl enable fail2ban
```

I didn't change any of the `fail2ban` defaults. On Debian it runs on the `sshd`
server automatically and this is the only service I have running currently.

I waited a while and then checked the status:

```sh
sudo fail2ban-client status sshd
```

Which gave me:

```
Status for the jail: sshd
|- Filter
|  |- Currently failed: 2
|  |- Total failed:     55
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 0
   |- Total banned:     6
   `- Banned IP list:

```

The IP addresses of actors that make repeated failed login attempts are put in a
temporary "jail" by `fail2ban` which means they are blocked for a limited time
period. The data above tells me that there have been 55 total attempted failed
logins and this has resulted in 6 IPs being put in jail! However this is just
normal background noise in serverland and nothing to particularly worry about.

When I take a look in `var/log/auth.log`, I see entries like:

```
Disconnected from authenticating user root 218.92.0.236 port 22272 [preauth]

```

This indicates a malicious actor trying gain access as root but being blocked by
the earlier root protections.

There are also entries like this:

```
 Connection closed by invalid user postgres 195.211.190.228 port 47454 [preauth]

```

Which shows a speculative attempt to try and connect to a non-existing
PostgreSQL service.

These will typically be bots trying their luck at scale rather than actual
people.

Alongside the malicious attempts I see my own legimate logins and invocations of
root as `sudo`:

```
pam_unix(sudo:session): session opened for user root(uid=0) by <my_username>(uid=1000)
```

This was enough for one Sunday! In my next post I'll detail how I set up DNS and
SSL for the new server.

## Resources

In researching how to do the above, I created or expanded the following entries
in my Zettelkasten:

- [Firewalls](https://thomasabishop.github.io/eolas/Firewalls)
- [UFW firewall management](https://thomasabishop.github.io/eolas/UFW_firewall_management)
- [Disable non-root SSH access](https://thomasabishop.github.io/eolas/Disable_non-root_ssh_access)
- [IP addresses](https://thomasabishop.github.io/eolas/IP_addresses)
- [Internet fundamentals](https://thomasabishop.github.io/eolas/Internet_fundamentals)
- [The Application Layer of the Internet Protocol](https://thomasabishop.github.io/eolas/Application_Layer_of_Internet_Protocol)
