Abstract: Part 2 of a series, this article covers Cfengine public key encryption, running the necessary daemons, and writing some Cfengine scripts for use in a centralized environment.
In the previous article, Centralized Host Configuration With
Cfengine, I covered the basic principles of Cfengine, the suite of Cfengine tools, and
their installation. In this article, I'll cover the use of Cfengine public
key encryption, running the necessary daemons, and writing some Cfengine
scripts for use in a centralized environment. I'll also discuss using
Cfengine to help automate machine installs via jumpstart and
provide a pointer to information on automatically pulling Cfengine scripts
and files from a source code repository such as CVS.
Secure Authentication Using Public Keys
To work in a distributed environment, Cfengine negotiates a secured
transaction between hosts using public key
encryption (see Cfengine configuration directory). To generate the keys on each host, run the command
cfkey. This places the machine's public key in
/var/cfengine/ppkeys/localhost.pub and the private key in
/var/cfengine/ppkeys/localhost.priv. The server's public key
needs to be copied over to each client as
/var/cfengine/ppkeys/192.168.1.1.pub, assuming that the server's
IP is 192.168.1.1. Each client key must be copied to the server, also using
the IP address as an identifier. A client with the IP of 198.162.1.254 would
store its public key on the server as
/var/cfengine/ppkeys/root-192.168.1.254.pub.
These steps are optimally performed when a machine is jumpstarted so that a
secure connection exists from the initial installation. The postinstall
script of the Cfengine package, installed via scripts during a jumpstart, can
create a local key if it does not already exist. The Cfengine package can
also contain the root key, or it can be copied over from the jumpstart server
as a regular file during the jumpstart. With the former, the Cfengine package
must be rebuilt any time the server key changes. Once the keys are exchanged,
the clients and server can authenticate and communicate securely.
Starting the Necessary Daemons and Programs
The server, and possibly each client, will need to run the programs
cfenvd and cfservd. These programs can be started
at boot time by creating init files and placing them in
/etc/init.d and linking them from the appropriate
/etc/rc.d directories. A sample cfservd init file
would contain:
#!/bin/sh
#
# Start the cfservd, the cfengine server daemon
#
CFINPUTS=/var/cfengine/inputs
PATH=/var/cfengine/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin
export CFINPUTS PATH
PID=`/bin/pgrep -d '' -x cfservd`
if [ -f /var/cfengine/bin/cfservd ]
then
case $1 in
start)
if [ "X$PID" = "X" ]; then
echo "Starting cfservd..."
cfservd
else
echo "cfservd may already be running."
fi
exit 0
;;
stop)
if [ "X$PID" != "X" ]; then
pkill cfservd
fi
exit 0
;;
*)
echo "usage: $0 {start|stop}"
;;
esac
fi
For cfenvd, replace occurrences of cfservd with
cfenvd in the above init file.
Any machine running cfservd needs a configuration file,
/var/cfengine/inputs/cfservd.conf. The syntax of this
configuration file is modeled on Cfengine's configuration file syntax, but you
cannot mix the contents of the two files. Similarly, it uses classes to label
entries, so one configuration file controls all hosts on the network. The
configuration file can be split into modules by using groups and
imports. The cfservd.conf file lists
the classes of hosts which obey the access and deny commands when the file is
parsed, not which classes of hosts have access to files and directories. An
cfservd.conf file might look like the following if it allows
access from all hosts in my.domain and the class C sized networks
192.168.1.0 and 192.168.2.0:
###############################################################################
# Who and what we allow access to, and who we trust
###############################################################################
control:
domain = ( my.domain )
cfrunCommand = ( "/var/cfengine/bin/cfagent" )
MaxConnections = ( 15 )
MultipleConnections = ( true )
IfElapsed = ( 60 )
DenyBadClocks = ( false )
TrustKeysFrom = ( 192.168.1.0/24
192.168.2.0/24 )
grant:
/var/cfengine
*.my.domain
192.168.1.*
192.168.2.*
/local/cfengine
*.my.domain
192.168.1.*
192.168.2.*
The update.conf Configuration File
The first step to any run of Cfengine should be to make sure that the
binaries and input files are up-to-date copies retrieved from the central
Cfengine server. To accomplish this, Cfengine first parses and runs the file
/var/cfengine/inputs/update.conf before the main configuration
whenever cfagent runs. The necessary update.conf
file on each machine would contain something like the following:
###############################################################################
#
# update.conf - ensure that the inputs and binaries in /var/cfengine are
# synced to those on the server. This runs before the main configuration
# file, so we always have a way to fix broken configurations or programs that
# were mistakenly pushed out.
#
###############################################################################
control:
actionsequence = ( copy processes tidy ) # sequence of actions to
# perform
domain = ( my.domain ) # our domain
DefaultCopyType = ( checksum ) # Copy based on checksum, not
# mtime
# Which host/dir is the master for configuration and binary files
cfhost = ( cfengine.my.domain ) # host running cfd
workdir = ( /var/cfengine ) # the workdir
inputs_src = ( /local/cfengine/inputs ) # location of inputs on the
# master host
bin_src = ( /usr/local/sbin ) # location of binaries on the
# master host
input_mode = ( 700 ) # mode for input files
bin_mode = ( 755 ) # mode for binaries
fowner = ( root ) # file owner
ogroup = ( other ) # file group
AddInstallable = ( new_cfenvd new_cfservd ) # classes which may become
# defined during a cfengine run
any:: # the class to match
SplayTime = ( 10 ) # max number of minutes over
# which cfengine will share
# its load on the server
############################################################################
copy:
$(inputs_src) dest=$(workdir)/inputs
recurse=inf
owner=$(fowner) group=$(ogroup)
mode=700
exclude=*~ # ignore emacs backup files
exclude=#* # ignore CVS conflict files
server=$(cfhost) trustkey=true
$(bin_src)/cfagent dest=$(workdir)/bin/cfagent
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
$(bin_src)/cfdoc dest=$(workdir)/bin/cfdoc
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
$(bin_src)/cfenvd dest=$(workdir)/bin/cfenvd
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
# copied a new one
define=modified_cfenvd
$(bin_src)/cfenvgraph dest=$(workdir)/bin/cfenvgraph
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
$(bin_src)/cfexecd dest=$(workdir)/bin/cfexecd
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
$(bin_src)/cfkey dest=$(workdir)/bin/cfkey
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
$(bin_src)/cfrun dest=$(workdir)/bin/cfrun
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
$(bin_src)/cfservd dest=$(workdir)/bin/cfservd
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
# copied a new one
define=modified_cfservd
$(bin_src)/cfshow dest=$(workdir)/bin/cfshow
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
$(bin_src)/vicf dest=$(workdir)/bin/vicf
owner=$(fowner) group=$(ogroup)
mode=$(bin_mode)
backup=false
server=$(cfhost) trustkey=true
#####################################################################
processes:
# restart cfservd and/or cfenvd if we received new copies
modified_cfservd::
"cfservd" signal=term restart /var/cfengine/bin/cfservd
modified_cfenvd::
"cfenvd" signal=kill restart "/var/cfengine/bin/cfenvd -H"
#####################################################################
tidy:
# delete output files older than 31 days so we don't accumulate too many
$(workdir)/outputs pattern=* age=31
With an up-to-date copy of all of the binaries and input files, Cfengine is
ready to move onto the main configuration file, cfagent.conf.
Sample cfagent.conf Configuration
Files
Let's take a look at a simple but useful cfagent.conf. The
following example contains seven different sections, import,
control, processes, copy,
directories, files, and tidy. The
control section defines a number of global variables and an
actionsequence. Each section called by the
actionsequence has the ability to apply to certain classes or to
any class at all, and this program shows several implementations of such.
###############################################################################
import:
###############################################################################
/var/cfengine/inputs/groups # define a bunch of groups
###############################################################################
control:
###############################################################################
any::
## Basic Cfengine configurations
access = ( root )
editfilesize = ( 0 ) # 0 turns off the editfilesize limit.
netmask = ( 255.255.255.0 ) # Sets the netmask if you use netconfig.
timezone = ( EST )
nfstype = ( nfs )
## How and Where do we output stuff?
sysadmin = ( cfengine@my.domain )
smtpserver = ( mx1.my.domain )
EmailMaxLines = ( inf ) # No maximum lines
OutputPrefix = ( "" )
## How many cfengines can run, what do we do when the server's busy
SplayTime = ( 0 ) # Run everything immediately
MaxCfengines = ( 5 )
## Set some defaults
DefaultCopyType = ( checksum ) # Copy based on checksum, not mtime
DeleteNonUserFiles = ( false ) # Do not delete unowned files
Exclamation = ( on ) # Inform us of problems.
ExpireAfter = ( 60 ) # Don't let cfengine run longer then 60 minutes.
HostnameKeys = ( off ) # Store keys based on hostname, not IP
Inform = ( on ) # Show diagnostic output
ShowActions = ( off ) # Do not show verbose diagnostic output
domain = ( ExecResult(/bin/domainname) ) # Obtain our domain name
# automatically
cfhost = ( cfengine.my.domain )
workdir = ( /var/cfengine )
srcdir = ( /local/cfengine )
bin_mode = ( 755 ) # mode for binaries
fowner = ( root ) # file owner
ogroup = ( other ) # file group
sgroup = ( sysadmin ) # sysadmin group
actionsequence = ( processes copy directories files tidy )
###############################################################################
processes:
###############################################################################
aix::
"inetd" action=warn
"usr/local/sbin/httpd" restart
"/usr/local/bin/apachectl graceful>/dev/null 2>&1"
###############################################################################
copy:
###############################################################################
# Copy over additional OS specific input files
solaris::
$(srcdir)/solaris dest=$(workdir)/inputs/ recurse=inf server=$(cfhost)
aix::
$(srcdir)/aix dest=$(workdir)/inputs/ recurse=inf server=$(cfhost)
###############################################################################
directories:
###############################################################################
# Check/fix permissions on some important directories
solaris::
/usr/local/bin mode=$(bin_mode)
owner=$(fowner) group=$(ogroup)
action=fixall
/usr/local/lib mode=$(bin_mode)
owner=$(fowner) group=$(ogroup)
action=fixall
/usr/local/sbin mode=$(bin_mode)
owner=$(fowner) group=$(ogroup)
action=fixall
/usr/local/secure mode=0700
owner=$(fowner) group=$(sgroup)
action=fixall
###############################################################################
files:
###############################################################################
# Check/fix permissions on some important files
/etc/group mode=0644
owner=$(fowner) group=$(fowner)
action=fixall
/etc/passwd mode=0644
owner=$(fowner) group=$(fowner)
action=fixall
/etc/shadow mode=0600
owner=$(fowner) group=$(fowner)
action=fixall
/etc/sudoers mode=0440
owner=$(fowner) group=$(fowner)
action=fixall
###############################################################################
tidy:
###############################################################################
# clean up some tmp and core files
solaris::
/var/crash pat=*core age=14 r=inf rmdirs=true inform=true
any::
/tmp pat=* age=14 r=inf rmdirs=true inform=false
/tmp pat=core* age=0 r=inf inform=true
home pat=*~ age=14 r=inf inform=false
Here's another copy section from a Cfengine script that distributes Solaris
patches and patch_order files to various OS types. It purges
files on the client that are not also on the server, so no out-of-date patches
are kept on the clients. The system administrator can later apply the patches during a
maintenance window (unattended patching isn't recommended).
This Cfengine snippet performs a number of security functions like
comparing md5 checksums, turning off suid bits on root-owned files, and fixing
permissions on some files and directories.
###############################################################################
control:
###############################################################################
ChecksumDatabase = ( /var/cfengine/cache.db )
ChecksumUpdates = ( false )
###############################################################################
filters:
###############################################################################
{
root_owned_files
Owner: "root"
Result: "Owner"
}
###############################################################################
files:
###############################################################################
# Find all suid files that we don't know about, and remove the suid bit
# also checksum other files while we're at it (you may want to tailor this
# so md5 checksums are not done on ALL files but those listed below, e.g. home
# directories. MD5 checksumming is resource intensive.
solaris::
/
filter=root_owned_files
mode=u-s # no SUID bit may be set
recurse=inf
action=fixall
checksum=md5
inform=true
# Do not modify files that SHOULD be SUID root (partial listing, use find
# to determine the correct files on your system)
ignore=/usr/bin/sparcv7/ps
ignore=/usr/bin/sparcv7/uptime
ignore=/usr/bin/sparcv7/w
ignore=/usr/bin/at
.
.
.
ignore=/files/usr/local/bin/sudo
any::
# make sure the permissions on the md5 database are good
/var/cfengine/cache.db mode=600 owner=root group=other action=fixall
# make sure /etc/sudoers exists and has the right permissions
/etc/sudoers mode=0440 owner=root group=root action=touch
###############################################################################
directories:
###############################################################################
mailservers::
# make sure the spool dirs have the right permissions
/var/spool/mqueue mode=0700 owner=root group=other action=fixall
/var/spool/clientmqueue mode=0770 owner=smmsp group=smmsp action=fixall
/etc/mail mode=0755 owner=root group=other action=fixall
any::
# create a /root directory where we will store root's home files
/root mode=0700 owner=root group=other action=touch recursive=0
Once a cfagent.conf file is in place, set up cfagent to run
from cron to parse and run cfagent.conf:
00 23 * * * /var/cfagent/bin/cfagent
You can also add cron entries for cfagent to run other files besides the
default cfagent.conf by specifying the --file
flag:
Cfengine can be used in conjunction with jumpstart technology to
automatically configure a host upon installation. Jumpstart finish scripts
can place an init script on the client machine so that when it reboots, it
connects to the Cfengine server, handing off its key and downloading and
running a first set of scripts. These scripts can set up
initial accounts, configure Ethernet interfaces, add entries to
/etc/resolv.conf, and do anything else that can be written into a script. The
following example comments out entries in /etc/inetd.conf, disables rhosts
auth files, kills off inetd entirely, and makes sure that some EEPROM settings
are set correctly.
###############################################################################
files:
###############################################################################
any::
/var/log/ipflog mode=0644
owner=root group=sys
action=touch
###############################################################################
editfiles:
###############################################################################
sunos_5_8 | sunos_5_9::
{ /etc/inetd.conf
# comment out everything in /etc/inetd.conf
HashCommentLinesContaining "tcp6"
HashCommentLinesContaining "udp6"
HashCommentLinesContaining "tcp"
HashCommentLinesContaining "udp"
HashCommentLinesContaining "rpc"
}
{ /etc/init.d/inetsvc
# comment out inetd in /etc/init.d/inetsvc
HashCommentLinesContaining "/usr/sbin/inetd"
}
sun4u::
{ /etc/services
# add cfengine to /etc/services so we can call the port number by name
AppendIfNoSuchLine "cfengine 5308/tcp"
}
any::
{ /etc/syslog.conf
# add a line for ipfilter
AppendIfNoSuchLine "local1.info /var/log/ipflog"
DefineClasses "modified_syslog"
}
###############################################################################
disable:
###############################################################################
# rename the specified files to <filename>.cfdisabled and make them mode 600
any::
/root/.rhosts
/etc/hosts.equiv
###############################################################################
shellcommands:
###############################################################################
sun4u::
# make sure that the eeprom is set the way we want
"/usr/sbin/eeprom 'diag-level=min'"
"/usr/sbin/eeprom 'diag-device=net'"
"/usr/sbin/eeprom 'auto-boot?=true'"
"/usr/sbin/eeprom 'watchdog-reboot?=false'"
"/usr/sbin/eeprom 'local-mac-address?=true'"
"/usr/sbin/eeprom 'use-nvramrc?=true'"
###############################################################################
processes:
###############################################################################
# kill inetd if it's running
"inetd" signal=term
# The /etc/syslog.conf file was modified, so we need to HUP syslogd
modified_syslog::
"syslogd" signal=hup
Using Revision Control to Store Cfengine Configuration
Files
Luke Kanies has written an article
on integrating CVS with Cfengine. His
Cfengine scripts perform a cvs update and obtain files straight
from the CVS repository. With this method, a clear revision history is always available for every file that is changed within Cfengine. Keeping files
under some sort of remotely accessible revision control system makes
centralized administration of configuration files even easier once the initial
setup work is done.