Skip to content

Managing Systems with Foreman and Puppet

Purpose

Puppet and Foreman are commonly used in labs for configuration management and system control due to their complementary roles.

Puppet provides a way to define and enforce system configurations through code, which helps maintain consistency across multiple machines.

Foreman acts as an External Node Classifier which is fancy-talk for 'automatically grouping and classifying which modules to assign to systems'. It also provides a nice web UI that tracks your managed assets.

Together, they enable centralized control, making it easier to manage lab environments, automate routine tasks, and maintain system configurations efficiently.

Foreman has many features. We'll only use those that extend Puppet to manage Environments and HostGroups. Auto-provisioning on boot, DNS management, subnet management, integration with hypervisors, and all the other things Foreman can do are not covered here.


Requirements

In order to complete this guide, you will need:

If you do not have an authoritative DNS resolver on your network, you can put the Foreman's hostname/IP address mapping into each client's /etc/hosts file.

IMPORTANT: Ensure your Ubuntu server instance is configured with a static IP address and has an FQDN with a hostname of puppet or foreman. If you do not use either of these hostnames, you will run into problems with SSL certificates that are not trivial to resolve. A hostname like foreman.internal.my-domain.com works well. Ensure your /etc/hosts file on your Foreman system has been updated with its static IP address and the system's FQDN.


Installing and Configuring Foreman

Installing Foreman

The Foreman Quickstart Guide has instructions for installing Foreman. The guide covers installing and performing the initial configuration of your Puppetmaster.

Make sure you are using the latest version of the Foreman installer. The quickstart guide web page will complain at the top if you are not viewing the latest version.

After completing the Foreman installer steps, log into the Foreman Web UI as the 'admin' user. Save the password somewhere useful, and you won't be told what it is again.

If you need to reset the credentials for the 'admin' user, run sudo foreman-rake permissions:reset on the Foreman system.


Puppet Modules

A Puppet module is a structured package of code and data used to manage specific configurations or tasks on a system. It contains all the necessary files, such as manifests, templates, and files, to define and enforce the desired state of a resource or service. Modules are reusable, can be shared across different environments, and make organizing and applying configurations consistently across multiple systems easier.

For example, you may have a Puppet module for Nginx, another for MySQL, and another one that manages local user accounts.


Foreman HostGroups

Foreman's HostGroups organize Puppet modules into groups.

For example, the Nginx and MySQL modules may be combined into a HostGroup named 'WebStack'. Assigning the 'WebStack' HostGroup to a system would ensure the Nginx and MySQL modules were installed on the target.


Foreman Environments

Foreman's Environments map to directories on disk. This allows you to check out different versions of your Puppet code into different environments. This is useful when you want to test changes to Puppet code without merging the master branch.


Example Puppet Modules

Example Puppet modules below explain:

  • how to manage a local user account
  • how to install and configure Nginx

Puppet is written in Ruby, and Puppet code is written to conform to the Puppet DSL.

Puppet modules are contained in their own directories. Name the directory the same name as the module.

Puppet module names cannot have a dash (-) character in them. Use an underscore (_) instead.

Each module must have an init.pp file in the modules/ directory.

Static files for each module go into the files/ directory.

Templates for each module go into the templates/ directory.


Managing User Accounts

Managing a local user's account on a Linux or FreeBSD machine with Puppet is straightforward.

There are several things you need to keep track of for a user's shell account:

  • the username
  • the user's password hash or SSH key used to authenticate
  • the group ID the account belongs to
  • the user ID for the user
  • should this user be able to sudo?
  • does this user's home dir exist?
  • are the permissions on the user's home dir correct?
  • does this user have useful dotfiles installed so their shell is useable?

All of these things can easily be managed with a Puppet manifest.

This example will create and manage a local shell account named 'myaccount'.

To create a module that manages local users named local_users, create a directory named local_users and manifests and files directories inside it:

mkdir local_users
mkdir local_users/manifests local_users/files

Create a file named local_users/manifests/init.pp that will contain the instructions for managing local users.

The class name should match the module and directory names.

Start with a header specifying the types of systems to run this code on. You'll use this for every module you write:

class local_users {
  case $::osfamily {
    'redhat':  {} # redhat/centos/rocky
    'freebsd': {} # FreeBSD
    'debian':  {  # debian based systems
      case $::operatingsystemmajrelease {
        '20.04': {} # ubuntu 20.04
        '22.04': {} # ubuntu 22.04
      }
    }
  }
}

Place your code in the appropriate section for your target. This guide will use Ubuntu 22.04 system as the target.

You can generate a SHA512 password hash manually by running:

mkpasswd -m sha-512 MySecurePassword

Add code that defines the user's group and shell account inside the proper scope. This example only ensures that the account exists on Ubuntu 22.04 systems. If it was included after the 'debian': { line and before the case $:operatingsystemmajrelease { line, the account would be managed on all Debian-class systems.

class local_users {
  case $::osfamily {
    'redhat':  {} # redhat/centos/rocky
    'freebsd': {} # FreeBSD
    'debian':  {  # debian based systems
      case $::operatingsystemmajrelease {
        '20.04': {} # ubuntu 20.04
        '22.04': {  # ubuntu 22.04
          # create a group named 'myaccount' 
          group {'myaccount':
            ensure => 'present',
            name   => 'myaccount',
            gid    => 1000,
          }

          # create a user named 'myaccount'
          user {'myaccount':
            ensure   => 'present',
            home     => '/home/myaccount',
            password => '$6$GKD9hZsgVD76ucjV$omVt7ZtV9GDU5VaJynUyaJ1GVbrTofkNOlj7o1J9/s5Gxkhgih7ZyAfvj13U3bX.HwA9qGS/9Cl2gH2M0Nntt.',
            shell    => '/bin/bash',
            uid      => 1000,
            groups   => ['adm','cdrom','sudo','dip','plugdev','myaccount'],
          }
        }
      }
    }
  }
}

You're not done; the user needs a home directory and dotfiles.

Add code that manages the user's home directory:

class local_users {
  case $::osfamily {
    'redhat':  {} # redhat/centos/rocky
    'freebsd': {} # FreeBSD
    'debian':  {  # debian based systems
      case $::operatingsystemmajrelease {
        '20.04': {} # ubuntu 20.04
        '22.04': {  # ubuntu 22.04
          # create a group named 'myaccount' 
          group {'myaccount':
            ensure => 'present',
            name   => 'myaccount',
            gid    => 1000,
          }

          # create a user named 'myaccount'
          user {'myaccount':
            ensure   => 'present',
            home     => '/home/myaccount',
            password => '$6$GKD9hZsgVD76ucjV$omVt7ZtV9GDU5VaJynUyaJ1GVbrTofkNOlj7o1J9/s5Gxkhgih7ZyAfvj13U3bX.HwA9qGS/9Cl2gH2M0Nntt.',
            shell    => '/bin/bash',
            uid      => 1000,
            groups   => ['adm','cdrom','sudo','dip','plugdev','myaccount'],
          }

         # create the user's home directory
          file {'/home/myaccount':
            ensure  => 'directory',
            owner   => 'myaccount',
            group   => 'myaccount',
            mode    => '0700',
            require => User['myaccount'],
          }          
        }
      }
    }
  }
}

Puppet now manages your user's home directory.

Now, add an SSH key so you can log in securely.

Create a file in the module named files/local_users/authorized_keys.myaccount. This file should contain the SSH public key(s) for users you want to be able to SSH into the system with no password. If you don't want to manage SSH public keys with Puppet, remove or comment out the file directive below for the /home/myaccount/.ssh/authorized_keys file.

Add the definitions to manage the user's files to the manifest:

class local_users {
  case $::osfamily {
    'redhat':  {} # redhat/centos/rocky
    'freebsd': {} # FreeBSD
    'debian':  {  # debian based systems
      case $::operatingsystemmajrelease {
        '20.04': {} # ubuntu 20.04
        '22.04': {  # ubuntu 22.04
          # create a group named 'myaccount' 
          group {'myaccount':
            ensure => 'present',
            name   => 'myaccount',
            gid    => 1000,
          }

          # create a user named 'myaccount'
          user {'myaccount':
            ensure   => 'present',
            home     => '/home/myaccount',
            password => '$6$GKD9hZsgVD76ucjV$omVt7ZtV9GDU5VaJynUyaJ1GVbrTofkNOlj7o1J9/s5Gxkhgih7ZyAfvj13U3bX.HwA9qGS/9Cl2gH2M0Nntt.',
            shell    => '/bin/bash',
            uid      => 1000,
            groups   => ['adm','cdrom','sudo','dip','plugdev','myaccount'],
          }

          # create the user's home directory
          file {'/home/myaccount':
            ensure  => 'directory',
            owner   => 'myaccount',
            group   => 'myaccount',
            mode    => '0700',
            require => User['myaccount'],
          }

          # create the user's ~/.ssh directory
          file {'/home/myaccount/.ssh':
            ensure  => 'directory',
            owner   => 'myaccount',
            group   => 'myaccount',
            mode    => '0700',
            require => User['myaccount'],
          }

          # the public key used to SSH into the system
          file {'/home/myaccount/.ssh/authorized_keys':
            ensure  => 'present',
            owner   => 'myaccount',
            group   => 'myaccount',
            mode    => '0600',
            source  => 'puppet:///modules/local_users/authorized_keys.myaccount',
            require => File['/home/myaccount/.ssh'],
          }
        }
      }
    }
  }
}

You probably want to manage the shell's dotfiles so there is a decent environment when you get a shell on the system. Grab the default shell files that adduser installs for a new shell user on your system. This is usually:

  • ~/.profile
  • ~/.bashrc
  • ~/.zshrc (if you use zsh)

but it depends on the shell your system uses.

Copy these files into the local_users/files/ directory. Remove the starting . from the filename so you have profile, bashrc, and zshrc.

Add Puppet code that manages these files for your user account:

class local_users {
  case $::osfamily {
    'redhat':  {} # redhat/centos/rocky
    'freebsd': {} # FreeBSD
    'debian':  {  # debian based systems
      case $::operatingsystemmajrelease {
        '20.04': {} # ubuntu 20.04
        '22.04': {  # ubuntu 22.04
          # create a group named 'myaccount' 
          group {'myaccount':
            ensure => 'present',
            name   => 'myaccount',
            gid    => 1000,
          }

          # create a user named 'myaccount'
          user {'myaccount':
            ensure   => 'present',
            home     => '/home/myaccount',
            password => '$6$GKD9hZsgVD76ucjV$omVt7ZtV9GDU5VaJynUyaJ1GVbrTofkNOlj7o1J9/s5Gxkhgih7ZyAfvj13U3bX.HwA9qGS/9Cl2gH2M0Nntt.',
            shell    => '/bin/bash',
            uid      => 1000,
            groups   => ['adm','cdrom','sudo','dip','plugdev','myaccount'],
          }

          # create the user's home directory
          file {'/home/myaccount':
            ensure  => 'directory',
            owner   => 'myaccount',
            group   => 'myaccount',
            mode    => '0700',
            require => User['myaccount'],
          }

          # create the user's ~/.ssh directory
          file {'/home/myaccount/.ssh':
            ensure  => 'directory',
            owner   => 'myaccount',
            group   => 'myaccount',
            mode    => '0700',
            require => User['myaccount'],
          }

          # the public key used to SSH into the system
          file {'/home/myaccount/.ssh/authorized_keys':
            ensure  => 'present',
            owner   => 'myaccount',
            group   => 'myaccount',
            mode    => '0600',
            source  => 'puppet:///modules/local_users/authorized_keys.myaccount',
            require => File['/home/myaccount/.ssh'],
          }

          file {'/home/myaccount/.profile':
            ensure => 'present',
            owner  => 'myaccount',
            group  => 'myaccount',
            mode   => '0644',
            source => 'puppet:///modules/local_users/profile',
            require => File['/home/myaccount'],
          }

          file {'/home/myaccount/.bashrc':
            ensure => 'present',
            owner  => 'myaccount',
            group  => 'myaccount',
            mode   => '0644',
            source => 'puppet:///modules/local_users/bashrc',
            require => File['/home/myaccount'],
          }

          file {'/home/myaccount/.zshrc':
            ensure => 'present',
            owner  => 'myaccount',
            group  => 'myaccount',
            mode   => '0644',
            source => 'puppet:///modules/local_users/zshrc',
            require => File['/home/myaccount'],
          }
        }
      }
    }
  }
}

You now have a managed user account on your system.


Installing a Package

This example installs the snmpd daemon onto an Ubuntu 22.04 or 24.04 system. It also install a configuration file for the daemon into /etc/snmp/snmpd.conf.

This is a new module in Puppet, so create a new module directory named snmpd and the associated directories:

mkdir snmpd
mkdir snmpd/manifests snmpd/files

Create the init.pp file that is the entry point for this module with the contents and instruct Puppet to ensure the snmpd package is installed:

class snmpd {
  case $::osfamily {
    'redhat':  {} # redhat/centos/rocky
    'freebsd': {} # FreeBSD
    'debian':  {  # debian based systems
      case $::operatingsystemmajrelease {
        '20.04', '22.04': {
          # ensure the snmpd package is installed
          package {'snmpd':
            ensure => 'installed',
          }
        }
      }
    }
  }
}

To install your own /etc/snmpd.conf file, put the file into snmpd/files/snmpd.conf and tell Puppet to manage it.

Create snmpd/files/snmpd.conf with the contents:

# Managed by Puppet

syslocation MyInternalNetwork
syscontact [email protected]
sysservices 79
rocommunity stats
rouser cacti auth
createUser cacti MD5 secretpassword DES otherpassword

This tells the snmp daemon to respond to SNMPv2 requests that use the community string 'stats'. It will also respond to SNMPv3 requests that use MD5/DES with 'secretpassword' / 'otherpassword'. You should change these to something unique.

Modify the init.pp file to include directives that manage the file on the target. Notice the 'requires' directive that ensures the package is installed before Puppet tries to deal with the configuration file.

class snmpd {
  case $::osfamily {
    'redhat':  {} # redhat/centos/rocky
    'freebsd': {} # FreeBSD
    'debian':  {  # debian based systems
      case $::operatingsystemmajrelease {
        '20.04', '22.04': {
          # ensure the snmpd package is installed
          package {'snmpd':
            ensure => 'installed',
          }

          # manage the snmpd.conf file
          file {'/etc/snmp/snmpd.conf':
            owner    => 'root',
            group    => 'root',
            mode     => '0700',
            source   => 'puppet:///modules/snmpd/snmpd.conf',
            requires => Package['snmpd'],
          }
        }
      }
    }
  }
}

We should also add some directives that ensure the snmpd servie is enabled and running. Now that we are managing the service, we can tell Puppet to restart the snmp daemon if the configuration file changes using the notify directive.

class snmpd {
  case $::osfamily {
    'redhat':  {} # redhat/centos/rocky
    'freebsd': {} # FreeBSD
    'debian':  {  # debian based systems
      case $::operatingsystemmajrelease {
        '20.04', '22.04': {
          # ensure the snmpd package is installed
          package {'snmpd':
            ensure => 'installed',
          }

          # manage the snmpd.conf file, restart it if the config file changes
          file {'/etc/snmp/snmpd.conf':
            owner    => 'root',
            group    => 'root',
            mode     => '0700',
            source   => 'puppet:///modules/snmpd/snmpd.conf',
            notify   => Service['snmpd'],
            require  => Package['snmpd'],
          }

          # ensure the snmpd service is running and starts on boot
          service {'snmpd':
            ensure  => running,
            enable  => true,
            require => File['/etc/snmp/snmpd.conf'],
          }
        }
      }
    }
  }
}

Installing Puppet Client on Targets

Now that a Puppetmaster exists, you can install and configure the Puppet Agent on a system and manage it.

This example uses an Ubuntu 22.04 LTS server as the managed system.

Install the Puppet Agent

Install the Apt repository definition and GPG key for the repository, update the local Apt database, and install the Puppet Agent:

wget https://apt.puppetlabs.com/puppet7-release-jammy.deb
sudo dpkg -i puppet7-release-jammy.deb
sudo apt update
sudo apt -y install puppet-agent

The Puppet Agent configuration file is in /etc/puppetlabs/puppet/puppet.conf and should have the contents:

[main]
server = foreman.internal.my-domain.com
environment = production
runinterval = 15m

Register the System with the Puppetmaster

Run Puppet on the system to be managed once to bootstrap the client and register it with the Puppetmaster:

puppet agent -t

Puppet won't take any action on the managed system; you must sign the system's key on Foreman first. This is a gatekeeping procedure so no one can add systems to your Puppetmaster.

Signing New System's Keys

On the Foreman server as the root user, list all of the Puppet keys:

puppetserver ca list --all

You should see your system in the list. Sign the key:

puppetserver ca sign --certname="myclient.internal.my-domain.com"

If you rebuild a system and need to reset its keys, you can clean the old keys from the Puppetmaster:

puppetserver ca clean --certname="myclient.internal.my-domain.com"

Run Puppet Again

Now that your target's key has been signed on the Puppetmaster, you must re-run Puppet on the target so it appears in Foreman's Web UI:

puppet agent -t

Log into the Foreman Web UI, click on 'Hosts' in the navigation menu, then click on 'All Hosts' to see a list of all hosts managed by your Foreman instance.

Assign Modules

Assigning modules to a system will ensure Puppet enforces those manifests on the target.

Edit the system you just created, click on 'Puppet ENC', and assign some modules.

Re-run Puppet on the client; the modules should execute and manage the software or users.

puppet agent -t

Enable Puppet Service

Enable the Puppet Agent to start on boot:

systemctl enable puppet