Let’s Cook 'Compliance as Code' with Chef InSpec

Introduction

The concept of DevSecOps has introduced an array of changes to our traditional operations. One of the major changes was to move away from using tools, to learning to bake our own ‘code’. Of the many things required for an application or an environment to be production-ready, compliance is fundamental and we ought to look at 'Compliance as a code'.

'Compliance as Code' can be defined as writing a script/code to automate the process of auditing infrastructure for security compliance and ensuring that the security baseline standards are met in every release made. With the increasing adoption of DevSecOps, 'Compliance as Code' has become an imperative component of the modern-day CI/CD pipelines to validate the compliance of the DevOps infrastructure.

Talking of DevOps, here is our blog on achieving DevSecOps using open source tools which you can have a look at - Achieving DevSecOps with Open-Source Tools

In this blogpost, Jovin Lobo will introduce Chef InSpec - an open source framework for testing and auditing infrastructure and applications.

For the purpose of this blog, compliance checks will be performed on a Redis server using Chef InSpec scripts. The blog will walk you through the process of constructing a Redis InSpec profile using the Chef InSpec framework.

Please note, the compliance checks that we have selected are some of the most widely used best practices checks. This is not an exhaustive list. The Chef InSpec script that we will create is built to demonstrate Chef InSpec capabilities and should not be considered as a ‘de-facto’ for benchmarking your production Redis servers.

Towards the end of the blog, we will analyze the results from the Chef InSpec report and identify checks that have either passed or failed.

Installing Chef InSpec

Linux Command Line  : Download and Install Chef InSpec via Curl

curl https://omnitruck.chef.io/install.sh | sudo bash -s -- -P InSpec

Windows PowerShell  Download and Installation of Chef InSpec via PowerShell:

. { iwr -useb https://omnitruck.chef.io/install.ps1 } | iex; install -project InSpec

Docker Installation:

docker pull chef/InSpec
function InSpec { docker run -it --rm -v $(pwd):/share chef/InSpec "$@"; }

Refer to the official docs https://docs.chef.io/InSpec/install/ for guidelines on installing or uninstalling Chef InSpec.

Chef InSpec Command Line

Some of the frequently used Chef InSpec command line options  :

inspec help

This command is used to list the InSpec CLI help page.

inspec version

This command is used to print InSpec’s version information.

inspec detect

This command is used to detect the target OS.

inspec shell

This command is used to open the interactive InSpec shell.

inspec exec /path/of/check.rb

This command is used to run a control named ‘check.rb’ on the local system.

Note: ‘check.rb’ is a custom control (consisting of one or more tests) written using the Chef InSpec framework, in the next section below in this blog, we will have a look at how we define these controls/checks and later run them using InSpec.

inspec exec /path/of/check.rb -t ssh://user@target-host -i /path/to/key

This command is used to run the control ‘check.rb’ via SSH to a remote target.

inspec exec /path/of/check.rb -t docker://container_ID

This command is used to run the control ‘check.rb’  on a target Docker container.

In the latter part of this blog, we will run compliance checks on a target Redis Docker container.

Defining Controls in Chef InSpec

The InSpec control is defined in a Ruby file, for example suppose ‘control_01.rb’ file. We define the ‘control’ with a control name and then define one or more tests using InSpec code within the control block.

control_01.rb definition :

control  'control-name'  do
   <... Code …>
end

The code block consists of one or multiple ‘describe’ blocks, and each ‘describe’ block along with a corresponding resource name comprises various checks which we need to run against that specific resource.

As seen in the example below the ‘describe’ block consists of a check written in InSpec code that would either yield the result to True or False, thereby passing or failing the compliance check respectively.There can be multiple such describe blocks to add multiple checks for a control.

Example describe block :

describe file('/etc/redis.conf') do
   its('content') { should match 'bind 127.0.0.1' }
end

In our example here, we are reading the Redis configuration file located at ‘/etc/redis.conf’ via InSpec’s resource ‘file’ and checking the content of the config file for values ‘bind 127.0.0.1’

Though the control block can be run all by itself but it is a good practice to include some control metadata. The metadata includes values such as impact, title, description and in certain cases tags.
The complete control block would appear to look like :

control 'redis-02' do
   impact 7.5
   title 'Check if redis is bound to localhost'
   desc 'The Redis server must be bound to localhost. The redis configuration should not allow connections from other remote connections '
   describe file('/etc/redis.conf') do
       its('content') { should match 'bind 127.0.0.1' }
   end
end

The Compliance Requirement

Below we have a sample compliance requirement for auditing a Redis server. Redis is a popular in-memory caching server which stores data in a key-value format. The below checks should not be considered as a ‘de-facto’ standard to audit Redis servers.

Use Case - Compliance Requirement:

 Audit a Redis server running in docker container and check if it is compliant against the following checks :

  1.  The Redis Configuration file should be owned by user ‘redis_admin’ and only the ‘redis_admin’ user should have ‘write’ privileges.
  2. The Redis server should only bind to the localhost.
  3. The Redis server should not run on the default port (6379).
  4. The Redis server should have authentication enabled and if so, the password must contain at least one letter, at least one number, and be longer than18 characters.
  5. The Redis server must disable dangerous commands.

Setting-up the Redis Docker Container

Clone the Git repository https://github.com/NotSoSecure/InSpec-redis and navigate to the directory ‘redis-lab’

cd redis-lab

This folder contains a Dockerfile and a custom ‘redis.conf’ file. A docker image is built using this Dockerfile that copies the ‘redis.conf’ file that is eventually used to run the Redis Server as our target.

Build a docker image using the below command. Enlist all the docker images, you should see the newly created docker image.

sudo docker build --tag redis-nss:1.0 .
sudo docker images

Identify the newly created image, and make a note of the Image ID.

Run the docker container with the above image.

sudo docker run 4cdd1bbc9f63

Run 'sudo docker ps'  to identify the container ID:

Make a  note of the aforementioned 'Container_ID', this will be used subsequently to specify the target for InSpec.

Constructing the Redis Baseline Profile

The next step of the process is to write InSpec controls for each of the aforementioned compliance checks. We will define each control with a unique name and corresponding metadata followed by the code block.

1. Requirement : The Redis Configuration file should be owned by user ‘redis_admin’ and only the ‘redis_admin’ user should have write privileges.

We will define a control named 'redis-01' along with its metadata and control code block.

The Redis configuration file is located at '/usr/local/etc/redis/redis.conf' on the target machine. We will be using InSpec’s file resource to locate the config file and the resource properties owner, group & mode to identify the file owner, group and file permissions. The universal matchers eq and cmp are used to compare the resource values to the compliance requirements.

The first control - control_01.rb is shown below:

control 'redis-01' do
    impact 7.0
    title 'Check redis.conf file ownership & permissions'
    desc 'The redis configuration file should be owned by redis_admin, and only redis_admin should have write access while others should have read-only access. '
    describe file('/usr/local/etc/redis/redis.conf') do
        its('owner') { should eq "redis_admin" }
        its('group') { should eq "redis_admin" }
        its('mode') { should cmp '0600' }
    end
end

2. Requirement : The Redis server should only bind to the localhost.

We define the second control named redis-02 and use the file property content of the file  resource and match the bind compliance requirement.

The second control -control_02.rb is shown below :

control 'redis-02' do
    impact 7.5
    title 'Check if redis is bound to localhost'
    desc 'The Redis server must be bound to localhost. The redis configuration should not allow connections from other remote connections '
    describe file('/usr/local/etc/redis/redis.conf') do
        its('content') { should match 'bind 127.0.0.1' }
    end   
end

3. Requirement : The Redis server should not run on the default port (6379)

 We define the third control named redis-03 to check the Redis Configuration file for default port 6379 in use. We use the regular expression defined within the %r{...}  to identify the use of default ports. The check should_not match the regular expressions ‘  ^port\ 6379 ‘ or  ‘ ^port\ 0 ‘ i.e the Redis server should not be configured to run the service on port 6379 or the default port (represented by port 0) either of which mean the same.
We will also define another test using the port resource of InSpec to determine if there is any port listening on 6379. It is pretty much self-explanatory that the port 6379 should_not be_listening

The third control - control_03.rb is shown below :

 control 'redis-03' do
    impact 6.5
    title 'Check if Redis Server is using the default port 6379'
    desc 'The Redis server must be configured such that it does not run on the default port 6379. '
    describe file('/usr/local/etc/redis/redis.conf') do
        its('content') { should_not match(%r{^port\ 6379}) }
        its('content') { should_not match(%r{^port\ 0}) }
        end
    describe port(6379) do
    it { should_not be_listening }
    end
end

4. Requirement: The Redis server should have authentication enabled and if so, the password must contain at least one letter, at least one number, and be longer than 18 characters.        

We define the fourth control named redis-04 to check firstly if the Redis configuration has authentication enabled, and secondly we obtain the password defined below and check its compliance against the aforementioned password policy.

We use the file property content of the file resource and match it with a regular expression %r{^requirepass}  to check if authentication is enabled.
We then use the command  resource to fire up the Linux command

 cat /usr/local/etc/redis/redis.conf | grep  "^requirepass" | awk \'{print $2}\'

to obtain the defined password and match it with the regular expression

%r{^(?=.*[0-9]+.*)(?=.*[a-zA-Z]+.*)[0-9a-zA-Z]\{18,\}$}

to check the compliance of the password policy.

Note: The regular expression for the policy has been taken from  https://www.regexlib.com/

The fourth control -control_04.rb is shown below :

control 'redis-04' do
    impact 8.2
    title 'Check if Redis authentication is enabled and if so, check for the password complexity '
    desc 'Redis server must be configured for authenticated access. The defined password must contain at least one letter, at least one number, and be longer than 18 characters.'
    describe file('/usr/local/etc/redis/redis.conf') do
        its('content') { should match(%r{^requirepass}) }
    end
    describe command('cat /usr/local/etc/redis/redis.conf | grep  "^requirepass" | awk \'{print $2}\'') do
        its('stdout') { should match(%r{^(?=.*[0-9]+.*)(?=.*[a-zA-Z]+.*)[0-9a-zA-Z]\{18,\}$}) }
    end
 end

5. Requirement: The Redis server must disable dangerous commands.

There are several Redis commands that are considered to be dangerous in production environments. We need to write a compliance check to make sure these dangerous commands are  disabled.

The following commands need to be disabled for compliance:
"BGREWRITEAOF", "BGSAVE", "CONFIG", "DEBUG", "DEL", "FLUSHALL", "FLUSHDB", "KEYS", "PEXPIRE", "RENAME", "SAVE", "SHUTDOWN", "SPOP", "SREM"
The configuration file must rename the command to “” (blank) in order to disable it or rename it to some arbitrary value to prevent attackers from guessing the default command names.
Eg: rename-command CONFIG  "" should disable the CONFIG command in Redis.

We begin by defining the final control named redis-05 and define an array of all the dangerous methods as:

cmds = ["BGREWRITEAOF", "BGSAVE", "CONFIG", "DEBUG", "DEL", "FLUSHALL", "FLUSHDB", "KEYS", "PEXPIRE", "RENAME", "SAVE", "SHUTDOWN", "SPOP", "SREM"]

We then use a cmds.each do  loop to match  each of the commands and check if a corresponding entry is present in the ‘redis.conf’ to specifically disable these dangerous commands.

The final control control_05.rb is shown below :

control 'redis-05' do
    impact 6.7
    title 'Check if dangerous commands are disabled'
    desc ' The redis server should be configured to disable all dangerous commands i.e BGREWRITEAOF, BGSAVE, CONFIG, DEBUG, DEL, FLUSHALL, FLUSHDB, KEYS, PEXPIRE, RENAME, SAVE, SHUTDOWN, SPOP, SREM '
    cmds = ["BGREWRITEAOF", "BGSAVE", "CONFIG", "DEBUG", "DEL", "FLUSHALL", "FLUSHDB", "KEYS", "PEXPIRE", "RENAME", "SAVE", "SHUTDOWN", "SPOP", "SREM"]
    cmds.each do |cmd|
        describe file('/usr/local/etc/redis/redis.conf') do
            its('content') { should match "rename-command #{cmd} \"\"" }
        end
    end    
end

Structuring the Redis Profile

Now that we have all the controls defined for each of our compliance requirements, we move to the next step of structuring the Redis InSpec profile.

First we create a directory with some name for a profile (eg: InSpec-redis ) and then we will create a directory inside ‘InSpec-redis/’ named ‘controls’ and add all the controls defined earlier in it.
We will also create a 
README.md file and file named InSpec.yml with some information about the InSpec profile.

The Chef InSpec  profile directory structure should look like :


Running the Redis InSpec Profile

Finally fire the below InSpec command to run the InSpec-redis profile on the docker container.
(Replace container_ID with the redis docker container ID.)

inspec exec InSpec-redis/ -t docker://container_ID

Command Line Output of the InSpec Profile in Action:

https://asciinema.org/a/368755

Results:

We have got a total of 2 checks defined in controls 1 and 4 that have failed our compliance checks.

In this case the Redis server failed two compliance checks -

  1. The Redis configuration file permissions test from the first control.
    Here the file permissions are expected to match ‘600’ i.e only read-write to the redis_admin user, but instead the permissions returned are ‘644’ i.e read-write to redis_admin & read permissions to ‘group’ and ‘others’.
  2. The Redis server has enabled authentication, however it failed the password complexity check defined in the fourth control.

We would leave it upto you to change the file permissions and the password complexity such that it passes all our defined compliance checks. You can then re-run InSpec and check the results.

Git Location of InSpec Redis Profile

https://github.com/NotSoSecure/InSpec-redis 

Recommended Best Practice

In our example above we have used the command resource to fire some Linux commands to cover some checks.
For instance, to check if a target is listening on port 53 we could use the command resource :

describe command('netstat -tulpn | grep LISTEN | grep -o 53 | sort -u') do
    its('stdout') { should eq "53" }
end

The above check can be also written using InSpec’s native port resource. This approach is platform-independent and does not require the target machine to have special tools such as netstat installed..

describe port(53) do
    it { should be_listening }
end

Though the former approach works, we would encourage you to explore https://docs.chef.io/InSpec/resources/ and utilize InSpec’s native resources to build platform independent scripts.

Conclusion

The DevSec Project (https://github.com/dev-sec) has several CIS Bechmarks that have been
written as InSpec code. You can refer to these baselines and tweak the controls for your organization as per your compliance requirements.

The Chef InSpec framework provides us with a lot of control to define compliance checks. This can be leveraged by organizations to write complex compliance checks using InSpec code with a lot of ease and to achieve automation for compliance audits.

Mrketing

If you need help with building InSpec profiles we offer this as part of our consultancy services. We also offer in depth training on getting started with DevSecOps and cover InSpec configurations as part of it. To know more about training please visit Devsecops/

Reach out to us if you have a specific set of requirements against which you wish to build an InSpec profile. We will be happy to work on creating them for you :-)

References

Chef InSpec official Documentation : https://docs.chef.io/InSpec/

Redis Security Configuration : https://redis.io/topics/security

DigitalOcean Blog on Securing Redis : https://www.digitalocean.com/community/tutorials/how-to-secure-your-redis-installation-on-ubuntu-18-04

Regular Expressions: https://www.regexlib.com/