--- title: "Enroll" html_title: "Enroll - Reverse-engineering servers into configuration management" description: "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible, Puppet, or Salt manifests from what it finds. Harvest → Manifest → Manage." og_title: "Enroll - Reverse-engineering servers into configuration management" og_description: "Harvest a host's real configuration and turn it into Ansible, Puppet, or Salt code. Safe-by-default, with optional SOPS encryption." og_type: "website" ---
Reverse-engineering servers into Ansible, Puppet, or Salt

Get an existing Linux host into configuration management in seconds.

Enroll inspects a Debian-like or RedHat-like system, harvests the state that matters, and generates Ansible roles, Puppet modules, or Salt states so you can bring snowflakes under management fast.

Super fast Optional SOPS encryption Remote over SSH
Config in the blink of an eye
harvest once → render to your CM tool
$ enroll harvest --out ./harvest
$ enroll manifest --harvest ./harvest --target ansible --out ./ansible
$ enroll manifest --harvest ./harvest --target puppet --out ./puppet
$ enroll manifest --harvest ./harvest --target salt --out ./salt
./ansible  → playbook.yml, roles/, inventory/...
./puppet   → manifests/site.pp, modules/, data/...
./salt     → states/top.sls, states/roles/, pillar/...
Tip: for multiple hosts, use --fqdn to generate target-native host data: Ansible inventory, Puppet Hiera, or Salt pillar.

A simple mental model

Enroll is built around two phases, plus optional drift and reporting tools:

Harvest
Collect host facts + relevant files into a bundle.
Manifest
Render Ansible roles/playbooks, Puppet modules/Hiera data, or Salt states/pillar from the harvest.
Diff
Compare two harvests and notify via webhook/email.
Explain
Analyze what's included/excluded in the harvest and why.
Validate
Confirm that a harvest isn't corrupt or lacking artifacts.
Safe-by-default harvesting
Enroll avoids likely secrets with a path denylist, content sniffing, and size caps - then lets you opt in to more aggressive collection when you're ready.
Ansible, Puppet, or Salt
The harvest bundle is renderer-neutral. Choose --target ansible, --target puppet, or --target salt at manifest time.
Fewer roles/modules where possible
By default, package/service snapshots are grouped by package Section or equivalent metadata to reduce role/module sprawl. Use --no-common-roles, or --fqdn, to keep output more host-specific.
Multi-site without "shared role broke host2"
In --fqdn mode, host-specific data moves into the target's native host-data layer: Ansible inventory, Puppet Hiera, or Salt pillar.
Remote over SSH
Harvest a remote host from your workstation, then render Ansible, Puppet, or Salt output locally.
Encrypt bundles at rest
Use --sops to store harvests/manifests as a single encrypted .tar.gz.sops file (GPG) for safer long-term storage as a DR strategy.
Why sysadmins like it
• Rapid enrolling of existing infra into config management
• Tweak include/exclude paths as needed
• Capture what changed from package defaults
diff mode detects and alerts about drift

Quickstart

# Harvest once
enroll harvest --out ./harvest

# Render and noop-test Ansible
enroll manifest --harvest ./harvest --target ansible --out ./ansible
ansible-playbook -i "localhost," -c local ./ansible/playbook.yml --check --diff

# Render and noop-test Puppet
enroll manifest --harvest ./harvest --target puppet --out ./puppet
puppet apply --modulepath ./puppet/modules ./puppet/manifests/site.pp --noop

# Render and noop-test Salt
enroll manifest --harvest ./harvest --target salt --out ./salt
salt-call --local --file-root ./salt/states state.apply test=True
Good for
Disaster recovery snapshots, "make this one host reproducible", and comparing how the same harvest looks in Ansible, Puppet, or Salt before you commit to a workflow.

Using Ansible and want templates for structured configs? Install JinjaTurtle and use --jinjaturtle (or let it auto-detect).
# Remote harvest over SSH, then manifest locally
enroll single-shot \
  --remote-host myhost.example.com \
  --remote-user myuser \
  --harvest /tmp/enroll-harvest \
  --target ansible \
  --out ./ansible \
  --fqdn myhost.example.com
If you don't want/need sudo on the remote host, add --no-sudo (expect a less complete harvest). For remote sudo prompts use --ask-become-pass/-K. If your SSH private key is encrypted, use --ask-key-passphrase (interactive) or --ssh-key-passphrase-env ENV_VAR (non-interactive/CI).
# Multi-site mode: one harvest, target-native host data
fqdn="$(hostname -f)"
enroll harvest --out /tmp/enroll-harvest

# Ansible: inventory/host_vars and per-host playbook
enroll manifest --harvest /tmp/enroll-harvest --target ansible --out ./ansible --fqdn "$fqdn"
ansible-playbook ./ansible/playbooks/${fqdn}.yml -i ./ansible/inventory/hosts.ini -c local --check --diff

# Puppet: Hiera node data, certname selects the host
enroll manifest --harvest /tmp/enroll-harvest --target puppet --out ./puppet --fqdn "$fqdn"
puppet apply --modulepath ./puppet/modules --hiera_config ./puppet/hiera.yaml --certname "$fqdn" ./puppet/manifests/site.pp --noop

# Salt: pillar node data, minion id selects the host
enroll manifest --harvest /tmp/enroll-harvest --target salt --out ./salt --fqdn "$fqdn"
salt-call --local --id "$fqdn" --file-root ./salt/states --pillar-root ./salt/pillar state.apply test=True
Rule of thumb: single-site for "one server, easy-to-read roles/modules/states"; --fqdn for "many servers, host-specific data, fast adoption".
# Compare two harvests and get a human-friendly report (ignoring noise)
enroll diff --old /path/to/harvestA --new /path/to/harvestB --format markdown \
  --exclude-path /var/anacron \
  --ignore-package-versions

# Send a webhook when differences are detected
enroll diff \
  --old /path/to/harvestA \
  --new /path/to/harvestB \
  --webhook https://example.net/webhook \
  --webhook-format json \
  --webhook-header 'X-Enroll-Secret: ...' \
  --ignore-package-versions \
  --exit-code

# Ignore a path and changes to package versions, and optionally
# enforce the old state locally (requires ansible-playbook)
enroll diff --old /path/to/harvestA --new /path/to/harvestB \
  --exclude-path /var/anacron \
  --ignore-package-versions \
  --enforce
E-mail notifications are also supported. Run it on a systemd timer to alert to drift!
# Explain what's in a harvest
enroll explain /path/to/harvest

# JSON format, and using a SOPS-encrypted harvest
enroll explain /path/to/harvest.sops \
  --sops \
  --format json
'explain' tells you why something was included, but also why something was excluded.
# Validate a harvest is correct.
enroll validate /path/to/harvest

# Check against the latest published version of the state schema specification
enroll validate /path/to/harvest --schema https://enroll.sh/schema/state.schema.json
'validate' makes sure the harvest's state conforms to Enroll's state schema, doesn't contain orphaned artifacts and isn't missing any artifacts needed by the state. By default, it checks against the schema packaged with Enroll, but you can also check against the latest version on this site.

Demonstrations

Harvest
Collect state into a bundle.
Manifest
Render Ansible roles/playbooks, Puppet modules, or Salt states.
Single-shot
Harvest → Manifest in one command.
Diff
Drift report + webhook/email notifications, or optionally enforce the previous state!
News

Enroll 0.7.0 adds Puppet and Salt rendering

The same harvest bundle can now render Ansible, Puppet, or Salt output, with target-native multi-host layouts and package-section grouping where possible.

Install

Use your preferred packaging. An AppImage is also available.

sudo mkdir -p /usr/share/keyrings
curl -fsSL https://mig5.net/static/mig5.asc | sudo gpg --dearmor -o /usr/share/keyrings/mig5.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/mig5.gpg] https://apt.mig5.net $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/mig5.list
sudo apt update
sudo apt install enroll
sudo rpm --import https://mig5.net/static/mig5.asc

sudo tee /etc/yum.repos.d/mig5.repo > /dev/null << 'EOF'
[mig5]
name=mig5 Repository
baseurl=https://rpm.mig5.net/$releasever/rpm/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mig5.net/static/mig5.asc
EOF

sudo dnf upgrade --refresh
sudo dnf install enroll
pip install enroll
# or: pipx install enroll