Enforcing CIS benchmarks on Linux using Puppet

See more posts about: How-to & Use Cases and Products & Services

Enforcing CIS benchmarks on Linux using Puppet

CIS (Center for Internet Security) is a non-profit organization that aims to develop a best practice in relation to cyber security. The CIS benchmarks have been adopted by many organizations as the standard against which to measure their systems.

You can download a copy of the CIS standards for free from CIS Security; if you do, you'll see the high number of benchmarks. For example, for CentOS 7 there are 186 separate tests for the basic server benchmark. If you were to go away and start writing Puppet code to apply all those rules, your colleagues would not see you for a long time! Fortunately a lot of the work has already been done in the Puppet Forge module fervid/secure_linux_cis. We're going to take a look at how you can quickly apply the server level 1 benchmark.

What does the secure_linux_cis module actually give us?

  1. The first thing the Secure Linux CIS Module gives us is the CIS benchmarks as rule sets, so if we want to apply the server level 1 benchmark we can just use that rule set to get all the CIS rule numbers.

  2. The Secure Linux CIS Module maps each CIS rule number to a rule name via a class.

  3. The Secure Linux CIS Module provides a class that implements all those individual rule classes.

How are we going to apply the secure_linux_cis module?

We could just add the class to a node's classification and we'd get the full CIS benchmark applied, but what happens if you're running an online business? Shutting down our Squid Proxy (rule 2.2.13) on your web servers might be a bit of a disaster.

What if your developers like to access their systems via a desktop interface? If we disable X11 forwarding (rule 5.2.4) in development we could cause them a problem, but we don't want it enabled in production.

And what if the company standard for file sharing is Samba (rule 2.2.12)?

How are we going to apply the secure_linux_cis module without breaking everything?

We're going to need to apply some customizations. It might be tempting to copy the module and start customizing the code directly, but that'll give us a headache when a new version is released. We'll leave the module intact and add an abstraction layer, a profile.

The module already uses Hiera as a parameter lookup so it makes sense for us to do the same. We could put everything into our control-repo Hiera and leverage our current hierarchy, but we're going to do something different (reasons explained later). Instead, we're going to create a profile module (as opposed to a component module). We’ll call our new module cis_profile; it’s a catchy name that tells us exactly what it does. This module is going to be the location for all our Hiera data. You can only do this with version Hiera 5 or later.

The CIS module uses its own Hiera lookup to get the CIS rule set for any given operating system and passes it as a parameter to the class that implements the rules. We could copy that rule set and customize it for each application or environment, but that would give us a few problems.

  1. If we modify that rule set, we can't continue to call it a CIS rule set; it's now our own customized rule set.
  2. It's not a very layered approach. In our original example, we want a set of rules for our web server in production and a few less rules in development. We're not really taking advantage of Hiera.
  3. How do we track what rules we've removed? The security team is going to be asking not what CIS rules are we applying, but what rules we're not applying -- and more importantly, why.

fervid/secure_linux_cis helps us out here because it has exclude rules. This means we can apply the CIS benchmark to everything as defined in the module, then our implementation of the module can provide the exclude rules. We can exclude the HTTP proxy rule on our web servers and exclude the X11 rule in development. Instead of a big list of rules we're applying, we should have a very short list of rules we're not applying.

Let's take our original example of Squid, X11 and Samba. How might that look as a set of exclude rules in hiera?

First we need to configure the Hiera strategy our module is going to use. Unlike looking up an NTP server IP address, we don't want it to stop looking at the first positive result. We want to collect all the rules we want to exclude. We also don't want any duplicates so a unique merge is going to be the strategy for us; we can specify that in cis_profile/data/common.yaml. We also have that company-wide requirement to enable Samba, so we can put that in common.yaml too so everything gets that exclusion.

    merge: unique

# 2.2.12 Ensure Samba is not enabled (Scored)
# Excluded to allow company wide file share system.
  - secure_linux_cis::distribution::centos7::cis_2_2_12

Next we want to add an exclusion for X11 forwarding in development so we'll put that in cis_profile/data/environment/development.yaml.

# 5.2.4 Ensure SSH X11 forwarding is disabled (Scored)
# Enabled in development for debugging.
  - secure_linux_cis::distribution::centos7::cis_5_2_4

Finally, we want to allow HTTP proxy on our web server cis_profile/data/nodes/apache01.example.com.yaml.

# 2.2.13 Ensure HTTP Proxy Server is not enabled (Scored)
# See Security exception number:1234
  - secure_linux_cis::distribution::centos7::cis_2_2_13

We'll need to create a hierarchy that'll use facts on our machines to collect all those exclusions, starting with our common ones, the ones for our environment, and finally the ones for a specific node so our cis_profile/hiera.yaml will look something like this.

version: 5

  datadir: 'data'
  data_hash: 'yaml_data'

  - name: 'Node'
    path: "nodes/%{::trusted.certname}.yaml"

  - name: 'environment'
    path: "environment/%{::environment}.yaml"

  - name: 'common'
    path: 'common.yaml'

Finally, we're going to need a class, cis_profile/manifests/init.pp; it'll get all the exclude rules Hiera found for us and pass them into fervid/secure_linux_cis via $exclude_rules.

class cis_profile (
  Enum['firewall', 'firewalld']           $firewall_package        = 'firewalld',
  Enum['1', '2']                          $enforcement_level       = '1',
  Array[String]                           $exclude_rules           = [],
  class {'::secure_linux_cis':
    time_servers      => ['time1.google.com', 'time1.google.com'],
    profile_type      => 'server',
    allow_users       => ['centos'],
    firewall_package  => $firewall_package,
    enforcement_level => $enforcement_level,
    exclude_rules     => $exclude_rules,

Let's apply it to a node

I have a node which passes all the server_level_1 checks so I'll go in and make the following changes:

  • Enable Squid
  • Enable Samba
  • Make /etc/crontab world read/write and make it owned by the centos user.
  • Edit /etc/ssh/sshd_config and set X11Forwarding to yes

Then I'll run Puppet; here's what we get:

[root@apache01 ~]# puppet agent -t
Info: Using configured environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Retrieving locales
Info: Loading facts
Info: Caching catalog for apache01.example.com.yaml
Info: Applying configuration version 'peserver-production-b96c97c9a3b'
Notice: /Stage[main]/Secure_linux_cis::Rules::Ensure_permissions_on_etc_crontab_are_configured/File[/etc/crontab]/owner: owner changed 'centos' to 'root' (corrective)
Notice: /Stage[main]/Secure_linux_cis::Rules::Ensure_permissions_on_etc_crontab_are_configured/File[/etc/crontab]/group: group changed 'centos' to 'root' (corrective)
Notice: /Stage[main]/Secure_linux_cis::Rules::Ensure_permissions_on_etc_crontab_are_configured/File[/etc/crontab]/mode: mode changed '0777' to '0600' (corrective)
Notice: Applied catalog in 0.08 seconds

This is my development web server so we've got the exceptions above in place, we can see Samba, Squid and the sshd configuration were not touched, but there is no exception for the crontab rules so the CIS module has corrected its permissions and ownership.

So, that good reason for using a profile module...

Because of the way we've implemented the exclude rules, Hiera now only contains our exceptions. It also tells us exactly where those exceptions are, what server they are on, which environment they are in, etc. Our Hiera data has become our CIS audit document and it's not hidden amongst any other data like NTP server IP addresses or public ssh keys.

This is all related to security. A standard change control board may not be appropriate. Pulling this data out of the control-repo allows us to have a very specific set of approvers for changes. We don't even need to use Hiera, we could use a different backend such as a CMDB to provide our exclude rules. In fact, we could hand this entire profile to our security team to maintain.

Getting started

As I said at the start, there are 186 server_level_1 tests for a CentOS 7 machine. Some of those tests have dependencies; for example, the contab test requires puppetlabs/cron. You may be using a different cron module or even have written your own. You're also likely to find some things are already managed in your own baseline and that's going to give a duplicate resource declaration error. This can add up to a screen full of errors that can be overwhelming but a simple strategy to get started can help you out. Copy the entire server_level_1 test list into your exclude list. It sounds a bit excessive, but there is some logic in there.

You're now applying the CIS standard and you now have a list of all the standards you're not enforcing. It's not a bad place to be on day 1 of CIS enforcement.

Now you have your exclude list that you can work through, removing the tests you want to enforce.

Careful, there is a little gotcha

If you find yourself scratching your head after modifying the exclude list, running

puppet agent -t

and not seeing any changes, don't worry. Some of the tests eat up a not insignificant amount of CPU during the enforcement, so there is a scheduler that by default only applies the module once every hour. For testing you may want to add the following to your Hiera lookup with some more suitable values:

  period: hourly
  repeat: 2

These parameters are passed to the Resource Type: schedule.

Learn more

Puppet sites use proprietary and third-party cookies. By using our sites, you agree to our cookie policy.