OpenVox Server on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of OpenVox Server on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. OpenVox is the community maintained, fully open source continuation of Puppet, published under the Apache 2.0 licence by Vox Pupuli. OpenVox Server is its server side component: it compiles catalogues from your manifests and serves them to agents, runs the certificate authority that gives every managed node a verifiable identity, and collects the facts and run reports those nodes send back.
The image installs OpenVox Server 8.15.2 and OpenVox Agent 8.28.1 from the official OpenVox apt repository (apt.voxpupuli.org). The repository is wired up using the project's own release package, pinned by SHA-256, and the build asserts that the signing key it installs really is the Vox Pupuli key 0E26 4299 8700 2418 F951 3F2A 5FB9 99C2 D62F F3D9 before trusting a single package. Both packages are version pinned and held. Unattended security upgrades are configured to keep the operating system patched on your running VM.
What configuration management gives you. You describe the state a machine should be in, in code, and the agent makes it so on every run, correcting anything that has drifted. The same manifest that builds a machine also keeps it built, which makes rebuilds routine, audits factual rather than aspirational, and configuration drift something you detect instead of discover. OpenVox speaks the Puppet language, so manifests, modules and Puppet Forge content you already have continue to work, and existing Puppet agents can report to this server.
The agent is installed alongside the server, on purpose. openvox-server declares a dependency on openvox-agent, and this image installs both on the same VM. That means the appliance is complete and self demonstrating: on its very first boot it enrols itself, compiles a real catalogue and applies it, so you can see the whole loop working before you point a single other machine at it.
Security by design — no certificate authority ships in this image. OpenVox Server has no administrator password. Its credential is an X.509 identity, so a prebuilt certificate authority baked into the image would hand every deployment the same CA private key. This image therefore ships no certificate authority, no certificate and no private key at all. On the very first boot of every VM a one shot service creates that instance's own certificate authority and host certificate, bound to its own hostname and addresses, and only then opens the listener. Until that has happened OpenVox Server is bound to loopback only and is held stopped by a bootstrap guard, and a start time identity check runs before every start and refuses to serve unless the instance has its own authority and a certificate that verifies against it.
What is included:
-
OpenVox Server 8.15.2 from the official signature verified OpenVox repository, running under systemd as the unprivileged
puppetuser (puppetserver.service), serving its HTTPS API on port 8140 -
OpenVox Agent 8.28.1 on the same VM, enabled as
puppet.service, so the node keeps itself in the state its catalogue declares -
A per instance certificate authority and host certificate generated on first boot, with the CA fingerprint recorded in
/root/openvox-credentials.txt(mode 0600, root only) -
Mutual TLS between server and agents: every request is authenticated by client certificate, and anonymous requests to protected endpoints are refused
-
A JVM and JRuby pool sized for
Standard_B2s— a fixed 1536 MB heap and a single JRuby instance — rather than upstream's 2 GB default, which does not fit 4 GB of RAM. No swap is used to make it fit -
A small, deliberately idempotent site manifest in the
productionenvironment, so your first agent run compiles and applies a real catalogue instead of an empty one
Prerequisites
-
Active Azure subscription, SSH public key, VNet + subnet in target region
-
Subscription to the OpenVox listing on Azure Marketplace
-
Network Security Group rules allowing TCP 22 (admin) and TCP 8140 from the machines you intend to manage. Port 8140 should not be open to the whole internet: it is the control plane for your estate, and only your managed nodes need to reach it
-
A way for your agents to resolve this server by name (private DNS, or a
hostsentry). Agents verify the server's certificate, and the certificate is issued for this VM's name
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM). This image is tuned specifically for it and is proven to compile and apply catalogues at that size. For larger estates, see the scaling section in Step 10 — raise the JVM heap and the JRuby pool together.
Step 1: Deploy from the Azure Portal
Search OpenVox in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 8140 from the subnets containing the machines you will manage, and TCP 22 for administration. Leave port 8140 closed to the public internet.
Step 2: Deploy from the Azure CLI
RG="openvox-prod"; LOCATION="eastus"; VM_NAME="openvox-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/openvox-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$GALLERY_IMAGE_ID" \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku Standard --location "$LOCATION" \
--os-disk-delete-option Delete --nic-delete-option Delete
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 8140 --priority 1010
The VM name you choose becomes this server's certname — the identity its certificate is issued for, and the name your agents will point at. Choose something stable and meaningful, such as openvox-01.
Step 3: First boot
On the first boot of the VM, openvox-firstboot.service runs once. It:
-
Resolves this VM's own hostname and addresses
-
Renders
/etc/puppetlabs/puppet/puppet.conffor that certname, with the hostname, the Azure internal FQDN and the VM's addresses as subject alternative names -
Creates this instance's own certificate authority and host certificate with
puppetserver ca setup -
Moves the listener from loopback to all interfaces and starts OpenVox Server
-
Runs the local agent against it, which compiles and applies a real catalogue
-
Writes
/root/openvox-credentials.txt(mode 0600, root only) recording this instance's certname, server URL and CA fingerprint
First boot takes roughly 30 to 60 seconds on Standard_B2s, most of which is the JVM starting. Wait for the sentinel before continuing:
sudo test -f /var/lib/cloudimg/openvox-firstboot.done && echo "first boot complete"
Step 4: Confirm the services are running
systemctl is-active puppetserver.service puppet.service
sudo ss -tlnp | grep 8140
curl -sk https://127.0.0.1:8140/status/v1/simple; echo
Expected output:
active
active
LISTEN 0 50 *:8140 *:* users:(("java",pid=8468,fd=27))
running
puppetserver.service is OpenVox Server. puppet.service is the OpenVox Agent daemon, which re-applies this node's catalogue on a schedule. The status endpoint returns the single word running when the server is ready to compile catalogues.

Step 5: Read this instance's identity
OpenVox Server has no password. Its identity is its certificate authority, which was created on this VM's first boot and exists nowhere else.
sudo cat /root/openvox-credentials.txt
The file records the certname, the server URL, the paths to the CA and SSL directories, and the SHA-256 fingerprint of this instance's certificate authority. Confirm the recorded fingerprint really is the authority on disk:
CADIR=$(sudo grep '^openvox.ca.dir=' /root/openvox-credentials.txt | cut -d= -f2-)
sudo grep '^openvox.ca.fingerprint.sha256=' /root/openvox-credentials.txt
sudo openssl x509 -in "$CADIR/ca_crt.pem" -noout -fingerprint -sha256
Both commands print the same fingerprint, for example:
openvox.ca.fingerprint.sha256=2C:CA:9C:8A:91:4D:36:77:28:E5:D0:46:28:38:B3:2B:40:08:9B:80:00:36:BB:A2:28:B3:9A:A0:A8:B8:8E:EC
sha256 Fingerprint=2C:CA:9C:8A:91:4D:36:77:28:E5:D0:46:28:38:B3:2B:40:08:9B:80:00:36:BB:A2:28:B3:9A:A0:A8:B8:8E:EC
This fingerprint is unique to your instance. Note it down: it is what proves to an agent that it is talking to your server and not an impostor. If you launch two VMs from this image they will have two completely different authorities, because neither one shipped with the image.

Step 6: Run this node against its own server
This is the whole product in one command: the agent asks the server for a catalogue, the server compiles it from your manifests, and the agent applies it.
CERTNAME=$(sudo grep '^openvox.certname=' /root/openvox-credentials.txt | cut -d= -f2-)
sudo /opt/puppetlabs/bin/puppet agent --test --server "$CERTNAME"
Expected output:
Info: Using environment 'production'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Notice: Requesting catalog from openvox-01:8140 (10.0.0.10)
Notice: Catalog compiled by openvox-01
Info: Caching catalog for openvox-01
Info: Applying configuration version '1786073592'
Notice: /Stage[main]/Main/Node[default]/File[/var/lib/cloudimg/openvox-catalog-applied.txt]/ensure: defined content as '{sha256}2e1de2053488f2b94b421084633d7a9ef0736533c9f25aeac78d260b177698ed' (corrective)
Notice: Applied catalog in 0.04 seconds
Catalog compiled by <your server> is the server doing its job. Applied catalog is the agent doing its. The shipped manifest writes a small receipt describing what was applied:
cat /var/lib/cloudimg/openvox-catalog-applied.txt
# Written by OpenVox when this node applied its catalogue.
managed-by=openvox
certname=openvox-01
environment=production
os=Ubuntu 24.04
Now run it a second time:
CERTNAME=$(sudo grep '^openvox.certname=' /root/openvox-credentials.txt | cut -d= -f2-)
sudo /opt/puppetlabs/bin/puppet agent --test --server "$CERTNAME"
Notice: Catalog compiled by openvox-01
Info: Caching catalog for openvox-01
Info: Applying configuration version '1786073594'
Notice: Applied catalog in 0.03 seconds
Notice what is missing: there is no File[...]/ensure: line, because nothing needed changing. That is idempotence, and it is the property that makes configuration management safe to run continuously. puppet.service runs exactly this loop for you on a schedule.
You can assert it mechanically with --detailed-exitcodes, which returns 2 when changes were made and 0 when the node was already in the declared state:
CERTNAME=$(sudo grep '^openvox.certname=' /root/openvox-credentials.txt | cut -d= -f2-)
sudo /opt/puppetlabs/bin/puppet agent --test --detailed-exitcodes --server "$CERTNAME" >/dev/null 2>&1
echo "exit code: $? (0 = already in the declared state, 2 = changes applied)"

Step 7: Verify the mutual TLS boundary
Every OpenVox request is authenticated by client certificate. Prove it in both directions.
An authenticated request, using this VM's own certificate, succeeds:
CERTNAME=$(sudo grep '^openvox.certname=' /root/openvox-credentials.txt | cut -d= -f2-)
SSLDIR=$(sudo grep '^openvox.ssl.dir=' /root/openvox-credentials.txt | cut -d= -f2-)
sudo curl -s --cacert "$SSLDIR/certs/ca.pem" \
--cert "$SSLDIR/certs/$CERTNAME.pem" --key "$SSLDIR/private_keys/$CERTNAME.pem" \
"https://$CERTNAME:8140/status/v1/services?level=critical" | head -c 200; echo
{"puppet-profiler":{"service_version":"8.15.2","service_status_version":1,"detail_level":"critical","state":"running","status":{},"active_alerts":[]},"jruby-metrics":{"service_version":"8.15.2",...
An anonymous request for a catalogue is refused:
CERTNAME=$(sudo grep '^openvox.certname=' /root/openvox-credentials.txt | cut -d= -f2-)
curl -sk -o /dev/null -w 'anonymous catalog request: HTTP %{http_code}\n' \
"https://127.0.0.1:8140/puppet/v3/catalog/$CERTNAME?environment=production"
anonymous catalog request: HTTP 403
A node with no signed certificate cannot obtain a catalogue, and therefore cannot learn anything about how your estate is configured. Confirm the file permissions protecting the identity:
CERTNAME=$(sudo grep '^openvox.certname=' /root/openvox-credentials.txt | cut -d= -f2-)
CADIR=$(sudo grep '^openvox.ca.dir=' /root/openvox-credentials.txt | cut -d= -f2-)
SSLDIR=$(sudo grep '^openvox.ssl.dir=' /root/openvox-credentials.txt | cut -d= -f2-)
sudo stat -c '%a %U:%G %n' /root/openvox-credentials.txt "$CADIR/ca_key.pem" "$SSLDIR/private_keys/$CERTNAME.pem"
600 root:root /root/openvox-credentials.txt
640 puppet:puppet /etc/puppetlabs/puppetserver/ca/ca_key.pem
640 puppet:puppet /etc/puppetlabs/puppet/ssl/private_keys/openvox-01.pem

Step 8: Write your own manifest
The shipped manifest lives in the production environment and is ordinary OpenVox code. Look at it first:
sudo cat /etc/puppetlabs/code/environments/production/manifests/site.pp
Add your own resources to the same node default block, or create per node blocks. This example ensures a package is installed, a directory exists and a service is running:
node default {
package { 'htop':
ensure => installed,
}
file { '/opt/company':
ensure => directory,
owner => 'root',
mode => '0755',
}
service { 'chrony':
ensure => running,
enable => true,
}
}
Always check that a manifest parses before an agent run needs it:
sudo /opt/puppetlabs/bin/puppet parser validate /etc/puppetlabs/code/environments/production/manifests/site.pp && echo "manifest parses"
Install modules from the Puppet Forge into the environment's module path — OpenVox is compatible with Puppet language modules:
sudo /opt/puppetlabs/bin/puppet module list --modulepath /etc/puppetlabs/code/environments/production/modules
To install a module, use sudo /opt/puppetlabs/bin/puppet module install <author>-<module> --modulepath /etc/puppetlabs/code/environments/production/modules from a VM with outbound internet access.
Step 9: Enrol another machine as an agent
The commands in this step run on the agent machine, not on this server. Replace <vm-ip> with this server's private address and <server-certname> with the certname you read in Step 5.
The agent must be able to resolve the server by the name on its certificate — that is the name the agent verifies the server's certificate against. If you have no private DNS, add a hosts entry.
# RUN THESE ON THE AGENT MACHINE, NOT ON THE OPENVOX SERVER.
echo "<vm-ip> <server-certname>" | sudo tee -a /etc/hosts
curl -fsSLO https://apt.voxpupuli.org/openvox8-release-ubuntu24.04.deb
sudo dpkg -i openvox8-release-ubuntu24.04.deb
sudo apt-get update && sudo apt-get install -y openvox-agent
sudo /opt/puppetlabs/bin/puppet config set server <server-certname> --section main
sudo /opt/puppetlabs/bin/puppet agent --test --waitforcert 30
Back on this server, list and sign the pending request:
sudo /opt/puppetlabs/bin/puppetserver ca list --all
Signed Certificates:
openvox-01 (SHA256) 0C:3F:B6:C6:FD:7A:A2:2E:66:66:61:64:F0:63:BD:2F:21:0D:2A:81:B7:90:5D:CE:D8:44:A0:F7:20:A5:48:47 alt names: ["DNS:openvox-01", "DNS:localhost", "DNS:openvox", "DNS:puppet"]
Sign a waiting request with sudo /opt/puppetlabs/bin/puppetserver ca sign --certname <agent-certname>. Once signed, the agent's --waitforcert run continues automatically and applies its first catalogue.
Autosigning is deliberately disabled in this image (autosign = false in puppet.conf). Signing is the moment you decide a machine is allowed to receive your configuration, so it is a decision worth making explicitly.
Step 10: Scale the server for your estate
This image is tuned for Standard_B2s. The two settings that matter are the JVM heap and the JRuby pool, and they must be raised together — each JRuby instance needs roughly 512 MB of heap to compile catalogues comfortably.
grep -E '^JAVA_ARGS=' /etc/default/puppetserver
grep -E '^\s*max-active-instances' /etc/puppetlabs/puppetserver/conf.d/puppetserver.conf
JAVA_ARGS="-Xms1536m -Xmx1536m -XX:ReservedCodeCacheSize=192m -XX:+UseG1GC -XX:MaxMetaspaceSize=256m -Djruby.logger.class=com.puppetlabs.jruby_utils.jruby.Slf4jLogger"
max-active-instances: 1
A single JRuby instance compiles catalogues one at a time, which is right for a small estate on two vCPUs. To grow, move to a larger VM size and raise both values — for example, on a 4 vCPU / 16 GB machine, a 6 GB heap with 4 JRuby instances. Edit JAVA_ARGS in /etc/default/puppetserver and max-active-instances in /etc/puppetlabs/puppetserver/conf.d/puppetserver.conf, then sudo systemctl restart puppetserver.service.
Check current memory headroom at any time:
free -m | head -2
ps -eo rss,comm --no-headers | awk '$2=="java"{printf "OpenVox Server JVM RSS: %d MB\n", $1/1024}'
Do not add swap to squeeze in a larger heap. Move to a larger VM size instead.
Step 11: Where things are
ls /etc/puppetlabs/code/environments/production
sudo tail -3 /var/log/puppetlabs/puppetserver/puppetserver.log
| Path | What it is |
|---|---|
/etc/puppetlabs/code/environments/production/manifests/ |
Your manifests. site.pp is the entry point |
/etc/puppetlabs/code/environments/production/modules/ |
Modules for this environment, including Puppet Forge modules |
/etc/puppetlabs/puppet/puppet.conf |
Server and agent settings, rendered for this VM on first boot |
/etc/puppetlabs/puppetserver/ca/ |
This instance's certificate authority. Back it up; losing it means re-enrolling every agent |
/etc/puppetlabs/puppet/ssl/ |
This node's own certificate and private key |
/etc/default/puppetserver |
JVM arguments, including the heap size |
/var/log/puppetlabs/puppetserver/puppetserver.log |
Server log, including every catalogue compilation |
/root/openvox-credentials.txt |
This instance's identity summary and CA fingerprint (0600) |
The server log records every compilation, which is the first place to look when an agent run fails:
2026-08-07T03:33:14.621Z INFO [qtp2073451292-60] [puppetserver] Puppet Compiled catalog for openvox-01 in environment production in 0.22 seconds
Step 12: Harden the deployment
-
Keep port 8140 private. It is the control plane for your estate. Scope the Network Security Group to the subnets holding your managed nodes, never to the internet.
-
Back up the certificate authority.
/etc/puppetlabs/puppetserver/ca/is unique to this instance and cannot be regenerated. If you lose it, every agent must be re-enrolled. Back it up encrypted, and treat it with the same care as any private key. -
Leave autosigning off. Signing a certificate is the decision to trust a machine with your configuration.
-
Revoke decommissioned nodes so a retired machine's certificate cannot be reused:
sudo /opt/puppetlabs/bin/puppetserver ca clean --certname <agent-certname>. -
Keep secrets out of manifests. Manifests are compiled into catalogues and cached on the agent. Use an external lookup for secrets rather than literals in code.
-
Version control your manifests. The whole value of infrastructure as code is that the code is reviewable and revertible.
-
Keep the OS patched. Unattended security upgrades are enabled in this image. The OpenVox packages themselves are pinned and held, so upgrade them deliberately after testing.
Step 13: Support and Licensing
OpenVox is licensed under the Apache License 2.0 and is maintained by the Vox Pupuli community. The upstream project is at github.com/OpenVoxProject.
OpenVox is an independent open source continuation of, and is compatible with, Puppet. PUPPET is a trademark of Perforce Software, Inc. cloudimg and Vox Pupuli are not affiliated with, endorsed by, or sponsored by Perforce Software. References to Puppet in this guide describe compatibility only.
cloudimg provides 24/7 technical support for this image by email (support@cloudimg.co.uk) and live chat, with a one hour average response time for critical issues. We help with enrolling and signing agents, migrating existing manifests and modules, structuring environments, sizing the JVM and JRuby pool as your node count grows, certificate lifecycle, and troubleshooting failed compilations and agent runs.
Deploy on Azure
Find OpenVox Server on Ubuntu 24.04 by cloudimg on the Azure Marketplace.
Need Help?
Contact support@cloudimg.co.uk or visit cloudimg.co.uk/support.