Context-specific Security Settings

Posted by:

12 Feb 2019

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 is overridden if the context is determined
      "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
Quoin Logo