The simple blog of a humble hacker

Linux hardening guide for newbies

Mark Veidemanis

You finally did it. That one Linux person you know finally convinced you to lease a virtual server. Now you're stuck with the task of configuring it and you have absolutely no idea what to do. Let's take a dive into securely configuring a fresh Linux box, starting with the basics.

Although this guide is aimed at newbies, I'll assume your Linux friend has at least taught you basic UNIX commands and how to use SSH.

Once you log in, you'll probably see something like this:

Linux tutorial 4.9.0-8-amd64 #1 SMP Debian 4.9.110-3+deb9u6 (2018-10-08) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sat Mar  9 15:39:18 2019 from xxx.xxx.xxx.xxx
root@tutorial:~# 

If you didn't choose Debian, you'll see something else, but this guide should work regardless of the distribution you're running. Almost all distros are similar under the hood.

If your Linux friend has a more minimalist taste (like me!), they'll probably recommend something like Alpine Linux or Void Linux.

Before we do anything, let's update and reboot. In Debian, run apt-get update followed by apt-get upgrade. In Alpine, run apk update, then apk upgrade. In Void, run xbps-install -Syu. For Arch, run pacman -Syu. For others, consult the manual. Don't forget to reboot after running the upgrade!

Let's dig in!

Step 1 - Cut the shit

If you've chosen a Debian-based distro, like Ubuntu or, well, Debian, we'll start by stripping out some useless services in order to lessen the attack surface. Having fewer services running means there are fewer potential entry points for attackers.

Instead of telling you what to remove, I'll tell you what you'll need to keep for a minimal working system.

Log daemon – logging

Services don't have permissions to write directly to the system logs, otherwise they would be able to remove and manipulate them. For this reason, we have logging daemons. They accept connections and write to logs on behalf of other programs. Common ones include rsyslogd, syslog and syslog-ng.

Cron daemon – scheduling

Cron is a service to schedule tasks to run over and over, on a specific schedule. It's extremely flexible, and some distros come with preset Cron configurations (called Crontabs) to perform system tasks like archiving old logs. Common Cron daemons are cron, dcron and cronie.

SSH daemon – remote control

This is what we're going to use to control the server remotely, so you'll want to keep this. If you're setting up some sort of router, this will likely be dropbear. In literally every other case the SSH server is OpenSSH – with the binary creatively called sshd.

TTY daemon – console access

The TTY is what you use to fix your mess when you've screwed up the networking or misconfigured the firewall and are not able to SSH in. If you're on a Linux desktop, you can enter a TTY by pressing Control, Alt, and one of the function keys along the top row. On your new server, you should be able to access the console through the administration panel of your provider. If they don't provide this, you obviously won't be needing this daemon. TTY daemons are agetty and getty,

NTP – time synchronisation

NTP is the Network Time Protocol, it keeps the system clock in sync with, well, the time. Without this, your time will drift, which may cause issues with the verification of TLS certificates, as they only work between two fixed points in time. Want to see what happens? Set the time to 1975: date +%Y%m%d -s "19750101" then try to load some webpages. NTP is usually handled by ntpd, but I've seen chronyd offered in some distro installers as well.

Removing all the things

Let's start removing some of the useless stuff that distros ship with. If you're unfortunate enough to be using a SystemD-based distro like Debian, Ubuntu or Arch, the following should work:

root@tutorial:~# systemctl stop dbus
root@tutorial:~# systemctl disable dbus
Synchronizing state of dbus.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install disable dbus

If, however, you opted for something a little more minimalistic, then you won't need to disable anything. Minimal distros like Alpine or Void only ship with stuff you need. Just for kicks, here's how you'd disable and stop a service in runit-based distros like Void:

root@void:~# sv stop dbus
ok: down: dbus: 0s, normally up
root@void:~# rm /var/service/dbus

And OpenRC-based distros like Alpine:

root@alpine:~# service dbus stop
 * Stopping Dbus daemon ...
root@alpine:~# rc-update del dbus
 * service dbus removed from runlevel default

That will disable and stop dbus, a program for typically graphical programs to send messages to each other. Completely superfluous on a server.

If your provider doesn't give you access to TTYs, you won't need them. My Debian came with getty running on tty1. Disabling it is easy: systemctl stop getty@tty1.service followed by systemctl disable getty@tty1.service.

For Alpine, edit /etc/inittab with your favourite editor and comment the TTYs you don't want:

# Set up a couple of getty's
tty1::respawn:/sbin/getty 38400 tty1
#tty2::respawn:/sbin/getty 38400 tty2
#tty3::respawn:/sbin/getty 38400 tty3
#tty4::respawn:/sbin/getty 38400 tty4
#tty5::respawn:/sbin/getty 38400 tty5
#tty6::respawn:/sbin/getty 38400 tty6

# Put a getty on the serial port
ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100

I have kept tty1 and ttyS0 enabled, as my provider, Linode (affiliate link), gives me both graphical access with Glish and serial console access with Lish. You'll need to force the init system to recheck its configuration after you edit this file. Do this with kill -HUP 1.

If you're using Void, do the following:

root@void:~# sv stop agetty-tty1
ok: down: agetty-tty1: 1s, normally up
root@void:~# rm /var/service/agetty-tty1

Repeat for each TTY you want to disable. If you want to re-enable a TTY, do this: ln -s /etc/sv/agetty-tty1 /var/service/ and sv start agetty-tty1. Void actually starts the service once it detects the symlink, but I always like to check.

Step 2 - Hardening SSH

Next, let's make it harder for attackers to go through the front door.

First, let's generate an SSH key on our workstation (not on the server!):

user@workstation:~$ ssh-keygen -t ed25519
Enter file in which to save the key (/home/user/.ssh/id_ed25519): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/user/.ssh/id_ed25519.
Your public key has been saved in /home/user/.ssh/id_ed25519.pub.
...

You can enter a password when it prompts you, or leave it blank. If you set one, you'll need to type this when you first use SSH every time you log in to your workstation. It'll cache the password for the session once you enter it once. If your system isn't configured to use the SSH agent, you will have to enter the password each time you use SSH.

I'm using an ED25519 keypair. It's secure by modern standards (2019), but do your own research. This isn't a cryptography guide.

Open up /home/user/.ssh/id_rsa.pub and copy the contents to /root/.ssh/authorized_keys on the server.

The file /root/.ssh/authorized_keys will end up looking like this:

root@tutorial:~# cat /root/.ssh/authorized_keys 
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN/74jyGbUAv/OTzDsTsya1lPrkgF50AOCw0JbC5W0QN user@workstation

Next, fully close your SSH session and log in to the server again, as root. You should only have to enter your SSH key password if you set one, not the password for the root user.

Now that you're connected with public key authentication, we can nuke password authentication from orbit. Open up /etc/ssh/sshd_config and change this:

#PasswordAuthentication yes

to this:

PasswordAuthentication no

Change/add the following lines as well. Don't add them if they're already in the file. Duplicates will prevent SSHd from starting.

PermitRootLogin without-password
ChallengeResponseAuthentication no
PrintMotd no
UseDNS no
UsePam no

Which does this:

  1. Prevent the root user from logging in with a password, but not by other means
  2. Disable keyboard-interactive authentication, which is essentially the same as password authentication
  3. Don't print the message of the day when users connect, nobody wants to see the copyright notice
  4. Don't do reverse DNS lookups on users when they connect, saving precious time and resources, and making connections faster on busy servers and servers with slow connections.
  5. Don't use PAM (duh!)

Restart SSH with service ssh restart (works for both Debian and Alpine!) or systemctl restart ssh (Arch).

Let's also change the root password for good measure. Do this with passwd.

Log out then log back in. You'll see the message of the day has disappeared:

Last login: Sat Mar  9 15:39:19 2019 from xxx.xxx.xxx.xxx
root@tutorial:~# 

Yes, we've not disabled root login. No, this is not a security risk, because we've disabled password login. Nobody can log in as root without having the private SSH key, which we've encrypted. Sure, malware could easily track keystrokes, get our passphrase, and decrypt the key, but this is even easier with password login. If having a compromised workstation is part of your threat model, look into using a hardware security module like a smart card or YubiKey.

Step 3 - Firewall

We'll set up Uncomplicated Firewall - ufw - to restrict incoming connections:

root@tutorial:~# apt-get install ufw
...
root@tutorial:~# ufw default deny
Default incoming policy changed to 'deny'
(be sure to update your rules accordingly)
root@tutorial:~# ufw allow ssh
Rules updated
Rules updated (v6)
root@tutorial:~# ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
root@tutorial:~# systemctl start ufw
root@tutorial:~# systemctl enable ufw
Synchronizing state of ufw.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install enable ufw

For Alpine, do service ufw start and rc-update add ufw. For Void, do ln -s /etc/sv/ufw /var/service/ and sv start ufw.

We've only permitted one incoming port for SSH. If you want to open another port for something, use ufw allow 2222/tcp. If you change your mind, it's easy to delete rules as well:

root@tutorial:~# ufw status numbered
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] 22/tcp                     ALLOW IN    Anywhere                  
[ 2] 25/tcp                     ALLOW IN    Anywhere                  
[ 3] 22/tcp (v6)                ALLOW IN    Anywhere (v6)             
[ 4] 25/tcp (v6)                ALLOW IN    Anywhere (v6)             

root@tutorial:~# ufw delete 4
Deleting:
 allow 25/tcp
Proceed with operation (y|n)? y
Rule deleted
root@tutorial:~# ufw delete 2
Deleting:
 allow 25/tcp
Proceed with operation (y|n)? y
Rule deleted (v6)

Don't get caught out by the list entries changing. If you've got 3 rules and you remove number 1, the ones you saw in the status as 2 and 3 are now numbered 1 and 2.

Fun fact: ufw allow can accept mappings from /etc/services, so ssh is the same as 22/tcp. You can play around with this by looking up mappings with getent:

root@tutorial:~# getent services ssh
ssh                   22/tcp
root@tutorial:~# getent services 22/tcp
ssh                   22/tcp

There we have it. You've got a secured Linux server, for all your Linuxing needs!

If you want to take your security even further, you can set up login notifications and brute-force attack prevention, harden SSH ciphers and improve the security of your local SSH client to prevent side-channel cryptanalysis. Watch this space for links on how to do that, or subscribe to the RSS feed if you're one of the cool people who use RSS.