Personal Policy

Posted by Craig Comstock
July 6, 2020

My laptop was getting stale…

I’ve been using it every work day for about 2.5 years now and so much software is installed it just boggles my mind. I really love it otherwise, open source, trying to be transparent, generally has worked amazingly! I have a Librem 15v3 from Purism. librem15v3
laptop My home dir is a maze of old and new directories, odd files, tons of ~/Downloads junk. And the real kicker? I can’t build CFEngine core anymore! :( I tried to fix the situation but just couldn’t quite fix it. So the solution? Well reinstall PureOS of course and see if that helps things out.

Start fresh!!!

My first try at finding instructions failed a bit: https://docs.puri.sm/Librem_13/Maintenance/Reinstalling_PureOS.html says “TODO: write some instructions”.

(oops)

So a bit more poking around in the forums led me to a proper Live System Installation guide. I also found some more details about preparation and maintenance at the General Recommendations.

(instructions were great, live install was easy!)

That working well enough I decided to finally get serious about something I’ve been playing with for over a year: a “personal” policy.

The Goal: Use CFEngine for everything!

Ideally I’d like to manage all aspects of customization with CFEngine policy. Just to get a good understanding of how the policy language works and where things are difficult and how we might improve CFEngine.

Install CFEngine

If you have a typical OS you can just use a one-liner quick install provided for Enterprise or Community versions. For this work I am going to use community. Sadly our quick install script doesn’t understand Pure OS 9 as a version. I also tried our rather helpful tool cf-remote. Sadly it doesn’t understand Pure OS either and tries to install an ubuntu package, which fails. So I had to download the Debian 10 3.15.2 package manually from this page.

Bootstrap

I am just working locally on this policy and on WiFi so the IP could change. This could be a problem later but I’m just working locally for now so I will deal with that later by setting up a policy server that has a stable IP. The first step after installing is getting our Policy framework in place. This is called bootstrapping. As root:

# cf-agent --bootstrap 192.168.43.233
R: Bootstrapping from host '192.168.43.233' 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: Triggered an initial run of the policy
notice: Bootstrap to '192.168.43.233' completed successfully!

Now my system is ready for experimentation/development. Generally you should develop on something that is not your primary workstation just to be safe. But I wanted to live on the edge and eat my own dog food. So I will proceed.

Track changes

I want to be sane about knowing what I have changed in the policy so will initialize a git repo in /var/cfengine/masterfiles

# cd /var/cfengine/masterfiles
# git init
# cat > .gitignore
cf_promises_release_id
cf_promises_validated
(ctrl-d)
# git add .gitignore
# git add .
# git commit -m initial

Update Policy and Evaluate

Running the agent on every change is necessary when developing policy. Normally cf-execd takes care of updating policy from the policy server (in this case localhost) and the evaluating the policy. This is done in two steps: cf-agent -KIf update.cf which updates policy, in this case copying files from /var/cfengine/masterfiles to /var/cfengine/inputs where it will be evaluated by the command cf-agent -KI. A bit of explanation about the options/commands above: -K or --no-lock Normally cf-agent won’t evaluate the same promise more often than 1 minute due to Promise Locking. -K will ignore these locks making development easier. -I or --inform This gives a bit more verbose output than normal and will generally tell you what cf-engine has done to your system during each run. -f of --file This specifies the first policy file to read and evaluate. If a relative path this will refer to a path relative to /var/cfengine/inputs. It is useful sometimes to specify a local/absolute path such as ./test.cf. To make this go a little quicker I made a cf script in /usr/local/sbin/cf

# cat /usr/local/sbin/cf
cf-agent -KIf update.cf && cf-agent -KI

Policy Organization

I haven’t written or managed large bodies of policy just yet. But I do know that our autorun services help when adding custom policy to our policy framework called masterfiles. To enable autorun services we should add a few things via our Augments feature to a file at /var/cfengine/masterfiles/def.json. First, if we might rename our services cf files then by default any agents would end up with both the old and new files. So we define a class which purges files on the agent which are not present on the policy server: cfengine_internal_purge_policies. Next, if we make any adjustments to the way CFEngine services are configured in our policy we will need to force these components to restart with mpf_augments_control_enabled.

{
  "classes": {
    "services_autorun": [ "any::" ],
    "cfengine_internal_purge_policies": [ "any::" ]
  }
}

Note the use of any::. “any” is a hard class which is always defined so these two classes in the augments file (def.json) will always be defined. The two semi-colons afterward cause the string to be considered a class expression instead of a regex. See the details about how these JSON strings are handled here. And now on to the basic things I need to have a usable computer.

User accounts

When installing linux I created an account so I’ll leave that one alone for now. Just for fun I’ll create two accounts: me and too. First I make a json file to hold variable data so that others can re-use this policy without so much trouble, at /var/cfengine/masterfiles/services/autorun/personal.json:

{
  "users": [ "me", "too" ]
}

Then the policy will load that json file and I make sure that works by printing out a diagnostic with a reports promise. The autorun service policy files MUST be in the /var/cfengine/masterfiles/services/autorun directory to be picked up automatically. There are other ways but this is the easiest for autorun services. So I create users.cf in that directory.

bundle agent users
{
  meta:
    "tags" slist => { "autorun" };

  vars:
    "personal" data => readjson("$(this.promise_dirname)/personal.json");

  reports:
    "$(personal[users])";
}

Running my cf script I get:

    R: me
    R: too

Next I add a users promise to ensure these users exist

  users:
    "$(personal[users])" policy => "present";

And with another run of the agent I have users!

info: User promise repaired
info: User promise repaired

And inspection of /etc/passwd shows the users are there!

me:x:1001:1001::/home/me:/bin/sh
too:x:1002:1002::/home/too:/bin/sh

Wait, where’s the home dir? I have a user but no home dir for it to use! So let’s back up, remove the users (with policy) and add the homedir and some basic files from /etc/skel with the home_bundle attribute of the users promise. In the policy I change “present” to “absent”

"$(personal[users])" policy => "absent";

The users are removed, though the inform output doesn’t say “removed” but rather “repaired”. That’s because promises can promise many different things and in this case the removal was done and so is repaired.

info: User promise repaired
info: User promise repaired

Checking /etc/passwd, me and too are gone. :) I refactor to get the users into a list data variable so I don’t have to keep typing personal[users]

"users" slist => getvalues("personal[users]");

I take the skel var from the home_bundle example:

  "skel" string => "/etc/skel";

Rewrite the users promise to use home_dir and home_bundle:

  users:
    "$(users)"
      policy => "present",
      home_dir => "/home/$(users)",
      home_bundle => home_skel($(users), $(skel));

and add the home_skel bundle:

bundle agent home_skel(user, skel)
{
  files:
    "/home/$(user)/."
    create => "true",
    copy_from => seed_cp($(skel)),
    depth_search => recurse("inf");
}

and the result from an agent run:

  info: Created directory '/home/me/.'
  info: Copying from 'localhost:/etc/skel/.bash_logout'
  info: Copying from 'localhost:/etc/skel/.profile'
  info: Copying from 'localhost:/etc/skel/.bashrc'
  info: User promise repaired
  info: Created directory '/home/too/.'
  info: Copying from 'localhost:/etc/skel/.bash_logout'
  info: Copying from 'localhost:/etc/skel/.profile'
  info: Copying from 'localhost:/etc/skel/.bashrc'
  info: User promise repaired

Who’s stuff is this anyway?

Uh oh, these dirs aren’t very usable for our new users. They are owned by root! :(

  drwxr-xr-x 2 root root 4096 Jun 12 12:34 me
  drwxr-xr-x 2 root root 4096 Jun 12 12:34 too

So we add a files promise to take care of that. I could have added a few attributes in the home_bundle body but that only runs on initial user creation and I’d like to enforce these permissions all the time. Notice that I use the body og() from the policy framework to enforce the owner and group for the user.

  files:
    "/home/$(users)/."
    perms => og("$(users)","$(users)"),
    depth_search => recurse("inf");

agent output:

  info: Owner of '/home/me/./.bash_logout' was 0, setting to 1001
  info: Group of '/home/me/./.bash_logout' was 0, setting to 1001
  info: Owner of '/home/me/./.profile' was 0, setting to 1001
  info: Group of '/home/me/./.profile' was 0, setting to 1001
  info: Owner of '/home/me/./.bashrc' was 0, setting to 1001
  info: Group of '/home/me/./.bashrc' was 0, setting to 1001
  info: Owner of '/home/too/./.bash_logout' was 0, setting to 1002
  info: Group of '/home/too/./.bash_logout' was 0, setting to 1002
  info: Owner of '/home/too/./.profile' was 0, setting to 1002
  info: Group of '/home/too/./.profile' was 0, setting to 1002
  info: Owner of '/home/too/./.bashrc' was 0, setting to 1002
  info: Group of '/home/too/./.bashrc' was 0, setting to 1002

Uh, what’s the password?

I manually set the password for the me user and then check /etc/shadow for the hash. Then use that in the policy. For now I will just use the same password for both me and too since I want to keep my json simple and for the near future I will really only be working with one user: craig ;) I use another policy framework body called hashed_password() to make things a bit easier.

  password => hashed_password("$6$IvdxUGIwiwcDYPaH$MALkFPHXtbt3lu1WzJSoHZ27zoHV/ow553.0nxARTLktL.xKuDRamUcaj2SIqmsMohPknPWqsZinMGDR1vkKy0"),

agent output:

  info: User promise repaired

And try to login!

  craig@other:~$ su - me
  Password: ********
  $ whoami
  me

точно, молодец

I studied Russian for several years in college. That means “perfect, you’re a genius” or something like that. (google says “accurate, well done”, that’s pretty close). Here is the final policy.

bundle agent users
{
  meta:
    "tags" slist => { "autorun" };

  vars:
    "personal" data => readjson("$(this.promise_dirname)/personal.json");
    "skel" string => "/etc/skel";
    "users" slist => getvalues("personal[users]");

  users:
    "$(users)"
      policy => "present",
      password => hashed_password("$6$IvdxUGIwiwcDYPaH$MALkFPHXtbt3lu1WzJSoHZ27zoHV/ow553.0nxARTLktL.xKuDRamUcaj2SIqmsMohPknPWqsZinMGDR1vkKy0"),
      home_dir => "/home/$(users)",
      home_bundle => home_skel($(users), $(skel));

  files:
    "/home/$(users)/."
      perms => og("$(users)","$(users)"),
      depth_search => recurse("inf");

  reports:
    "$(users)";
}

bundle agent home_skel(user, skel)
{
  files:
    "/home/$(user)/."
    create => "true",
    copy_from => seed_cp($(skel)),
    depth_search => recurse("inf");
}

More later…

I plan on continuing to refine and expand this personal policy of mine and blog about the steps along the way. The most pressing things to handle are:

  • password store software install and git pull/push to keep things up-to-date
  • synchronize important data from device to device so it’s safe and available wherever I want it, such as gpg, config files, music, pictures, email, documents
  • /etc/hosts config/distribution for all the devices on my home wifi so that when IPs change I can still just ssh hostname and not worry about it.