Home   Profile   Fun
#130 Linux  03.06.2007

Gentoo configuration management with Cfengine and Subversion


Cfengine is an autonomous agent. It allows centralized setup and maintenance of a large number of hosts. Another aspect of Cfengine is that it makes these hosts more independent of system administrators. With every run of Cfengine the hosts get closer to a defined state. This state is described through policy rules inside the Cfengine configuration files.

The article describes a very basic setup of Cfengine. The main purpose is to show how to get things up and running. Of course there are alternative scenarios possible. Cfengine is very flexible.

We will have one policy master and one client machine. Additionally Subversion and Bind will be installed on the master. Subversion is great to have all our policies version controlled. In case there are problems with a configuration we can simply run an old version and thus have something which acts as a rollback.
The involved servers are
master1.home, 192.168.1.34
client1.home, 192.168.1.33

A typical workflow may look like this:
+On the master make changes to the Cfengine configuration files inside the working directory (/home/cfwork)
+Commit these changes to SVN
+Update the Cfengine configuration files inside the master repository from SVN (/var/cfengine/masterfiles/inputs)
+Run cfagent to distribute and apply these new files from the repository to the clients, and to the master itself, as it is also controlled by Cfengine. (/var/cfengine/inputs)

Some background info may be necessary to understand how Cfengine works: The main configuration files are cfservd.conf, update.conf, cfagent.conf. First the file update.conf is used to transfer all configuration files from the master to the client. This file should only be changed during the initial setup. Later on all configuration is done inside cfagent.conf. The reason is to make sure that a correct cfagent.conf can always be copied to the client. Imagine there were syntax errors in cfagent.conf which would prevent cfagent from running at all. In this scenario a corrected version of cfagent.conf could not be fetched from the master any more.
cfservd ist the main daemon. It serves as a file server on the master host. On the clients it is needed to access cfagent from outside.

Overview of the necessary steps to set up Cfengine
1. Set up Cfengine on the master
2. Set up SVN on the master
(3. Set up Bind on the master)

4. Set up Cfengine on the client



1. Set up Cfengine on the master
emerge -va cfengine
mkdir /var/cfengine/masterfiles



2. Set up SVN on the master
emerge -va subversion

Create SVN repository:
mkdir /usr/local/svn
svnadmin create /usr/local/svn/repos
mkdir /tmp/cfwork

Create all necessary Cfengine configuration files in here:
#cfagent.conf
control:
        actionsequence = ( tidy copy shellcommands )
        domain     = ( home )
        policyhost = ( 192.168.1.34 )
        access     = ( root )
        cfrunCommand = ( "/usr/sbin/cfagent" )
        IfElapsed = ( 0 )

groups:
        master = ( mobile1 )
        clients = ( client1 )

tidy:
       /tmp  pattern=*.txt age=3 recurse=inf

copy:
       /tmp/test.txt dest=/tmp/test1.txt

shellcommands:
       master::
               "/bin/sh -c 'svn update /var/cfengine/masterfiles/inputs >/dev/null 2>/dev/null'"


#cfrun.hosts
domain = home
master1.home
client1.home


#cfservd.conf
groups:
        policyhost = ( master1 )

control:
        domain = ( home )
        TrustKeysFrom = ( 192.168.1 )
        AllowUsers = ( root )
        AllowAccessFrom = ( 192.168.1 )
        AllowMultipleConnectiosFrom = ( 192.168.1 )
        #Use this in dhcp environment 
        #DynamicAddresses = ( 192.168.1 )
        cfrunCommand = ( "/var/cfengine/bin/cfagent" )

        IfElapsed = ( 0 )
        ExpireAfter = ( 15 )
        MaxConnections = ( 50 )
        MultipleConnections = ( true )

grant:
        any::
                /var/cfengine/inputs   *.home
                /var/cfengine/masterfiles/inputs   *.home
                /var/cfengine/bin/cfagent  *.home
                /var/cfengine *.home
                /tmp 192.168.1.0/24


#update.conf
control:
        actionsequence  = ( copy )
        domain          = ( home )
        policyhost      = ( master1 )
        IfElapsed       = ( 0 ) 

        master_cfinput  = ( /var/cfengine/masterfiles/inputs )
        repository      = ( /var/cfengine/outputs )

        copy:
                $(master_cfinput)/cfagent.conf dest=/var/cfengine/inputs/cfagent.conf
                                               mode=600
                                               server=$(policyhost)
                                               force=true
                                               trustkey=true

                $(master_cfinput)/update.conf dest=/var/cfengine/inputs/update.conf
                                              server=$(policyhost)
                                              force=true
                                              trustkey=true

                $(master_cfinput)/cfservd.conf dest=/var/cfengine/inputs/cfservd.conf
                                               server=$(policyhost)
                                               force=true
                                               trustkey=true

                $(master_cfinput)/cfrun.hosts dest=/var/cfengine/inputs/cfrun.hosts
                                              server=$(policyhost)
                                              force=true
                                              trustkey=true


Initially import these files into SVN:
svn import /tmp/cfwork file:///usr/local/svn/repos/cfwork -m "Initial import"

Create our working directory:
cd /home
svn checkout file:///usr/local/svn/repos/cfwork

Initially checkout the files to /var/cfengine/masterfiles/inputs:
cd /var/cfengine/masterfiles
svn checkout file:///usr/local/svn/repos/cfwork
mv cfwork inputs

Later on we go to /home/cfwork, work on the configuration files and commit/update everything like this:
cd /home/cfwork
svn commit -m "Test"
svn update /var/cfengine/masterfiles/inputs



(3. Set up Bind on the master)
Bind is not necessarily needed for this example environment. Just in case you want to use it, here is how to create a basic setup.
emerge -va bind

Edit/create the following three files
/etc/bind/named.conf:
options {
        directory "/var/bind";

        listen-on-v6 { none; };
        listen-on { 127.0.0.1; 192.168.1.34; };

        pid-file "/var/run/named/named.pid";
};

zone "." IN {
        type hint;
        file "named.ca";
};

zone "localhost" IN {
        type master;
        file "pri/localhost.zone";
        allow-update { none; };
        notify no;
};

zone "127.in-addr.arpa" IN {
        type master;
        file "pri/127.zone";
        allow-update { none; };
        notify no;
};

zone "home" IN {
        type master;
        file "pri/home";
        allow-update { none; };
        notify no;
};

zone "0.168.192.in-addr.arpa" IN {
        type master;
        file "pri/home_rev";
        allow-update { none; };
        notify no;
};

The last two entries are relevant for our setup. The first one defines the forward zone which is configured in /etc/bind/pri/home. The second one defines the reverse zone which is configured in /etc/bind/pri/home_rev.
/etc/bind/pri/home:
$TTL 1W
@       IN      SOA     ns.localhost. root.localhost.  (
                                      2007060201 ; Serial
                                      28800      ; Refresh
                                      14400      ; Retry
                                      604800     ; Expire - 1 week
                                      86400 )    ; Minimum
@               IN      NS      ns
ns              IN      A       127.0.0.1

master1         IN      A       192.168.1.34
client1         IN      A       192.168.1.33

Don't forget to set the actual Serial: yearmonthdaynr
/etc/bind/pri/home_rev:
$TTL 1W
@       IN      SOA     ns.localhost. root.localhost.  (
                                      2007060201 ; Serial
                                      28800      ; Refresh
                                      14400      ; Retry
                                      604800     ; Expire - 1 week
                                      86400 )    ; Minimum
@               IN      NS      ns
ns              IN      A       127.0.0.1

34              IN      PTR     master1.home.
33              IN      PTR     client1.home.

Also here, don't forget the Serial and the dots at the end of the PTR records.

If you want to use Bind /etc/resolv.conf must contain the IP of master1.
nameserver 192.168.1.34

/etc/hosts must contain these records:
127.0.0.1  localhost
192.168.1.34 master1.home master1 
192.168.1.33 client1.home client1

Now let's (re)start the daemons:
/etc/init.d/named restart
/etc/init.d/svnserve restart

For the initial start of cfservd we copy the configuration files manually from the repository to /var/cfengine/inputs:
cp /var/cfengine/masterfiles/inputs/* /var/cfengine/inputs
cfservd -f /var/cfengine/inputs/cfservd.conf
(cfservd is the main daemon. It runs on the master and all clients by using the files in /var/cfengine/inputs.)



4. Set up Cfengine on the client
The last step is to set up Cfengine on the client machine.
emerge -va cfengine

For the initial start of cfservd we copy the configuration files manually from the repository of the master:
scp 192.168.1.34:/var/cfengine/masterfiles/inputs/* /var/cfengine/inputs/
and start the daemon
cfservd -f /var/cfengine/inputs/cfservd.conf

If you use Bind /etc/resolv.conf must contain the IP of master1.
nameserver 192.168.134

/etc/hosts must contain these records:
127.0.0.1  localhost
192.168.1.34 master1.home master1 
192.168.1.33 client1.home client1


Now all is set up and we can make some tests. First we go back to the master and change a policy. Then we check if it is processed on the master:
cd /home/cfwork
vi cfagent.conf

In this example we want the file /tmp/test.txt to be copied to /tmp/test2.txt instead of /tmp/test1.txt. So we change the following line accordingly:
/tmp/test.txt dest=/tmp/test2.txt

Then create the source file /tmp/test.txt.
echo "Test" > /tmp/test.txt

(Make sure that /tmp/test2.txt does no exist.)
Commit und update with SVN:
svn commit -m "Test"
svn update /var/cfengine/masterfiles/inputs

Execute cfagent to apply the new policy:
cfagent -v

The output shows clearly that the policy rule was applied. Thus we find test2.txt in /tmp:
*********************************************************************
 Main Tree Sched: copy pass 1 @ Sat Jun  2 09:02:30 2007
*********************************************************************

Checking copy from localhost:/tmp/test.txt to /tmp/test2.txt
cfengine:mobile1: /tmp/test2.txt wasn't at destination (copying)
cfengine:mobile1: Copying from localhost:/tmp/test.txt
cfengine:mobile1: Object /tmp/test2.txt had permission 600, changed it to 644

Ok, on the master it works as expected. Finally we want cfagent to run on all clients. One possibility is to manually execute cfrun:
cfrun -d -f /var/cfengine/inputs/cfrun.hosts
This command executes cfagent on all clients defined in cfrun.hosts (on the master as well as it is listed in cfrun.hosts). As this is the first communication between master and client, you will be prompted to accept the public keys of the clients.

On client1 we see that cfagent has copied /tmp/test.txt to /tmp/test2.txt which is exactly what we wanted. It shows that the communication between master and client works and that the client can now be controlled by Cfengine.

The next steps are:
+Securing the installation, now it's completely open!
+Use cfenvd for anomaly detection
+Use cron or cfexecd to automatically run cfagent at certain intervals


So far for the example Cfengine setup. From here it should be easy to further explore the world of configuration management with cfengine. The best resource I found is the new
booklet from sage.org. It's really great and makes things very clear.

Alternatives for Cfengine are Puppet and bcfg.