Learn how to write compliance profiles with Chef InSpec using controls and describe blocks, the package, service, file, and command resources, then run inspec exec to scan nodes, including the CIS- and STIG-style profile workflow and integration with Chef Automate.

In episode 10 you already learned about Policyfiles and how Policyfile.lock.json guarantees full reproducibility for every release. You know how to define run lists and cookbooks declaratively, then push them to policy groups such as staging and production without mutating cookbooks on the server. Infrastructure can now be rebuilt identically, but nothing yet answers one important question: is the configuration that was released actually secure and compliant with the standards?
Running chef-client can ensure the desired state is achieved, but it does not guarantee that no drift occurs afterwards. A configuration file can be modified, a service can be stopped, or a port can be opened. To detect this drift, you need Chef InSpec, a code-based compliance testing engine.
This episode 11 covers how to write InSpec profiles and controls with describe blocks, the package, service, file, and command resources, running inspec exec to scan nodes, the compliance workflow with CIS- and STIG-style profiles, and integrating the results with Chef Automate.
An InSpec profile is a collection of controls that describe the security conditions a system must satisfy. A control is a single test with a unique identity and description. The profile structure is generated with the command:
inspec init profile baselineThis command creates the following structure:
baseline/
├── README.md
├── attributes/ # atribut profil (opsional)
├── controls/
│ └── example.rb # file kontrol utama
└── inspec.yml # metadata profilThe inspec.yml file is the profile metadata: name, version, maintainer, and supported platforms.
Controls are written in Ruby inside controls/. Each control has a short description, tags for tracking, and one or more describe do blocks. InSpec executes the describe do blocks and reports the results as passing or failing controls.
control 'cis-1-1' do
impact 0.7
title 'Set sudoers file permissions'
desc 'sudoers file harus dimiliki oleh root dan mode 0440'
tag cci: 'CCI-000225'
tag cis: '1.1'
describe file('/etc/sudoers') do
it { should exist }
it { should be_owned_by 'root' }
it { should be_grouped_into 'root' }
it { should be_mode 0440 }
end
endThe control above tests for the existence of the /etc/sudoers file, root ownership, and 0440 permissions. impact expresses the severity on a scale from 0 to 1, while tag links the control to external references such as the CIS Control Version.
Note
Every describe block may contain multiple it statements. InSpec evaluates them all and reports per statement. If one statement fails, the control still runs to completion so you get the full picture, rather than stopping at the first failure.
InSpec provides built-in resources for testing various aspects of a system. The four most frequently used:
describe file('/etc/ssh/sshd_config') do
it { should be_file }
it { should be_owned_by 'root' }
it { should be_mode 0644 }
its('content') { should match 'PermitRootLogin no' }
endThe command resource runs a command on the target system and tests its output, exit status, and stderr. This is most useful when there is no specific resource for the thing you want to test.
Tip
InSpec resources follow consistent naming rules. If you need another one, inspec resource list shows all available resources, and inspec help resource <name> explains the syntax. For example, the port, user, and group resources are very useful for network security audits.
To scan a system, use inspec exec with a profile and a target:
inspec exec baseline
inspec exec baseline -t ssh://deploy@203.0.113.30 -i ~/.ssh/id_ed25519The first command scans the local machine, while the second connects to a remote machine over SSH using a private key. The scan output shows the number of passing, failing, and not-applicable controls, along with details for each failing control. To make the scan exit with a failing code whenever any control does not pass, use inspec exec baseline --fail-if 'failed > 0':
inspec exec baseline --fail-if 'failed > 0' --reporter cli json:/tmp/scan.jsonThe --reporter cli json:/tmp/scan.json option writes the scan results in two formats at once: a summary to the terminal and full details in JSON. This JSON file is what gets sent to Chef Automate later.
Important
JSON output is the bridge between InSpec and other systems. CI pipelines, compliance dashboards, and Chef Automate all read this format. Always get into the habit of including a JSON reporter in every scan that will be integrated into a workflow.
Production compliance profiles usually follow public standards such as CIS Benchmarks or STIG (Security Technical Implementation Guide), and Chef provides official profiles that can be used as a starting point:
inspec supermarket profile cis-ubuntu-22.04
inspec exec cis-ubuntu-22.04 -t ssh://deploy@203.0.113.30For internal needs, inherit a standard profile and then harden it with additional controls:
name: corporate-baseline
version: 0.1.0
depends:
- name: cis-ubuntu-22.04
url: https://supermarket.chef.io/profiles/cis-ubuntu-22.04Inherited profiles are used through the include_controls tag in the main control, allowing you to add custom controls on top of the baseline standard. The recommended workflow: check compliance in staging after every policy release, and only promote to production if the staging scan passes all mandatory controls.
Warning
CIS and STIG profiles often contain controls that conflict with business needs. Manage exceptions explicitly through tag and profile attributes, rather than by deleting controls, so the audit trail stays complete and explainable.
Chef Automate is the platform that displays InSpec scan results in a compliance dashboard. The integration flow: nodes use the chef_automate resource or scan-jobs agents that run inspec exec periodically, then send the results as JSON to Automate via the data collection endpoint. Automate then shows the compliance score per node, per profile, and per control as graphs, so node health, chef-client run status, and compliance results all appear on one screen.
To send scan results directly from an InSpec profile to Automate, run the scan with the automate reporter:
inspec exec baseline \
--reporter automate \
--inspec-opt automate-url https://automate.example.com \
--inspec-opt token $AUTOMATE_TOKENMake sure the token you use is an API token with compliance scanning permissions. Once submitted, the Automate dashboard shows score trends over time, so drift that occurs after a release can be seen and acted upon.
In this episode 11 you learned how to write InSpec profiles and controls with describe blocks, use the package, service, file, and command resources to test various aspects of a system, and run scans with inspec exec both locally and remotely. You also understood the compliance workflow with CIS and STIG profiles and how to integrate scan results into Chef Automate.
Key takeaways:
it statements that evaluate resources such as file, package, service, and command.In the next episode, episode 12, we will discuss Chef Automate in full: the compliance dashboard and node visibility, node health, cookbook run status, the compliance pipeline for CI/CD, and the role of the data collection service in the Automate platform. See you there.