Chef InSpec defines security and compliance rules as executable code for testing and auditing applications and infrastructure. Chef InSpec compares the actual state of a system with the desired state that is expressed in easy-to-read and easy-to-write code.
As more and more diverse technologies are deployed across data centers, hybrid, or cloud estates, validating compliance organization-wide has become increasingly difficult. When defined with Chef InSpec, compliance, security, and other policy requirements become automated tests that can be run against traditional servers, containers, and cloud APIs, ensuring standards are enforced consistently in every managed environment.
When working with Chef InSpec, you create profiles – sets of controls, or tests, that you wish to run against a system or infrastructure to determine if it is in compliance. For example, you might require that Windows passwords include a number and a symbol. Many powerful profiles are included with Chef Automate, or you can write your own for custom compliance needs.
Often your profiles are straightforward and don’t need to change over time. However, sometimes you need more flexibility, as if you could add “dials” to your profiles to make them behave differently in different settings. That’s where Chef InSpec Inputs come in: Inputs are parameters you can pass into profiles to control their behavior and make settings changes. As a profile consumer, you can “turn the dial” using a variety of mechanisms to provide Input values. As a profile author, there are many things you can do when reading the Input.
Below are some examples of how you can use inputs to expand your use of Chef InSpec. Think of these examples as inspiration for leveraging this powerful tool in your continuous compliance journey.
1. Keep your secrets apart from your profiles
Suppose your organization stores its data in a database. One of your Chef InSpec profiles queries your database to check an audit log for violations:
# my-profile/controls/audit-db.rb
dbh = postgres_session('inspec', 'p@ssw0rd', 'db1.acme.biz')
describe dbh.query('SELECT * FROM audit_log WHERE type='infraction';') do
its('output') { should eq '' }
end
This profile will integrate the database check into your existing Chef InSpec compliance workflow. Now, you go to check the code into version control, but wait – there is a raw password in there, which is a big security no-no! How could we push that value out of the source file? It’s time for a Chef InSpec Input!
Let’s replace that bare password with a call to the input() DSL (Domain Specific Language) call:
dbh = postgres_session('inspec', input('audit_db_password'), 'db1.acme.biz')
The value may come from a number of sources – including the command line:
inspec exec my-profile --input audit_db_password='passw@rd'
(The `–input` option is new as of Chef InSpec 4.16.0) This approach of using Chef InSpec Inputs allows you to keep the profile itself under one set of version control – edited and managed by one set of people, primarily security developers – while controlling access to the actual password used. You can decide how to manage that password: you could pull it from another tool on the command line; use another InSpec Input provider, such as a YAML file; use another profile that stores the actual password value; or use an Input plugin that handles secrets management such as inspec-vault.
2. Use an Input to look for a specific software package on your Habitat installations
Suppose your organization runs most of its applications in Chef Habitat, the application automation solution. Like many security engineers, you need to be able to quickly answer whether a particular version of software is deployed at your site. Each new day brings a new vulnerability – sometimes many in one day. Here’s how you can use Chef InSpec Inputs to make this easy.
First, set up an inspec profile with metadata:
# vuln-checker/inspec.yml
name: vuln-checker
title: Looks for specific software packages
author: You
# This section loads the special Habitat resources, like habitat_package
depends:
- name: habitat
url: https://github.com/inspec/inspec-habitat/archive/master/inspec-habitat.tar.gz
inputs:
- name: vulnerable_packages
type: array
value: [] # This means "make an empty list if no values are provided"
Next, write a control file that can loop over all the names of the software:
# vuln-checker/controls/habitat-vulns.rb
control "Check for vulnerable software" do
impact 1.0
input('vulnerable_packages').each do |package_id|
describe habitat_package(package_id) do
it { should_not exist }
end
end
end
Take a moment to follow the inspec-habitat setup instructions, and verify that you are able to run the profile. Since you are not providing any input data for vulnerable_packages, Chef InSpec relies on the default value of an empty list. Thus, it doesn’t run any tests, because no vulnerabilities were provided.
$ inspec exec vuln-checker -t habitat://acme-uat
Profile: Looks for specific software packages (vuln-checker)
Version: (not specified)
Target: habitat://
No tests executed.
Now it’s time to make a real YAML file that you can add to every day.
# vulns.yaml
# Packages that have been determined to be problematic
# (examples, dummy package numbers - not really vulnerable)
vulnerable_packages:
- core/openssl/1.0.1z/20181305210149
- core/zlib/1.1.2/20171315003728
You now can run the vulnerability scanner, this time providing :
$ inspec exec vuln-checker -t habitat://acme-uat --input-file vulns.yaml
Profile: Looks for specific software packages (vuln-checker)
Version: (not specified)
Target: habitat://
✔ Check for vulnerable software: Habitat Package core/openssl/1.0.1z/20181305210149
✔ Habitat Package core/openssl/1.0.1z/20181305210149 should not exist
✔ Habitat Package core/zlib/1.1.2/20171315003728 should not exist
3. Test Your Profiles with Test Kitchen
You can use the test-kitchen tool, included with Chef Workstation, to create temporary environments that you can configure using a variety of tools and then audit with Chef InSpec. Any profile you may use in production may also be used in Test Kitchen. So, can you use Inputs to control those profiles? Of course!
For example, you might work with an application engineer who is testing a stack of profiles for their application, a webserver. When in production, the web server connects to a production API server, but during testing, they want to connect to a test API server – and verify that the connection is correct. Handling this flexibility is a perfect job for Chef InSpec Inputs.
Let’s start with a fairly straightforward Chef InSpec profile. By tradition for Test Kitchen setups, place the profile in a directory named test/integration/default/:
# test/integration/default/controls/api-check.rb
control "API host should be reachable" do
describe http("http://" + input("api_server_host")) do
its("status") { should cmp 200 }
end
end
To use Test Kitchen, create a configuration file that drives the Kitchen setup. We won’t go into the full details here – there is a lot you can do with choice of virtualization, test suites, and so on. We’ll assume typical defaults for most things, and focus on the inspec-related parts:
# kitchen.yml
# We're omitting some config here. See https://docs.chef.io/kitchen.html
# driver: # Use Vagrant or dokken or similar
# provisioner: # You'd provision the machine with Chef Infra or similar
# This enables Chef InSpec to run the tests
verifier:
name: inspec
# You can test multiple platforms in parallel
platforms:
- ubuntu-18.04
# Each
suites:
- name: default
verifier:
inspec_tests:
- test/integration/default # This is the path to your test profile
inputs:
# Here is where you can inject input values
api_server_host: api-01.uat.mycorp.lax
With an input value in place for the development environment, you can now run your test build using kitchen verify.
Advanced Feature: Building on this, you may realize that you need to pass the same value – the hostname of the API server – to both the provisioner and the verifier. One way to handle this is to take advantage of Test Kitchen’s ERB interpolation feature, which gives you one more layer of parameterization. If you place your variables in OS environment variables, you can then read them repeatedly in the Kitchenfile, like this:
# kitchen.yml - with ERB ENV interpolation
# Much is omitted here
suites:
- name: default
attributes: # This is a hash of Chef Infra Attributes
my_app:
# The <%= %> causes Kitchen to inject the evaluated value
api_server_host: <%= ENV["API_SERVER_HOSTNAME"] %>
verifier:
inspec_tests:
- test/integration/default # This is the path to your test profile
inputs:
api_server_host: <%= ENV["API_SERVER_HOSTNAME"] %>
By relying on ENV variables, you can make one setting and drive both your provisioner (such as Chef Infra) and your verifier (Chef InSpec). However, by having your provisioner and your auditor share one source of truth, you make it impossible to detect some kinds of errors (for example, a typo in the hostname for the API server could be entered by hand, provisioned, then the auditor would faithfully check that it was “correctly” set to the wrong value). Use sparingly.
4. Share Profiles and Validate Inputs
The configuration that the application team uses requires several layers of configuration – a base operating system configuration, then a layer of security hardening, some monitoring and control configuration, then the application itself. With Chef InSpec, these configurations are often validated independently by “stacking” dedicated profiles together. One base profile know how to audit the base OS configuration; another profile will audit the security configuration, another will audit the monitoring setup, etc. Each layer builds upon the next. Crucially, different teams within the organization may own these profiles. For example, a security engineer may author the security profile, while the app team writes the application profile to audit that their application is being deployed correctly.
What happens when layers profiles need to share information? For example, suppose that the organization requires that the machine forward root’s email to a real address, and that it be someone on the application team. While we can’t validate that the address is real (or that someone will read the mail), the security layer can define a control which is based on a validated input. A validated input checks its value and fails the resource if its value is not valid, according to its validation rules.
Write a new profile, security, with a control file:
# security/controls/email-check.rb
control "email forwarding for root" do
# See also postfix_conf for more sophistication
describe file("/root/.forward") do
it { should exist }
its("contents") { should include input("root_email_forward") }
end
end
And in the metadata file add a special validation clause:
# security/inspec.yml
name: security
# Many common things omitted here
inputs:
root_email_forward:
# Next two fields enforce type validation on the input
type: String # String, Numeric, Regexp, Array, Hash, Boolean
required: true # A value of some kind must be provided
At this point, if you just ran the security profile without providing any value for root_email_forward,you would get a failure on the control.
Next, the app team assembles their wrapper profile:
# my-app/controls/main.rb
include_controls "security"
# my-app/inspec.yml
name: my-app
# much omitted here
depends:
- name: security
path: ../security
inputs:
root_email_forward:
value: bob@acme.com
profile: security
Now, when the application is audited using both the security and application layers, the proper value is injected and validated. If you decide you want to add new inputs which are required, you may do so – but you’ll need to coordinate with the app team.
5. You can now fetch values from HashiCorp Vault
One of Chef InSpec’s latest features is the ability to read input values from HashiCorp Vault, using the inspec-vault plugin.
To install inspec-vault, make sure you have Chef InSpec 4.16 or later, then using the terminal or PowerShell, run:
your-machine $ inspec plugin install inspec-vault
Chef InSpec will fetch the latest version of the plugin from RubyGems, and install it.
To configure your plugin to connect to vault, simply set two environment variables:
- VAULT_TOKEN – set to your authentication token. Contact your Vault administrator for instructions.
- VAULT_ADDR – set to the URL of your vault server, including the port.
Let’s revisit the first example, in which there was a profile named acme-profile that had an input named audit_db_password. In the first example, we provided the password using the –input option. But if you have a Vault server available, you can store the password there. For example, you could run:
$ vault kv put secret/inspec/acme-profile audit_db_password=passwor@d
This stores the password in vault, using the default layout of secret/inspec/<profile_name>. (You may also use custom locations; see the inspec-vault documentation for additional options.)
Each profile is its own KV document in Vault, and each input is one key.
Now that inspec-vault is installed and configured, you can simply run the original profile (this time omitting the password both from the command line as well as not embedding it in the profile). The password will be looked up in Vault without any modification to the profile:
machine $ inspec exec acme-profile
inspec exec acme-profile
PostgreSQL query: SELECT * from audit_log
✔ output should cmp == ""
The password can be updated in the database and in the secrets store (Vault) without needing to cascade the change into the Chef InSpec profiles or configuration.
Next Steps
How will you use Inputs? When writing profiles, stay aware of these “code smells” and consider them as an opportunity to use Inputs to reduce the amount of profile code you must write, while increasing the flexibility of your code.
- Have you ever duplicated a profile, just so you can allow a setting to vary?
- Do you need to author profiles for other people to use and want to enforce that they provide certain settings with certain types?
- Do you want to prevent certain secrets from being stored with the rest of the profile code?
- Do you want to go one step further and have those secrets retrieved dynamically from a secret storage system?
You can read more about Chef InSpec Inputs at the Inputs Documentation Page.