Audit mode is an new phase in Chef which allows you to evaluate custom rules,
defined in your recipes, on every node during each chef-client
run. Use audits
to ensure nodes fall into existing "known states" categories even before Chef
converges, and to validate your infrastructure after Chef converges.
Audits are written inside your recipes and Chef will run all the audit groups it finds in the expanded run list. You can have recipes that only include audits, or you can add audits to existing recipes.
Auditing occurs as its own phase during a full chef-client
run, running independently
of convergence. By default, audit mode always runs after chef-client
converges.
Client can be configured to only converge or only audit as well.
You can tell chef-client
to skip audits using the command line flag
--no-audit-mode
, or by adding the line audit_mode :disabled
to your config file.
To configure chef-client
to only run audits, use the command line flag
--audit-mode
, or add the line audit_mode :audit_only
to your config file.
Chef's resource reporter sends converge and audit data back to the Chef server after each client run. Two reports are generated, one for converge results and another for audit results. If there is an error in the converge phase, the audit phase, or both, then each report will contain the error details, respectively.
Errors in the converge phase will not affect the execution of the audit phase. Further, errors in the audit phase will not prevent sending reporting data from the converge phase. The phases are run independently and errors are collected and provided to the error handlers once each phase is finished.
Audits are written inside your recipe files. Audit groups define rules you
expect your infrastructure to align with. To distinguish audit groups from
other recipe resources, use controls
. Audits can only be defined within
controls
, which creates an audit group. The controls
method requires a
"name"
for the group, which serves to identify one audit group from another.
# cookbook: example
# recipe: default
controls "a thing" do
# define audits here
end
Within an audit group, you can define your audit rules. Rules are declared using
it
. Rules can be grouped together with the control
method. You can have multiple
control
statements in an audit group, and nest control
statements together.
When I include this example::default
recipe in my run list
# cookbook: example
# recipe: default
controls "MySQL" do
it "is installed" do
end
end
and I run chef-client
with audit mode enabled, I expect to see
MySQL
is installed
When I include this example::control
recipe in my run list
# cookbook: example
# recipe: control
controls "MySQL" do
control "config directory" do
it "exists with correct permissions" do
end
end
end
and I run chef-client
with audit mode enabled, I expect to see
MySQL
config directory
exists with correct permissions
When I include this example::nested_control
recipe in my run list
# cookbook: example
# recipe: nested_control
controls "MySQL" do
it "is installed" do
end
control "config directory" do
it "exists with correct permissions" do
end
control "config directory file" do
it "contains the required configuration" do
end
end
end
end
and I run chef-client
with audit mode enabled, I expect to see
MySQL
is installed
config directory
exists with correct permissions
config directory file
contains the required configuration
Audit rules are written using Serverspec types and matchers,
in conjunction with RSpec's built-in matchers.
Matchers are used with expect(..).to
for positive rules expect(..).to_not
for negative rules. The expect
is given the type, or object, you want to
assert matchers on.
(An important note when looking at the Serverspec documentation: the examples
use :should
syntax, which we do not support. Use :expect
syntax instead.)
When I include this example::mysql
recipe in my run list
# cookbook: example
# recipe: mysql
controls "mysql audit" do
control "mysql package" do
it "is installed" do
expect(package("mysql")).to be_installed.with_version("5.6")
end
end
end
and I run chef-client
with audit mode enabled, I expect to see
mysql audit
mysql package
is installed
You can also use matchers to verify that a type does not have a property with
to_not
. When I include this example::postgres
recipe in my run list
# cookbook: example
# recipe: postgres
controls "postgres audit" do
control "postgres package" do
it "is not installed" do
expect(package("postgresql")).to_not be_installed
end
end
end
and I run chef-client
with audit mode enabled, I expect to see
postgres audit
postgres package
is not installed
The service
type helps you ensure that the packages you installed are enabled
and running. When I include this example::mysql_service
recipe in my run list
# cookbook: example
# recipe: postgres
controls "mysql service audit" do
control "mysql service" do
it "is enabled" do
expect(service("mysql")).to be_enabled
end
it "is running" do
expect(service("mysql")).to be_running
end
end
end
and I run chef-client
with audit-mode enabled, I expect to see
mysql service audit
mysql service
is enabled
is running
There are many other types and matchers available for writing audit rules.
When I include this example::config
recipe in my run list
# cookbook: example
# recipe: config
controls "mysql config" do
control "mysql config file" do
let(:config_file) { file("/etc/mysql/my.cnf") }
it "exists with correct permissions" do
expect(config_file).to be_file
expect(config_file).to be_mode(0400)
end
it "contains required configuration" do
expect(its(:contents)).to match(/default-time-zone='UTC'/)
end
end
end
and I run chef-client
with audit mode enabled, I expect to see
mysql config
mysql config file
exists with correct permissions
contains required configuration
Having two audit groups with the same name is explicitly forbidden and will
raise an error. Be sure to give controls
descriptive names to avoid such
collisions.
When I include this example::duplicate
recipe in my run list:
# cookbook: example
# recipe: duplicate
controls "mysql audit" do
control "mysql package" do
it "is installed" do
expect(package("mysql")).to be_installed.with_version("5.6")
end
end
end
controls "mysql audit" do
control "mysql package" do
it "is installed" do
expect(package("mysql")).to be_installed.with_version("5.6")
end
end
end
and I run chef-client
with audit mode enabled, I expect to see output similar to
Compiling Cookbooks...
================================================================================
Recipe Compile Error in /var/chef/cache/cookbooks/tyler-mysql/recipes/default.rb
================================================================================
Chef::Exceptions::AuditControlGroupDuplicate
--------------------------------------------
Audit control group with name 'mysql audit' has already been defined
Cookbook Trace:
---------------
/var/chef/cache/cookbooks/tyler-mysql/recipes/default.rb:29:in `from_file'
Relevant File Content:
----------------------
/var/chef/cache/cookbooks/example/recipes/duplicate.rb:
22: it "is installed" do
23: expect(package("mysql")).to be_installed.with_version("5.6")
24: end
25: end
26:
27: end
28:
29>> controls "mysql audit" do
30:
31: control "mysql package" do
32: it "is installed" do
33: expect(package("mysql")).to be_installed.with_version("5.6")
34: end
35: end
36:
37: end
38:
When an audit fails, Chef will provide feedback in the logs to help you diagnose
what went wrong. Suppose you ran example::default
, but Postgres was installed
on the node. You would see output similar to
mysql audit
mysql package
is installed
postgres audit
postgres package
is not installed (FAILED - 1)
mysql config
mysql config file
exists with correct permissions
contains required configuration
Failures:
1) postgres audit postgres package is not installed
Failure/Error: expect(package("postgres")).not_to be_installed
expected Package "postgres" not to be installed
# Backtrace (excluded for this example)
The failure output should include a backtrace to help you identify where the audit failed.