Integration and Compliance with InSpec
Introduction
Chef InSpec is an open-source framework for testing and auditing your applications and infrastructure. Chef InSpec works by comparing the actual state of your system with the desired state that you express in easy-to-read and easy-to-write Chef InSpec code. Chef InSpec detects violations and displays findings in the form of a report, but puts you in control of remediation. [1]
After we got past that well marketed description on what InSpec is, let us be more specific on what you can do with it.
When developing Chef Cookbooks you usually have a default InSpec test file created for you which is also defined in the kitchen.yml manifest, i.e:
t0rrant@testing:~$ chef generate cookbook mycookbook
....
Your cookbook is ready. Type `cd mycookbook` to enter it.
....
Why not start by writing an InSpec test? Tests for the default recipe are stored at:
test/integration/default/default_test.rb
...
t0rrant@testing:~$ cd mycookbook/
|
|
|
|
So you can see that it is fairly simple to start smashing keys and add more tests for your cookbook.
Now imagine that besides that specific cookbook you have a running system, in production, with some of the same integration requirements. Let us assume the following requirement:
- the machine should be serving MySQL from port 3306
according to those requirements we could define some rules for the system:
For the sake of simplicity, we assume the target system is running a debian-based Linux distribution
- the package mariadb-server should be installed
- the user mysql must exist
- the mysqld process should be listening on port 3306
This can be easily described as:
|
|
The result of having that evaluated successfully should be something like:
Package mariadb-server
✔ is expected to be installed
User mysql
✔ is expected to exist
Port 3306
✔ is expected to be listening
✔ processes is expected to include "mysqld"
Cool, but wait... what are we actually testing here? Configuration, not functionality. These tests do not tell us if the MySQL server is actually working or that the SSH server is properly accepting Public Keys and not Passwords.
This shows that the system is compliant for our specific rules, but what we probably want is also a way to test for functionality.
As a first step let us organize these tests and create a profile for them. Let us call it database.
Creating InSpec Profiles
Chef InSpec supports the creation of complex test and compliance profiles, which organize controls to support dependency management and code reuse. Each profile is a standalone structure with its own distribution and execution flow. [2]
Here we will only cover the basic aspects of InSpec profiles, you should check the docs [2] for more information.
Profile Structure
A profile can be seen as a directory with the following structure:
t0rrant@testing:~/inspec/profile$ tree
.
├── controls
│ ├── control1.rb
│ └── control2.rb
├── files
│ └── example_input.yml
├── inspec.yml
└── README.md
The README.md file should contain information about the profile, I usually leave some examples of a successful execution on the profile.
The inspec.yml file has general information about the profile, including inputs (we will get to that later), the profile name, supported platforms, the profile's version, among others.
The controls directory contains the InSpec tests which will be applied when executing the profile.
The files directory can contain additional files, accessible by the profile, or just arbitrary files useful outside of the testing scope, and it is optional.
We can also have a libraries directory in which we can have InSpec resources extensions and helper functions, and is also optional.
Database Profile
Let us create the previously mentioned structure with two control files (one for compliance and another for integration testing), the main inspec.yml file describing our profile and an input.yml (which we will use to pass custom values to variables we will be using in the integration testing).
t0rrant@testing:~/database$ tree
.
├── controls
│ ├── compliance.rb
│ └── integration.rb
├── files
│ └── input.yml
├── inspec.yml
└── README.md
First the inspec.yml file:
|
|
Looks good, we defined four input variables so that we can test different settings for the database connection. And also we made the db_user and db_password mandatory in order for any control that depends on either of them fails if they are not defined.
Next let us create the tests for compliance in controls/compliance.rb:
|
|
As you can see we just did the same as in the introduction, plus we also check for the main config file's permissions.
Finally, we will create the tests for integration in controls/integration.rb:
|
|
Here we are testing if the connection works, by executing a common query to list the existing database, which should not fail if we can authenticate successfully.
Now let us test this database profile:
t0rrant@testing:~/database$ inspec exec . --log-level=error
Profile: Basic Database (database)
Version: 1.0.0
Target: local://
✔ mariadb-compliance: Check for proper configuration
✔ System Package mariadb-server is expected to be installed
✔ User mysql is expected to exist
✔ File /etc/my.cnf is expected not to be more permissive than "644"
✔ Port 3306 is expected to be listening
✔ Port 3306 processes is expected to include "mysqld"
× mariadb-integration: Checks for proper DB connection
× Control Source Code Error ./controls/integration.rb:1
Input 'db_user' is required and does not have a value.
Profile Summary: 1 successful control, 1 control failure, 0 controls skipped
Test Summary: 4 successful, 1 failure, 0 skipped
As we can see the compliance control seems ok, however we can't run the integration tests because we did not define neither db_user or db_password. So, let us create the file files/input.yml and define them there:
|
|
t0rrant@testing:~/database$ inspec exec . --input-file=files/input.yml --log-level=error
Profile: Basic Database (database)
Version: 1.0.0
Target: local://
✔ mariadb-compliance: Check for proper configuration
✔ System Package mariadb-server is expected to be installed
✔ User mysql is expected to exist
✔ File /etc/my.cnf is expected not to be more permissive than "644"
✔ Port 3306 is expected to be listening
✔ Port 3306 processes is expected to include "mysqld"
✔ mariadb-integration: Checks for proper DB connection
✔ Command: `mysql -uavaliduser -pa6v8a8l2li0dp01as0s0w6r0d -h 127.0.0.1 --port 3306 -s -e "show databases;"` exit_status is expected to eq 0
Profile Summary: 2 successful controls, 0 control failures, 0 controls skipped
Test Summary: 5 successful, 0 failures, 0 skipped
Looks ok, although we may not want the output showing the whole command, including the user and password used to access the database, so let us modify the integration test:
|
|
Here we refer to RSpec syntax and use the subject block to define our test and the expect method for the assertion, while using the it block to validate the subject and customize the output through the title. Let us run the profile again:
t0rrant@testing:~/database$ inspec exec . --input-file=files/example_input.yml --log-level=error
Profile: Basic Database (database)
Version: 1.0.0
Target: local://
✔ mariadb-compliance: Check for proper configuration
✔ System Package mariadb-server is expected to be installed
✔ User mysql is expected to exist
✔ File /etc/my.cnf is expected not to be more permissive than "644"
✔ Port 3306 is expected to be listening
✔ Port 3306 processes is expected to include "mysqld"
✔ mariadb-integration: Checks for proper DB connection
✔ A simple SQL command should execute successfully
Profile Summary: 2 successful controls, 0 control failures, 0 controls skipped
Test Summary: 6 successful, 0 failures, 0 skipped
Nice, now we don't see anything sensitive and can safely show the output of the test.
Conclusion
In this post we saw how to create InSpec tests, and that we are able to test both configuration settings, and functionality using InSpec. We also saw how to customize a test's output so we can hide sensitive information.
The files described in this post are available here
Feel free to leave comments below, any improvement to this and other articles is always welcome.
Thanks for reading!
Tags: linux devops chef inspec compliance integration testing
Related Content
- Linux Networking Introduction
- Hacking diskimage-builder for Fun and Profit
- Virtualenvwrapper Installation and Usage
- Creating Custom Resources for Your Cookbooks
- Hitchhiker's Guide to Chef