CFEngine is very simple to set up and use, especially if all of the
clients and the hub are going to be using the same promises. But what if
there are certain things you want to enforce on a hub and not a client?
What if there are certain things you want to enforce on a client but not
on a hub? For example, if you are using the Git
Setup,
you want the hub to pull from the Git repository, but you don’t want the
clients to do this. You want the hub to make those changes available to
the clients only after it’s verified them. So how do you have a promise
that only enforces on the hub, and not on a client? A simple solution is
to use the am_policy_hub
class to conditionally pull from Git if the
server is a hub:
# Very over-simplified example of a git pull for a policy hub only
bundle agent git_update_masterfiles
{
vars:
"git_branch" string => "branch_name";
commands:
am_policy_hub::
"/usr/bin/git pull origin $(git_branch)";
}
This am_policy_hub
class is great if you only plan to have a couple of
promises that are different between the hub and the client. It becomes
more tedious if you have many promises that differ between the hub and
the client. You’d have to remember to conditionally check each promise
to make sure a promise wasn’t inadvertently applied to the wrong
environment. Now, imagine you work for an organization that has
thousands of servers. Imagine you are tasked to enforce hundreds of
security policies on these servers. The problem is, the hubs need to
enforce different security policies than the clients. Some clients
should enforce all policies, while other clients (like the ones already
deployed in production) should have the option to select which policies
are enforced and which are only checked. Oh, and in case you can think
of some sophisticated ways this might be possible with some of the new
features of CFEngine Community 3.12, this solution has to be backward
compatible with CFEngine Community 3.10.4. This is the scenario I found
myself in while working for a client. As you can imagine, a simple
conditional class like am_policy_hub
is not enough for this kind of
challenge. Creating additional classes that need to be checked in each
promise would also not be very practical. I needed something a little
more robust and flexible that wouldn’t make each promise look so
cluttered with conditionals. Below, I describe the approach I used.
Contexts
First, we implement the concept of contexts. We define categories (i.e.
contexts) for the servers that need special handling, create directories
for each of those contexts, then place promises inside those directories
that only apply to that context. This also allows us to have
context-specific settings, which we will see below. Then we just need to
instruct CFEngine to only run the policies that apply to a particular
context. Here are the contexts I needed for my client: context_clients
,
context_clients_enforced
, context_hubs
, context_shared
, and
context_unknown
. The context_shared
context contains all promises that
apply to every context. This promotes the DRY (don’t repeat yourself)
principle. The security policies are in this context. They will be run
in every context. However, every context contains settings to determine
how the security policies will work, as described below. The
context_clients
context is the default context for production clients.
It contains some promises that only apply to clients, and also contains
a setting to indicates which security policies to enforce and which to
only check. Initially, it doesn’t enforce any security policies, but
only checks them. The context_clients_enforced
context is for a
separate set of clients that are not yet in production. Similar to the
context_clients context, it contains some client-specific promises as
well as the setting to indicate which security policies will be
enforced. This context is set to enforce all by default. The
context_hubs
context contains hub-specific promises, such as pulling
the most recent masterfiles from Git. It also contains the setting to
determine which security policies to enforce. Because these are also
production servers, this context also doesn’t enforce any security
policies initially, but only checks them. The context_unknown
context
is a “catch-all” when the context cannot be determined. Nothing happens
here.
The Masterfiles Structure
One of the goals of this implementation is to not modify any of the
CFEngine policy files that ship with cfengine-community. This will make
upgrades easier. In order to do that, we take advantage of the augments
file
to add a custom policy file called promises_company_name.cf
to inputs
that will be run at the end of the bundlesequence as defined in body
common control of promises.cf
. This custom policy file will determine
the context and then instruct CFEngine to run whatever additional
promises we want for that context. Here is a list of all of the files
required for a basic example of this solution: All of these files will
be described below. They have also been made available on
GitHub.
Step-by-step Instructions
The Augments file:
Create the def.json
file in the root of the masterfiles directory, and
insert these contents:
{
"inputs": ["promises_company_name.cf"],
"vars": {
"control_common_bundlesequence_end": ["company_name_main"]
}
}
The Custom Promises File:
Create the promises_company_name.cf file in the root of the masterfiles directory, and insert these contents:
body file control
{
# Load whatever inputs are determined by the contexts bundle below
inputs => {
@(contexts.inputs),
@(context_shared.inputs),
};
}
bundle common contexts
{
classes:
# CFEngine creates classes for the hostname and IP address of
# each server. This aproach assumes that you have a way of
# separating client servers into various contexts based on one
# of those classes.
"context_hubs"
expression => classmatch("am_policy_hub");
"context_clients"
expression => classmatch("^reg_expression_match$");
"context_clients_enforce"
expression => classmatch("^reg_expression_match$");
vars:
any::
# Default is unknown, but overridden below
"active" string => "unknown", policy => "overridable";
"inputs" slist => { "context_unknown/context_unknown.cf" };
"bundles" slist => { "context_unknown" };
context_hubs::
"active" string => "hubs", policy => "overridable";
"inputs" slist => { "context_hubs/context_hubs.cf" };
"bundles" slist => { "context_hubs" };
context_clients::
"active" string => "clients", policy => "overridable";
"inputs" slist => { "context_clients/context_clients.cf" };
"bundles" slist => { "context_clients" };
context_clients_enforce::
"active" string => "clients_enforce", policy => "overridable";
"inputs" slist => { "context_clients_enforce/context_clients_enforce.cf" };
"bundles" slist => { "context_clients_enforce" };
}
bundle common context_shared
{
vars:
any::
"inputs" slist => { "context_shared/context_shared.cf" };
"bundles" slist => { "context_shared"};
# Here we get the "check_only" array as it is defined for the
# specific context we are in. This will be used in the shared
# security policies to determine whether to check only or enforce.
context_hubs::
"check_only" slist => { @(context_hubs.check_only) };
context_clients::
"check_only" slist => { @(context_clients.check_only) };
context_clients_enforce::
"check_only" slist => { @(context_clients_enforce.check_only) };
}
bundle agent company_name_main
{
methods:
# Now that we have determined the context and the context-specific
# security policy settings, run the shared promises and then the
# context-specific promises. Remember, the shared promises contain
# the security policies.
# Promises shared by all contexts
"$(context_shared.bundles)"
usebundle => $(context_shared.bundles);
# Context-specific promises
"$(contexts.bundles)"
usebundle => $(contexts.bundles);
}
The Additional Files:
Now we will create all of the additional files needed to support this structure. Each file is listed below with the path relative to the masterfiles root directory. Create these files and insert the corresponding contents into each of them.
body file control
{
# Defines the input files to load for this particular context
inputs => {
"context_$(contexts.active)/sample_promises.cf",
};
}
bundle common context_clients
{
vars:
any::
# This array is loaded by the context_shared promise in the
# promises_company_name.cf file if this is the context to be used.
# Potentially hundreds more of these; this is just a sample
"check_only" slist => {
"cis_01_01_01_01",
"cis_01_01_01_02"
};
}
bundle agent context_clients
{
# This is where we tell CFEngine which bundles to run for this
# context
methods:
"any" usebundle => sample_bundle;
}
bundle agent sample_bundle
{
reports:
any::
"This is a sample bundle that only runs in the context_clients context.";
}
body file control
{
# Defines the input files to load for this particular context
inputs => {
"context_$(contexts.active)/sample_promises.cf",
};
}
bundle common context_clients_enforce
{
vars:
any::
# This array is loaded by the context_shared promise in the
# promises_company_name.cf file if this is the context to be used.
# None are set to check_only, meaning ALL will be enforced.
"check_only" slist => { };
}
bundle agent context_clients_enforce
{
# This is where we tell CFEngine which bundles to run for this
# context
methods:
"any" usebundle => sample_bundle;
}
bundle agent sample_bundle
{
reports:
any::
"This is a sample bundle that only runs in the context_clients_enforce context.";
}
body file control
{
# Defines the input files to load for this particular context
inputs => {
"context_$(contexts.active)/sample_promises.cf",
};
}
bundle common context_hubs
{
vars:
any::
# This array is loaded by the context_shared promise in the
# promises_company_name.cf file if this is the context to be used.
# Potentially hundreds more of these; this is just a sample
"check_only" slist => {
"cis_01_01_01_01",
"cis_01_01_01_02"
};
}
bundle agent context_hubs
{
# This is where we tell CFEngine which bundles to run for this
# context
methods:
"any" usebundle => git_update_masterfiles;
}
bundle agent git_update_masterfiles
{
reports:
any::
"This is a sample promise that only runs in the context_hubs context.";
}
body file control
{
# Defines the input files to load for this particular context
inputs => {
# Potentially hundreds more of these; this is just a sample
"context_shared/security_promises.cf",
};
}
bundle agent context_shared
{
# This is where we tell CFEngine which bundles to run for this
# context
methods:
# Potentially hundreds more of these; this is just a sample
"any" usebundle => cis_01_01_01_01;
"any" usebundle => cis_01_01_01_02;
}
bundle agent cis_01_01_01_01
{
classes:
# This variable will be true if this bundle name is found in the
# check_only array.
"check_only"
expression => strcmp("$(context_shared.check_only)", "$(this.bundle)");
reports:
check_only::
"This promise only checks CIS security policy 1.1.1.1";
!check_only::
"This promise enforces CIS security policy 1.1.1.1";
}
bundle agent cis_01_01_01_02
{
classes:
# This variable will be true if this bundle name is found in the
# check_only array.
"check_only"
expression => strcmp("$(context_shared.check_only)", "$(this.bundle)");
reports:
check_only::
"This promise only checks CIS security policy 1.1.1.2";
!check_only::
"This promise enforces CIS security policy 1.1.1.2";
}
bundle agent context_unknown
{
# This is where we tell CFEngine which bundles to run for this
# context
reports:
any::
"Could not determine the context. No promises to run.";
}
Eli Taft Senior Solutions Architect Quoin, Inc. www.quoininc.com