With the release of build.cfengine.com, I have been working to migrate some of our own security related policy into modules of their own.
CFEngine Build and the cfbs
tooling allows us to organize policy into modules, which are easy to update independently and share with other users.
Let’s take the scenic route and look at what life is like with cfbs
.
One of our security policies requires that the password hashing algorithm in /etc/login.defs
is set to SHA512
.
Traditionally, with CFEngine we would simply promise the desired state:
bundle agent _etc_login_defs_ENCRYPT_METHOD
{
meta:
"tags" slist => { "autorun" };
vars:
"_config[ENCRYPT_METHOD]" string => "SHA512";
"_etc_login_defs"
string => "/etc/login.defs";
files:
"$(_etc_login_defs)"
edit_line => set_line_based(
"$(this.namespace):$(this.bundle)._config",
" ",
"\s+",
".*",
"^\s*(#.*|$)");
}
When the policy runs, it simply fixes the configuration issue. For example:
[root@hub]# grep ENCRYPT_METHOD /etc/login.defs
ENCRYPT_METHOD MD5
[root@hub]# cf-agent -KI
info: Replaced pattern '^\s*(ENCRYPT_METHOD\s+(?!SHA512$).*|ENCRYPT_METHOD)$' in '/etc/login.defs'
info: replace_patterns promise '^\s*(ENCRYPT_METHOD\s+(?!SHA512$).*|ENCRYPT_METHOD)$' repaired
info: Edited file '/etc/login.defs'
But, I don’t want to blindly apply this policy to the infrastructure. I could warn instead of fixing the state, but that will only tell me where it’s not correct and it won’t give me any information about what it’s currently set to. In order to get an overview of the current configuration before making promises about the state I want, I need to inventory the configuration.
In CFEngine Enterprise reporting, inventory is achieved by tagging variables and/or classes with metadata. Specifically, tagging a variable or class with inventory
and attribute_name=My Name
will result in a new inventory attribute for My Name
. So, we need to parse the file into one or more variables that we can tag for inventory so that we can report about the currently configured state.
The following policy generates the necessary data, we won’t dive into the details here, but the result is a new inventory attribute, /etc/login/.defs
holding the KEY=VALUE pairs parsed from the configuration file as seen in the preceding image.
bundle agent inventory_etc_login_defs
{
vars:
"_etc_login_defs_path" string => "/etc/login.defs";
# A KEY <space> VALUE formatted file
"_etc_login_defs"
data => data_readstringarrayidx(
"$(_etc_login_defs_path)", # File
"#[^\n]*", # Comment
"\s+", # Split
inf, # Max entries
inf), # Max bytes
if => fileexists( "$(_etc_login_defs_path)" );
# We need to iterate over the data structure created by
# data_readstringarrayidx() so that we can build tag each item.
"_etc_login_defs_idx" slist => getindices( _etc_login_defs );
# Here we use the index to iterate and build up an associative array of
# KEY=Values We use an associative array so that we can stay under the
# data limit reporting a single variable
"etc_login_defs[$(_etc_login_defs_idx)]"
string => "$(_etc_login_defs[$(_etc_login_defs_idx)][0])=$(_etc_login_defs[$(_etc_login_defs_idx)][1])",
meta => { "inventory", "attribute_name=$(_etc_login_defs_path)" };
}
bundle agent __main__
{
methods:
"inventory_etc_login_defs";
}
Using cfbs
you no longer have to copy and paste this policy to integrate it into your policy set.
Let’s use cfbs
to build a policy set that includes this inventory leveraging the inventory-etc-login-defs module.
First, let’s bring up an environment to play with. Here I use the CFEngine Enterprise Vagrant Environment.
Note: Alternatively you can also use cf-remote
to provision hosts on AWS or GCP.
Once your hub is up, log in as root
and get cfbs
installed.
First, install pip and get it upgraded, yum -y install python3-pip; pip3 install pip --upgrade
.
Next, install cfbs
, and put it in PATH for ease of use pip3 install cfbs; export PATH=$PATH:/usr/local/bin
.
Now that we have cfbs
let’s make a project directory, switch into it, and initialize it, mkdir -p /root/cfbs-project; cd /root/cfbs-project; cfbs init
.
[root@hub ~]# mkdir -p /root/cfbs-project; cd /root/cfbs-project; cfbs init
Initialized - edit name and description cfbs.json
To add your first module, type: cfbs add masterfiles
Let’s do as instructed and run cfbs add masterfiles
.
[root@hub cfbs-project]# cfbs add masterfiles
Added module: masterfiles
Currently, you need autotools
to build masterfiles from source, so let’s install that as well:
[root@hub ~]# yum -y install automake
Now, let’s add the inventory-etc-login-defs
module.
[root@hub cfbs-project]# cfbs add inventory-etc-login-defs
Added module: inventory-etc-login-defs
We can see that cfbs
populated cfbs.json
with the information needed to build the project.
[root@hub cfbs-project]# cat cfbs.json
{
"name": "Example",
"type": "policy-set",
"description": "Example description",
"build": [
{
"name": "masterfiles",
"description": "Official CFEngine Masterfiles Policy Framework (MPF)",
"tags": ["supported", "base"],
"repo": "https://github.com/cfengine/masterfiles",
"by": "https://github.com/cfengine",
"version": "0.1.1",
"commit": "5c7dc5b43088e259a94de4e5a9f17c0ce9781a0f",
"steps": [
"run ./autogen.sh",
"delete ./autogen.sh",
"run ./cfbs/cleanup.sh",
"delete ./cfbs/cleanup.sh",
"copy ./ ./"
],
"added_by": "cfbs add"
},
{
"name": "inventory-etc-login-defs",
"description": "Inventory useful bits from /etc/login.defs",
"tags": ["supported", "inventory"],
"repo": "https://github.com/nickanderson/cfengine-inventory-etc-login-defs",
"by": "https://github.com/nickanderson",
"version": "0.0.1",
"commit": "52f659510ddd3bc65e804306a1bff17d6c6f4299",
"steps": [
"copy ./inventory-etc-login-defs.cf services/inventory-etc-login-defs/inventory-etc-login-defs.cf",
"json cfbs/def.json def.json"
],
"added_by": "cfbs add"
}
]
}
Now, let’s build and install!
[root@hub cfbs-project]# cfbs build
Modules:
001 masterfiles @ 5c7dc5b43088e259a94de4e5a9f17c0ce9781a0f (Downloaded)
002 inventory-etc-login-defs @ 52f659510ddd3bc65e804306a1bff17d6c6f4299 (Downloaded)
Steps:
001 masterfiles : run './autogen.sh'
001 masterfiles : delete './autogen.sh'
001 masterfiles : run './cfbs/cleanup.sh'
001 masterfiles : delete './cfbs/cleanup.sh'
001 masterfiles : copy './' 'masterfiles/'
002 inventory-etc-login-defs : copy './inventory-etc-login-defs.cf' 'masterfiles/services/inventory-etc-login-defs/inventory-etc-login-defs.cf'
002 inventory-etc-login-defs : json 'cfbs/def.json' 'masterfiles/def.json'
Generating tarball...
Build complete, ready to deploy 🐿
-> Directory: out/masterfiles
-> Tarball: out/masterfiles.tgz
To install on this machine: cfbs install
To deploy on remote hub(s): cf-remote deploy --hub hub out/masterfiles.tgz
[root@hub cfbs-project]# cfbs install
Installed to /var/cfengine/masterfiles
Within a few minutes we should be able to find /etc/login.defs
reported in inventory.
By querying the API:
[root@hub cfbs-project]# curl --silent --insecure --user $AUTHUSER:$PASSWORD \
--request POST \
https://$HUB/api/inventory \
-H 'content-type: application/json' \
-d '{
"sort":"Host name",
"select":[
"Host name",
"/etc/login.defs"
]}'
{
"data": [
{
"header": [
{
"columnName": "Host name",
"columnType": "STRING"
},
{
"columnName": "\/etc\/login.defs",
"columnType": "STRING"
}
],
"queryTimeMs": 17,
"rowCount": 1,
"rows": [
[
"hub.example.com",
"UID_MIN=1000, UID_MAX=60000, UMASK=077, USERGROUPS_ENAB=yes, SYS_GID_MAX=999, CREATE_HOME=yes, ENCRYPT_METHOD=MD5, MD5_CRYPT_ENAB=yes, MAIL_DIR=\/var\/spool\/mail, GID_MAX=60000, SYS_GID_MIN=201, SYS_UID_MAX=999, GID_MIN=1000, PASS_MIN_LEN=5, PASS_MAX_DAYS=99999, PASS_MIN_DAYS=0, PASS_WARN_AGE=7, SYS_UID_MIN=201"
]
]
}
],
"meta": {
"count": 1,
"page": 1,
"timestamp": 1636491107,
"total": 1
}
}
Or by looking in Mission Portal:
From here we could continue to build a compliance report checking that /etc/login.defs
matches ENCRYPT_METHOD=SHA512
, but this post is about cfbs
.
Here is where things get really great!
While authoring this post, I noticed that the comment regex in the policy could be improved.
So, I updated the module, and now I can quickly get that change integrated into my cfbs
project.
First, let’s initialize our cfbs
project as a git repository.
This will make it very easy to see the changes cfbs
is making to the project.
[root@hub cfbs-project]# git init
Initialized empty Git repository in /root/cfbs-project/.git/
[root@hub cfbs-project]# git add cfbs.json
[root@hub cfbs-project]# git commit -m "Initialized cfbs project with masterfiles and inventory-etc-login-defs"
Okay, now, let’s run cfbs update
to update all of our modules to the latest versions available in the index. We can use git diff
to see the changes made by cfbs
.
[root@hub cfbs-project]# cfbs update
[root@hub cfbs-project]# git diff
diff --git a/cfbs.json b/cfbs.json
index 1540e08..bc0a03f 100644
--- a/cfbs.json
+++ b/cfbs.json
@@ -27,8 +27,8 @@
"tags": ["supported", "inventory"],
"repo": "https://github.com/nickanderson/cfengine-inventory-etc-login-defs",
"by": "https://github.com/nickanderson",
- "version": "0.0.1",
- "commit": "52f659510ddd3bc65e804306a1bff17d6c6f4299",
+ "version": "0.0.3",
+ "commit": "8f4747e4856737aa44fd96b9b8d38d941afec9f3",
"steps": [
"copy ./inventory-etc-login-defs.cf services/inventory-etc-login-defs/inventory-etc-login-defs.cf",
"json cfbs/def.json def.json"
Using cfbs status
we can see that the latest inventory-etc-login-defs
has yet to be downloaded.
[root@hub cfbs-project]# cfbs status
Name: Example
Description: Example description
File: cfbs.json
Modules:
001 masterfiles @ 5c7dc5b43088e259a94de4e5a9f17c0ce9781a0f (Downloaded)
002 inventory-etc-login-defs @ 8f4747e4856737aa44fd96b9b8d38d941afec9f3 (Not downloaded)
Let’s build the new policy set.
[root@hub cfbs-project]# cfbs build
Modules:
001 masterfiles @ 5c7dc5b43088e259a94de4e5a9f17c0ce9781a0f (Downloaded)
002 inventory-etc-login-defs @ 8f4747e4856737aa44fd96b9b8d38d941afec9f3 (Downloaded)
Steps:
001 masterfiles : run './autogen.sh'
001 masterfiles : delete './autogen.sh'
001 masterfiles : run './cfbs/cleanup.sh'
001 masterfiles : delete './cfbs/cleanup.sh'
001 masterfiles : copy './' 'masterfiles/'
002 inventory-etc-login-defs : copy './inventory-etc-login-defs.cf' 'masterfiles/services/inventory-etc-login-defs/inventory-etc-login-defs.cf'
002 inventory-etc-login-defs : json 'cfbs/def.json' 'masterfiles/def.json'
Generating tarball...
Build complete, ready to deploy 🐿
-> Directory: out/masterfiles
-> Tarball: out/masterfiles.tgz
To install on this machine: cfbs install
To deploy on remote hub(s): cf-remote deploy --hub hub out/masterfiles.tgz
We can see the diff of the policy file does indeed show the change in comment regex.
[root@hub cfbs-project]# diff -u /var/cfengine/masterfiles/services/inventory-etc-login-defs/inventory-etc-login-defs.cf out/masterfiles/services/inventory-etc-login-defs/inventory-etc-login-defs.cf
--- /var/cfengine/masterfiles/services/inventory-etc-login-defs/inventory-etc-login-defs.cf 2021-11-09 20:38:52.342254892 +0000
+++ out/masterfiles/services/inventory-etc-login-defs/inventory-etc-login-defs.cf 2021-11-09 21:58:45.229784693 +0000
@@ -13,7 +13,7 @@
"_etc_login_defs"
data => data_readstringarrayidx(
"$(_etc_login_defs_path)", # File
- "#[^\n]*", # Comment
+ "\s*#[^\n]*", # Comment
"\s+", # Split
inf, # Max entries
inf), # Max bytes
Let’s go ahead and cfbs install
to deploy the updated policy set.
[root@hub cfbs-project]# cfbs install
Installed to /var/cfengine/masterfiles
Tip: cfbs install
requires elevated privileges to write in /var/cfengine/masterfiles
, execute as root
user or use sudo
to elevate privileges.
All the other cfbs
commands only edit files inside current working directory and your home directory, and don’t need elevated privileges.
Throughout this post I’ve been using the root
user for convenience, but this is inside a “throw-away” virtual machine.
If you are running this on your development machine or production infrastructure, you should only use the root
user when necessary.
I hope this kindles the same excitement in you as it has for me.
This is definitely going to make it easier to manage a policy set, especially when considering working with various policy authors, and that’s without even considering the other kinds of content already in cfbs
, like custom promise types or content that is on the horizon like compliance reports.