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. 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 justssh hostname
and not worry about it.