BigAdmin System Administration Portal
Feature Article
Print-friendly VersionPrint-friendly Version

Centralized Host Configuration With Cfengine, Part II

Amy Rich, May, 2005

Centralized Host Configuration With Cfengine, Part I

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.

Contents:


Introduction

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).


###############################################################################
copy:
###############################################################################

  # Copy over additional OS specific input files
  sunos_5_8::
    $(patchdir)/5.8 dest=$(patchdir)/ recurse=inf
                    owner=$(fowner) group=$(ogroup)                   
                    server=$(cfhost) purge=true

  sunos_5_9::
    $(patchdir)/5.9 dest=$(patchdir)/ recurse=inf
                    owner=$(fowner) group=$(ogroup)                   
                    server=$(cfhost) purge=true

  sunos_5_10::
    $(patchdir)/5.10 dest=$(patchdir)/ recurse=inf
                     owner=$(fowner) group=$(ogroup)                   
                     server=$(cfhost) purge=true

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:

00 23 * * * /var/cfagent/bin/cfagent --file /var/cfengine/inputs/security.conf

Using Cfengine in Conjunction With JumpStart

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.


Resources

Unless otherwise licensed, code in all technical manuals herein (including articles, FAQs, samples) is provided under this License.


BigAdmin