Basic Ubuntu Server hardening and best security practices

Basic Ubuntu Server hardening and best security practices

Here are some ways to make a standard Ubuntu Server install more secure. These are some common server hardening tips.

March 16, 2025
Table of Contents

Exposing an Ubuntu Server to the Internet can pose some security risks. Malicious bots and scanners are crawling known IPs and ports to find security vulnerabilities. While Ubuntu Server is rather secure by default, here are some tips and tricks on how to improve security of a standard Ubuntu Server installation.

If you check the SSH and web server logs on a freshly created server, you might be surprised to see the amount of traffic that is sent. Bots are scouring the Internet looking for open servers and vulnerable software to exploit. A compromised server might become part of a botnet, data might be exfiltrated or ransomware installed. Therefore, special care must be taken to ensure that the server is secure.

For best results, I recommend sticking to an LTS (Long Term Support) version of Ubuntu. This provides the longest window of security updates (5 years) without the need to upgrade the server. Whereas, standard versions are only supported for 9 months.

Securing the login

Creating a non-root user

Often, when Ubuntu servers are provisioned, you are given the root login through SSH. This is necessary to get started with the server, however you should create a non-root user as soon as possible.

This can be done easily using the adduser command as so:

sudo adduser [mynewuser]

Avoid creating a user with a common login such as admin, user, sysadmin, etc. Use a unique or uncommon name to make probing for logins more difficult.

Make the user able to use sudo by adding the user to the sudo group:

sudo usermod -aG sudo [mynewuser]

Using a private key to login

Next, you shouldn't log in to the server using a password. Instead, consider using a private key. A key with a few thousand bits of entropy is impossible to guess, unlike passwords which can be bruteforced, guessed from a dictionary or stolen from password dumps.

Log in as your custom user and run the following command to generate a key pair:

ssh-keygen

For extra security, set a passphrase on your key.

This will create two files: id_ed25519 (the private key) and id_ed25519.pub (the public key). These keys use elliptic curve cryptography which is more secure than traditional RSA keys. If you're using an older Ubuntu version you may get an id_rsa key file instead. This is not necessarily a problem though.

To allow logging with this key, we need to add the public key to the authorized_keys file and adjust the file permissions accordingly.

cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

View the private key and copy it to your local machine:

cat ~/.ssh/id_ed25519

Disabling root and password login

Before you proceed, make sure you can log in to your user account with the previously generated private key.

ssh [user]@[host] -i ~/somewhereonmylocalmachine/id_ed25519

It's a good practice to disable logging in with passwords or as the root user.

To do this, edit the /etc/ssh/sshd_config :

sudo vim /etc/ssh/sshd_config
# or if you prefer
sudo nano /etc/ssh/sshd_config

Set the following values in the file:

PermitRootLogin no
PasswordAuthentication no

Finally, restart the SSH server to apply the changes:

sudo systemctl restart ssh.service

SSH Two-Factor Authentication

Alternatively, if you don't want to use a private key, you can enable two-factor authentication on your login. This means that along with your password, you will need to provide a 6-digit code generated from a TOTP client (Android or iOS device).

Install the google-authenticator package and start the setup:

sudo apt install libpam-google-authenticator
google-authenticator

Say Yes to "Do you want authentication tokens to be time-based?".

You will be shown an ASCII QR code in the terminal that you can scan in an app. Make sure you use a 2FA app the allows backups such as Stratum, this means that you won't lose access to your server if you lose your phone.

#
# ASCII QR code displayed here
#
Your new secret key is: MU5GW5A2RY7M5BZ4XIJINWUYP4
Enter code from app (-1 to skip): 912198
Code confirmed
Your emergency scratch codes are:
  31487191
  68248222
  21255915
  96687320
  65438781

You will be asked to confirm proper setup by entering the code generated on your device. You can then provide the following recommended answers to the questions:

To enable the verification code prompt, edit the following file and add the next line:

sudo vim /etc/pam.d/sshd
auth required pam_google_authenticator.so

Edit your SSH daemon configuration to enable interactive authentication and disable plain passwords:

sudo vim /etc/ssh/sshd_config
KbdInteractiveAuthentication yes
PasswordAuthentication no

Finally, restart the SSH daemon to apply changes:

sudo systemctl restart ssh.service

Now when you log in, you will need to first provide your password and then a 6-digit code from the authenticator app.

Enabling UFW (Uncomplicated FireWall)

If your beard is not grey enough to understand iptables, you can use UFW (Uncomplicated FireWall) instead. As the name suggests, this is a simpler way of managing a firewall if your needs are relatively simple.

To get a list of available profiles, run the sudo ufw app list command:

myuser@myserver:~$ sudo ufw app list
Available applications:
  Nginx Full
  Nginx HTTP
  Nginx HTTPS
  OpenSSH

Allow OpenSSH connections through the firewall.

sudo ufw allow OpenSSH

And then, enable the firewall:

sudo ufw enable

If you didn't get kicked from the server, that means you did it in the right order. Any other connections will now be refused.

Assuming you have NGINX installed, you can enable HTTP and HTTPS requests by enabling the Nginx Full profile.

Blocking bruteforce logins with Fail2Ban

Fail2Ban is a daemon that scans for incorrect login attempts through SSH and web servers. If a malicious client is detected, it will block that IP address for a set period of time. To install it, run the following command:

sudo apt install fail2ban

Then enable and start the Fail2Ban daemon:

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

By default, Fail2Ban is configured to look for SSH authentication failures. If you want it to scan for authentication failures in NGINX or Apache, you must configure the service. More information can be found in the Fail2Ban wiki.

Enable Unattended Upgrades

On an up-to-date Ubuntu install, unattended upgrades should be enabled by default. This allows the system to apply security patches automatically without having to do apt upgrade manually.

Make sure than the unattended-upgrades package is installed using the following command:

sudo apt install unattended-upgrades

If the package is already installed you won't be prompted to configure it. Run the following command and select 'Yes' to enable auto upgrades.

sudo dpkg-reconfigure unattended-upgrades

Enable Livepatch and ESM (Ubuntu Pro)

In the same vein, you can enable automatic kernel patching using Livepatch. This is a feature of Ubuntu Pro that performs automatic kernel updates without rebooting. Also with Ubuntu Pro you can use ESM (Expanded Security Maintenance) which provides additional security updates for installed software.

Ubuntu Pro is a paid service, however you can enable it for free on up to 5 machines for personal use. To get a token you must first subscribe to Ubuntu Pro.

Once you have a token, you can enable the service with the following command:

sudo pro attach [mytoken]

You can check the status using this command:

sudo pro status

Web server hardening

The primary use for many Ubuntu and Linux servers is as a web server. Here are a few tips for this use case.

Enable HTTPS

Websites should be served over a secure connection. In the past, this involved purchasing an SSL certificate. Thankfully, Let's Encrypt provides a free and convenient way of enabling HTTPS on your website.

First install certbot, this will manage our TLS certificates:

sudo snap install --classic certbot

Create a symbolic link to certbot in somewhere on the PATH.

sudo ln -s /snap/bin/certbot /usr/bin/certbot

You can request an HTTPS certificate for NGINX using the following command. Certbot will modify the NGINX configuration to use the generated certificate.

sudo certbot --nginx

Certbot will automatically renew HTTPS certificates for you. However, you can test this using the following command to be sure it works:

sudo certbot renew --dry-run

Disable Server header

If you're using NGINX (or Apache), you should disable the Server header. This returns the server name a version when an HTTP request is made. There is no benefit to disclosing this information, so it should be disabled.

Edit your NGINX configuration:

sudo vim /etc/nginx/nginx.conf

Change the following directive:

server_tokens off;

Use rate limiting and connection limits

If your server is exposed directly to the Internet, in order to mitigate attacks on your web server, consider enabling the following:

These features are covered in depth in the following articles on the NGINX blog: Rate Limiting with NGINX and Mitigating DDoS Attacks with NGINX.

Cloudflare firewall and caching

Cloudflare is a service that provides bot and DDoS protection for websites. The free tier is generous and provides a decent level of protection. Setting up Cloudflare for your server involves changing the DNS nameservers to allow routing traffic through the Cloudflare network.

The official documentation provides an overview of how to add a domain.

Origin mTLS

You can enable mTLS (Mutual TLS) between the Cloudflare network and your server. This ensures that the traffic your server receives has been sent through Cloudflare.

First enable Authenticated Origin Pulls in the Cloudflare dashboard: SSL/TLS → Origin Server → Authenticated Origin Pulls

Next, download the Cloudflare certificate authority file on the server:

wget https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem
sudo mv authenticated_origin_pull_ca.pem /etc/nginx/cloudflare.pem

Then, edit the NGINX configuration (or a virtualhost) to only accept signed requests:

sudo vim /etc/nginx/nginx.conf
http {
    ...
    
    # Cloudflare mTLS
    ssl_verify_client on;
    ssl_client_certificate /etc/nginx/cloudflare.pem;
}

Run software in Docker containers

Modern software practices involve running applications in containers, this provides a great deal of convenience but also a high level of security. Containers are isolated from the host machine and have very little access if configured correctly. This means that a vulnerable application can't do damage to the system or other containers.

For most application servers, I recommend using NGINX as a reverse proxy on the host and proxying to a Docker container on a port that's not exposed through the firewall (such as 8080).

Rootless containers

By default, Docker containers run as root. Consider using a custom user for the application, this limits the impact of a compromised application within a container. For most applications, you can simply set the USER directive in your Dockerfile to something other than root.


Any comments or suggestions about this article? Feel free to contact me!

Latest posts

Bundling Typescript and SCSS using Vite with Python and Flask

Vite is a great tool for bundling JS modules and stylesheets. Here's how to use it with the Flask microframework.

March 16, 2025
Managing secrets in Docker Compose and GitHub Actions deployments

When deploying Docker Compose applications, here's how you can manage secrets without embedding them in your containers

October 20, 2024
Deploy Docker Compose applications with zero downtime using GitHub Actions

This example demonstrates Blue-Green deployments using Docker Compose and GitHub Actions, deploy an app with zero downtime

July 21, 2024