Droplet in the DigitalOcean
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
- Initial Server Setup on DigitalOcean
- Debian Wiki: SourcesList
- UFW Essentials
- UFW on Ubuntu
- Add swap space on Debian
You can like, share, or comment on this post on the Fediverse 💬
« Previous: Secure remote access to Linux servers using SSH keys