Droplet in the DigitalOcean

Last edited on 2024-11-03 Tagged under  #vps   #server   #selfHosting   #debian   #linux 

A drop striking a liquid surface under magnification

My blog has been hosted on Netlify for the past few years.

Its been practically maintenance-free. Write posts in Markdown on my local device and push the contents to a GitLab repository. Netlify makes note of any changes, builds the site into HTML and CSS, and makes everything available to the Internet hosted on their platform. And it hasn't cost me a cent!

So why would I consider self-hosting this blog and mess with a good thing? Couple of reasons. I would like to have a better idea of what is involved in hosting a website and learn how to do it myself. There are some other public services I'm thinking of self-hosting in the future. This recent kerfuffle over a $104k bill for a simple static site was concerning.

Sometime in the future I might self-host on my laptop home server, but to start I went looking for a VPS (Virtual Private Server) from one of the cloud hosting providers. DigitalOcean has received positive feedback in the past from my local Linux Users Group. Their basic "droplet" (VPS) offers:

  • 1GB RAM
  • 1 CPU
  • 25GB storage
  • 1TB transfer
  • $6US/month

I created a new account (which came with a $200US, 60-day credit), spun up a droplet running Debian 12 "Bookworm", and moved this blog from Netlify to self-hosted using the Caddy web server. I will describe my experience configuring Caddy in a future post.

So... Nice and shiny new droplet. What next?

Before adding any hosted services, this is what I do...

1. Create non-root user

During the creation of the droplet I created a password for root.

Login via SSH as root this one time:

$ ssh root@<droplet_ip_address>

Create a new user:

# adduser <my_username>

Add <my_username> to multiple groups:

# usermod -a -G sudo,adm,systemd-journal <my_username>

Logout, then log back in as the new non-root user:

$ ssh <my_username>@<droplet_ip_address>

2. Sudo without password

I like to give myself sudo privileges without prompting for a password:

$ echo '<my_username> ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/sudoer_<my_username> > /dev/null

3. Switch to SSH key authentication

See: Secure remote access to Linux servers using SSH keys

On the DROPLET:

$ mkdir ~/.ssh && chmod 700 ~/.ssh
$ touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys

On the CLIENT:

Upload the client's SSH public key to DROPLET and append to its authorized_keys files:

$ ssh-copy-id -i ~/.ssh/id_ed25519.pub <droplet_ip_address>

Verify login to DROPLET using key-based authentication:

$ ssh -o PasswordAuthentication=no <droplet_ip_address>

4. Disable root and password logins

On the DROPLET, open sshd_config for editing:

$ sudo vim /etc/ssh/sshd_config

Make the following changes:

PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no

Save changes, and restart SSH:

$ sudo systemctl restart ssh

While remaining logged into the server, open another terminal and verify the changes by attempting a new login using password authentication from CLIENT (which should fail):

$ ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no <droplet_ip_address>
<my_username>@<droplet_ip_address>: Permission denied (publickey).

Verify key-based authentication continues to work as before:

$ ssh <droplet_ip_address>

DROPLET is now secured to accept only SSH key authentication for logins.

5. Change SSH port

By default, SSH listens on port 22 for connections. Changing this to a private port between 49152 through 65535 will frustrate automated attacks.

On the DROPLET, re-open sshd_config for editing:

$ sudo vim /etc/ssh/sshd_config

Change:

#Port 22

Uncomment the setting, and set to a different port number (example: 52222):

Port 52222

Save changes, and restart SSH:

$ sudo systemctl restart ssh

Open another terminal, and test the new port assignment by trying to login:

$ ssh -p 52222 <my_username>@<droplet_ip_address>

6. Forward root email to my user's inbox

Debian installs exim4, a combination MTA/MDA (Mail Transport Agent/Mail Delivery Agent).

Forwarding of mail for root to the regular user account is configured in aliases:

$ vim /etc/aliases

Alias root to my user:

root: <my_username>

Save changes, and let the MTA know about the change by running:

$ sudo newaliases

When system e-mails are delivered they are added to a file in /var/mail/<account_name>.

Verify that mail is indeed being forwarded by using the mail command to send root a message:

$ mail root
Subject: Test new alias 
Is it working?

Press CTRL-d to exit and send message.

Open the inbox:

$ mail   
Mail version 8.1.2 01/15/2001.  Type ? for help.
"/var/mail/<my_username>": 1 message 1 new

7. Set timezone

See what timezones are available:

$ timedatectl list-timezones

Set timezone (example: Canada/Eastern):

$ sudo timedatectl set-timezone Canada/Eastern

Status:

$ timedatectl 
               Local time: Fri 2024-11-01 18:16:44 EDT
           Universal time: Fri 2024-11-01 22:16:44 UTC
                 RTC time: Fri 2024-11-01 22:16:44
                Time zone: Canada/Eastern (EDT, -0400)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

8. Configure APT package manager

Note: Droplets do not use the sources.list style I'm familiar with. Instead, it contains equivalent entries in deb822 style under /etc/apt/sources.list.d/debian.sources.

Backup file:

$ sudo cp /etc/apt/sources.list.d/debian.sources /etc/apt/sources.list.d/debian.sources.bak

Open file for editing:

$ sudo vim /etc/apt/sources.list.d/debian.sources

Modify:

Components: main

Set to:

Components: main contrib non-free non-free-firmware

Save changes, then update and upgrade system:

$ sudo apt update && sudo apt full-upgrade

9. Automatic upgrades

On my daily desktop, I do manual updates. However, on servers, once you get into several devices and infrequent logins, upgrading can quickly get repetitive and timely security updates may be put off.

I use unattended-upgrades to automate the process: Automatic upgrades in Debian

10. Set up basic firewall

UFW (Uncomplicated FireWall) is a firewall configuration tool that runs on top of iptables:

$ sudo apt install ufw

Systemd auto-enables the ufw.service to start at boot. By default, UFW is set to deny all incoming connections and allow all outgoing connections.

NOTE: Before enabling the firewall, ensure that the firewall allows SSH connections so that logins will continue to work.

NOTE: I changed the SSH port from its default 22 to 52222 (see above).

Add a rule to allow connections to port 52222 and further restrict it to tcp:

$ sudo ufw allow 52222/tcp

To verify which rules were added so far, even when the firewall is still disabled:

$ sudo ufw show added
Added user rules (see 'ufw status' for running firewall):
ufw allow 52222/tcp

After confirming that a rule exists to allow incoming SSH connections, start the firewall:

$ sudo ufw enable

View status and rules that are set:

$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
52222/tcp                  ALLOW IN    Anywhere                  
52222/tcp (v6)             ALLOW IN    Anywhere (v6)

The firewall is currently blocking all incoming connections except for SSH. Enabling future services will require adjusting the firewall settings to allow acceptable traffic in.

To disable the firewall at any time:

$ sudo ufw disable

11. Fail2ban

Fail2ban is a daemon that can block IP addresses after a certain number of authentication failures:

$ sudo apt install fail2ban python3-systemd 

Default configuration file is /etc/fail2ban/jail.conf. Don't modify this file directly. Create an empty jail.local file to write custom settings:

$ sudo vim /etc/fail2ban/jail.local

Settings in the jail.local file will supersede the defaults in jail.conf file.

Add:

[DEFAULT]

bantime  = 1d
findtime  = 10m
maxretry = 2
backend = systemd


[sshd]
enabled = true
mode    = aggressive
port    = 52222     # my custom port setting for SSH (see above)
logpath = %(sshd_log)s
backend = %(sshd_backend)s

Save changes, start the fail2ban.service, and check for any errors or failures:

$ sudo systemctl start fail2ban.service
$ systemctl status fail2ban.service

View all enabled jails:

$ sudo fail2ban-client status

View the status of a particular jail:

$ sudo fail2ban-client status sshd

A log of fail2ban activity is created at /var/log/fail2ban.log.

12. Add swapfile

Create 1GB swapfile:

$ sudo fallocate -l 1G /swapfile

Enable:

$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile && sudo swapon /swapfile
$ free -h

Configure swapfile to autostart at boot:

$ sudo cp /etc/fstab /etc/fstab.bak
$ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

The swappiness parameter is a value between 0 and 100 that determines how often the system swaps data out of RAM to the swap space. For a desktop system, a swappiness of 60 is a reasonable value (especially when Zram swap is configured). On a droplet/server, its probably a good idea to set this to a lower value.

Display the current swappiness:

$ cat /proc/sys/vm/swappiness
60

Set a new value of 10:

$ sudo sysctl vm.swappiness=10
vm.swappiness = 10

This setting will persist until the next reboot.

Set this value automatically at restart by editing sysctl.conf:

$ sudo vim /etc/sysctl.conf

Add the line:

vm.swappiness=10

Save the changes.

13. Resources

You can like, share, or comment on this post on the Fediverse 💬

Thanks for reading! Read other posts?

« Previous: Secure remote access to Linux servers using SSH keys