CFEngine

Overview

CFEngine comes in two versions, either commercial or community build. The difference between the two really comes down if you are a large corporation with compliance requirements and complex inventory, otherwise the basic features are the same.

For the demo we’ll use the community version. It’s odd they don’t have a repository, we have to download/install the package manually. There is a separate package for the master nodes (called Policy Hubs) and the client nodes (called Hosts), also there is a single liner to install the community package.

Install

Install the Policy server

root@sandbox:~/tmp/cfengine# wget -O- http://cfengine.package-repos.s3.amazonaws.com/quickinstall/quick-install-cfengine-community.sh | sudo bash
--2018-10-21 21:01:56--  http://cfengine.package-repos.s3.amazonaws.com/quickinstall/quick-install-cfengine-community.sh
Resolving cfengine.package-repos.s3.amazonaws.com (cfengine.package-repos.s3.amazonaws.com)… 52.216.65.160
Connecting to cfengine.package-repos.s3.amazonaws.com (cfengine.package-repos.s3.amazonaws.com)|52.216.65.160|:80… connected.
HTTP request sent, awaiting response… 200 OK
Length: 3160 (3.1K) [text/x-shellscript]
Saving to: 'STDOUT'

100%[====================================================================================================>]   3.09K  --.-KB/s    in 0s      
2018-10-21 21:01:56 (517 MB/s) - written to stdout [3160/3160] Get:1 http://security.ubuntu.com/ubuntu xenial-security InRelease [107 kB] Hit:2 http://us.archive.ubuntu.com/ubuntu xenial InRelease ... Unpacking cfengine-community (3.12.0-1) … Processing triggers for systemd (229-4ubuntu21.2) … Processing triggers for ureadahead (0.100.0-19) … Setting up cfengine-community (3.12.0-1) … Ready to bootstrap using /var/cfengine/bin/cf-agent --bootstrap

Bootstrap itself so it becomes the master/policy server:

root@sandbox:~/tmp/cfengine# ip address show eth1 | grep inet
    inet 192.168.200.129/24 brd 192.168.200.255 scope global eth1
    inet6 fe80::a00:27ff:fe94:5e63/64 scope link 
root@sandbox:~/tmp/cfengine# /var/cfengine/bin/cf-agent --bootstrap 192.168.200.129 R: Bootstrapping from host '192.168.200.129' via built-in policy '/var/cfengine/inputs/failsafe.cf' R: This host assumes the role of policy server R: Updated local policy from policy server R: Restarted systemd unit cfengine3 notice: Bootstrap to '192.168.200.129' completed successfully!

Install the Host

Same one liner as before.

root@build0:~# wget -O- http://cfengine.package-repos.s3.amazonaws.com/quickinstall/quick-install-cfengine-community.sh | sudo bash
--2018-10-21 21:11:46--  http://cfengine.package-repos.s3.amazonaws.com/quickinstall/quick-install-cfengine-community.sh
Resolving cfengine.package-repos.s3.amazonaws.com (cfengine.package-repos.s3.amazonaws.com)… 52.216.85.139
Connecting to cfengine.package-repos.s3.amazonaws.com (cfengine.package-repos.s3.amazonaws.com)|52.216.85.139|:80… connected.
HTTP request sent, awaiting response… 200 OK
...
Processing triggers for systemd (229-4ubuntu21.2) … Processing triggers for ureadahead (0.100.0-19) … Setting up cfengine-community (3.12.0-1) … Processing triggers for systemd (229-4ubuntu21.2) … Processing triggers for ureadahead (0.100.0-19) … Ready to bootstrap using /var/cfengine/bin/cf-agent --bootstrap 

Bootstrap the client, by pointing to the master’s IP:

root@build0:~# /var/cfengine/bin/cf-agent --bootstrap 192.168.200.129
  notice: Bootstrap mode: implicitly trusting server, use --trust-server=no if server trust is already established
  notice: Trusting new key: MD5=f2f6fccef9a620208ad8ebbf1cfd2249
R: Bootstrapping from host '192.168.200.129' via built-in policy '/var/cfengine/inputs/failsafe.cf'
R: This autonomous node assumes the role of voluntary client
R: Updated local policy from policy server
R: Restarted systemd unit cfengine3
  notice: Bootstrap to '192.168.200.129' completed successfully!

Check version:

sendai@sandbox:~/tmp/cfengine$ /var/cfengine/bin/cf-promises --version
CFEngine Core 3.12.0

Basics

The basics of CFEngine:

  • declarative: You define the desired state, not the steps to get there
  • requires agent: on the hosts you manage there is an agent running
  • two type of hosts: master (Policy server) and slave (Host)
  • pull based architecture: Hosts occasionally fetch policies from server, loosely coupled system
  • the desired states you’ll write are called promises, which are typed
  • promises are grouped together into bundles
  • group target hosts by defining classes
  • the Hosts are constantly validated to ensure compliance
  • promises supports functions, variables, templates for flexibility

So, it’s not just an execution framework, but also a state enforcer framework which ensures that state you defined are preserved at the clients. As a matter of fact because of the pull architecture, it can’t run commands interactively on clients as Ansible or Chef can.

Example

Hello world

We write a basic configuration file with a single promise, and store it in /var/cfengine/masterfiles. The file consist of a body and bundle sections, both are typed. The body type is common which is a section for generic configuration, while the bundle type is agent meaning it can be interpreted by cf-agent, and which will contain our promises. The bundlesequence parameter will ensure that we run the hello_world bundle, which will use a reports promise, which will print “Hello World!”:

root@sandbox:/var/cfengine/masterfiles# more hello_world.cf 
body common control
{
  bundlesequence => { "hello_world" };
}
bundle agent hello_world
{
  reports:
any::
"Hello World!";
}
root@sandbox:/var/cfengine/masterfiles# cf-agent --no-lock --file ./hello_world.cf
R: Hello World!

Execute command

root@sandbox:/var/cfengine/masterfiles# cat hostname.cf
body common control
{
      bundlesequence  => { "commands_1", "commands_2" };
}
bundle agent commands_1 { commands: "/bin/hostname"; }
bundle agent commands_2 {
vars: "my_result" string => execresult("/bin/hostname","noshell");
reports:
"Output is : $(my_result)";
}

We are defining two bundles using three promises here using promise types commands, vars and reports. At commands_2 we are also using a function, called execresult, which returns the output of the command. Note, that because of the pull,  loosely coupled architecture we are NOT reaching out synchronously to our clients, this only runs locally. This is something that most modern CF tool (Ansible, Chef) can/would do. I believe here this is a conceptual question, to ensure stability in extreme amount of servers too, without overloading certain components. The result (hostname is sandbox):

root@sandbox:/var/cfengine/masterfiles# cf-agent --no-lock hostname.cf      
  notice: Q: "…n/hostname": sandbox
R: Output is : sandbox

Install a package

body common control
{
      bundlesequence  => { "apache" };
}
body package_module apt_get { default_options => { }; }
bundle agent apache { packages:
debian:: "apache2" version => "2.4.18-2ubuntu3.9", policy => "present", package_module => apt_get, comment => "Install apache from repository";
}

We use the packages Promise here. Result:

root@sandbox:/var/cfengine/masterfiles# cf-agent --no-lock packages.cf 
root@sandbox:/var/cfengine/masterfiles# apt list apache2
Listing… Done
apache2/xenial-updates,now 2.4.18-2ubuntu3.9 amd64 [installed]

Adding our bundles to our main policy

As a final test, we’ll add our second test to our main promise file promises.cf, and see as the client (build0) is executing it, every 5 minutes. We’ll include our config file, also comment out the body common control section, since it would conflict with the same section at the main promise file.

root@sandbox:/var/cfengine/masterfiles# cat get_hostname.cf 
# body common control # { # bundlesequence => { "commands_1", "commands_2" }; # }
bundle agent commands_1 { commands: "/bin/hostname"; }
bundle agent commands_2 {
vars:
"my_result" string => execresult("/bin/hostname","noshell");
reports:
"Output is : $(my_result)";
}

Include the file by listing in the inputs section in promises.cf:

inputs => {             # File definition for global variables and classes              @(cfengine_controls.def_inputs),             # Inventory policy              @(inventory.inputs),
# Design Center "sketches/meta/api-runfile.cf", @(cfsketch_g.inputs), # CFEngine internal policy for the management of CFEngine itself @(cfe_internal_inputs.inputs), # Control body for all CFEngine robot agents @(cfengine_controls.inputs), # COPBL/Custom libraries. Eventually this should use wildcards. @(cfengine_stdlib.inputs), # autorun system @(services_autorun.inputs), "services/main.cf", "get_hostname.cf", };

Finally, call the bundle by adding command_1 and command_2 to bundlesequence in promises.cf:

 bundlesequence => {
                    # Common bundle first (Best Practice)
                      inventory_control,
                      @(inventory.bundles),
                      def,
                      @(cfengine_enterprise_hub_ha.classification_bundles),

                      # Design Center
                      cfsketch_run,

                      # autorun system
                      services_autorun,
                      @(services_autorun.bundles),

                     # Agent bundle
                      cfe_internal_management,   # See cfe_internal/CFE_cfengine.cf
                      main,
                      @(cfengine_enterprise_hub_ha.management_bundles),
                      @(def.bundlesequence_end),
                      commands_1,
                      commands_2,

  };

Once that’s done, we can check the only client we have, build0’s syslog:

Oct 22 00:04:13 build0 cf-serverd[18063]: CFEngine(server)  Rereading policy file '/var/cfengine/inputs/promises.cf'
Oct 22 00:09:06 build0 cf-agent[19774]: CFEngine(agent)  Q: "…n/hostname": build0
Oct 22 00:09:06 build0 cf-agent[19774]: CFEngine(agent)  R: Output is : build0
Oct 22 00:14:05 build0 cf-agent[19817]: CFEngine(agent)  Q: "…n/hostname": build0
Oct 22 00:14:05 build0 cf-agent[19817]: CFEngine(agent)  R: Output is : build0
Oct 22 00:17:01 build0 CRON[19845]: (root) CMD (   cd / && run-parts --report /etc/cron.hourly)
Oct 22 00:19:04 build0 cf-agent[19862]: CFEngine(agent)  Q: "…n/hostname": build0
Oct 22 00:19:04 build0 cf-agent[19862]: CFEngine(agent)  R: Output is : build0

As you can see, every 5 minutes, our get_hostname.cf is running on the slave node. Also, when we edited the main Promise config file promise.cf, we didn’t restart any services, it picked up the changes automatically.

There are additional examples at their official site if you decide to continue experimenting.

Conclusion

CFEngine appears to be a decent, well designed and thought-out tool with tons of features to enforce policy and compliance. Its pull architecture may look uncomfortable compared to Ansible and Chef sometimes, but the concept of low risk configuration management is also a reasonable idea, so it’s a trade-off I can understand. I feel like CFEngine is in its own category and even though it looks like a meticulous tool, I would still rather pick Salt or Ansible over CFEngine.

Dictionary

Promise

You define actions and states via promises, suggesting that it’s not only a one time execution but it’s a constant enforcing of the state. Each promise has a type which decides how the state is processed. In this example our promise is about the existence of a file using the files promise type:

     files:
         "/home/mark/tmp/test_plain" -> "system blue team",
             create  => "true",
             perms   => owner("@(usernames)"),
             comment => "Hello World";

Bundle

Set of promises. Each bundle has a type, deciding which CFEngine component should interpret it. For example, cf-agent, the main state enforcer binary will interpret bundle and body types of agent and common. In this example, we define two promises of type files, with a class filter of !windows:

    bundle agent example
    {
      files:
        !windows::
          "/etc/passwd"
            handle => "example_files_not_windows_passwd",
            perms => system;

          "/home/bill/id_rsa.pub"
            handle => "example_files_not_windows_bills_priv_ssh_key",
            perms => mog("600", "bill", "sysop"),
            create => "true";
    }

Body

To support reusable code you can define complex Promise structures as Bodies, which then can be referenced in Promises. Bodies are typed and can accept arguments.

    body perms system
    {
      mode => "644";
      owners => { "root" };
      groups => { "root" };
    }

    body perms mog(mode,user,group)
    {
      owners => { "$(user)" };
      groups => { "$(group)" };
      mode   => "$(mode)";
    }

Policy Hub

The master servers.

Host

Slave servers, clients you manage.

Classes

Define groups to limit the scope of hosts.

Hard classes

Discovered by CFEngine itself. Hardware type, time of the day, day of the week, etc.

root@sandbox:~/tmp/cfengine# cf-promises --show-classes | grep hard     
10_0_2_15                                                    inventory,attribute_name=none,source=agent,hardclass
127_0_0_1                                                    inventory,attribute_name=none,source=agent,hardclass
192_168_200_129                                              inventory,attribute_name=none,source=agent,hardclass
1_cpu                                                        source=agent,derived-from=sys.cpus,hardclass
64_bit                                                       source=agent,hardclass                  
Day21                                                        time_based,cfengine_internal_time_based_autoremove,source=agent,hardclass
Evening                                                      time_based,cfengine_internal_time_based_autoremove,source=agent,hardclass
...
root@sandbox:~/tmp/cfengine# cf-promises --show-classes | grep -c hardclass 102

Soft classes

Defined by user.

Augments

Augment files are JSON files with variables to override parameters.

"vars":
{
  "phone": "22-333-4444",
  "myplatform": "$(sys.os)",
}

Namespaces

Allows to avoid naming conflicts. Everything is within a namespace in CFEngine, the default namespace is called .. default.

Functions

Can be used within Promises and makes CFEngine very powerful. Few examples: