5. Writing InSpec Controls
Writing InSpec Controls
Let's work through some example requirements to write InSpec controls.
Security & Configuration Requirements
Review the requirements we're testing for:
1. NGINX should be installed as version 1.27.0 or later.
2. The following NGINX modules should be installed:
* `http_ssl`
* `stream_ssl`
* `mail_ssl`
3. The NGINX configuration file - `/etc/nginx/nginx.conf`- should exist as a file.
4. The NGINX configuration file should:
* be owned by the `root` user and group.
* not be readable, writeable, or executable by others.
5. The NGINX shell access should be restricted to admin users.
6. NGINX admins should have documentation on security procedures.
Requirement (1) - NGINX Version
The first requirement is for the NGINX version to be 1.27.0 or later
.
We can check this using the InSpec cmp
matcher.
Replace the contents of my_nginx/controls/example.rb
with this:
control 'nginx-version' do
impact 1.0
title 'NGINX version'
desc 'The required version of NGINX should be installed.'
describe nginx do
its('version') { should cmp >= '1.27.0' }
end
end
The nginx_conf
resource docs
The test has an impact of 1.0, meaning it is considered a critical issue if this test fails. A failure might indicate to the team that this issue should be resolved as soon as possible, likely by upgrading NGINX to a newer version. The test compares nginx.version
against version 1.27.0.
cmp
is one of InSpec's built-in matchers. cmp
understands version numbers and can use the operators ==, <, <=, >=, and >. cmp
compares versions by each segment, not as a string. For example, "7.4" is less than than "7.30".
Next, run inspec exec
to execute the profile on the remote target.
inspec exec my_nginx -t docker://nginx
inspec exec my_nginx -t docker://{DOCKER_CONTAINER_ID or DOCKER_CONTAINER_NAME}
Redirecting to cinc-auditor...
Profile: InSpec Profile (my_nginx)
Version: 0.1.0
Target: docker://DOCKER_CONTAINER_ID
Target ID: TARGET_ID
β nginx-version: NGINX version
β Nginx Environment version should cmp >= "1.27.0"
Profile Summary: 1 successful control, 0 control failures, 0 controls skipped
Test Summary: 1 successful, 0 failures, 0 skipped
You see that the test passes. We get back our test results and some metadata about the target we ran it against. The 'Target ID' in the output is the UUID generated by InSpec to uniquely identify the target system/node.
Requirement (2) - NGINX Modules are Installed
The second requirement verifies that our required modules are installed.
- http_ssl
- stream_ssl
- mail_ssl
Append your control file to add this describe block:
control 'nginx-modules' do
impact 1.0
title 'NGINX modules'
desc 'The required NGINX modules should be installed.'
describe nginx do
its('modules') { should include 'http_ssl' }
its('modules') { should include 'stream_ssl' }
its('modules') { should include 'mail_ssl' }
end
end
The second control resembles the first; however, this version uses multiple its
statements and the nginx.modules
method. The nginx.modules
method returns a list; the built-in include
matcher verifies whether a value belongs to a given list.
Run inspec exec
on the target.
inspec exec my_nginx -t docker://nginx
inspec exec my_nginx -t docker://{DOCKER_CONTAINER_ID or DOCKER_CONTAINER_NAME}
Redirecting to cinc-auditor...
Profile: InSpec Profile (my_nginx)
Version: 0.1.0
Target: docker://DOCKER_CONTAINER_ID
Target ID: TARGET_ID
β nginx-version: NGINX version
β Nginx Environment version should cmp >= "1.27.0"
β nginx-modules: NGINX version
β Nginx Environment modules should include "http_ssl"
β Nginx Environment modules should include "stream_ssl"
β Nginx Environment modules should include "mail_ssl"
Profile Summary: 2 successful controls, 0 control failures, 0 controls skipped
Test Summary: 4 successful, 0 failures, 0 skipped
This time, both controls pass.
Requirement (3) - The nginx_conf
file
The third requirement verifies that the NGINX configuration file - /etc/nginx/nginx.conf
- exists as a file.
Append this describe block to your control file:
control 'nginx-conf-file' do
impact 1.0
title 'NGINX configuration file'
desc 'The NGINX config file should exist as a file.'
describe file('/etc/nginx/nginx.conf') do
it { should be_file }
end
end
Run inspec exec
on the target.
inspec exec my_nginx -t docker://nginx
inspec exec my_nginx -t docker://{DOCKER_CONTAINER_ID or DOCKER_CONTAINER_NAME}
Redirecting to cinc-auditor...
Profile: InSpec Profile (my_nginx)
Version: 0.1.0
Target: docker://DOCKER_CONTAINER_ID
Target ID: TARGET_ID
β nginx-version: NGINX version
β Nginx Environment version is expected to cmp >= "1.27.0"
β nginx-modules: NGINX modules
β Nginx Environment modules is expected to include "http_ssl"
β Nginx Environment modules is expected to include "stream_ssl"
β Nginx Environment modules is expected to include "mail_ssl"
β nginx-conf-file: NGINX configuration file
β File /etc/nginx/nginx.conf is expected to be file
Profile Summary: 3 successful controls, 0 control failures, 0 controls skipped
Test Summary: 5 successful, 0 failures, 0 skipped
Requirement (4) - Permission on the nginx_conf
file
The fourth requirement verifies that the NGINX configuration file, /etc/nginx/nginx.conf
:
- is owned by the root user and group.
- is not be readable, writeable, or executable by others.
Append your control file to add this describe block:
control 'nginx-conf-perms' do
impact 1.0
title 'NGINX configuration'
desc 'The NGINX config file should owned by root, be writable only by owner, and not writeable or and readable by others.'
describe file('/etc/nginx/nginx.conf') do
it { should be_owned_by 'root' }
it { should be_grouped_into 'root' }
it { should_not be_readable.by('others') }
it { should_not be_writable.by('others') }
it { should_not be_executable.by('others') }
end
end
The first two tests use should
to verify the root
owner and group. The last three tests use should_not
to verify that the file is not readable, writable, or executable by others.
Run inspec exec
on the target.
inspec exec my_nginx -t docker://nginx
inspec exec my_nginx -t docker://{DOCKER_CONTAINER_ID or DOCKER_CONTAINER_NAME}
Redirecting to cinc-auditor...
Profile: InSpec Profile (my_nginx)
Version: 0.1.0
Target: docker://DOCKER_CONTAINER_ID
Target ID: TARGET_ID
β nginx-version: NGINX version
β Nginx Environment version is expected to cmp >= "1.27.0"
β nginx-modules: NGINX modules
β Nginx Environment modules is expected to include "http_ssl"
β Nginx Environment modules is expected to include "stream_ssl"
β Nginx Environment modules is expected to include "mail_ssl"
β nginx-conf-file: NGINX configuration file
β File /etc/nginx/nginx.conf is expected to be file
Γ nginx-conf-perms: NGINX configuration (1 failed)
β File /etc/nginx/nginx.conf is expected to be owned by "root"
β File /etc/nginx/nginx.conf is expected to be grouped into "root"
Γ File /etc/nginx/nginx.conf is expected not to be readable by others
expected File /etc/nginx/nginx.conf not to be readable by others
β File /etc/nginx/nginx.conf is expected not to be writable by others
β File /etc/nginx/nginx.conf is expected not to be executable by others
Profile Summary: 3 successful controls, 1 control failure, 0 controls skipped
Test Summary: 9 successful, 1 failure, 0 skipped
This time you see a failure. You discover that /etc/nginx/nginx.conf
is potentially readable by others. Because this control also has an impact of 1.0, your team may need to investigate further.
Requirement (5) - NGINX shell access
The next requirement checks whether NGINX shell access is provided to non-admin users. In this case, access to bash
needs to be restricted to admin users.
Append this describe block to your control file:
control 'nginx-shell-access' do
impact 1.0
title 'NGINX shell access'
desc 'The NGINX shell access should be restricted to admin users.'
describe users.shells(/bash/).usernames do
it { should be_in ['admin']}
end
end
Run inspec exec
on the target.
inspec exec my_nginx -t docker://nginx
inspec exec my_nginx -t docker://{DOCKER_CONTAINER_ID or DOCKER_CONTAINER_NAME}
Redirecting to cinc-auditor...
Profile: InSpec Profile (my_nginx)
Version: 0.1.0
Target: docker://DOCKER_CONTAINER_ID
Target ID: TARGET_ID
β nginx-version: NGINX version
β Nginx Environment version is expected to cmp >= "1.27.0"
β nginx-modules: NGINX modules
β Nginx Environment modules is expected to include "http_ssl"
β Nginx Environment modules is expected to include "stream_ssl"
β Nginx Environment modules is expected to include "mail_ssl"
β nginx-conf-file: NGINX configuration file
β File /etc/nginx/nginx.conf is expected to be file
Γ nginx-conf-perms: NGINX configuration (1 failed)
β File /etc/nginx/nginx.conf is expected to be owned by "root"
β File /etc/nginx/nginx.conf is expected to be grouped into "root"
Γ File /etc/nginx/nginx.conf is expected not to be readable by others
expected File /etc/nginx/nginx.conf not to be readable by others
β File /etc/nginx/nginx.conf is expected not to be writable by others
β File /etc/nginx/nginx.conf is expected not to be executable by others
Γ nginx-shell-access: NGINX shell access
Γ ["root"] is expected to be in "admin"
expected `["root"]` to be in the list: `["admin"]`
Diff:
["root"]
Profile Summary: 3 successful controls, 2 control failures, 0 controls skipped
Test Summary: 9 successful, 2 failures, 0 skipped
This new test also fails against our test container. It seems that what we called the "admin" account was labelled "root" on the container. InSpec thinks the test is failing even though we're really talking about the same user account. We'll fix this in the next section; let's just get our controls written out for now.
Requirement (6) - NGINX interview
The last requirement is a bit different than the rest. This one requires that the admin for the webserver should have a document on security procedures for the webserver. Put simply, it's not really possible for us to tell if there exists security documentation for our NGINX webserver by only looking at the actual webserver itself. To accomplish this test, we will have to interview an admin. There's no way to do this automatically (at least not without seriously annoying our admin!) so we will instead use an InSpec feature called a skip
message to indicate that we need to go talk to our admin.
Note that we are not just going to ignore writing an InSpec control for this requirement. We are skip
-ing it in the sense that we are not executing automated test code; we are still responsible for making sure the requirement is assessed at some point. We'll show you how to do this the SAF way in Section 12. Don't worry, we made it easy.
Append this describe block to your control file:
control 'nginx-interview' do
impact 1.0
title 'NGINX interview'
desc 'NGINX admins should have documentation on security procedures.'
describe "Manual Review" do
skip "This control must be manually reviewed."
end
end
Run inspec exec
on the target.
inspec exec my_nginx -t docker://nginx
inspec exec my_nginx -t docker://{DOCKER_CONTAINER_ID or DOCKER_CONTAINER_NAME}
Redirecting to cinc-auditor...
Profile: InSpec Profile (my_nginx)
Version: 0.1.0
Target: docker://DOCKER_CONTAINER_ID
Target ID: TARGET_ID
β nginx-version: NGINX version
β Nginx Environment version is expected to cmp >= "1.27.0"
β nginx-modules: NGINX modules
β Nginx Environment modules is expected to include "http_ssl"
β Nginx Environment modules is expected to include "stream_ssl"
β Nginx Environment modules is expected to include "mail_ssl"
β nginx-conf-file: NGINX configuration file
β File /etc/nginx/nginx.conf is expected to be file
Γ nginx-conf-perms: NGINX configuration (1 failed)
β File /etc/nginx/nginx.conf is expected to be owned by "root"
β File /etc/nginx/nginx.conf is expected to be grouped into "root"
Γ File /etc/nginx/nginx.conf is expected not to be readable by others
expected File /etc/nginx/nginx.conf not to be readable by others
β File /etc/nginx/nginx.conf is expected not to be writable by others
β File /etc/nginx/nginx.conf is expected not to be executable by others
Γ nginx-shell-access: NGINX shell access
Γ ["root"] is expected to be in "admin"
expected `["root"]` to be in the list: `["admin"]`
Diff:
["root"]
βΊ nginx-interview: NGINX interview
βΊ This control must be manually reviewed.
Profile Summary: 3 successful controls, 2 control failures, 1 control skipped
Test Summary: 9 successful, 2 failures, 1 skipped
Our output correctly indicates that nginx-interview
neither passed nor failed but is instead a skipped control.
Fixing Problems
Recall that what we're doing with InSpec falls under MITRE SAF's Validate pillar, which is where you identify where problems are with your software component, so that you can accurately assess risk and prioritize remediation actions.
For the second step, Hardening (sometimes variously referred to as implementation or remediation) you can use a configuration management tool or some other automation framework to correct compliance failures for you.
We're focused on InSpec right now, so we won't correct this issue yet. Later in this class we will discuss using Ansible to harden our test target. Another option is Chef's Secure your Infrastructure tutorial course to learn more about how to correct compliance failures using Chef.