Computer security is once again becoming a hot topic for administrators. There are dozens of new sites springing up around the web, and each is slinging their own ‘Perfect’ setup instructions. They have the usual bell curve of good advice, okay advice, and advice that will effectively leave you with a smoldering pile of rubble where your data used to be. Here, we're going to discuss locking down a CentOS 5 system the proper way. This proper way is based on the NSA RHEL5 guide, Steve Grubb's RHEL Hardening presentation, and other reputable sources.

For the purposes of this wiki article, we are assuming that we are configuring a server. Laptops and workstations may have different requirements, such as encrypted filesystems which are not covered here. Likewise, they may require the use of USB storage or wireless modules, which are disabled in this security guide.

File System Partitioning

By separating file systems into various partitions, you can fine tune permissions and functionality. Doing so will provide you greater granularity for permissions, as well as adding a layer of security for any potential bad guys to work through.

Steve Grubb suggests, and quite rightly so, that areas where users have write privileges be kept on their own partition. This allows you to prevent hard link privilege escalation attempts, prevent creative device additions, and other unsavory behavior.

Modifying fstab

Once you have your partitions broken out and sized accordingly, you can begin to restrict the various mount points as much as possible. You should add nodev, noexec, and nosuid wherever possible. An example of a decently restricted /etc/fstab file is below:

/dev/VG_OS/lv_root          /        ext3      defaults     1 1
/dev/VG_OS/lv_tmp           /tmp     ext3      defaults,nosuid,noexec,nodev  1 2
/dev/VG_OS/lv_vartmp        /var/tmp ext3      defaults,nosuid,noexec,nodev 1 2
/dev/data_vol/lv_home       /home    ext3      defaults,nosuid,nodev  1 2
/dev/VG_OS/lv_var           /var     ext3      defaults,nosuid     1 2
/dev/data_vol/lv_web        /var/www ext3      defaults,nosuid,nodev  1 2
/dev/sda1                   /boot    ext3      defaults,nosuid,noexec,nodev  1 2
tmpfs                       /dev/shm tmpfs     defaults 0 0
devpts                      /dev/pts devpts    gid=5,mode=620 0 0
sysfs                       /sys     sysfs     defaults    0 0
proc                        /proc    proc      defaults    0 0
/dev/_VG_OS/lv_swap         swap     swap      defaults    0 0

Obviously you'll need to modify this example to suit your own system. LVM, volume names, labels etc are all subject to change. Please don't copy this example verbatim and expect it to work for you.

The webserver mount can also be set noexec, however this will impact cgi based applications, as well as server side includes which rely on the execute bit hack. If you're not using cgi applications, I would recommend at least testing noexec and using it if there are no negative side-effects.

Package installs

When it comes to the packages you install on your systems, it's good to remember that less is more. More things on the system mean more things to track for vulnerabilities and updates, as well as more things which can potentially get in the way of something you're trying to do. Since server tasks are often very different, I won't even attempt to give a list of what should or should not be installed. Instead, I recommend a basic strategy of customizing the package list and unchecking everything but base. Once you have this done, generate a list of what's currently installed, and further prune back what you don't need.

yum list installed >> ~/installed.txt

For x86_64 users: If you don't need i386/i686 packages for compatability purposes, you may want to remove them as well, by using yum remove *.i?86, and then keep them gone by adding exclude = *.i?86 to your /etc/yum.conf

Regular Updates

Now that we have our minimal package set in place, we have to update them periodically. I do not recommend the use of yum-updatesd. I've had far too many bad experiences with it hogging resources for me to advise its use. You could set a cron job to update, or check for updates, which is what the NSA guide recommends. You could simply manually apply the updates on a weekly basis, or you could go for broke and set up a spacewalk server to push and manage updates across multiple systems. However you decide to manage your update policy, it basically boils down to this. Apply updates in a timely fashion to avoid problems. Additionally, subscribing to the CentOS-Announce mailing list is always a good idea. This way you will be sent a notification every time there is an update, and you can apply any critical patches early if need be.

Once you have your package list sufficiently pruned and updated, you will want to go through your services list and disable anything you won't be using on your server. Again, since every environment is different, I won't pretend to tell you what you should or should not turn off; however, you do need to ask yourself if that server REALLY needs bluetooth running :-P

Basic Hardening

Now that we have the partitioning done, restrictions added, and package list pruned, it's time to get around to the meat of the matter. It's time to lock down the system. Some of these may or may not apply based on your environment. You should consider all of them, but your particular situation may dictate doing things a different way.

Physical Protection

For directions on protecting grub, see BIOS and Boot Loader Security. To require root's password for single user mode, you can use:

echo "# Require the root pw when booting into single user mode" >> /etc/inittab
echo "~~:S:wait:/sbin/sulogin" >> /etc/inittab
echo "Don't allow any nut to kill the server"
perl -npe 's/ca::ctrlaltdel:\/sbin\/shutdown/#ca::ctrlaltdel:\/sbin\/shutdown/' -i /etc/inittab

Now disable USB mass storage, if you're not using it in your environment

echo "Disabling USB Mass Storage"
echo "blacklist usb-storage" > /etc/modprobe.d/blacklist-usbstorage

User Rights

This is by far the largest category, with some of the most important stuff. Since each organization is different not all of these may apply to you, but you should consider them.

By default, users are given quite a bit of freedom. Much of this freedom can easily be stripped from them to help secure the system. Since root has the most power, we'll start with restricting root first.

Restricting Root

Once a server is up and running, root shouldn't be logging in directly except in emergency situations. These usually require hands at the console, so that's the only place root should be allowed to log in. To do this, we need to modify /etc/securetty. Additionally, no one other than root should be allowed in root's home directory. The default settings are close to this, but not quite paranoid enough.

echo "tty1" > /etc/securetty
chmod 700 /root

Since we have effectively removed root's ability to log in from anywhere but the local console, it becomes necessary to use su and sudo. This offers a few secondary benefits in a multi-admin environment.

Password Policies

Now that we have root mostly restricted, it's time to move on to everyone else. First, we need to set down some ground rules when it comes to new accounts.

echo "Passwords expire every 180 days"
perl -npe 's/PASS_MAX_DAYS\s+99999/PASS_MAX_DAYS 180/' -i /etc/login.defs
echo "Passwords may only be changed once a day"
perl -npe 's/PASS_MIN_DAYS\s+0/PASS_MIN_DAYS 1/g' -i /etc/login.defs

The command below will update your system to use sha512 instead of md5 for password protection. This alleviates a number of bureaucratic security issues regarding the security of md5 for password protection. It also keeps the people wearing tinfoil hats happy too.

authconfig --passalgo=sha512 --update

Umask restrictions

Modifying the default umask can make things interesting. A umask of 077 is recommended for security, but can tend to be a pain for users who regularly share files. Be careful if you decide to implement this, and listen to your users.

perl -npe 's/umask\s+0\d2/umask 077/g' -i /etc/bashrc
perl -npe 's/umask\s+0\d2/umask 077/g' -i /etc/csh.cshrc

Now is where things get a little more tricky. If a user fails to enter the correct credentials, pam_tally2 will deny access until the unlock_time is reached. Translated, if you fail to log in properly 3 times you will have to wait certain period of time before you can try again.

Pam modifications

And now we need to update /etc/pam.d/system-auth

touch /var/log/tallylog
cat << 'EOF' > /etc/pam.d/system-auth
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth        required
auth        sufficient nullok try_first_pass
auth        requisite uid >= 500 quiet
auth        required
auth        required deny=3 onerr=fail unlock_time=60

account     required
account     sufficient uid < 500 quiet
account     required
account     required per_user

password    requisite try_first_pass retry=3 minlen=9 lcredit=-2 ucredit=-2 dcredit=-2 ocredit=-2
password    sufficient sha512 shadow nullok try_first_pass use_authtok remember=10
password    required

session     optional revoke
session     required
session     [success=1 default=ignore] service in crond quiet use_uid
session     required

The file /var/log/tallylog is a binary log containing failed login records for pam. You can see the failed attempts by running the pam_tally2 command without any options, and unlock user accounts early by using pam_tally2 --reset -u username

Reaping idle users

Now that we've restricted the login options for the server, lets kick off all the idle folks. To do this, we're going to use a bash variable in /etc/profile. There are some reasonably trivial ways around this of course, but it's all about layering the security.

echo "Idle users will be removed after 15 minutes"
echo "readonly TMOUT=900" >> /etc/profile.d/
echo "readonly HISTFILE" >> /etc/profile.d/
chmod +x /etc/profile.d/

Restricting cron and at

In some cases, administrators may want the root user or other trusted users to be able to run cronjobs or timed scripts with at. In order to lock these down, you will need to create a cron.deny and at.deny file inside /etc with the names of all blocked users. An easy way to do this is to parse /etc/passwd. The script below will do this for you.

echo "Locking down Cron"
touch /etc/cron.allow
chmod 600 /etc/cron.allow
awk -F: '{print $1}' /etc/passwd | grep -v root > /etc/cron.deny
echo "Locking down AT"
touch /etc/at.allow
chmod 600 /etc/at.allow
awk -F: '{print $1}' /etc/passwd | grep -v root > /etc/at.deny

Network Security

Now that we have the basics for the OS protected, it's time to take a look at the basic network functionality. We're not interested in too many services here. We're simply looking at the interfaces themselves and ssh for management purposes.

Kernel Network Security

There are a number of methods to improving network security with just a few modifications, and some module blacklisting.

Wireless has to go

Since we're looking at server security, wireless shouldn't really be an issue. If you need a wireless network, you can skip this step, because we're about to disable all the wireless drivers. You could go through the contents of /lib/modules for your current kernel and remove all the wireless drivers. This will certainly disable wireless, however it's not a permanent solution. The next time you upgrade the kernel, they'll be right back, and you'll be doing this all over again. Instead, a simple loop can be used to disable them via a blacklist file in /etc/modprobe.d

for i in $(find /lib/modules/`uname -r`/kernel/drivers/net/wireless -name "*.ko" -type f) ; do echo blacklist $i >> /etc/modprobe.d/blacklist-wireless ; done

Sysctl Security

Next we need to have a look inside /etc/sysctl.conf and make some basic changes. If these lines exist, modify them to match below. If they don't exist, simply add them in. If you have multiple network interfaces on the server, some of these may cause issues. Test these before you put them into production. If you want to know more about any of these options, install the kernel-doc package, and look in Documentation/networking/ip-sysctl.txt

net.ipv4.ip_forward = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.tcp_max_syn_backlog = 1280
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_timestamps = 0

Using TCP Wrappers

TCP wrappers can provide a quick and easy method for controlling access to applications linked to them. Examples of TCP Wrapper aware applications are sshd, and portmap. A restrictive example is below. This example blocks everything but ssh.

echo "ALL:ALL" >> /etc/hosts.deny
echo "sshd:ALL" >> /etc/hosts.allow

Beefing up IPTables

The default iptables ruleset in CentOS is a little too lenient. The policy defaults are to allow traffic, there are open ports, and no real accountability for the traffic. We can do a better job.

Open up /etc/sysconfig/iptables in a text editor, and lets have a look. In the first 3 lines, there are already two problems. The INPUT and FORWARD tables are set to accept everything. Further down we see that ports, 50, 51, 5353, 631 and 22 are open. Now port 22 I don't have a problem with. The rest of them need to go, unless you want mDNS, cups, and ipsec talking to the outside world. I generally don't like strangers using my printer.

There's also no real logging of any malicious scanning or other unsavory behavior. A stronger ruleset might look like this:

#Drop anything we aren't explicitly allowing. All outbound traffic is okay
:RH-Firewall-1-INPUT - [0:0]
-A INPUT -j RH-Firewall-1-INPUT
-A FORWARD -j RH-Firewall-1-INPUT
-A RH-Firewall-1-INPUT -i lo -j ACCEPT
-A RH-Firewall-1-INPUT -p icmp --icmp-type echo-reply -j ACCEPT
-A RH-Firewall-1-INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
-A RH-Firewall-1-INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
# Accept Pings
-A RH-Firewall-1-INPUT -p icmp --icmp-type echo-request -j ACCEPT
# Log anything on eth0 claiming it's from a local or non-routable network
# If you're using one of these local networks, remove it from the list below
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP SPOOF A: "
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP SPOOF B: "
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP SPOOF C: "
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP MULTICAST D: "
-A INPUT -i eth0 -s -j LOG --log-prefix "IP DROP SPOOF E: "
-A INPUT -i eth0 -d -j LOG --log-prefix "IP DROP LOOPBACK: "
# Accept any established connections
-A RH-Firewall-1-INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Accept ssh traffic. Restrict this to known ips if possible.
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT
#Log and drop everything else
-A RH-Firewall-1-INPUT -j LOG
-A RH-Firewall-1-INPUT -j DROP

Now arguably since we're responding to pings, dropping the traffic instead of rejecting it isn't fooling anyone. It's really personal preference. If you would rather reject the traffic, you could change the last line before COMMIT to read this way instead:

-A RH-Firewall-1-INPUT -j REJECT --reject-with icmp-host-prohibited

Tamper Resistance

Since you've put a fair amount of your time into hardening the system and configuring it to do your bidding, it's generally a good idea to make sure no one comes along behind you to mess with it. This is just as valid for shops with multiple administrators as it is for keeping an eye on the bad guys. There are two very good tools built into CentOS for guarding your system. The first of these is aide. Aide is somewhat like tripwire, and can be configured to periodically check your system against a hash of key files for modifications. The second is auditd. The audit subsystem in CentOS will watch your system in real time, based on rules that you set. It will log anything and everything you tell it to, and probably more.

If it's at all possible, you should consider having a central log collection server on a non-public network interface. It's much harder for a malicious user to cover their tracks if the logs are being sent to a remote system they can't access.


Rather than re-invent the wheel telling you how to configure aide, please see instead. However we suggest to replace the crontab entry from that site with this one.

00 01 * * * /usr/sbin/aide --check | mail -s 'Daily Check by AIDE' root

Even if the site mentions CentOS 7, you can use the same instructions for previous CentOS releases.


To Be Written

HowTos/OS Protection (last edited 2016-02-02 15:38:17 by carltm)