Blog
Cfengine Cookbook
Abstract:
Contents
- Introduction
-
Help with Cfengine
-
Initial setup
-
Bootstrapping clients
-
Version Control
-
DMZ mirror
-
Organizing Cfengine files
-
Custom classes
-
Package management
-
Distributing public keys
-
CRON tables
-
DNS services
-
Process control
-
Editing files
-
Editing complex host specific files
-
Application deployment
-
Pushing changes immediately
-
Running manual commands on all clients
-
Retrieving remote files
-
Redundancy and load balancing
-
Security policies
-
Audits
-
Disaster recovery
- Working with free time
Introduction
Cfengine is a configuration management tool that can centrally manage the configuration of UNIX servers. When discussing configuration management a common limiting thought is that it applies only to like systems. Render farms and other distributed server groups are typically configured nearly identical. It is in this environment that many administrators think to deploy a tool like Cfengine.
Even heterogeneous systems all have common configurations. A typical group of UNIX servers, regardless of their different functions, have many things in common. Package installations, NTP clients, resolv.conf, log rotation, ssh keys, sudoers files and general temp directory cleanups can all be common among different hosts. Centralizing these things makes a sysadmin more efficient.
What about centralizing an application? Consider a typical commercial LAMP web server. The server must be configured with SSL certs, Apache virtual hosts, database connectors and new content. Using Cfengine all of these things could be defined externally from the server itself.
What you end up with is a blue print for your servers and server based services that is stored in one place, has history and is backed up. This blue print can be used again and again to replace or deploy new servers and server based services with minimal repetition.
Interested? The recipes contained in this tome represent a small sample of how Cfengine can be used to make a sysadmin's work day more productive and elevate him or her from sysadmin to super sysadmin. This is very handy during review time.
Help with Cfengine
Problem
You know nothing about Cfengine.
Solution
Cfengine is configuration management system that allows a sysadmin to define the ideal state of a host. As a Cfengine agent runs on the host it makes changes to ensure that the host matches the defined state.
It is not the purpose of this paper to teach the reader how to set up their own Cfengine service. To learn more about Cfengine and how to implement it please refer to the following sites:
An introduction to Cfengine
http://www.onlamp.com/pub/a/onlamp/2004/04/15/Cfengine.html
Cfengine website
http://www.Cfengine.org
Sys Admin magazine
http://www.samag.com/documents/s=9936/sam0601e/0601e.htm
A System Engineer's Guide to Host Configuration and
Maintenance Using Cfengine
http://www.sage.org/pubs/16_cfengine/
Initial setup
Problem
You want to configure an initial setup.
Solution
Cfengine is a client server application and needs configuration for both its client and its server. These files are called cfagent.conf and cfservd.conf respectively. The file update.conf is a special file used by the client. This file is parsed first before the client opens cfagent.conf. Using this method it is possible to create a stable, basic method to update cfagent.conf. This is desirable as complex cfagent.conf files can sometimes develop syntax errors by the sysadmin's own mistake. This can render the cfagent.conf file inoperable. By ensuring that update.conf is stable and simple, errors in the cfagent.conf file are easy to correct.
The following is an example of a client, in the network 172.16.1.0/24 is configured to communicate with one server with the IP address of 172.16.1.66. This is presented as an example but not with a full explanation. Such explanations are better left to Cfengine's official documentation.
#
# update.conf
#
# This file hold initial configuration information for cfengine clients.
#
# DO NOT EDIT THIS FILE UNLESS YOU REALLY KNOW WHAT YOU ARE DOING
#
# This file should hold JUST enough information to create a working
# client. This ensures that if other configuration information is in
# error this file will always work.
control:
actionsequence = ( directories copy links shellcommands tidy )
domain = ( example.com )
trustkey = ( true )
#
# Master server configuration
policyhost = ( 172.16.48.66 )
#
# Other files for distribution
masterfiles = ( /var/cfengine/masterfiles )
#
# Master configuration files
masterinputs = ( ${masterfiles}/config/inputs )
#
# Other Variables
workdir = ( /var/cfengine )
cf_install_dir = ( /usr/local/sbin )
#
# Ease traffic flow by deferring cleint connections by a random interval.
SplayTime = ( 5 )
directories:
/var/cfengine/bin
#
# Now we want to distribute the update.conf and cfengine binary files to
# the client.
copy:
#
# Add cfengine .conf files.
${masterinputs}/conf/
dest=${workdir}/inputs/
r=inf mode=600
type=checksum
ignore=.svn
server=${policyhost}
#
# Add .Cfengine import files
${masterinputs}/imports
dest=${workdir}/inputs/
r=inf mode=600
type=checksum
ignore=.svn
server=${policyhost}
#
# Copy binaries for Redhat AS3
redhat_as_3::
${masterfiles}/config/bin/redhat_as_3
dest=${cf_install_dir}
r=inf
mode=755
ignore=.svn
backup=false
type=checksum
server=${policyhost}
# define this class to indicate that files were copied
define=bin_update
#
# Copy binaries for Redhat AS4
redhat_as_4::
${masterfiles}/config/bin/redhat_as_4
dest=${cf_install_dir}
r=inf
mode=755
ignore=.svn
backup=false
type=checksum
server=${policyhost}
# define this class to indicate that files were copied
define=bin_update
links:
/var/cfengine/bin/cfagent -> /usr/local/sbin/cfagent
shellcommands:
#
# If binaries were updated above then restart.
bin_update::
"/bin/sh -c '/sbin/service cfexecd.sv restart'"
"/bin/sh -c '/sbin/service cfservd.sv restart'"
tidy:
#
# Clean up old working files
${workdir}/outputs pattern=* age=7
# EOF
#
# cfagent.conf
#
control:
domain = ( example.com )
#
# Other files for distribution
masterfiles = ( /var/cfengine/masterfiles )
#
# Master server configuration
server = ( 172.16.48.66 )
workdir = ( /var/cfengine )
actionsequence = (
directories
resolve
disable
copy
links
editfiles
files
shellcommands
processes
packages
tidy )
#
# For pacakge management
DefaultPkgMgr = ( rpm )
RPMInstallCommand = ( "/usr/sbin/up2date --nox %s" )
#
# Tidy old entries from resolv.conf
EmptyResolvConf = ( true )
#
# When should the cfexecd client wake up
schedule = ( Min05_10 Min25_30 Min45_50 )
#
# Log any actions that Cfengine takes.
any::
Syslog = ( true )
SyslogFacility = ( LOG_DAEMON )
#
# Here we import or include separate config files
import:
any::
# Always import classes FIRST
classes.cf
config::
masterfiles.cf
any::
bind.cf
cfservd.cf
cfexecd.cf
cron.cf
db2.cf
env.cf
httpd.cf
iptables.cf
mast.cf
misc.cf
ntp.cf
openssh.cf
packages.cf
resolv.cf
sendmail.cf
serial.cf
stunnel.cf
sudo.cf
syslog.cf
tomcat.cf
# EOF
#
# cfservd.conf
#
# This file configures cfservd, the server daemon.
control:
domain = ( example.com )
#
# Trust keys, one time, from my network
TrustKeysFrom = ( 172.16.1.0/24 )
trustkey = ( true )
#
# These options allow remote cfrun commands
AllowUsers = ( root )
cfrunCommand = ( "/usr/local/sbin/cfagent" )
any::
#
# Traffic control to prevent clients for overloading
# The server with simultaneous requests.
IfElapsed = ( 1 )
ExpireAfter = ( 15 )
MaxConnections = ( 50 )
# This allows the server to update itself using cfrun
AllowMultipleConnectionsFrom = ( 172.16.1.66 )
grant:
#
# Grant host access for clients to access master files
/var/cfengine/masterfiles/ 172.16.1.0/24
#
# Grant access for server to allow cfrun remote command.
/usr/local/sbin/cfagent 172.16.1.66
# EOF
Note that the imported *.Cfengine files are not included in this example. These will be used as the Cfengine service expands to control services in the network.
Bootstrapping clients
Problem
You want to bootstrap a host to be a Cfengine client.
Solution
As with most things in the UNIX world there is more than one way to accomplish this. The manual method would involve creating the directory hierarchy under /var/cfengine. Then the cfagent, cfkey and update.conf files would be installed on the system. A key is generated using the cfkey program. Finally the cfagent program is run for the first time.
A more automated approach might involve using GNU Make. A makefile could be used to bundle a tar ball on the Cfengine server. The tar ball is then copied to the would be client and extracted. A makefile in the tar ball is used on the client to configure, install and start the agent. The example below assumes that all Cfengine binaries and configuration files are stored in a hierarchy, preferably a version control repository, as shown below.
./inputs/ ./inputs/imports ./bin/ ./bin/redhat_as_3 ./bin/redhat_as_4
#
# makefile for bootstrapping Cfengine
#
# This file will build a boot strap client.
SHELL=/bin/bash
AWK=/bin/awk
SVN=/usr/local/bin/svn
PUBLICKEY=root-172.16.1.66.pub
WORKDIR=/var/cfengine
BINDIR=/usr/local/sbin
TARFILES=makefile bin/${VERSION}/cfagent bin/${VERSION}/cfkey bin/${VERSION}/cfexecd inputs/conf/update.conf init.d/cfexecd.sv ppkeys/${PUBLICKEY} REVISION.txt
INSTALLFILES= ${BINDIR}/cfagent ${BINDIR}/cfkey ${BINDIR}/cfexecd ${WORKDIR}/inputs/update.conf ${WORKDIR}/bin /etc/rc.d/init.d/cfexecd.sv /etc/rc.d/rc3.d/S51cfexecd.sv
# Vpath works like PATH. The inputs directory will be
# searched for any .conf files that are referenced.
vpath %.conf inputs
vpath %.sv init.d
.PHONY: help
help:
make --print-data-base --question | ${AWK} '/^[^.%][-A-Za-z0-9]*:/ { print substr($$1, 1, length($$1)-1) }' | sort
# USAGE:
# make client VERSION=[redhat_as_3 | redhat_as_4]
# make install
# -- makes client tarball to be copied to new client.
#
# Copy tarball to client system.
# make install: install tarball on client.
# Make client tarball to be copied to client.
.PHONY: client
# Tarball file that will be copied to the client.
client: ${TARFILES}
tar -cvzpf cfengine-bs-v$(shell cat REVISION.txt).tgz $^
rm REVISION.txt
# server public key
${PUBLICKEY}:
cp /var/cfengine/ppkeys/${PUBLICKEY} ${PUBLICKEY}
REVISION.txt:
${SVN} info | ${AWK} '/^Revision.*/ { print $$2 }' > $@
# Install client
.PHONY: install
install: ${INSTALLFILES}
ln -s ${BINDIR}/cfexecd ${WORKDIR}/bin/cfexecd
ln -s ${BINDIR}/cfagent ${WORKDIR}/bin/cfagent
${BINDIR}/cfkey
cp ppkeys/* ${WORKDIR}/ppkeys
chown root:root ${WORKDIR}/ppkeys/*
chmod 600 ${WORKDIR}/ppkeys/*
service cfexecd.sv start
# Run cfagent to receive first updates.
${BINDIR}/cfagent:
cp bin/redhat_as_?/cfagent $@
chown root:root $@
chmod 755 $@
${BINDIR}/cfkey:
cp bin/redhat_as_?/cfkey $@
chown root:root $@
chmod 755 $@
${BINDIR}/cfexecd:
cp bin/redhat_as_?/cfexecd $@
chown root:root $@
chmod 755 $@
${WORKDIR}/inputs/update.conf: ${WORKDIR}/inputs
cp inputs/*_0/update.conf $@
chown root:root $@
chmod 644 $@
${WORKDIR}/ppkeys: ${WORKDIR}
mkdir ${WORKDIR}/ppkeys
chown root:root $@
chmod 700 $@
${WORKDIR}/bin: ${WORKDIR}
mkdir ${WORKDIR}/bin
chown root:root $@
chmod 700 $@
${WORKDIR}/inputs: ${WORKDIR}
mkdir ${WORKDIR}/inputs
chown root:root $@
chmod 700 $@
${WORKDIR}:
mkdir $@
chown root:root $@
chmod 755 $@
/etc/rc.d/init.d/cfexecd.sv: ${BINDIR}/cfexecd
cp init.d/cfexecd.sv /etc/rc.d/init.d
chmod 755 $@
/etc/rc.d/rc3.d/S51cfexecd.sv: /etc/rc.d/init.d/cfexecd.sv
ln -s $< $@
# EOF
Using this makefile a client tar ball is created with ``make client''. The tar ball is copied to the client host and extracted. Inside is a makefile. The command ``make install'' will install and start the client.
Gravy
Add Cfengine to your automated installs, like kickstart, by having the installer download and install the tar ball.
Version Control
Problem
How can I combine Cfengine with version control to keep history and have an audit trail available?
Solution
In recipe 3 we noted that the Cfengine master files were stored in /var/cfengine/masterfiles. It is this location that Cfengine has access to revision controlled files. In this example we'll use Subversion. Subversion is a revision control system similar to CVS. In this instance one or more Subversion repositories can be hosted locally on the Cfengine master host or on a remote host. The method of file retrieval is the same. In this example Cfengine checks the most recent copy of a repository into a preexisting working copy. Cfengine considers this working copy to be its set of master files.
#
# masterfiles.cf
#
# This ensures that the masterfiles are current on policy servers
copy:
# Update copy of masterfiles on dmz from internal server.
dmz_mirror::
${masterfiles}/config
dest=${masterfiles}/config
r=inf mode=600
type=checksum
# Skip SVN control files.
ignore=.svn
# Skip internal only files.
ignore=internal
ignore=172_16_*
# Skips docs
ignore=docs
# Do not keep old files
purge=true
backup=false
server=${server}
shellcommands:
# Update cfengine repositories
config_server::
"/bin/sh -c 'cd ${masterfiles}/config; /usr/local/bin/svn update'"
This example performs two tasks. The first is to copy selective master files from the internal master server to a mirror (see recipe 6). The second task is a shell command that updates the current working copy of Cfegine's master files from the Cfengine master files Subversion repository. Note that during your initial setup of this working copy a manual check out will need to be performed for the first time. Afterward this update should work automatically.
Gravy
Cfengine can use this method to distribute other files such as DNS files. This can allow your hostmaster access to his DNS repository but still have Cfengine distribute the changes. See recipe 12.1 for more details.
DMZ mirror
Problem
Cfengine client hosts query a master server for updates. This ``pull'' method of communication can break firewall policies.
Solution
Cfengine is often described as using a ``pull'' method for client server communication. In other words the client initiates a connection with the master server. Since the master server is typically located on an internal network and some clients can be located on DMZ network security policies can be broken. The way to minimize this security exception is to create a DMZ mirror.
A DMZ mirror is a Cfengine server located on the DMZ network. This server pulls selected master files from the internal master server and in turn acts as the master server for DMZ clients.
Consider this addition to the masterfiles.cf file demonstrated in recipe 5.
copy:
# Update copy of masterfiles on dmz from internal server.
dmz_mirror::
${masterfiles}/config
dest=${masterfiles}/config
r=inf mode=600
type=checksum
# Skip SVN control files.
ignore=.svn
# Skip internal only files.
ignore=internal
ignore=172_16_*
# Skips docs
ignore=docs
# Do not keep old files
purge=true
backup=false
server=${server}
In this copy action master files are copied or downloaded to the DMZ mirror server. Files that are relevant to internal hosts only or sensitive documentation are skipped using the ignore clause. Now clients located on the DMZ network can query the DMZ server for relevant changes without allowing them access to the internal network. The DMZ mirror server still needs access to the internal network to contact the master server but this is now limited to a single host that can be hardened.
Organizing Cfengine files
Problem
As a Cfengine configuration grows how should it be organized to keep it maintainable?
Solution
A good way to organize a Cfengine configuration files is by service. When Cfengine is configured to manage a service it is often convenient to keep the configuration in a separate file that is imported into the cfagent.conf file. For example, consider a sudoers file.
In the cfagent.conf file import the sudoers related actions.
import:
any::
sudoers.cf
The sudoers file would look like this.
#
# sudo.cf
#
copy:
${masterfiles}/config/sudo/sudoers
dest=/etc/sudoers
server=${server}
owner=root
group=root
encrypt=true
mode=440
type=checksum
This instructs the client to copy the sudoers file from the
Cfengine server to the destination on the client. Because the
sudoers file contains sensitive information the copy operation is
encrypted. Since the class is not listed before the copy Cfengine
defaults to the any class. Thus the
sudoers file is maintained on all Cfengine clients.
Now we see that the sudoers service is organized into two parts. The first is the configuration file sudoers.cf that is imported into the cfagent.conf file. The second is the sudoers files which is listed in the master files hierarchy. Expanding on this could lead to cfagent.conf imports like this:
import:
any::
bind.cf
cron.cf
httpd.cf
iptables.cf
misc.cf
ntp.cf
openssh.cf
resolv.cf
sendmail.cf
serial.cf
stunnel.cf
sudo.cf
syslog.cf
tomcat.cf
The master files hierarchy on the Cfengine server might look like this:
./bin ./cron ./httpd ./init.d ./inputs ./iptables ./misc ./ntp ./openssh ./ppkeys ./stunnel ./sudo ./syslog ./tomcat
Custom classes
Problem
How do I group certain hosts into a single class for an action?
Solution
Note that I mentioned a classes.cf file in recipe 7. This file is used to configure custom classes.
classes:
any::
# DB2 servers
db2 = ( db2srv1 db2srv2 db2test db2mirror )
# DNS slaves
dnsslave = ( ipv4_10_0_0_1 ipv4_10_0_0_2
ipv4_10_0_0_3 ipv4_10_0_0_4 )
# group of servers to patch
update01 = ( dsnslave db2 )
The class ``db2'' includes four hostnames. The class
``dnsslave'' includes four IP addresses. Note that the underscore
is used instead of a period in the addresses. Cfengine interprets
the period as a logical 'and' in classes. Thus
dnsslave.Hr20 is interpreted by Cfengine to mean any
host of the dnsslave class and of the hour twenty ( eight PM )
class.
Package management
Problem
You want to ensure that certain packages are installed in some or all of your hosts.
Solution
Cfengine is able to manage packages for multiple UNIX distributions including Solaris, Red Hat and Debian. In this example we'll configure Cfengine for use with Red Hat hosts. First, Cfengine needs to know what package manager to use by default. This is placed in the control section of the cfagent.conf file:
DefaultPkgMgr = ( rpm ) RPMInstallCommand = ( "/usr/sbin/up2date --nox %s" )
You may want to have a packages.cf file that is imported by cfagent.conf. In that file we configure which packages are managed and how.
packages:
any::
screen action=install
sysstat action=install
vim-enhanced action=install
rpm-devel action=install
rpm-build action=install
compat-db action=install
ntp action=install
The above action ensures that the named RPMs are installed if they are not present. Note that versions are not important in this example. However, it is possible to instruct Cfengine to maintain the packages based the package version.
You could have a packages section in any other service related import file. For example, if you had an ntp.cf file to handle network time configuration you could include a packages section to ensure that any needed NTP packages were installed.
Gravy
Being a lazy sysadmin I don't like to manually update basic, non-masked RPMs on certain Red Hat hosts that I manage. I configure Cfengine to perform these updates automatically for me.
update01.Monday.Hr20::
"/usr/sbin/up2date --nox -u"
ifelapsed=60
expireafter=60
The class ``update01'' is a class that contains an arbitrary
group of hosts that I would like to update. The compound class is
interpreted as any host that is a member of the ``update01''
class and is a member of the day class ``Monday'' and is a member
of the time class ``Hr20''. This means that all ``update01'' host
will be updated, using up2date, on Mondays at 2000
hours. The ``ifelapsed'' statement tells Cfengine to run this
command only if at least sixty minutes has elapsed since the last
time it was run. This ensures that up2date will not
run multiple times during the eight o'clock hour even if Cfengine
runs more than once per hour. The ``expireafter'' statement tells
Cfengine to kill this job if it takes more than sixty minutes to
complete. This is good housekeeping and helps guard against any
run away jobs.
Distributing public keys
Problem
You want to distribute your public SSH key to all hosts.Solution
This is a relatively simply task using Cfengine's files and directories actions.
directories:
# Create .ssh directorise
# Neil Watson
/home/nhwatson/.ssh
mode=700
owner=nhwatson
group=nhwatson
copy:
# Copy authorized_keys file.
# Neil Watson
${masterfiles}/config/openssh/nhwatson/authorized_keys
dest=/home/nhwatson/.ssh/authorized_keys
server=${server}
mode=644
owner=nhwatson
group=nhwatson
type=checksum
In this configuration we first ensure that the user's .ssh directory is created and is set to the correct mode of ``0700''. In the copy action we copy a stored public key to the remote host and ensure that the mode is set to ``0644''.
Gravy
This method could be expanded to include an entire user environment. Files like.profile, .vimrc and even whole
directories such as .vim could all be managed and
distributed using this method.
CRON tables
Problem
Have you ever had someone ask you to change a production cron table entry but, that someone cannot describe what host the cron table on is or for what user? To avoid this you'd like to centralize all cron tables to Cfengine.Solution
If your crons are very convoluted and dependant upon one another you may want to consider using an enterprise scheduler. However, Cfengine can still be helpful in organizing cron tables.Cfengine can work as a scheduler without using cron at all. Consider this example.
#
# crons.cf
#
classes:
firstweekday = ( (Day1|Day2|Day3).!(Saturday|Sunday) )
import:
# Jobs that run at 0000 hours
Hr00:: dailyjobs.cf
Hr05::
# Nightly ELT jobs.
etl.cf
# Nightly reports
reports.cf
# Jobs that run every Monday
Monday:: mondayjobs.cf
# Jobs that run the first weekday of the month
firstweekday:: firstweekday.cf
Organizing in this way ensures that Cfengine did not have to import and parse any files that are not due. For example, on Tuesdays the mondayjobs.cf file would not be imported.
The imported file will contain the information Cfengine needs to determine what job to run and on which client. For example:
#
# dailyjobs.cf
#
actionsequence = ( copy shellcommands )
# Ensure that script is up to date
copy:
webapps01::
${masterfiles}/config/jobs/webapps01/clean.sh
dest=/home/secacct/bin/clean.sh
mode=700
backup=false
owner=sevacct
group=secacct
type=checksum
# Run up to date script
shellcommands:
tor_lx_webapps01::
"/home/devacct/bin/clean.sh"
owner=sevacct
group=sevacct
ifelapsed=60
The ``ifelapsed=60'' clause ensures that the shell command is only executed once per hour even if cfagent is run more than once. Using this method Cfengine controls both when the script is run and the contents of the script .
One caveat to this set up is reliability. If cron jobs are for production work then the Cfengine server may need to be redundant or even highly available in order to ensure that these jobs are run consistently. If a Cfengine cluster is not available it is suggested that Cfengine be used to distribute cron table files instead. This still offers a central copy of all jobs but allows the highly available production servers to run them autonomously. For example:
# cron.cf
tor_lx_svn::
# cron tables.
${masterfiles}/config/cron/internal/tor_lx_svn
dest=/var/spool/cron/
server=${server}
r=inf
ignore=.svn
owner=root
group=root
encrypt=true
mode=600
type=checksum
# script for root's crontab
${masterfiles}/infotech/subversion/svn_backup.sh
dest=/usr/local/bin/svn_backup.sh
server=${server}
owner=root
group=root
mode=755
type=checksum
# script for hostmaster's crontab
${masterfiles}/infotech/dns/whois.pl
dest=/usr/local/bin/whois.pl
server=${server}
owner=root
group=root
mode=755
type=checksum
In above example the cron tables, typically found in /var/spool/cron are copied from the master files location to the target host. Additionally, certain cron entries use custom scripts. This scripts are also copied by Cfengine from its master files location.
Regardless of which method is used, finding a specific cron entry is now simplified. Recursive text searches are now possible.
DNS services
Problem
You want use Cfengine to manage your DNS services.
Solution
Since the Bind DNS daemon uses normal text files Cfengine is a natural choice to manage these files. Additionally since Cfengine using version control all changes to Bind files are recorded for audits and roll backs.Expanding on recipe 5 we can add a new repository strictly for Bind files and configure Cfengine to keep its working copy current. Using a separate repository allows a separation of duties amongst the sysadmin team. Now certain team members will be able to manage DNS services without needing access to Cfengine's own configuration.
copy:
# Update copy of masterfiles on dmz from internal server.
dmz_mirror::
# Update copy of config masterfiles on dmz from internal server.
${masterfiles}/bind
dest=${masterfiles}/bind
r=inf mode=600
type=checksum
# Skip SVN control files.
ignore=.svn
# Skip bcp files
ignore=bcp
# Skip internal only files.
ignore=internal
ignore=172_16_*
# Skips docs
ignore=docs
# Do not keep old files
purge=true
backup=false
server=${server}
shellcommands:
# Update cfengine repositories
config_server::
"/bin/sh -c 'cd ${masterfiles}/bind; /usr/local/bin/svn update'"
Now we create Cfengine rules for managing Bind configurations.
#
# dns.cf
#
###################
# External bind services.
copy:
###################
# named.conf files
ext_dns_master::
${masterfiles}/bind/trunk/dmz/master/etc/named.conf
dest=/etc/named.conf
server=${server}
owner=root
group=named
mode=640
type=checksum
define=dns_reload
ext_dns_slave::
${masterfiles}/bind/trunk/dmz/slave/etc/named.conf
dest=/etc/named.conf
server=${server}
owner=root
group=named
mode=640
type=checksum
define=dns_reload
###################
# record files
ext_dns_master::
${masterfiles}/bind/trunk/dmz/master/var/named/data
dest=/var/named/data
server=${server}
recurse=inf
include=*.hosts
include=*.ptr
include=*.rev
include=db.*
owner=named
group=named
mode=640
type=checksum
define=dns_reload
ns2::
# loopback
${masterfiles}/bind/trunk/dmz/slave/var/named/slaves/db.127.0.0.ns2
dest=/var/named/slaves/db.127.0.0
server=${server}
owner=root
group=named
mode=640
type=checksum
define=dns_reload
ns3::
# loopback
${masterfiles}/bind/trunk/dmz/slave/var/named/slaves/db.127.0.0.ns3
dest=/var/named/slaves/db.127.0.0
server=${server}
owner=root
group=named
mode=640
type=checksum
define=dns_reload
ext_dns_master::
# root hint file
${masterfiles}/bind/trunk/dmz/db.cache
dest=/var/named/data/db.cache
server=${server}
owner=root
group=named
mode=640
type=checksum
define=dns_reload
ext_dns_slave::
# root hint file
${masterfiles}/bind/trunk/dmz/db.cache
dest=/var/named/slaves/db.cache
server=${server}
owner=root
group=named
mode=640
type=checksum
define=dns_reload
shellcommands:
dns_reload::
"/bin/sh -c '/sbin/service named reload'"
In this configuration there is one master Bind service and two slaves. All three are controlled by Cfengine which will ensure that the files are current and that Bind reloads the files when they change.
Process control
Problem
You want to ensure that a service is running on a host.Solution
The processes action allows Cfengine to test for running processes, start them or send any number of signals (e.g. kill, HUP, term). For example, suppose you wish for Cfengine to monitor a web server and ensure that its web service is always running.
processes:
webserver::
# ensure apache is running
"/usr/sbin/httpd"
matches=>4
signal=kill
restart "/sbin/service httpd restart"
The above example runs on any host that is a member of the ``webserver'' class. Cfengine examines the list of running processes and looks for the regular expression ``/usr/sbin/httpd''. If Cfengine finds four or less running processes then they are killed and the restart command is run.
The above processes example shows how to ensure a process is running. Suppose you want to ensure that a process is not running? Suppose a group of developers show ``initiative'' and install a telnet service on their development hosts. Telnet is strictly against our security policy. Educating the developers is a good step but Cfengine can help ensure that ``accidents'' don't happen.
processes:
development::
# ensure no telnet services are running
"telnet" match=<1 signal=kill
Above we target any ``development'' class hosts. Here Cfengine will attempt to match one or more processes that match the ``telnet'' regular expression and kill any that it finds.
Editing files
Problem
You need to edit some files in place without replacing them.Solution
Sometimes a configuration file provided by the package install is close enough to what you need that only a few minor changes are required. Having Cfengine manage the entire file is a little inefficient in such cases. Fortunately Cfengine is able to edit text files for you. This can allow you to change just a few part of the file. Consider this collection of examples:
editfiles:
any::
# Prevent ctl-alt-del from rebooting the system
{ /etc/inittab
CommentLinesMatching "ca::ctrlaltdel:/sbin/shutdown -t3 -r now"
}
In the tangle of KVM wires have you ever though that you had
issued a ctrl-alt-delete to Windows host only to realize with
horror that is was a Linux host and that host is now rebooting?
This editfiles clause ensures that our
hosts do not respond to a ctrl-alt-delete. It does so by
commenting, that is inserting a ``#'' character in front, in the
/etc/inittab file any line matching what is found in
quotations.
editfiles:
any::
# Rotated logs should be compressed
{ /etc/logrotate.conf
ReplaceFirst "^[[:space:]]*#[[:space:]]*compress[[:space:]]*$"
With "compress"
}
Many default log rotation configurations do not compress old
log files. If space is at a premium compression is a good way to
go. This clause uses a POSIX regular expression to replace the
first instance of the commented compress line with an uncommented
compress line in the /etc/logrotate.conf file.
editfiles:
any.!x86_64::
# Run sysstat every 5 minutes
# Note that these rules are different for 32bit versus 64 bit
# hosts.
{ /etc/cron.d/sysstat
ReplaceFirst "^\*/10.*sa1.*" With "*/5 * * * * root /usr/lib/sa/sa1 1 1"
Backup "false"
}
In this example the sysstat cron job is changed so that the
sysstat data collector runs every five minutes instead of every
ten. Note that the string to search for uses a regular expression
but the replacement string does not. Also note that the class
string omits 64bit hosts.
editfiles:
x86_64::
# Run sysstat every 5 minutes
# Note that these rules are different for 32bit versus 64 bit
# hosts.
{ /etc/cron.d/sysstat
ReplaceFirst "^\*/10.*sa1.*" With "*/5 * * * * root /usr/lib64/sa/sa1 1 1"
Backup "false"
}
This example is the almost the same as the previous one but
in this case it is for 64bit hosts.
From these four examples we can see that Cfengine has the ability to comment lines, perform a search and replace and use regular expressions. Cfengine's ability to edit files is extensive and often complex. Studying the official Cfengine manual is recommended.
Editing complex host specific files
Problem
You have file (e.g. snmpd.conf, sudoers) that is common for many hosts but with specific differences for many others.Solution
A simple solution would be to maintain a separate configuration file for each host. If much of the file is same for each host this becomes inefficient when changes are made. The goal of edit once, deploy everywhere would be lost. A better option would be to maintain a common template file. This file could be copied to target hosts and then edited for each class of host. Additionally, some services allow ``included'' import files. Those could be maintained for specific hosts and copied where needed.Using the copy action a template file could be centrally maintained and distributed.
# snmp.cf
copy:
snmp::
${masterfiles}/config/snmp/snmpd-temp.conf
dest=/etc/snmp/snmpd-temp.conf
server=${server}
owner=root
group=root
encrypt=true
type=checksum
mode=400
Here we copy a template snmpd.conf file, called
``snmpd-temp.conf''. Also note that we encrypt this copy operation
since the snmpd.conf file contains sensitive community strings.
Now we perform edits based on specific classes. Here we will use another feature of Cfengine's editfiles action. This feature is called expandvariables.
control:
# Define load for normal hosts
snmp.!highload::
load = ( load 2 2 2 )
# Define load for high load hosts
highload::
load = ( load 5 4 4 )
editfiles:
# Create SNMP file from template file
snmp::
{
/etc/snmp/snmpd.conf
EmptyEntireFilePlease
AutoCreate
Backup "false"
InsertFile "/etc/snmp/snmpd-temp.conf"
ExpandVariables "true"
DefineClasses "snmpdrestart"
}
files:
/etc/snmp/snmpd.conf mode=0600 owner=root group=root
shellcommands:
snmpdrestart::
"/bin/sh -c '/sbin/service snmpd restart'"
The example shows four distinct parts. The first part, control, defines Cfengine variables ``load'' to a value based on class. In this case we want the snmpd.conf file to have different load numbers to consider if the host is of the highload class versus any other SNMP class of host.
If you know a little about the Net-SNMP configuration file you know that there is a load stanza that looks like the variable names we've defined. However, in our template file we've listed this part as a Cfengine style variable called ``load''. You'll see why shortly.
The next part is the editfiles
action. In this part we identify the file, empty it to start
fresh, we ensure that Cfengine will create the file even it it
doesn't exist and we do not backup the old file. Next we insert
the template file into the empty file that was just created. Now
here is the magic. ExpandVariables "true" tells
Cfengine to read the file we've just created. When Cfengine finds
any Cfengine style variables (e.g. ${load}) it
expands them to their current values. In this example we defined
the current value of the ``load'' variable in the first step to
be either ``load 2 2 2'' or ``load 5 4 4''. Lastly we define a
new class called snmpdrestart each
time our target file (snmpd.conf) is edited.
Next is a files action. The editfiles action will result in a file with a mode of 644. An snmpd.conf file, which contains community strings and passwords should be less visible. Thus we use the files action to set the mode to 600 as well as set the owner and group.
The final part is a shellcommands action. This action restarts the snmpd service if the ``snmpdrestart'' class is defined. In reality this means that when the snmpd.conf file is changed the snmpd daemon is restarted.
Consider another example for monitoring disk space.
control:
# Define disk space
snmp.oracle::
diskoptoracle = ( "disk /opt/oracle 200000" )
editfiles:
# Create SNMP file from template file
snmp::
{
/etc/snmp/snmpd.conf
EmptyEntireFilePlease
AutoCreate
Backup "false"
InsertFile "/etc/snmp/snmpd-skel.conf"
ExpandVariables "true"
DefineClasses "snmpdrestart"
}
files:
/etc/snmp/snmpd.conf mode=0600 owner=root group=root
shellcommands:
snmpdrestart::
"/bin/sh -c '/sbin/service snmpd restart'"
Here we configure the SNMP daemon to monitor the available disk space of an Oracle partition. Note that the rest of this example is the same as the first. Thus in practice only the variables in the control section and perhaps your class definitions elsewhere need to be altered. The ExpandVariables action will work with any variables you have defined.
One last thing to consider is economy. Suppose you have Cfengine scheduled to run several times per hour. It might not be efficient to have Cfengine create this snmpd.conf file and restart the daemon for each run. If you like you could instruct Cfengine, using a time class, to perform this edit once an hour or even less.
editfiles:
# Create SNMP file from template file
snmp.Min15_20::
{
/etc/snmp/snmpd.conf
EmptyEntireFilePlease
AutoCreate
Backup "false"
IfElapsed 60
InsertFile "/etc/snmp/snmpd-skel.conf"
ExpandVariables "true"
DefineClasses "snmpdrestart"
}
Here we've added a time class of ``15_20'' minutes ensure
that this is run only at 15 to 20 minutes after the hour. Also the
``IfElapsed 60'' line tells Cfengine not to perform this operation
if it has already been performed less than 60 minutes ago.
Application deployment
Problem
In recipe 9 we looked at how to deploy RPMs using Cfengine. Suppose you have a custom application, like a web site, that needs to be deployed and maintained in production and possibly development and QA settings.Solution
Many web or middleware applications are simply a collection of files that are copied from one server to another as they move from development to QA and to production. J2EE applications should use WAR or EAR files for deployments. Applications that are built in different languages may not have any type of deployment tools. In such cases, Cfengine's copy action is a good method for ensuring that applications are kept current on the target servers.Suppose there is a web application that consists of a collection of HTML, PDF, CGI and Apache configuration files. This application goes through the standard development, QA and production cycles. Cfengine could be configured to copy these files to each of the separate development, QA and production servers. The magic here is in which master files Cfengine uses. A typical version control repository will consist of a trunk and various branches. The trunk is typically considered production and the branches can consist of development, QA and perhaps personal developer branches.
copy:
###############
# Production web services
###############
web01::
${masterfiles}/mywebapp/trunk/www
dest=/var/www/mywebapp
server=${server}
r=inf
ignore=.svn
owner=root
group=root
mode=644
type=checksum
backup=false
purge=true
${masterfiles}/mywebapp/trunk/etc/httpd/conf
dest=/etc/httpd/conf
server=${server}
r=inf
ignore=.svn
purge=true
backup=false
owner=root
group=root
mode=640
type=checksum
# This will be used for shellcommand below.
define=httpd_reload
${masterfiles}/mywebapp/trunk/etc/httpd/conf.d
dest=/etc/httpd/conf.d
server=${server}
r=inf
ignore=.svn
purge=true
backup=false
owner=root
group=root
mode=640
type=checksum
# This will be used for shellcommand below.
define=httpd_reload
###############
# development web services
###############
devweb01::
${masterfiles}/mywebapp/branches/dev/www
dest=/var/www/mywebapp
server=${server}
r=inf
ignore=.svn
owner=root
group=root
mode=644
type=checksum
backup=false
purge=true
${masterfiles}/mywebapp/branches/dev/etc/httpd/conf
dest=/etc/httpd/conf
server=${server}
r=inf
ignore=.svn
purge=true
backup=false
owner=root
group=root
mode=640
type=checksum
# This will be used for shellcommand below.
define=httpd_reload
${masterfiles}/mywebapp/branches/dev/etc/httpd/conf.d
dest=/etc/httpd/conf.d
server=${server}
r=inf
ignore=.svn
purge=true
backup=false
owner=root
group=root
mode=640
type=checksum
# This will be used for shellcommand below.
define=httpd_reload
###############
# QA web services
###############
qaweb01::
${masterfiles}/mywebapp/branches/qa/www
dest=/var/www/mywebapp
server=${server}
r=inf
ignore=.svn
owner=root
group=root
mode=644
type=checksum
backup=false
purge=true
${masterfiles}/mywebapp/branches/qa/etc/httpd/conf
dest=/etc/httpd/conf
server=${server}
r=inf
ignore=.svn
purge=true
backup=false
owner=root
group=root
mode=640
type=checksum
# This will be used for shellcommand below.
define=httpd_reload
${masterfiles}/mywebapp/branches/qa/etc/httpd/conf.d
dest=/etc/httpd/conf.d
server=${server}
r=inf
ignore=.svn
purge=true
backup=false
owner=root
group=root
mode=640
type=checksum
# This will be used for shellcommand below.
define=httpd_reload
shellcommands:
# Restat httpd services if configuration is changed
httpd_reload::
"/bin/sh -c '/sbin/service httpd reload'"
In this example production, development and QA Apache
configurations and content are maintained and copied as needed.
Notice that Apache is reloaded automatically when needed. Notice
also that legacy files will be purged
at the target location. This ensures that only the files that we
want there will reside there. Promotion from development to QA or
QA to production is done by simply merging branches in the
repository and waiting for Cfengine to notice the changes and
update them on the target hosts. Additionally revision control and
Cfengine logging will leave an audit trail of who promoted code and
when.
Gravy
Managing Apache SSL keys can be tedious. If you store your key files in a repository like the master files in the above example, Cfengine can distribute them as needed.
Pushing changes immediately
Problem
You've made emergency changes and need Cfengine to deploy them now.Solution
Typically Cfengine is a gradual convergent service. Over scheduled runs Cfengine will bring a host up to date. Sometimes you need Cfengine to fix a problem immediately. In such cases there are two options. The first is to log onto the target host and run the cfagent program manually. This is done as root. The length of time it takes to complete this operation can vary wildly depending on what has to be done and how busy the master server is so be patient. You may even have to run cfagent more than once if there are action dependencies.Since Cfengine is a system where the clients pull from the server the client cannot be instructed remotely from the server. However, it is possible to run a Cfengine server process on each target client. The master server can then use Cfengine's cfrun command to contact the client's server process and instruct it to run cfagent.
In recipe 3 we actually enabled our setup to use cfrun already. In the cfservd.conf file of that recipe there are four lines that reference the cfrun command:
.. # These options allow remote cfrun commands AllowUsers = ( root ) cfrunCommand = ( "/usr/local/sbin/cfagent" ) .. # This allows the server to update itself using cfrun AllowMultipleConnectionsFrom = ( 172.16.1.66 ) .. # # Grant access for server to allow cfrun remote command. /usr/local/sbin/cfagent 172.16.1.66This file controls the server component of Cfengine called cfservd. Our last step is to define a file called the cfrun.hosts
domain=example.com # Hosts that cfrun is allowed to connect to. # KEEP THIS UP TO DATE. 172.16.1.140 172.16.1.172 172.16.1.177 172.16.1.203 172.16.1.204 172.16.1.215 172.16.1.239 172.16.1.35 172.16.1.64 172.16.48.66 192.168.81.43This file tells the server which hosts it should connect to when a cfrun command is issued. If a host is not in this file cfrun will not make a connection attempt. Once this is in place it should be simple to force a client to contact the server by running the command
cfrun 172.16.1.35from the master server where 172.16.1.35 is the IP address of the remote client. Multiple IP addresses could also be listed. If no IP addresses are listed then cfrun will contact all hosts listed in the cfrun.hosts file. Since this activates the cfagent process on the remote client it will take a variable amount of time to run. For more information on cfrun please consult the official Cfengine documentation.
Running manual commands on all clients
Problem
Occasionally you wish to run a manual command on all clients. This command is run at your whim with no set schedule. How do you avoid logging on to each host manually?Solution
The traditional solution may be to create a custom SSH script that logs on to each machine sequentially and executes the command. Why go through that hassle when Cfengine can already do this for you? Suppose that as part of your disaster recovery plan requires that you be able to shut down all of your hosts in short order. During a power outage you may have only a few minutes to perform this shutdown before the backup batteries run dry. In such a situation a manual operation will take to long.The first step is to define a shell command within the cfagent.conf file.
shellcommands:
emergency_shutdown::
"/sbin/halt"
Now we have a special class called emergency_shutdown. What hosts are a member of
that class? None until you define it. Recall in recipe 17 that we used cfrun
to affect a ``push'' action. We will do so again here but we will
also define a class for all hosts at the same time.
cfrun -D emergency_shutdown
The above command instructs all remote clients, listed in the
cfrun.hosts file, to run immediately.
Additionally all clients will be members of the class
emergency_shutdown. The result is
that all clients run the halt command.
This concept can be expanding to include more commands. Temperamental services have been known to require manual restarts when things go wrong. A temporary fix would be to configure Cfengine to restart the service when an operator issues the appropriate cfrun command. An organization with a network monitoring system could automatically issue the cfrun command when it detects a failure. This can lead system administrators into the nirvana of a self healing network.
Retrieving remote files
Problem
You want to copy files, such as logs or backups, from the clients to the policy host.Solution
If the cfservd process is running on the clients then cfagent on the policy host can retrieve client files. There are two requirements, a grant command in cfservd.conf and at copy command in cfagent.conf
# cfservd.conf
# Grant access for server to allow picking up logs from
# clients
grant:
/var/log/updates/ 172.16.48.66
# cfagent.conf
control:
clients = ( host1:host2:host3 )
copy:
# copy results of update check from clients to cfserver
policyhost::
/var/log/updates/updates.log
dest=/var/log/updates/${this}.log
server=${clients}
The grant statement gives the policy host (IP 172.16.48.66)
access to /var/log/updates on the clients. The agent is given a
list, clients which it iterates over
during the copy stanza. The variable ${this} is
built in to Cfengine. It will be replaced by the current value in
the list.
Redundancy and load balancing
Problem
Your Cfengine deployment has scaled to many hosts. You would like to add more master hosts for redundancy and load balancing.Solution
Cfengine does not currently possess much in the way of redundancy or load balancing but, there are ways to achieve at least partial results. Strategies is a way for Cfengine to choose randomly from a group of classes. Each class in the group can be assigned a weight in the random selection process.
strategies:
{ random_policy_host
policyhost1: "1"
policyhost2: "1"
}
control:
polcyhost1::
policyhost = ( 172.16.48.66 )
polcyhost2::
policyhost = ( 172.16.48.67 )
In this example the strategies
section defines to classes, ``policyhost1'' and ``policyhost2''.
Each class is defined a weight of one. In the
control section each of the random classes defines a
variable called ``policyhost''. Cfengine will randomly choose to
define the server variable to 172.16.48.66 or 172.16.48.67. Since
each class is weighted as one there is a 50 percent chance for
each assignment. In previous recipes Cfengine code referred to a
${policyhost} variable. In the recipe 3 ``policyhost'' was defined as a single IP address.
Using the strategies method ``policyhost'' is defined randomly
each time cfagent runs. Thus during
each run about half of the Cfengine clients should contact each
policy host.
If one of your policy hosts should go down this example will ensure that the client will contact the running host half of the time. Your frequency of updates is about half of what it was before but, at least updates are still available. You could add more hosts to allow better redundancy and load balancing. You could also alter the weight of the random classes to account for more powerful or more reliable policy hosts.
Another redundancy option to consider is that of the client's cfagent process. This is typically controled by the daemon cfexecd which will execute the client according to the schedule defined in the agent configuration. Suppose the cfexecd daemon dies? Running cfexecd from a cron job will ensure even if the daemon dies the agent will still be run. Additionally, the agent itself can be configured to check for the cfexecd daemon process and start it if required. Using this method you'll be ensured that both the running daemon and the cron job will check up on each other.
Caveat
Since the client and policy host authenticate via key exchange be sure that the clients have the keys of all policy hosts and that the policy hosts each have all of the client keys.
Security policies
Problem
You need to maintain a tedious and constantly changing security policy across some or all of your hosts.Solution
Many aspects of a security policy define how a service or host should be configured. A typical security policy may touch upon PAM files, log file permissions, log history retention, home directory permissions and SSH configuration to name a few. Cfengine can be used to maintain all of these examples and more. In using Cfengine you are assured that hosts meet the current policy requirements. New hosts will have the policy automatically applied. Policy changes need only be defined in Cfengine which will then apply them automatically to all current and future hosts.
Audits
Problem
A security auditor wants to know how you track host configuration changes. Suppose a new configuration was deployed but failed. You want to determine when the deployment happened and what exactly was changed.Solution
All configurations are stored in Cfengine's master files location. The master files are actually a working copy of a Subversion, or other revision control repository. It is beyond the scope of this paper to explain the details of version control. However, with version control one can easily determine every line that has been changed, when that change was checked in and by whom.Additionally, using Cfengine's Syslog facility (see section 3) Cfengine will log to a host's syslog service any changes it makes or errors it encounters. Thus not only will you know how and when a file was changed you'll also know when Cfengine deployed it.
Disaster recovery
Problem
You need to recover a host which has had a catastrophic failure.Solution
The good news is that if this host is managed by Cfengine then your job may already be done. Imagine that all the previous recipes have been used to manage this failed host. Imagine that you have expanded these recipes to encompass other configurations that you wish to manage. With the failure of the host only a fresh install is an option. After the OS is installed and the network configured, boot strap Cfengine onto the host. Since this new host is replacing an old host its Cfengine classes will not change. Now Cfengine will automatically restore all the configurations, settings and services that it was managing on the failed host.Gravy
Since your configurations are all stored in a repository and copied to target hosts using Cfengine there may be no need to backup target hosts. As long as the revision control repositories are backed up you should be safe. Any file lost on a target host that exist in the repository will not only be available for restoration but will even be restored automatically by Cfengine.In some cases a hot spare host will be setup at a disaster recovery site. This host is configured to mirror a production host and take over in the event of failure. Often replicating changes to the off site host is done via SAN replication or proprietary replication services. Cfengine can do this with little effort. Since the production host is managed by Cfengine it is easy to add the spare host by a compound class or by defining your own classes. See the example below.
copy:
###################
# named.conf files for production AND disaster recovery hosts
ext_dns_master|ext_dns_master_dr::
${masterfiles}/bind/trunk/dmz/master/etc/named.conf
dest=/etc/named.conf
server=${server}
owner=root
group=named
mode=640
type=checksum
define=dns_reload
Working with free time
Problem
Now that Cfengine is managing your host's you find yourself with much more free time.Solution
Is this really a problem? I could suggest taking a vacation or working part time but a more practical suggestion would be to talk to your boss. If you have free time you or your boss will no doubt have a list of ``nice to have'' projects that have been in waiting. Having automated the day to day tasks you can become a contractor within your own company. Start those new projects. Offer your services to other departments for their projects. Make your boss, your department and you a more valuable asset to the organization. Be sure to remind your boss of this at review time.Neil Watson 2008-11-28
