Basic Linux Hardening

This guide follows on from the Initial Linux hardening guide and will cover the basic steps to take to harden Linux after you’ve completed the initial installation and established secure access.

Contents

Installed Packages

As this guide is based on the minimal install, it’s likely that you won’t need to remove or disable any of the installed packages. Assuming you don’t need to remove any packages (or already have removed any you don’t need), update the installed packages:

yum update

You should also make sure the system regularly checks for updates. You could automatically install these updates, but I would advise against it so that you can at least review the updates and patch notes before installing. You can do this by using the yum-cron package:

yum install yum-cron
systemctl enable yum-cron.service --now

Then open /etc/yum/yum-cron.conf and make sure the following values are set to ensure you’re notified about updates but they’re not automatically downloaded and installed:

update_messages = yes
download_updates = no
apply_updates = no

You can also configure the email settings to send notifications to a remote recipient, which would be wise if you’re not going to be logging into this machine regularly. Likewise, you may want to set the hourly cron job to automatically install security updates by using the following settings in /etc/yum/yum-cron-hourly.conf:

update_cmd = security
update_messages = yes
download_updates = yes
apply_updates = yes

When you’re finished configuring yum-cron, restart the service and check the status:

systemctl restart yum-cron.service
systemctl status yum-cron.service

Time and Logging

NTP with Chrony

We should configure the machine to keep time properly, to ensure the best accuracy of logs. The package installed in CentOS 7 by default is chrony, so we’ll use that rather than ntp. Check the settings in /etc/chrony.conf and amend it to your requirements (for example, if you already have a time server in your environment, you’ll want to configure it as a source in /etc/chrony.conf).
To allow NTP through the firewall, use the command:

firewall-cmd --permanent --zone=internal --add-port=123/udp

By default, chrony will only accept commands from the localhost; if you need to issue commands from a remote host, add the following lines to /etc/chrony.conf (where aaa.bbb.ccc.ddd/xx is the CIDR address of the remote host form which commands will be received):

bindcmdaddress 0.0.0.0
cmdallow aaa.bbb.ccc.ddd/xx

For remote control, you will also need to open 323/udp in the firewall:

firewall-cmd --permanent --zone=internal --add-port=323/udp

The RedHat documentation has further information on chrony, should you wish to learn more.

Logging with Rsyslog

With the system time accurately set, now would be as good a time as any to make sure logging is enabled. The minimal install we’ve been using comes with the rsyslog package installed, so just make sure the service starts at boot and is running:

systemctl enable rsyslog.service
systemctl start rsyslog.service

We’ll look at configuring rsyslog in another post; for now we can leave the config with default settings and retrieve logs from our /var/log partition.
We should ensure the correct file permissions are set:

chown root /var/log            # root owns the directory
chmod 0640 /path/to/audit-file # Owner read/write, group read; maximum

Basic Hardening

Up to this point, while ensuring secure configuration, most of what we’ve done has been to get the system up and running; from here we’ll be looking at configuration more specific to the task of securing the system. First, we’ll begin by controlling how users interact with the system, and what they’re capable of when they do.

Password Quality

First, enforce a secure password policy by editing /etc/security/pwquality.conf and amend the values according to your organisation’s existing password policy. You should also make corresponding changes to /etc/login.defs, otherwise commands that do not leverage PAM tools (such as useradd) will not follow your password policy enforcement.
There are a couple of schools of thought as far as password policies are concerned. Some believe forcing regular password changes is a good way to deny an attacker the opportunity to crack/brute passwords. I prefer to enforce a longer minimum length of password instead, and force their expiry only if there is indication of a compromise. This means users are more likely to select a passphrase, rather than a password and, in my experience, users choose much weaker passwords when forced to regularly change them (say, once a month). Whichever approach you use, you can configure the system to enforce your policy here.
The pam_quality module will check user-defined passwords against the rules you’ve just configured in /etc/security/pwquality.conf; in order to actually use the module (i.e. when a user changes their password with the passwd command), it should be called in one of the pam.d config files. There should already be a line in /etc/pam.d/system-auth as follows:

password    requisite     pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=

The key here is that the password stack is making a call to the pam_pwquality module.
The RHEL documentation has you add a similar line to /etc/pam.d/passwd, but this will cause the module to be called twice on setting a new password, which will prompt the user twice - not particularly user-friendly! However, the RHEL documentation is otherwise a good starting point to learn about PAM.

Failed Logins

While you’re in /etc/pam.d/system-auth, add the following line to the session stack immediately after the call to pam_limits.so:

session    required    pam_lastlog.so showfailed

When users log in, they will now see the date, time and source of the last failed login, and the number of failed login attempts since the last successful login.
In order to implement account lockout after failed logins, add the following two lines immediately below the pam_unix.so call in the auth stack of both /etc/pam.d/system-auth and /etc/pam.d/password-auth:

auth   [default=die] pam_faillock.so authfail deny=3 unlock_time=600 fail_interval=900
auth   required pam_faillock.so authsucc deny=3 unlock_time=600 fail_interval=900

There’s quite a bit going on here - it’s important pam_faillock.so is called first with authfail and then the authsucc option, so that successful logins reset the failed login counter. The deny option sets the acceptable number of failed login attempts, while unlock_time sets the amount of time (in seconds) to lock the account, and fail_interval sets the duration (in seconds) during which consecutive login failures must be made in order to trigger the lockout condition.

If you force password expiry, you’re likely to want to make sure users don’t reuse old passwords. In /etc/pam.d/system-auth, add remember=x where x is the number of passwords to remember to the pam_unix.so call in the password stack. The line should look like this:

password   sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok remember=24

Finally, you should also ensure SHA512 algorithms are used to hash passwords by running the command:

authconfig --passalgo=sha512 --update

Interactive Shells

You may or may not want to use screen to allow users to lock their terminals; I’m not going to cover it here because I have no use for it (I have a fairly typical environment; admins use Windows workstations that automatically lock after x minutes, etc). However, what I will do is log idle sessions off (because I have a fairly typical environment, where admins will lock their Windows machine with a PuTTY session still connected). This is largely doubling up with the SSH timeout config we’ve already done, but this will also clean up any local terminal sessions that may be running (such as from a KVM in the server room, or a VMWare console). Create the file /etc/profile.d/auto-logout.sh and add the following line:

readonly TMOUT=600

Don’t forget to use chmod +x /etc/profile.d/auto-logout.sh to make the script executable.
This will time out any shell session after 10 minutes (600s) of idle time (the same as our SSH config) and set the variable as read only, so the user can’t change/remove it.
For added hardening, consider adding another script in the /etc/profile.d/ directory to globally set other shell options, such as setting the HISTFILE environmental variable as read only to stop users from changing the location of their shell history, or an attacker redirecting it to /dev/null to cover their tracks.
You should also edit both /etc/profile and /etc/bashrc to find where umask is set, and change it from the default umask 002 and umask 022 to just umask 077. Setting umask 007 will grant read/write/execute permissions to the file owner only. If you intend to allow users to share files on this system, umask 007 will cause issues.

Restricting Root

We already covered permitting/denying root login over SSH in an earlier section, but you may want to restrict root access further, depending on your requirements. If you’ve already denied root login over SSH, you can also enforce root login over a local console only by making sure /etc/securetty exists and only contains one line: tty1. The file /etc/securetty is used by pam.d to determine what terminals root can use to log in. If /etc/securetty does not exist, root will be allowed to log in using any method; if the file exists but is blank, root will not be able to log in. Adding the line tty1 will effectively restrict root to log in via the local console only.
You can also disable the shell for root by changing the shell value in /etc/passwd to /bin/false or /sbin/nologin. The effect is the same, but the latter is more user-friendly because you can provide feedback in the form of a message in /etc/nologin.txt, whereas the /bin/false method will simply exit the login attempt.
NB: Disabling root login over SSH will also affect programs that rely on SSH, such as sftp and scp. Similarly, disabling the shell for root will not impact services that don’t require a shell, such as FTP and email clients. If you want finer control over root access to services, I suggest you use PAM (assuming the services you want to configure are PAM-aware).

Removable Media

There are two main reasons to control the behaviour of removable media: one is to prevent data exfiltration, the other is to prevent malicious uploads to your network.
To stop removable media being used to remove data from your network, you can tell force the media to be mounted as read-only using a udev rule. I’ve created the file /etc/udev/rules.d/ro_removable_media.rules with the following udev rule:

SUBSYTEM=="block",ATTRS{removable}=="1",RUN{program}="/sbin/blockdev --setro %N"

If you have a legitimate need to mount a writeable removable storage device (for example, backing up some config), you can change the last parameter in the line above from --setro (read-only) to --setrw (read-write); udev will detect changes to configuration files and reload accordingly, but you can force a reload if need be by using the command udevadm control --reload. You could also target very specific attributes as an exception to your udev rule, if you happen to use a particular make and model of removable storage device; use man udev for more information.
Similar rules can be used to block new USB input devices (such as keyboards), which is useful if, say, your server is already connected to a KVM and you know no new input devices should be added.

If you wish to block the mounting of all removable storage devices, you can do so using modprobe. Add a file /etc/modprobe.d/block-usb-storage.conf containing the line:

install usb-storage /bin/false

Securing Cron

A poorly managed crontab is a great way to pivot or maintain persistence on a system, so you’ll want to make sure only authorised users have access to cron. There are a couple of options here: if /etc/cron.allow exists, it will act as an explicit whitelist of accounts that can use cron, while accounts not present in the file will be implicitly blacklisted; if only /etc/cron.deny exists, you’ll have an explicit blacklist with an implicit whitelist. If both files exist, /etc/cron.deny is ignored. The choice is yours! Blacklist or whitelist, pick whichever suits you and your environment, but I would suggest an explicit whitelist (always) because a user will soon tell you if you’ve forgotten to whitelist them, but might not be so quick to mention that you’ve forgotten to blacklist them from something…never forget the principle of least privilege! The allow/deny files are a simple list of system account names, one per line; regardless of the file contents, root can always use cron.
Whichever option you pick, just remember to make sure that root owns the file and you use chmod 600 to prevent unauthorised edits! Repeat this process for /etc/at.allow and/or /etc/at.deny (at is like cron, but runs tasks at a specific time, as opposed to on a regular schedule).

Disable Core Dumps

When a program terminates unexpectedly, the kernel will create a core dump, which is a file containing the address space (memory) of the file at the time of crash. They are useful debugging tools, but are of little use on a stable production system and present the added risk of potentially sensitive data being leaked. We’re better of just disabling the feature.
To disable core dumps for all users, first edit /etc/security/limits.conf and add the line:

* hard core 0

This sets a hard limit of size 0 for all users, so they cannot increase the limit in their own sessions.
You can also do this using /etc/profile, or a custom script in the directory /etc/profile.d/, if you prefer. Append the line:

echo 'ulimit -S -c 0 > /dev/null 2>&1'

This will set a soft limit of 0 for every user when they log in. Omit the -S option to set both a hard and soft limit of 0.
Lastly, you can use systemd to prevent the creation of core dumps by editing /etc/systemd/coredumps.conf and setting the following values:

Storage=none
ProcessSizeMax=0

To stop setuid programs creating core dumps, and a configuration file under /etc/sysctl.d/, as we have previously, and set the value fs.suid_dumpable=0 and reload the sysctl config with the command sysctl -p.

Kernel Hardening with Modprobe

We’ll use modprobe to disable uncommon protocols and filesystems, to exercise more control over how the operating system functions and prevent users (maliciously or otherwise) from doing unexpected things. We could use modprobe to blacklist certain kernel modules, but this will only serve to stop them being loaded at boot; they could still be loaded manually or as a dependency of an allowed module. Instead, we’ll redirect their install command to /bin/true.
NB: we’re redirecting to /bin/true as opposed to /bin/false to avoid any potential problems caused by the loading of the module returning a ‘false’ result. It doesn’t appear as intuitive if you look at the config, as /bin/false clearly shows a deliberate attempt to make sure something doesn’t happen, but let’s assume that if you’re tuning kernel modules, you know that /bin/true would have the same effect.
All modules that can be possible loaded are listed in /lib/modules. To list them all, use the command:

find /lib/modules/$(uname -r) -type f -name '*.ko*'

You can grep the results to find particular modules. The following modules are present that should be blocked from loading:

echo "install cramfs /bin/true" > /etc/modprobe.d/cramfs.conf
echo "install squashfs /bin/true" > /etc/modprobe.d/squashfs.conf
echo "install udf /bin/true" > /etc/modprobe.d/udf.conf
echo "install dccp /bin/true" > /etc/modprobe.d/dccp.conf
echo "install sctp /bin/true" > /etc/modprobe.d/sctp.conf

There may be more depending on your use case and requirements, but these are a good place to start in general.

At this stage, you have a relatively secure Linux installation and can begin using it in a production environment, assuming you have no further specific requirements, such as any imposed by regulatory frameworks. However, you could also proceed with further security configuration explained in the next part of the guide: Intermediate Linux Hardening.

Updated: