Skip to content

Ubuntu Server Hardening
The Basic Guide To Server Security

Last updated: May 2022. For advanced users. Solid tech skills required.

Disclaimer

No system is safe. This is the first of two chapters on server security. While it presents some basic measures to protect your Ubuntu server from most immediate threats, any skilled hacker or organisation with sufficient resources will probably find a way into the system.

Ubuntu firewall

Ubuntu firewall open port

What is a firewall? A server permanently interacts with devices located inside and outside its network. It needs to be protected by a server security system called firewall, which controls incoming and outgoing traffic. The Uncomplicted Firewall (ufw) is a popular choice on Ubuntu servers, more details below.

Show me the step-by-step guide

Installation

Install the Uncomplicated Firewall, enable auto-start after each reboot, and verify it's running (the status should be Active):

sudo apt install ufw
sudo ufw enable
sudo systemctl status ufw

Ubuntu firewall settings

Deny all incoming and outgoing traffic by default:

sudo ufw default deny outgoing
sudo ufw default deny incoming

Make sure the network firewall only authorises required traffic. This is done by opening the respective communication endpoints, also called ports. For the purpose of this tutorial, we'll only open a few of them. Adjust the commands according to your specific needs:

sudo ufw allow 80,443/tcp
sudo ufw allow 22/tcp
sudo ufw allow 123/udp
sudo ufw allow in from any to any port 53
sudo ufw allow out 80,443/tcp
sudo ufw allow out 22/tcp
sudo ufw allow out 123/udp
sudo ufw allow out from any to any port 53

Some additional information about those ports:

Port Description
80 HTTP requests: assigned to the commonly used internet communication protocol, Hypertext Transfer Protocol (HTTP). Your server might use it to send data to and receive data from the Web.
443 HTTPS requests: standard port for all secured HTTP traffic (HTTPS). Your server might use it to send and receive encrypted Web traffic.
22 SSH requests: usually used to run the Secure Shell (SSH) protocol. Your server might use it for remote logins.
123 NTP synchronisation: allows to synchronise time between computers.
53 DNS requests: used for domain name resolution. Your server might use it to turn human readable domain names into IP addresses.

Finally, display the Ubuntu firewall status and check if all rules are set correctly:

sudo ufw status numbered

Here's what the rules should look like:

Status: active

        To                  Action          From
        --                  ------          ----
[ 1]    80,443/tcp          ALLOW IN        Anywhere
[ 2]    22/tcp              ALLOW IN        Anywhere
[ 3]    123/udp             ALLOW IN        Anywhere
[ 4]    53                  ALLOW IN        Anywhere
[ 5]    80,443/tcp          ALLOW OUT       Anywhere
[ 6]    22/tcp              ALLOW OUT       Anywhere
[ 7]    123/udp             ALLOW OUT       Anywhere
[ 8]    53                  ALLOW OUT       Anywhere
[ 9]    80,443/tcp (v6)     ALLOW IN        Anywhere (v6)
[10]    22/tcp  (v6)        ALLOW IN        Anywhere (v6)
[11]    123/udp  (v6)       ALLOW IN        Anywhere (v6)
[12]    53  (v6)            ALLOW IN        Anywhere (v6)
[13]    80,443/tcp  (v6)    ALLOW OUT       Anywhere (v6)
[14]    22/tcp  (v6)        ALLOW OUT       Anywhere (v6)
[15]    123/udp  (v6)       ALLOW OUT       Anywhere (v6)
[16]    53  (v6)            ALLOW OUT       Anywhere (v6)

Router settings

You might also want to check the router settings and make sure all unused ports are disabled. Refer to the router's manual for more information.

Show me the 1-minute summary video


What does ntp stand for

What does NTP mean?

Many security protocols rely on the system time. An incorrect time can have negative impacts on security. The Network Time Protocol (or NTP protocol) makes sure the Ubuntu server time remains synchronised with reference computers. These so-called NTP servers are organised in hierarchical layers, or strata. Read on below for more details on how to set up NTP on your server.

What is NTP

Layer Description
Stratum 0 Hardware clocks, for example atomic clocks, GPS or radio time receivers.
Stratum 1 Computers with a direct connection to hardware clocks.
Stratum 2 Computers synchronised over a network with stratum 1 servers.
NTP clients Computers periodically requesting the time from NTP servers.

Show me the step-by-step guide

NTP server list

Back up the configuration file:

sudo cp --archive /etc/systemd/timesyncd.conf /etc/systemd/timesyncd.conf-COPY-$(date +"%Y%m%d%H%M%S")

Open the file:

sudo vi /etc/systemd/timesyncd.conf

Modify the content to access an open-source, public NTP server operated by the pool.ntp.org project or Ubuntu:

[Time]
NTP=0.pool.ntp.org 1.pool.ntp.org
FallbackNTP=ntp.ubuntu.com

Save and close the file (:wq!). Finally, restart the systemd-timesyncd service and make sure it's running (the status should be Active):

sudo systemctl restart systemd-timesyncd
sudo systemctl status systemd-timesyncd

Time zone

Let's configure the server's time zone:

timedatectl | grep Time

Assuming you live near Budapest, the terminal should display something like:

Time zone: Europe/Budapest (CEST, +0200)

If not, set up the correct time zone (adjust accordingly):

sudo timedatectl set-timezone Europe/Budapest
Show me the 1-minute summary video


NTP server IP

Secure SSH

We've already explained how to remotely connect to the server from another computer. Read on below for more details on how to secure SSH:

Security feature Description
Secure SSH port Harden SSH by changing the port from 22 to another value. For the purpose of this tutorial, we'll choose port 2222; any other value will do, make sure to adjust the corresponding commands accordingly.
Login restrictions Limit the number of login attempts.
Access restrictions Authorise only one user to log in. In this case the administrator user gofossadmin (adjust accordingly).
Logging Keep track of each login.
Root restrictions Forbid remote SSH access to the root account.
Authentication Use authentication keys with passphrases instead of password authentication.
Disconnection Enable automatic disconnection after 5 minutes of inactivity.
Encryption keys Remove short encryption keys for increased security.
Legal banner Add a legal banner to warn unauthorised users.

Show me the step-by-step guide

SSH port & security settings

Open the new SSH port 2222 (adjust accordingly):

sudo ufw allow 2222/tcp
sudo ufw allow out 2222/tcp

Back up the SSH configuration file. If something goes wrong, you'll be able to recover the initial settings:

sudo cp --preserve /etc/ssh/sshd_config /etc/ssh/sshd_config.$(date +"%Y%m%d%H%M%S")

Open the SSH configuration file with the command:

sudo vi /etc/ssh/sshd_config

Delete the file content by typing :%d. Then enter or copy/paste the following content, while making sure to adjust settings where necessary (e.g. the SSH port or the admin user name):

# supported HostKey algorithms by order of preference:

HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
HostKey /etc/ssh/ssh_host_ecdsa_key
KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com

# various security settings:

Port                            2222        # change from standard SSH port 22 (adjust accordingly)
LogLevel                        VERBOSE     # log user's key fingerprint on login to have a clear audit track of which key was using to log in
Protocol                        2           # only use the newer, more secure protocol
PermitUserEnvironment           no          # don't let users set environment variables
PermitRootLogin                 no          # don't allow root login
PubkeyAuthentication            yes         # allow public key authentication
PasswordAuthentication          no          # login with password not allowed
PermitEmptyPasswords            no          # don't allow login if the account has an empty password
MaxAuthTries                    3           # maximum allowed attempts to login
MaxSessions                     2           # maximum number of open sessions
X11Forwarding                   no          # disable X11 forwarding
IgnoreRhosts                    yes         # ignore .rhosts and .shosts
UseDNS                          no          # verify hostname matches IP
ClientAliveCountMax             0           # maximum number of client alive messages sent without response
ClientAliveInterval             300         # timeout in seconds before a response request
AllowUsers                      gofossadmin # only allow user gofossadmin to login remotely (adjust accordingly)

# disable port forwarding:

AllowAgentForwarding            no
AllowTcpForwarding              no
AllowStreamLocalForwarding      no
GatewayPorts                    no
PermitTunnel                    no

# log sftp level file access (read/write/etc.):

Subsystem sftp  /usr/lib/openssh/sftp-server

# other security settings:

Compression                     no
PrintMotd                       no
TCPKeepAlive                    no
ChallengeResponseAuthentication no
UsePAM                          yes
AcceptEnv LANG LC_*

Save and close the SSH configuration file (:wq!). Restart the SSH server and verify that the new SSH port is set to 2222 (or whatever port you chose):

sudo systemctl restart sshd
sudo ss -tlpn | grep ssh

Disconnect inactive connections

Make sure all remote SSH connections are disabled after 5 minutes of inactivity:

echo 'TMOUT=300' >> .bashrc
tail .bashrc

Remove short encryption keys

SSH uses the Diffie-Hellman algorithm to establish a secure connection. For security reasons, it's recommended to use keys which are at least 3072 bits long.

Back up the SSH configuration file. If something goes wrong, you'll be able to recover the initial settings:

sudo cp --archive /etc/ssh/moduli /etc/ssh/moduli-COPY-$(date +"%Y%m%d%H%M%S")

Remove keys shorter than 3072 bits:

sudo awk '$5 >= 3071' /etc/ssh/moduli | sudo tee /etc/ssh/moduli.tmp
sudo mv /etc/ssh/moduli.tmp /etc/ssh/moduli

A warning message should be displayed to any user trying to remotely log into the server. Open the following file:

sudo vi /etc/issue.net

Add a legal banner, such as this one for example:

This system is for the use of authorised users only.

Individuals using this computer system without authority, or in excess of their authority, are subject to having all of their activities on this system monitored and recorded by system personnel.

In the course of monitoring individuals improperly using this system, or in the course of system maintenance, the activities of authorised users may also be monitored.

Anyone using this system expressly consents to such monitoring and is advised that if such monitoring reveals possible evidence of criminal activity, system personnel may provide the evidence of such monitoring to law enforcement officials.

Test configuration & close port 22

That's it! From now on you can securely use your client machine to log into the server. Just open a terminal, switch to the administrator account and connect to the server using your passphrase. Don't forget to adjust the username, IP address and SSH port as required:

su - gofossadmin
ssh -p 2222 gofossadmin@192.168.1.100

Try it a couple of times. If everything works as expected, close the unused port 22:

sudo ufw delete allow 22/tcp
sudo ufw delete allow out 22/tcp
Show me the 2-minute summary video


What is server hardening

MySQL security

MySQL databases run as a back-end to many web services. To protect the information maintained by MySQL from unauthorised access, follow the instructions below.

Show me the step-by-step guide

MySQL hardening

Launch MySQL:

sudo mysql

Change the authentication parameters for the root user. Make sure to replace the string StrongPassword with a strong, unique password:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password by 'StrongPassword';

Exit MySQL:

exit

Now, run the MySQL hardening script:

sudo mysql_secure_installation

Follow the on-screen instructions:

  • no need to change the root password, which was just defined
  • remove the anonymous user
  • forbid root to login remotely
  • remove the test database
  • reload the privileges table

Prevent unauthorised access

Back up the MySQL configuration file. If something goes wrong, you'll be able to recover the initial settings:

sudo cp --archive /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/mysql.conf.d/mysqld.cnf-COPY-$(date +"%Y%m%d%H%M%S")

Open the configuration file:

sudo vi /etc/mysql/mysql.conf.d/mysqld.cnf

Disable remote MySQL access and prevent unauthorised access to local files by adding or adjusting the following two lines:

bind-address = 127.0.0.1
local-infile = 0

Save and close the MySQL configuration file (:wq!).

User permissions

Note that it's best practice to create a dedicated user with limited privileges for each application requiring MySQL access. For example the cloud storage or photo gallery we'll set up at a later stage will each have their own MySQL user. This way, applications remain isolated and can only access databases on a need-to-know basis.

Show me the 1-minute summary video


Apache hardening

Apache security

Apache is the world's most used web server. Configure it properly to enhance security. More details below.

Show me the step-by-step guide

Enable security modules

Install the following two Apache modules:

sudo apt install libapache2-mod-security2 libapache2-mod-evasive

Enable the security modules, as well as other common modules required for this tutorial, and restart Apache:

sudo a2enmod security2
sudo a2enmod evasive
sudo a2enmod rewrite
sudo a2enmod headers
sudo a2enmod ssl
sudo a2enmod proxy
sudo a2enmod proxy_html
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel
sudo a2enmod xml2enc
sudo a2enmod expires
sudo systemctl restart apache2

Some additional information about those modules:

Module Description
Security2 Protects your server from various attacks, such as SQL injection, session hijacking, cross-site scripting, bad user agents, etc.
Evasive Provides evasive actions in the event of a Denial of Service (DoS) or Distributed Denial of Service (DDoS) attack, or a brute force attack.
Rewrite Allows to rewrite URLs to enable re-directions, for example from http:// to https://
Headers Allows to control and modify HTTP requests and response headers.
SSL Enables encryption via SSL and TLS.
Proxy, proxy_html, proxy_http Creates a proxy/gateway for your server, ensures that links work for users outside the proxy, and proxies HTTP and HTTPS requests.
Xml2enc Provides enhanced internationalisation support.
Expires Enhances page load time by determining how long an image will be stored by the browser.

Hide sensitive information

Per default, the server sends HTTP headers containing information on the Apache version, modules, operating system, and so on. This data can be used to exploit vulnerabilities. Create a custom Apache configuration file to hide such sensitive information:

sudo vi /etc/apache2/conf-available/custom.conf

Add the following content:

ServerTokens        Prod
ServerSignature     Off
TraceEnable         Off
Options all -Indexes
Header always unset X-Powered-By

Save and close the file (:wq!).

Apply the custom configuration, check for syntax errors (the terminal should display OK) and restart Apache:

sudo a2enconf custom.conf
sudo apachectl configtest
sudo systemctl restart apache2

Some additional information about those settings:

Setting Description
ServerTokens Prod Only return "Apache" in the server header.
ServerSignature Off Hide the server version on pages generated by Apache.
TraceEnable Off Disable the HTTP TRACE method, which can expose your server to cross-site scripting attacks.
Options all -Indexes Disable directory views.
Header always unset X-Powered-By Hide the information "X-Powered-By ..." and avoid displaying which software is used.

Configure ModSecurity

ModSecurity is an open-source web application firewall. This tool can be configured to protect your server from various attacks. Open the configuration file:

sudo mv /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
sudo vi /etc/modsecurity/modsecurity.conf

Modify/add the following parameters:

SecRuleEngine                   On
SecResponseBodyAccess           Off
SecRequestBodyLimit             8388608
SecRequestBodyNoFilesLimit      131072
SecRequestBodyInMemoryLimit     262144

Save and close the file (:wq!).

Some additional information about those settings:

Setting Description
SecRuleEngine On Enable ModSecurity using the basic default rules.
SecResponseBodyAccess Off Disable response body buffering to use less RAM and CPU.
SecRequestBodyLimit 8388608 Set the maximum request body size accepted for buffering to 8 MB.
SecRequestBodyNoFilesLimit 131072 Set the maximum data size accepted for buffering to 128 KB.
SecRequestBodyInMemoryLimit 262144 Set the maximum request body size stored in RAM to 256 KB.

Enable the latest OWASP ModSecurity Core Rule Set (CRS), a set of generic attack detection rules maintained on GitHub:

sudo rm -rf /usr/share/modsecurity-crs
sudo apt install git
sudo git clone https://github.com/SpiderLabs/owasp-modsecurity-crs.git /usr/share/modsecurity-crs
cd /usr/share/modsecurity-crs
sudo mv crs-setup.conf.example crs-setup.conf

Back up the ModSecurity configuration file. If something goes wrong, you'll be able to recover the initial settings:

sudo cp --archive /etc/apache2/mods-enabled/security2.conf /etc/apache2/mods-enabled/security2.conf-COPY-$(date +"%Y%m%d%H%M%S")

Open the configuration file:

sudo vi /etc/apache2/mods-enabled/security2.conf

Delete the content with the command :%d and add or copy/paste the following lines:

<IfModule security2_module>
    SecDataDir /var/cache/modsecurity
    IncludeOptional /etc/modsecurity/*.conf
    IncludeOptional /usr/share/modsecurity-crs/*.conf
    IncludeOptional /usr/share/modsecurity-crs/rules/*.conf
</IfModule>

Save and close the file (:wq!). Check for syntax errors (the terminal should display OK) and restart Apache to apply the new configuration:

sudo apachectl configtest
sudo systemctl restart apache2

Back up the ModEvasive configuration file. If something goes wrong, you'll be able to recover the initial settings:

sudo cp --archive /etc/apache2/mods-enabled/evasive.conf /etc/apache2/mods-enabled/evasive.conf-COPY-$(date +"%Y%m%d%H%M%S")

Configure ModEvasive to protect your server from Denial of Service (DoS), Distributed Denial of Service (DDoS) and brute force attacks.

Open the configuration file:

sudo vi /etc/apache2/mods-enabled/evasive.conf

Modify or add the following parameters:

<IfModule mod_evasive20.c>
    DOSPageCount        5
    DOSSiteCount        50
    DOSPageInterval     1
    DOSSiteInterval     1
    DOSBlockingPeriod   600
    DOSLogDir           "/var/log/mod_evasive"
</IfModule>

Save and close the file (:wq!). The following commands create a directory for the log files, check for syntax errors (the terminal should display OK), restart Apache and verify if the status is Active:

sudo mkdir /var/log/mod_evasive
sudo chown -R www-data: /var/log/mod_evasive
sudo apachectl configtest
sudo systemctl restart apache2
sudo systemctl status apache2
Show me the 2-minute summary video


Network firewall security

PHP security

PHP is a widely-used programming language for web services. Configure it properly to enhance security. More details below.

Show me the step-by-step guide

PHP version

PHP supports various versions. At the time of writing, PHP 8.1 is included by default in Ubuntu's 22.04 repositories. Check which version is installed on your system:

php -v

PHP modules

Install some common PHP modules required for this tutorial, applying the same version number as above:

sudo apt install php8.1-common php8.1-mbstring php8.1-xmlrpc php8.1-gd php8.1-xml php8.1-intl php8.1-mysql php8.1-cli php8.1-ldap php8.1-zip php8.1-curl php8.1-pgsql php8.1-opcache php8.1-sqlite3

PHP security settings

The php.ini configuration files contain all relevant PHP security settings. First, let's find out where these files are stored:

sudo find / -name php.ini

Depending on the PHP version, the command might return something like:

/etc/php/8.1/apache2/php.ini
/etc/php/8.1/cli/php.ini

The /etc/php/8.1/apache2/php.ini file is used by the Apache server, while the /etc/php/8.1/cli/php.ini file is used by the CLI PHP program.

Back up both configuration files. If something goes wrong, you'll be able to recover the initial settings:

sudo cp --archive /etc/php/8.1/apache2/php.ini /etc/php/8.1/apache2/php.ini-COPY-$(date +"%Y%m%d%H%M%S")
sudo cp --archive /etc/php/8.1/cli/php.ini /etc/php/8.1/cli/php.ini-COPY-$(date +"%Y%m%d%H%M%S")

Open the first configuration file:

sudo vi /etc/php/8.1/apache2/php.ini

Modify or add the following parameters:

expose_php          =   Off
allow_url_fopen     =   Off
allow_url_include   =   Off
display_errors      =   Off
mail.add_x_header   =   Off
disable_functions   =   show_source,system,passthru,phpinfo,proc_open,allow_url_fopen,curl_exec
max_execution_time  =   90
max_input_time      =   90
memory_limit        =   1024M

Save and close the file (:wq!).

Open the second configuration file:

sudo vi /etc/php/8.1/cli/php.ini

Modify or add the following parameters:

expose_php = Off
allow_url_fopen = Off

Save and close the file (:wq!).

Some additional information about those settings:

Setting Description
expose_php = Off Hides the PHP version in headers.
allow_url_fopen = Off Disables remote PHP code execution to reduce code injection vulnerabilities.
allow_url_include = Off Disables remote PHP code execution to reduce code injection vulnerabilities.
display_errors = Off Disables error display.
mail.add_x_header = Off Removes PHP header from emails.
disable_functions = show_source, system, passthru, phpinfo, proc_open, allow_url_fopen, curl_exec Disables potentially harmful PHP functions. The following three functions have not been disabled, as this breaks Pihole v5.2: exec, shell_exec, popen.
max_execution_time = 90 Sets the maximum time a script is allowed to run (in seconds).
max_input_time = 90 Sets the maximum time a script can parse data (in seconds).
memory_limit = 1024M Sets the maximum amount of memory allocated to a script.

Show me the 2-minute summary video


Unattended upgrades Ubuntu

Unattended upgrades

The server needs to be up-to-date to reduce security vulnerabilities. Enable automatic Ubuntu security updates, as detailed below.

Show me the step-by-step guide

Enable automatic updates:

sudo apt install unattended-upgrades

Create a configuration file:

sudo vi /etc/apt/apt.conf.d/51myunattended-upgrades

Add the following content to configure unattended upgrades:

// Enable the update/upgrade script (0=disable)
APT::Periodic::Enable "1";

// Do "apt-get update" automatically every n-days (0=disable)
APT::Periodic::Update-Package-Lists "1";

// Do "apt-get upgrade --download-only" every n-days (0=disable)
APT::Periodic::Download-Upgradeable-Packages "1";

// Do "apt-get autoclean" every n-days (0=disable)
APT::Periodic::AutocleanInterval "7";

// Send report mail to root
//     0:  no report             (or null string)
//     1:  progress report       (actually any string)
//     2:  + command outputs     (remove -qq, remove 2>/dev/null, add -d)
//     3:  + trace on
APT::Periodic::Verbose "2";
APT::Periodic::Unattended-Upgrade "1";

// Automatically upgrade packages from these
Unattended-Upgrade::Origins-Pattern {
    "o=Ubuntu,a=stable";
    "o=Ubuntu,a=stable-updates";
    "origin=Ubuntu,codename=${distro_codename},label=Ubuntu-Security";
};

// You can specify your own packages to NOT automatically upgrade here
Unattended-Upgrade::Package-Blacklist {
};

// Run dpkg --force-confold --configure -a if a unclean dpkg state is detected to true to ensure that updates get installed even when the system got interrupted during a previous run
Unattended-Upgrade::AutoFixInterruptedDpkg "true";

//Perform the upgrade when the machine is running because we wont be shutting our server down often
Unattended-Upgrade::InstallOnShutdown "false";

// Send an email to this address with information about the packages upgraded.
Unattended-Upgrade::Mail "root";

// Always send an e-mail
Unattended-Upgrade::MailOnlyOnError "false";

// Remove all unused dependencies after the upgrade has finished
Unattended-Upgrade::Remove-Unused-Dependencies "true";

// Remove any new unused dependencies after the upgrade has finished
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";

// DO NOT automatically reboot WITHOUT CONFIRMATION if the file /var/run/reboot-required is found after the upgrade.
Unattended-Upgrade::Automatic-Reboot "false";

// DO NOT automatically reboot even if users are logged in.
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";

Save and close the file (:wq!). Perform a test run to make sure everything is working:

sudo unattended-upgrade -d --dry-run
Show me the 1-minute summary video


Ubuntu server security