Introduction
Recently, I have started managing a small fleet of servers for personal and research purposes. Managing a server or two isn’t that bad to do manually, but once you get much more than this, things like adding a new SSH key, installing a new piece of software, or making a configuration change across all of them becomes burdensome and repetitive. This negative effect compounds as you add systems and complexity.
There are also things that I end up doing almost universally when I provision new systems. Things like adding myself a user, configuring sudo, installing my dotfiles, installing tools I like to use, ssh hardening, unattended updates, … I know I do more than this and often I forget things when doing it manually.
I may do the same setting on two different systems in non-uniform ways that make automating the task in the future cumbersome. Things like installing software to different file paths or using non-matching file names in conf.d directories. The settings are functionally the same, but you can’t easily write a script to change settings within these because they’re different.
I could configure the system perfectly, but not take good documentation on how I got there. Typically when I am installing new software, it is an iterative process. I can make best guesses on how things should look, but the end result often changes as I encounter constraints, unforeseen issues, or simply learn more about the software I am using. Updates happen that introduce breaking changes or useful features, forcing me to correct them. A lot of mature software has the habit of running reliably for long periods of time with little oversight–shocker, i know. I have had stuff go for several years without needing much attention, but suddenly I need to move it, the hardware fails, or it reaches end of life and i have to install the next generation of software because the update process for the new software isn’t clean. Now I have to repeat the installation process but have no documentation on what i did or why and its been a few years since I thought about it.
This is a common problem in IT to some degree at every single organization I’ve worked at, consulted with, or heard about secondhand. Doing it perfectly where everything is set up correctly, completely, and everything is accounted for is profoundly difficult to do in practice.
I remember the concept explained plainly in The Phoenix Project. Something along the lines of “you should treat infrastructure like cattle, not pets”. This basically refers to uniformity, repeatability, and the ease of recovering from failures–its usually much easier to get back up and running if you have backups, documentation, and have actually tested the restoration/recovery processes a few times. A non-trivial chunk of system engineering is deploying and configuring services. A large chunk of this chunk can and should be automated. For various reasons, this doesn’t happen and the work gets done manually or in an ad-hoc manner, leading to non-uniformity, non-correctness, non-completeness, and configuration drift.
There are several methods to do this. Depending on your operation’s maturity, budget, etc. your options may be wildly different. Even something like some deploy scripts kept in Git that you copy and paste into a terminal to get things up and running is far better than nothing.
It has been several years since I’ve had to do any of this, but the time has come–I have a half dozen systems running and expect to add more, so its time to get serious about configuration management. I had used Puppet in the past and liked it, so I figured I’d just start there.
Quickly I realized that the Puppet community edition was no longer available because Puppet was acquired by Perforce Software and they have been aligning to a more commercialized operating model. I didn’t take too much time to investigate, but saw that they do have a developer edition that requires registration (boooo!) and it is limited to 25 nodes. Bummer. I support their decision to run their business how they want to, but I am not a fan of registering to use software, being marketed to, and being subjected to arbitrary restrictions when I know there are free open-sourced alternatives that don’t do this.
https://www.puppet.com/downloads/puppet-core
I quickly find that OpenVox is now what Puppet community edition used to be in spirit. I figured I’d give it a shot: https://voxpupuli.org/openvox/
I know from experience that the process
Architecture Overview
tl;dr, basic client/server architecture with a CA for authentication
and traffic encryption. you can do a server-less setup with puppet apply as well, but that’s not what I will be doing.
For more information about the architecture see https://docs.openvoxproject.org/openvox/8.x/architecture.html
OpenVox Server -- server1
|- server2
|- server3
|- ...
|- workstation1
The server maintains a CA to authenticate clients so that unauthorized users may not simply grab your configurations.
The configurations are stored in plaintext files using a Ruby-based DSL.
This configuration is typically stored in Git for reviewing, version control, and data redundancy/integrity.
Agents run on each endpoint. These connect outbound to the server, authenticate with their certificate, and the server provides them with their desired state configuration. The agent checks how the machine is configured and applies changes where appropriate. This prevents “configuration drift” and negates the need to login to your entire fleet of computers to make changes, corrects changes made that do not align with the desired state, and provides an audit trail that something has changed.
Initial Configuration
Arguably, you can do stuff like the hostname and installing packages with OpenVox once it is set up.
VPS provisioning
Since I only have a handful of hosts, I manually provisioned the server on a cheap $5/month VPS running Debian 13. This is a bit lower specs than the recommendations, but lowering the RAM usage of the JVM should make it work fine for now. If it becomes a problem later, I can bump up the RAM or add another swap file.
After this is done, ssh in and test the machine for basic functionality.
DNS
I added A and AAAA DNS records for the newly-provisioned machine for
its public IP addresses. I used puppet.mydomain.tld.
Update the OS
apt update
apt full-upgrade -y
reboot
Install some prereqs
apt install -y curl wget gnupg vim git openjdk-21-jre-headless rsync make
hostname
I set the hostname
hostnamectl set-hostname puppet.mydomain.tld
adding a user
I added a user for administration, configured them to be able to use sudo, and set up their ssh keys. My VPS provider installs my pre-configured ssh keys to the root user, so i just use those.
adduser admin
usermod -a -G sudo admin
install -d -m 700 -o admin -g admin /home/admin/.ssh
cp /root/.ssh/authorized_keys /home/admin/.ssh/authorized_keys
chown admin:admin /home/admin/.ssh/authorized_keys
chmod 600 /home/admin/.ssh/authorized_keys
leave your root session running and verify that you can login as the new user. If all goes well, you sh
SSH
I applied basic ssh hardening: no root logins, no password logins.
verify the config:
sshd -t
reload the config:
systemctl reload ssh
Installing the Server
cd /tmp
wget https://apt.voxpupuli.org/openvox8-release-debian13.deb
dpkg -i openvox8-release-debian13.deb
apt update
apt install openvox-server ### server depends on openvox-agent, so no need to install it explicitly
if all goes well, this should work:
/opt/puppetlabs/bin/puppet --version
this should present you with a version number similar to:
8.29.0
Configure JVM for lower resources
This step isn’t necessary if you run the server on a machine with more resources, but for now i only have a handful of systems in my fleet so paying extra money for more resources that i technically don’t even need yet isn’t worth it to me.
Update the JAVA_ARGS variable in /etc/default/puppetserver to use
512mb of RAM instead of 2gb by default.
Mine came with this:
JAVA_ARGS="-Xms2g -Xmx2g -Djruby.logger.class=com.puppetlabs.jruby_utils.jruby.Slf4jLogger"
Change it to:
JAVA_ARGS="-Xms512m -Xmx512m -Djruby.logger.class=com.puppetlabs.jruby_utils.jruby.Slf4jLogger"
Next, uncomment this line in /etc/puppetlabs/puppetserver/conf.d/puppetserver.conf
max-active-instances: 1
Establish the server’s identity
Change puppet.mydomain.tld to whatever you set in DNS earlier.
sudo /opt/puppetlabs/bin/puppet config set certname puppet.mydomain.tld --section main
Now configure it to use itself as the OpenVox server:
sudo /opt/puppetlabs/bin/puppet config set server puppet.mydomain.tld --section main
verify:
sudo /opt/puppetlabs/bin/puppet config print certname server
Start OpenVox
sudo systemctl enable --now puppetserver
check status:
sudo systemctl status puppetserver
If it failed, check the logs:
sudo journalctl -u puppetserver -n 100 --no-pager
Verify it us listening:
sudo ss -lnpt | grep 8140
You should see a java process listening on port 8140:
LISTEN 0 50 *:8140 *:* users:(("java",pid=11520,fd=27))
Test the agent:
sudo /opt/puppetlabs/bin/puppet agent --test
You should see no errors here. If any are encountered, investigate and correct them before proceeding. At this point, the server should be running but no configurations are present.
Back up certificate data
First, stop the services:
sudo systemctl stop puppet
sudo systemctl stop puppetserver
Next, create an encrypted copy of the certificates using
gnupg. Enter a strong passphrase when prompted.
export GPG_TTY="$(tty)"
set -o pipefail
umask 077
tar -C / -cpf - \
etc/puppetlabs/puppetserver/ca \
etc/puppetlabs/puppet/ssl \
| gpg --symmetric --cipher-algo AES256 \
--pinentry-mode loopback \
--output "/root/openvox-identity-$(date +%Y%m%d).tar.gpg"
Test decryption:
gpg --decrypt "/root/openvox-identity-$(date +%Y%m%d).tar.gpg" \
| tar -tf - > /dev/null
echo "Verification exit status: $?"
Exit status should be 0 if decryption was successful.
Now, transfer this file off of the machine and keep it somewhere safe. You will need this if the machine running OpenVox needs to be reprovisioned.
Create a Git repository for the configurations
This should be somewhat private and never store secrets.
Take your own steps to secure the Git repository, as this will be different depending on your org.
Create the initial configuration
First, cd into the directory of the repository you just created.
Create the directory structure:
mkdir -p manifests site/role/manifests site/profile/manifests data
Create environment.conf:
cat > environment.conf <<'EOF'
modulepath = site:$basemodulepath
EOF
cat > manifests/site.pp <<'EOF'
node 'puppet.mydomain.tld' {
include role::puppetserver
}
EOF
cat > site/role/manifests/puppetserver.pp <<'EOF'
class role::puppetserver {
include profile::base
}
EOF
cat > site/profile/manifests/base.pp <<'EOF'
class profile::base {
notify { 'Test Message':
message => 'Test message to show git is working.',
}
}
EOF
You should be left with something like this:
% tree
.
├── data
├── environment.conf
├── manifests
│ └── site.pp
└── site
├── profile
│ └── manifests
│ └── base.pp
└── role
└── manifests
└── puppetserver.pp
8 directories, 4 files
add the stuff to git:
git add .
git commit -am "initial commit"
git push -u origin main
The stuff should be pushed to the server at this point.
Configure read-only git for the OpenVox server
Generate a key on the OpenVox server
sudo ssh-keygen -t ed25519 \
-f /root/.ssh/openvox-control \
-C 'openvox-control@puppet.badoperation.net' \
-N ''
sudo cat /root/.ssh/openvox-control.pub
install it on git with read-only access. you will need to put the key and set your own path to the git file.
restrict,command="git-upload-pack '/path/to/openvox-control.git'" ssh-ed25519 AAAA... openvox-control@puppet.mydomain.tld
Configure the ssh client on OpenVox server:
sudo tee /root/.ssh/config > /dev/null <<'EOF'
Host openvox-git
HostName git.mydomain.tld
User git
IdentityFile /root/.ssh/openvox-control
IdentitiesOnly yes
StrictHostKeyChecking yes
EOF
sudo chmod 600 /root/.ssh/config
Install the git server’s verified SSH host key in root’s known_hosts
file.
Verify that this is what is running on the git server from the git server, and also on the OpenVox server. They should match. Do not proceed if they do not match and investigate what is happening. Do not blindly trust this, either.
On Git server:
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
On OpenVox server:
ssh-keyscan -t ed25519 git.mydomain.tld 2>/dev/null \
| ssh-keygen -lf -
If they match, add the scanned key on OpenVox server:
ssh-keyscan -t ed25519 git.mydomain.tld >> /root/.ssh/known_hosts
chmod 600 /root/.ssh/known_hosts
Clone the repo on the OpenVox server
git clone openvox-git:/srv/git/openvox-control.git
install the files for OpenVox:
sudo install -d -m 0755 /etc/puppetlabs/code/environments/production
sudo rsync -a --delete \
--exclude='.git/' \
/root/openvox-control/ \
/etc/puppetlabs/code/environments/production/
validate them:
sudo /opt/puppetlabs/bin/puppet parser validate \
/etc/puppetlabs/code/environments/production/manifests/site.pp \
/etc/puppetlabs/code/environments/production/site/role/manifests/puppetserver.pp \
/etc/puppetlabs/code/environments/production/site/profile/manifests/base.pp
Start OpenVox
sudo systemctl start puppetserver
sudo systemctl status puppetserver --no-pager
Everything should be running now. test the agent:
sudo /opt/puppetlabs/bin/puppet agent --test
You should see something like this if everything is working:
Info: Using environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Notice: Requesting catalog from puppet.mydomain.tld:8140 (172.16.15.25)
Notice: Catalog compiled by puppet.mydomain.tld
Info: Caching catalog for puppet.mydomain.tld
Info: Applying configuration version '1789854362'
Notice: Test message to show git is working.
Notice: /Stage[main]/Profile::Base/Notify[Test Message]/message: defined 'message' as 'Test message to show git is working.'
Notice: Applied catalog in 0.03 seconds
The design now is that configs can be pulled from the git server, then copied into production with rsync. This will change later into something a bit more elegant, but for now that’s what im going to do to get started. This will be configured in the next section.
Create Makefile
In the git repo, create Makefile
.PHONY: deploy validate
PUPPET_BIN := /opt/puppetlabs/bin/puppet
PRODUCTION := /etc/puppetlabs/code/environments/production
validate:
find manifests site -type f -name '*.pp' -exec $(PUPPET_BIN) parser validate {} +
deploy:
git pull --ff-only
$(MAKE) validate
sudo rsync -a --delete --exclude='.git/' --chmod=D755,F644 ./ $(PRODUCTION)/
Commit and push this to the repo, then pull it on the OpenVox server.
Now, you can use ssh from your workstation to deploy after you make changes and validate them:
ssh -t admin@puppet.mydomain.tld 'cd ~/openvox-control && make deploy'
This method is primitive and may be replaced in the future with r10k and maybe some git hooks, but for now it works fine.
Workflow:
-
develop locally where i can use an IDE and a browser and such and not have to use some janky text editor over SSH.
-
all of this is in git, so i get the benefits there. when i make new files, add them. make changes, commit them. push to main when i am satisfied its working.
-
run the
make deploycommand via ssh to actually run the configuration.
Keep in mind that Puppet can be very complex and other features and deployment glue can be refined as you need it. I am not to the point where i need CI/CD yet for a handful of relatively simple configurations on some non-critical systems that don’t matter if they have a bit of downtime here and there. When the time comes, much like the resources on the VPS i am running this on are exhausted, i can upgrade the setup.
A simple first configuration
This is where Puppet gets kinda hairy and setups can differ wildly.
This is a pretty simple one to set DebianBanner no on Debian and
Ubuntu hosts to stop them from advertising the OS and build
information in the server’s banner.
If you have never worked with Puppet or OpenVox before, you should familiarize yourself with the documentation about modules and the Puppet language. They are a DSL written in a Ruby dialect with classes and other conveniences for the domain of configuration management.
Puppet being its own DSL and having a steeper learning curve are criticisms it faced when I used it last. I never felt that it was that complex, and like any other language, you’re going to be kind of terrible at it when you first get started and will learn more as you go.
In a perfect world, you will be working on a team of sysadmins who are also developers and they can guide you along and review your commits as part of a change process. This isn’t always true, but it is kind of necessary with Puppet. I also believe that any configuration management tool like this requires development work. There’s no silver bullet here. You will have to roll up your sleeves and get your hands dirty with this.
I also would get discouraged sometimes because i discovered that I did something kind of wonky but it worked. Or that i didnt have 100% coverage of every configuration. Realize that it is a lot of work, may never be “complete” and is more of a continuous process than a final solution. It is impossible to offer a final solution for this kind of thing because organizations vary wildly with what they use behind the scenes. I took more of a Kaizen approach where i would just strive to make at least one meaningful change for the better per day. Even if this change was re-wording a comment to be more clearer, i counted this as a win.
In a perfect world though, you would want to be able to provision a new machine, hook it into OpenVox, and a few moments later its configured and running. This is possible with time and effort, but being incomplete is not a waste, either. If you can get rid of 90% of your configuration when starting up a new machine, this is a tremendous time savings. You can also look up in the code and read comments about why things are as they are, and look at the history in your revision control to see when changes were made and by whom.
The modules fundaemtnals and language summary are here:
https://docs.openvoxproject.org/openvox/8.x/modules_fundamentals.html
https://docs.openvoxproject.org/openvox/8.x/lang_summary.html
Additionally, you should probably read the Style Guide for some suggestions on writing clean, maintainable code: https://docs.openvoxproject.org/openvox/8.x/style_guide.html
Enable the puppet agent:
sudo systemctl enable --now puppet
edit site/profile/manifests/ssh.pp:
class profile::ssh {
if $facts['os']['name'] in ['Debian', 'Ubuntu'] {
file { '/etc/ssh/sshd_config.d/10-debianbanner.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
content => "DebianBanner no\n",
}
}
}
get rid of the old notify banner in base.pp and include
profile::ssh instead.
class profile::base {
include profile::ssh
}
commit and push this, then deploy it using the ssh + make one-liner.
Once it is deployed, run puppet test and it should output that the
10-debianbanner.conf file was created.
Verify the file exists, then check the Debian banner with netcat:
nc puppet.mydomain.tld 22
This should give something like:
SSH-2.0-OpenSSH_10.0p2
Without it, it gives away this Debian-specific banner, which reveals information about the host:
SSH-2.0-OpenSSH_10.0p2 Debian-7+deb13u4
To test this, i deleted the 10-debianconf.conf file, refreshed sshd, and checked that the Debian banner has returned using netcat. it did.
By default, puppet checks in every 30 minutes for configurations. this
can be changed in /etc/puppetlabs/puppet/puppet.conf:
[agent]
runinterval = 10m
the puppet agent must be reloaded if you change the runinterval setting:
sudo systemctl reload puppet
To see the configured interval:
sudo /opt/puppetlabs/bin/puppet config print runinterval --section agent
Adding additional machines to the OpenVox server
edit manifests/site.pp, adding a default node:
node 'puppet.mydomain.tld' {
include role::puppetserver
}
node default {
include profile::base
}
commit, push, and pull this to the OpenVox server.
Next, install the agent on each system. In this case, I am doing it on a Debian 12 machine. I will need to download the openvox8 debian12 package to connect to OpenVox’s repositories, then install the OpenVox agent.
wget https://apt.voxpupuli.org/openvox8-release-debian12.deb
sudo dpkg -i openvox8-release-debian12.deb
sudo apt update
sudo apt install openvox-agent
Once this is installed, configure it to connect to the OpenVox server:
sudo /opt/puppetlabs/bin/puppet config set server puppet.mydomain.tld --section main
sudo /opt/puppetlabs/bin/puppet config set certname myhost.mydomain.tld --section main
Verify the settings:
sudo /opt/puppetlabs/bin/puppet config print server certname --section agent
now, test it on the new endpoint:
sudo /opt/puppetlabs/bin/puppet agent --test
This should fail with an error similar to this:
Couldn't fetch certificate from CA server; you might still need to sign this agent's certificate (myhost.mydomain.tld).
Exiting now because the waitforcert setting is set to 0.
Login to the OpenVox server to sign this certificate:
sudo /opt/puppetlabs/bin/puppetserver ca sign --certname myhost.mydomain.tld
Go back to the new endpoint and run the test again. it should apply the DebianBanner configuration:
sudo /opt/puppetlabs/bin/puppet agent --test
If everything goes according to plan, you should see a line similar to this, showing it was successful.
Notice: /Stage[main]/Profile::Ssh/File[/etc/ssh/sshd_config.d/10-debianbanner.conf]/ensure: defined content as '{sha256}c417daa5c960635ce5b152e793af867c40d51dfe56ca71d427e2166038b3b100'
Now, enable the OpenVox agent on the new host:
systemctl enable --now puppet
At this point, you should be good to go with repeating the process for each host. For convenience, I’d recommend making small scripts to wget the appropriate package, install it, update your package manager, then install openvox-agent, enable the service, and run the test. You will have to login to the OpenVox server and manually sign each host’s certificate to allow them to use it.
I ended up with something like this:
OPENVOXSVR="puppet.badoperation.net"
. /etc/os-release
case "$ID:$VERSION_ID" in
debian:12|debian:13|ubuntu:24.04)
OPENVOXPKG="openvox8-release-${ID}${VERSION_ID}.deb"
;;
*)
echo "Unsupported OS: $PRETTY_NAME" >&2
exit 1
;;
esac
CERTNAME="$(hostname -f)"
echo "Operating system: $PRETTY_NAME"
echo "Puppet server: $OPENVOXSVR"
echo "Certificate name: $CERTNAME"
echo "Repository package: $OPENVOXPKG"
cd /tmp
wget -O "$OPENVOXPKG" "https://apt.voxpupuli.org/$OPENVOXPKG"
sudo dpkg -i "$OPENVOXPKG"
sudo apt update
sudo apt install -y openvox-agent
sudo /opt/puppetlabs/bin/puppet config set server "$OPENVOXSVR" --section main
sudo /opt/puppetlabs/bin/puppet config set certname "$CERTNAME" --section main
sudo /opt/puppetlabs/bin/puppet config print server certname --section agent
sudo /opt/puppetlabs/bin/puppet agent --test
Take note of the client’s certificate.
Then back to server. list certificates, then compare to the client’s certificate for authenticity. If they match, sign it:
sudo /opt/puppetlabs/bin/puppetserver ca list
sudo /opt/puppetlabs/bin/puppetserver ca sign --certname HOST_FQDN
then finally back to the new machine:
sudo /opt/puppetlabs/bin/puppet agent --test
sudo systemctl enable --now puppet
Mirroring for OpenVox packages
There are currently a apt and yum repositories on Vox Pupuli for OpenVox software downloads. They conveniently have an rsyncable copy of this data to allow you to set up a local mirror. This is highly recommended if you are managing several systems as you can do this locally on basically any web server and a scheduled rsync to keep it current. This prevents dozens of your own systems reaching out to the internet to download this software, making installation fast and take less bandwidth over time.
Doing this is a bit beyond the scope of this documentation, but it is not a very difficult project, and i will probably end up doing it and linking instructions in the future.
https://voxpupuli.org/blog/2025/03/04/openvox-downloads-and-mirroring/
Next steps
After all of your machines are hooked into OpenVox and verified to work, its time to start writing code for your configurations.
profile::base should contain settings meant for all systems.
roles are for hosts with distinct jobs. For example role::gitserver
will include profile::base and settings for running a Git server.
When you want to assign a node a role, add it in site.pp.
Remember to test your changes before rolling them out broadly. Doubly so when making changes to things like authentication or SSH as a bad configuration may lock you out of your system. A throwaway VM works great for this. I recommend using Vagrant for this:
-
boot a new VM
-
run your script to bootstrap the OpenVox agent installation
-
sign the certificate on the OpenVox server
-
assign the role you are testing/developing to your new VM only
-
once it is good to go, deploy it to the other hosts.
Remember to never store secrets in this code or Git either. For this, use Hiera.