From dd9a9113ba7ce290afab760780dd4e376eaf8416 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 17 Jun 2026 15:35:16 +1000 Subject: [PATCH 01/30] 0.7.0 --- src/content/_index.html | 125 ++++++++++++++++++++++----------- src/content/docs.html | 128 ++++++++++++++++++++++++---------- src/content/examples.html | 130 ++++++++++++++++++++++------------- src/content/news/0-7-0.html | 115 +++++++++++++++++++++++++++++++ src/content/news/_index.html | 12 ++++ src/content/security.html | 6 +- 6 files changed, 388 insertions(+), 128 deletions(-) create mode 100644 src/content/news/0-7-0.html create mode 100644 src/content/news/_index.html diff --git a/src/content/_index.html b/src/content/_index.html index 253e406..dcf6cd7 100644 --- a/src/content/_index.html +++ b/src/content/_index.html @@ -1,22 +1,23 @@ --- title: "Enroll" -html_title: "Enroll - Reverse-engineering servers into Ansible" -description: "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible roles/playbooks from what it finds. Harvest → Manifest → Manage." -og_title: "Enroll - Reverse-engineering servers into Ansible" -og_description: "Harvest a host's real configuration and turn it into Ansible roles/playbooks. Safe-by-default, with optional SOPS encryption." +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
-

Get an existing Linux host into Ansible in seconds.

-

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

+
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 @@ -29,24 +30,18 @@ og_type: "website"
Config in the blink of an eye
-
single-shot → ansible-playbook
+
harvest once → render to your CM tool
-
$ enroll single-shot --harvest ./harvest --out ./ansible
-
$ cd ./ansible && tree -L 2
-
.
-├── ansible.cfg
-├── playbook.yml
-├── roles/
-│   ├── cron/
-│   ├── etc_custom/
-│   ├── firewall/
-│   ├── nginx/
-│   ├── openssh-server/
-│   ├── users/
-└── README.md
+
$ 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 inventory-driven, data-driven roles.
+
Tip: for multiple hosts, use --fqdn to generate target-native host data: Ansible inventory, Puppet Hiera, or Salt pillar.
@@ -71,7 +66,7 @@ og_type: "website"
Manifest
-
Render Ansible roles & playbooks from the harvest.
+
Render Ansible roles/playbooks, Puppet modules/Hiera data, or Salt states/pillar from the harvest.
@@ -107,11 +102,27 @@ og_type: "website"
+
+
+
+
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, roles are data-driven and host inventory decides what gets managed per host.
+
In --fqdn mode, host-specific data moves into the target's native host-data layer: Ansible inventory, Puppet Hiera, or Salt pillar.
@@ -119,7 +130,7 @@ og_type: "website"
Remote over SSH
-
Harvest a remote host from your workstation, then manifest Ansible output locally.
+
Harvest a remote host from your workstation, then render Ansible, Puppet, or Salt output locally.
@@ -171,19 +182,28 @@ og_type: "website"
-
# Harvest → Manifest in one go
-enroll single-shot --harvest ./harvest --out ./ansible
+              
# Harvest once
+enroll harvest --out ./harvest
 
-# Then run Ansible locally
-ansible-playbook -i "localhost," -c local ./ansible/playbook.yml
+# 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 carving a golden role set you'll refine over time.
+
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.

-
Want templates for structured configs? Install JinjaTurtle and use --jinjaturtle (or let it auto-detect).
+
Using Ansible and want templates for structured configs? Install JinjaTurtle and use --jinjaturtle (or let it auto-detect).
@@ -197,6 +217,7 @@ enroll single-shot \ --remote-host myhost.example.com \ --remote-user myuser \ --harvest /tmp/enroll-harvest \ + --target ansible \ --out ./ansible \ --fqdn myhost.example.com @@ -206,14 +227,23 @@ enroll single-shot \
-
# Multi-site mode: shared roles, host-specific state in inventory
+          
# Multi-site mode: one harvest, target-native host data
+fqdn="$(hostname -f)"
 enroll harvest --out /tmp/enroll-harvest
-enroll manifest --harvest /tmp/enroll-harvest --out ./ansible --fqdn "$(hostname -f)"
 
-# Run the per-host playbook
-ansible-playbook ./ansible/playbooks/"$(hostname -f)".yml
+# 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"; --fqdn for "many servers, high abstraction, fast adoption".
+
Rule of thumb: single-site for "one server, easy-to-read roles/modules/states"; --fqdn for "many servers, host-specific data, fast adoption".
@@ -260,14 +290,14 @@ enroll explain /path/to/harvest.sops \
-
# Validate a harvest is correct.
+          
# 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 confirms 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.
+
'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.
@@ -295,7 +325,7 @@ enroll validate /path/to/harvest --schema https://enroll.sh/schema/state.schema.
Manifest
-
Render Ansible roles/playbooks.
+
Render Ansible roles/playbooks, Puppet modules, or Salt states.
@@ -325,6 +355,23 @@ enroll validate /path/to/harvest --schema https://enroll.sh/schema/state.schema. +
+
+
+
+
+
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.

+
+ +
+
+
+
+
diff --git a/src/content/docs.html b/src/content/docs.html index d6dbb48..5e6812d 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -18,6 +18,7 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
Mental model + Targets and grouping How harvesting works State schema Single-site vs multi-site @@ -38,7 +39,7 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."

Mental model

-

Enroll is intentionally simple: it collects facts first, then renders Ansible from those facts.

+

Enroll is intentionally simple: it collects facts first, then renders target-native configuration-management output from those facts.

@@ -54,7 +55,7 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
  • Detect installed packages and services
  • Collect config that deviates from packaged defaults (where possible)
  • Grab relevant custom/unowned files in service dirs
  • -
  • Capture non-system users & SSH public keys, .bashrc files etc
  • +
  • Capture non-system users, group memberships, SSH public keys, and dangerous-mode shell dotfiles when requested
  • @@ -64,13 +65,13 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
    2) Manifest
    -
    Generate an Ansible repo structure
    +
    Generate Ansible, Puppet, or Salt output
      -
    • Roles with files/templates and defaults
    • -
    • Playbooks to apply the captured state
    • -
    • Optional inventory structure for multi-host runs: each host gets its own playbook
    • +
    • Ansible roles/playbooks, Puppet modules/Hiera data, or Salt states/pillar
    • +
    • Noop-friendly output you can review before applying
    • +
    • Optional target-native host data for multi-host runs
    @@ -79,8 +80,52 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
    Typical flow
    $ enroll harvest --out /tmp/enroll-harvest
    -$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
    -$ ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml
    +$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible +$ ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml --check --diff + +$ enroll manifest --harvest /tmp/enroll-harvest --target puppet --out /tmp/enroll-puppet +$ puppet apply --modulepath /tmp/enroll-puppet/modules /tmp/enroll-puppet/manifests/site.pp --noop + +$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt +$ salt-call --local --file-root /tmp/enroll-salt/states state.apply test=True
    +
    +
    + +
    +

    Targets and grouping

    +

    The harvest bundle is the contract. Manifesting is a separate step, so the same harvest can be rendered into Ansible, Puppet, or Salt without re-probing the host.

    + +
    +
    +
    +
    Ansible
    +

    Generates playbooks, roles, defaults, files/templates, inventory, and host vars.

    +
    $ enroll manifest --harvest ./harvest --target ansible --out ./ansible
    +$ ansible-playbook -i "localhost," -c local ./ansible/playbook.yml --check --diff
    +
    +
    +
    +
    +
    Puppet
    +

    Generates a control-repo-style tree with manifests/site.pp, modules, files, and Hiera data in --fqdn mode.

    +
    $ enroll manifest --harvest ./harvest --target puppet --out ./puppet
    +$ puppet apply --modulepath ./puppet/modules ./puppet/manifests/site.pp --noop
    +
    +
    +
    +
    +
    Salt
    +

    Generates Salt state trees under states/, with pillar data under pillar/ in --fqdn mode.

    +
    $ enroll manifest --harvest ./harvest --target salt --out ./salt
    +$ salt-call --local --file-root ./salt/states state.apply test=True
    +
    +
    +
    + +
    +
    Reducing role/module/state sprawl
    +

    When Enroll can see package metadata, it tries to combine related package/service snapshots by the package Section category, or equivalent backend metadata. On Debian-like systems this can turn many tiny package roles into broader groups such as admin, web, net, libs, or utils. On RPM-like systems it uses comparable package group metadata where available.

    +

    This grouping is enabled by default in single-site output. It is disabled by --no-common-roles when you want one role/module/state per discovered package/service, and it is also disabled by --fqdn because host-specific output should stay isolated in inventory/Hiera/pillar.

    @@ -92,19 +137,19 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
  • Detects the OS and its package backend (e.g dpkg vs rpm)
  • Detects what packages are installed
  • For each package, it tries to detect files in /etc that have been modified from the default that get shipped with the package.
  • -
  • It detects running/enabled services and timers via systemd. For each of these, it looks for the unit files, any 'drop-in' files, environment variable files, etc, as well as what executable it executes, and tries to map those systemd services to the packages it's already learned about earlier (that way, those 'packages' or future Ansible roles, can also be associated with 'handlers' in Ansible, to handle restart of the services if/when the configs change)
  • +
  • It detects running/enabled services and timers via systemd. For each of these, it looks for the unit files, any 'drop-in' files, environment variable files, etc, as well as what executable it executes, and tries to map those systemd services to the packages it's already learned about earlier (that way, those 'packages' or future manifest output can also be associated with service handling in the selected target, to handle restart of the services if/when the configs change)
  • Aside from known packages already learned, it optimistically tries to capture extra system configuration in /etc that is common for config management. This is stuff like the apt or dnf configuration, crons, logrotate configs, networking settings, hosts files, etc.
  • -
  • For applications that commonly make use of symlinks (think Apache2 or Nginx's sites-enabled or mods-enabled), it notes what symlinks exist so that it can capture those in Ansible
  • +
  • For applications that commonly make use of symlinks (think Apache2 or Nginx's sites-enabled or mods-enabled), it notes what symlinks exist so that it can capture those in the generated manifest
  • It also looks for other snowflake stuff in /etc not associated with packages/services or other typical system config, and will put these into an etc_custom role.
  • Likewise, it looks in /usr/local for stuff, on the assumption that this is an area that custom apps/configs might've been placed in. These go into a usr_local_custom role.
  • -
  • It captures non-system user accounts, their group memberships and files such as their .ssh/authorized_keys, and .bashrc, .profile, .bash_aliases, .bash_logout if these files differ from the skel defaults
  • +
  • It captures non-system user accounts, group memberships, and files such as .ssh/authorized_keys. In --dangerous mode it can also capture shell dotfiles such as .bashrc, .profile, .bash_aliases, and .bash_logout when they differ from the skel defaults.
  • It takes into account anything the user set with --exclude-path or --include-path. For anything extra that is included, it will put these into an 'extra_paths' role. The location could be anywhere e.g something in /opt, /srv, whatever you want.
  • It writes the state.json and captures the artifacts.

  • Other things to be aware of:

    -
    Does Enroll use Ansible community/galaxy roles?
    -
    No, Enroll doesn't have any knowledge of Ansible Galaxy roles or community plugins. It generates all the roles itself. If you really want to use roles from the community, Enroll may not be the tool for you, other than perhaps to help get you started.
    +
    Does Enroll use community roles/modules?
    +
    No, Enroll does not pull in Ansible Galaxy roles, Puppet Forge modules, or Salt formulas. It generates its own output from the harvest state. If you want to adopt community content later, Enroll can still help you discover and bootstrap the host-specific state you need to port.

    Keep in mind that a lot of software config files are also good candidates for being Jinja templates with abstracted vars for separate hosts.

    -
    Enroll does use my companion tool JinjaTurtle if it's installed, but JinjaTurtle only recognises certain types of files (.ini style, .json, .xml, .yaml, .toml, but not special ones like Nginx or Apache conf files which have their own special syntax). When Enroll can't turn a config file into a template, it copies the raw file instead and uses it with ansible.builtin.copy in role tasks.
    +
    Enroll does use my companion tool JinjaTurtle if it's installed, but JinjaTurtle only recognises certain types of files (.ini style, .json, .xml, .yaml, .toml, but not special ones like Nginx or Apache conf files which have their own special syntax). When Enroll cannot turn a config file into a template, it copies the raw file instead. JinjaTurtle templating is currently most useful for Ansible output; Puppet and Salt output stay closer to raw managed files and target-native data.
    @@ -125,7 +170,7 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."

    State schema

    Enroll writes a state.json file describing what was harvested. The canonical definition of that file format is the JSON Schema below.

    -

    You can also validate a harvest state file against the schema by using enroll validate /path/to/harvest.

    +

    You can also validate a harvest state file against the schema by using enroll validate /path/to/harvest.

    @@ -137,17 +182,18 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."

    Single-site vs multi-site

    -

    Manifest output has two styles. Choose based on how you'll use the result.

    +

    Manifest output has two styles. The exact files differ by target, but the intent is the same.

    Single-site (default)
    -

    Best when you are enrolling one host, or you're producing a reusable "golden" role set that could be applied anywhere.

    +

    Best when you are enrolling one host, or producing a reusable baseline that you will review and refine.

      -
    • Roles are self-contained
    • -
    • Raw files live in each role's files/
    • -
    • Template variables live in defaults/main.yml
    • +
    • Ansible: self-contained roles and playbook.yml
    • +
    • Puppet: manifests/site.pp plus modules under modules/
    • +
    • Salt: states/top.sls plus states under states/roles/
    • +
    • Common role grouping is enabled unless --no-common-roles is passed
    @@ -156,23 +202,31 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
    Multi-site (--fqdn)

    Best when you want to enroll several existing servers quickly, especially if they differ.

      -
    • Roles are shared; raw files live in host-specific inventory
    • -
    • Inventory decides what gets managed on each host (files/packages/services)
    • -
    • Non-templated files go under inventory/host_vars/<fqdn>/<role>/.files
    • +
    • Ansible: host-specific state in inventory/host_vars/<fqdn>/
    • +
    • Puppet: host-specific data in Hiera under data/nodes/<fqdn>.yaml
    • +
    • Salt: host-specific data in pillar under pillar/nodes/
    • +
    • Common grouping is disabled so each host's harvested state remains isolated
    -
    Multi-site example
    -
    $ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$(hostname -f)"
    -$ ansible-playbook /tmp/enroll-ansible/playbooks/"$(hostname -f)".yml
    +
    Multi-site noop examples
    +
    $ fqdn="$(hostname -f)"
    +$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible --fqdn "$fqdn"
    +$ ansible-playbook /tmp/enroll-ansible/playbooks/${fqdn}.yml -i /tmp/enroll-ansible/inventory/hosts.ini -c local --check --diff
    +
    +$ enroll manifest --harvest /tmp/enroll-harvest --target puppet --out /tmp/enroll-puppet --fqdn "$fqdn"
    +$ puppet apply --modulepath /tmp/enroll-puppet/modules --hiera_config /tmp/enroll-puppet/hiera.yaml --certname "$fqdn" /tmp/enroll-puppet/manifests/site.pp --noop
    +
    +$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt --fqdn "$fqdn"
    +$ salt-call --local --id "$fqdn" --file-root /tmp/enroll-salt/states --pillar-root /tmp/enroll-salt/pillar state.apply test=True
    -
    Tip: role tags
    -
    Generated playbooks tag each role as role_<name> (e.g. role_users, role_services, role_other). You can target a subset with ansible-playbook ... --tags role_users.
    +
    Tip: targeting subsets
    +
    Ansible playbooks tag each role as role_<name>. Puppet and Salt output can be trimmed by editing generated class/state inclusion data. In every target, treat the first generated manifest as a reviewable bootstrap rather than magic.
    @@ -181,12 +235,12 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."

    Run Enroll on your workstation, harvest a remote host over SSH. The harvest is pulled locally.

    $ enroll harvest --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-harvest
    -$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-manifest
    +$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible
     
     # Alternatively, run both commands combined together with the 'single-shot' mode:
     
     $ enroll single-shot --remote-host myhost.example.com --remote-user myuser \
    -  --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible \
    +  --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible \
       --fqdn myhost.example.com
     
     # Encrypted SSH key examples:
    @@ -211,7 +265,7 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
     
             

    JinjaTurtle integration

    -

    If JinjaTurtle (one of my other projects) is installed, Enroll can also produce Jinja2 templates for ini/json/xml/toml-style config and extract variables cleanly into Ansible, instead of just storing the 'raw' files.

    +

    If JinjaTurtle (one of my other projects) is installed, Enroll can also produce Jinja2 templates for ini/json/xml/toml-style config and extract variables cleanly into Ansible, instead of just storing the 'raw' files. Puppet and Salt output currently focus on raw managed files plus target-native data.

    @@ -227,8 +281,8 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
    Where variables land
      -
    • Single-site: roles/<role>/defaults/main.yml
    • -
    • Multi-site: inventory/host_vars/<fqdn>/<role>.yml
    • +
    • Ansible single-site: roles/<role>/defaults/main.yml
    • +
    • Ansible multi-site: inventory/host_vars/<fqdn>/<role>.yml
    @@ -279,7 +333,7 @@ ignore_package_versions = true

    Drift detection with enroll diff

    -

    One of the things I miss from my Puppet days, was the way the Puppet 'agent' would check in with the server and realign itself to the declared desired state. With Ansible, it's easy for systems to fall 'out of date', especially if someone is doing the wrong thing and changing things on-the-fly instead of via config management!

    +

    One of the things I miss from my Puppet days, was the way the Puppet 'agent' would check in with the server and realign itself to the declared desired state. With any configuration-management system, it is easy for systems to fall out of date if someone changes things on-the-fly instead of via the desired-state workflow.

    The purpose of enroll diff is to compare two 'harvests' and detect what has changed - be it adding/removing of programs, change to systemd unit state, modifications, addition or removal of files, and so on.

    @@ -302,7 +356,7 @@ ignore_package_versions = true
    Optional: enforce the old harvest state (--enforce)
    -
    If drift exists and ansible-playbook is on PATH, Enroll can generate a manifest from the old harvest and apply it locally to restore expected state. It avoids package downgrades, and will often run Ansible with --tags role_... so only the roles implicated by the drift are applied. This is very much like a return to Puppet's agent mode!
    +
    If drift exists and ansible-playbook is on PATH, Enroll can generate a manifest from the old harvest and apply it locally to restore expected state. It avoids package downgrades, and will often run Ansible with --tags role_... so only the roles implicated by the drift are applied. This is intentionally Ansible-based today, even when you also use Puppet or Salt rendering for normal manifests.
    @@ -561,7 +615,7 @@ sudo journalctl -u enroll-harvest-diff.service -n 200 --no-pager
    Multi-host safety
    -

    For fleets, prefer multi-site output so roles stay generic and host inventory controls what is applied per host - reducing "shared role breaks other host" surprises.

    +

    For fleets, prefer multi-site output so target-native host data controls what is applied per host - reducing "shared role/module/state breaks another host" surprises.

    diff --git a/src/content/examples.html b/src/content/examples.html index 464eb1e..58e5ca5 100644 --- a/src/content/examples.html +++ b/src/content/examples.html @@ -1,13 +1,13 @@ --- title: "Examples" html_title: "Enroll Examples" -description: "Copy/paste recipes for Enroll: one host, fleets, drift detection, and safe storage." +description: "Copy/paste recipes for Enroll: Ansible, Puppet, Salt, fleets, drift detection, and safe storage." ---
    Examples

    Copy/paste recipes

    -

    Practical flows you can adapt to your environment.

    +

    Practical flows you can adapt to Ansible, Puppet, Salt, or all three.

    @@ -17,49 +17,85 @@ description: "Copy/paste recipes for Enroll: one host, fleets, drift detection,
    -
    Enroll a single host (local)
    +
    Harvest once, try each renderer locally
    $ enroll harvest --out /tmp/enroll-harvest
    -$ enroll manifest --harvest /tmp/enroll-harvest \
    -  --out /tmp/enroll-ansible
    -$ ansible-playbook -i "localhost," -c local \
    -  /tmp/enroll-ansible/playbook.yml --diff --check
    + +# Ansible noop +$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible +$ ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml --diff --check + +# Puppet noop +$ enroll manifest --harvest /tmp/enroll-harvest --target puppet --out /tmp/enroll-puppet +$ puppet apply --modulepath /tmp/enroll-puppet/modules /tmp/enroll-puppet/manifests/site.pp --noop + +# Salt noop +$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt +$ salt-call --local --file-root /tmp/enroll-salt/states state.apply test=True
    -

    Great for "make this box reproducible" or building a golden role set.

    +

    Great for “make this box reproducible”, testing renderer output, or building a golden baseline you will refine.

    -
    Enroll a remote host (over SSH)
    +
    Enroll a remote host over SSH
    $ enroll harvest \
       --remote-host myhost.example.com \
       --remote-user myuser \
       --out /tmp/enroll-harvest
    +
     $ enroll manifest \
       --harvest /tmp/enroll-harvest \
    -  --out /tmp/enroll-ansible
    + --target puppet \ + --out /tmp/enroll-puppet
    -

    No need to manually run commands on the server - your bundle lands locally. If your remote user needs a password for sudo, pass in --ask-become-pass or -K, just like in Ansible. If you don't want to use sudo, pass --no-sudo, but your harvest may contain less data.

    +

    The bundle lands locally. Change --target puppet to ansible or salt without re-harvesting. For remote sudo prompts use --ask-become-pass/-K; use --no-sudo for a less complete non-root harvest.

    -
    Fleets: multi-site output
    +
    Fleets: --fqdn output
    $ fqdn="$(hostname -f)"
    -$ enroll single-shot --remote-host "$fqdn" \
    -  --remote-user myuser \
    -  --out /tmp/enroll-ansible \
    -  --fqdn "$fqdn"
    -$ ansible-playbook "/tmp/enroll-ansible/playbooks/${fqdn}.yml"
    +$ enroll harvest --out /tmp/enroll-harvest + +# Ansible: inventory/host_vars and a per-host playbook +$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible --fqdn "$fqdn" +$ ansible-playbook /tmp/enroll-ansible/playbooks/${fqdn}.yml -i /tmp/enroll-ansible/inventory/hosts.ini -c local --check --diff + +# Puppet: Hiera data keyed by certname +$ enroll manifest --harvest /tmp/enroll-harvest --target puppet --out /tmp/enroll-puppet --fqdn "$fqdn" +$ puppet apply --modulepath /tmp/enroll-puppet/modules --hiera_config /tmp/enroll-puppet/hiera.yaml --certname "$fqdn" /tmp/enroll-puppet/manifests/site.pp --noop + +# Salt: pillar data keyed by minion id +$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt --fqdn "$fqdn" +$ salt-call --local --id "$fqdn" --file-root /tmp/enroll-salt/states --pillar-root /tmp/enroll-salt/pillar state.apply test=True
    -

    Shared roles + host inventory keeps one host's differences from breaking another.

    +

    --fqdn disables common grouping and moves host-specific state into the target’s native host-data layer: inventory, Hiera, or pillar.

    +
    +
    + +
    +
    +
    Control role/module/state grouping
    +
    + +
    # Default single-site mode: combine packages/services by package Section where possible
    +$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible
    +
    +# Keep output split more literally by discovered package/service role
    +$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible-raw --no-common-roles
    +
    +# --fqdn also disables common grouping because host data must remain isolated
    +$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt --fqdn "$(hostname -f)"
    +
    +

    Grouping keeps first-pass output manageable. Use --no-common-roles when you prefer more direct one-role-per-package/service output.

    @@ -68,15 +104,19 @@ description: "Copy/paste recipes for Enroll: one host, fleets, drift detection,
    Drift detection with enroll diff
    -
    $ enroll diff --old /path/to/harvestA --new /path/to/harvestB --format markdown --exclude-path /var/anacron --ignore-package-versions
    +            
    $ enroll diff --old /path/to/harvestA --new /path/to/harvestB \
    +  --format markdown \
    +  --exclude-path /var/anacron \
    +  --ignore-package-versions
    +
     $ enroll diff --old /path/to/golden --new /path/to/current \
    -  --webhook https://example.net/webhook  \
    +  --webhook https://example.net/webhook \
       --webhook-format json \
    -  --webhook-header 'X-Enroll-Secret: ...'  \
    -   --ignore-package-versions --exit-code
    -            
    + --webhook-header 'X-Enroll-Secret: ...' \ + --ignore-package-versions \ + --exit-code
    -

    Use it in cron or CI to alert on change.

    +

    Use it in cron, systemd timers, or CI to alert on change.

    @@ -87,13 +127,13 @@ description: "Copy/paste recipes for Enroll: one host, fleets, drift detection,
    $ enroll explain /tmp/enroll-harvest
     
    -# machine-readable (reasons, examples, inventory breakdown)
    +# machine-readable reasons, examples, and inventory breakdown
     $ enroll explain /tmp/enroll-harvest --format json | jq .
     
     # encrypted bundle
     $ enroll explain /var/lib/enroll/harvest.tar.gz.sops --sops
    -

    Great for answering "why did it include/exclude that file?" before you generate a manifest.

    +

    Great for answering “why did it include/exclude that file?” before you generate a manifest.

    @@ -101,39 +141,31 @@ description: "Copy/paste recipes for Enroll: one host, fleets, drift detection,
    Enforce the previous state with enroll diff --enforce
    - -
    $ enroll diff \
    +            
    +            
    $ enroll diff \
       --old /path/to/harvestA \
       --new /path/to/harvestB \
       --enforce \
    -  --format json
    -	    
    + --format json
    -

    Enforcing the old harvest will restore its files/perms, missing packages, changed services or users, if ansible-playbook is on the PATH.

    +

    Enforcement currently uses generated Ansible and ansible-playbook on PATH, even if you also use Puppet or Salt for normal manifest output.

    - -
    - -
    -
    -
    Safe harvesting (default)
    -

    Enroll tries to avoid harvesting files that might contain secrets. If you need to capture "everything", pass --dangerous and treat the output as sensitive.

    -

    You can still control what gets collected and what doesn't by using --include and --exclude flags.

    -
    $ enroll harvest --dangerous --out /tmp/enroll-harvest
    -
    -
    -
    -
    -
    Encrypt bundles at rest (SOPS)
    -

    Produce a single encrypted file for harvest and/or manifest output (requires SOPS to be installed).

    -

    This is especially a good idea if you are using --dangerous, which might sweep up secrets (see above).

    -
    $ enroll harvest --dangerous --out /tmp/harvest \
    +        
    +
    Encrypt bundles at rest with SOPS
    +
    + +
    $ enroll harvest --dangerous --out /tmp/harvest \
       --sops <FINGERPRINT>
    +
     $ enroll manifest --harvest /tmp/harvest/harvest.tar.gz.sops \
    -  --out /tmp/enroll-ansible --sops <FINGERPRINT>
    + --target salt \ + --out /tmp/enroll-salt \ + --sops <FINGERPRINT>
    +
    +

    Use this if you intend to store harvests/manifests long term or if you harvested with --dangerous.

    diff --git a/src/content/news/0-7-0.html b/src/content/news/0-7-0.html new file mode 100644 index 0000000..f4c5f97 --- /dev/null +++ b/src/content/news/0-7-0.html @@ -0,0 +1,115 @@ +--- +title: "Enroll 0.7.0: Puppet and Salt renderer support, and more" +html_title: "Enroll 0.7.0 News" +description: "Enroll 0.7.0 adds first-class Puppet and Salt manifest rendering alongside Ansible, along with other improvements." +date: 2026-06-17 +summary: "Enroll can now render Puppet and Salt manifests alongside Ansible, detect Flatpak and Snap packages and other obscure runtime state, and produce better organised roles/modules." +--- +
    +
    +
    Release notes
    +

    Enroll 0.7.0: Puppet and Salt join Ansible

    +

    A single harvest can now be rendered into Ansible, Puppet, or Salt output.

    +
    +
    + +
    +
    +
    +
    +
    +

    Welcome to the first of Enroll's new, erm, news section! To celebrate, Enroll 0.7.0 has been released, and makes manifest rendering target-selectable. Ansible remains the default, but Puppet and Salt are now first-class manifest targets rather than ad-hoc exports.

    + +

    Highlights

    +
      +
    • --target puppet renders Puppet module/control-repo style output., and in --fqdn mode, renders per-host Hiera data.
    • +
    • --target salt renders Salt state trees and, in --fqdn mode, Salt pillar data.
    • +
    • Ansible works basically as it always did, and is the default, but you can specify --target ansible too. As usual, in --fqdn mode, specific artifacts end up in host_vars inventory folders rather than polluting the 'golden' roles.
    • +
    • All three config management manifest renderers derive from the same harvest state. You can rendered repeatedly into different config management tools without re-harvesting the host!
    • +
    • Single-site output tries to combine package/service data by their package manager's Section (or equivalent metadata), to reduce role/module/state sprawl.
    • +
    • Flatpak and Snap detection!
    • +
    + +

    Dry-run examples

    +
    + +
    $ enroll harvest --out ./harvest
    +
    +$ enroll manifest --harvest ./harvest --target ansible --out ./ansible
    +$ ansible-playbook -i "localhost," -c local ./ansible/playbook.yml --check --diff
    +
    +$ enroll manifest --harvest ./harvest --target puppet --out ./puppet
    +$ puppet apply --modulepath ./puppet/modules ./puppet/manifests/site.pp --noop
    +
    +$ enroll manifest --harvest ./harvest --target salt --out ./salt
    +$ salt-call --local --file-root ./salt/states state.apply test=True
    +
    + +

    New grouping behaviour in roles/modules

    +

    Did you find the number of manifested roles overwhelming?

    +

    Previously, Enroll created an Ansible role (or, now, a Puppet module or Salt role) for pretty much every 'package' it found. In some cases (especially on desktops) this could result in hundreds of roles. Technically fine, but overwhelming to look at!

    + +

    As of 0.7.0, where Enroll can read that package metadata, it groups related package and service snapshots by the package manager's Section category (or comparable backend metadata), to make it less noisy. For example, network-related packages and config files might end up in a role called net. Meanwhile, vim, nano might both appear in editors, and mutt and Thunderbird may be in mail.

    + +

    If you're not a fan of this new layout, you can pass --no-common-roles to enforce the previous behaviour. Also, if you use --fqdn for host-specific data-driven output, the 'common' roles are disabled automatically, because it's then safer to avoid 'bleed in' of unnecessary package installation on other hosts from a role that otherwise 'assumes too much' for all hosts.

    + +

    Flatpak and Snap detection

    +

    Beyond deb and rpm

    +

    When using Ansible, Enroll now attempts to detect Flatpak and Snaps present on the system. For Flatpaks, this includes user-specific Flatpaks as well as system-wide ones. Manifesting to Ansible will attempt to use the community.general collection to create Flatpak and Snap tasks to enforce the presence of those packages.

    +
    +
    $ sudo ansible-playbook playbook.yml -i localhost, -c local --tags role_snap --diff
    +
    +PLAY [Apply all roles on all hosts] *******************************************************************************************************************************************************************************
    +
    +TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
    +ok: [localhost]
    +
    +TASK [snap : Install system-wide snaps with full detected attributes] *********************************************************************************************************************************************
    +ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'bare', 'notes': ['base'], 'revision': 5, 'source': 'snap-list'})
    +ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'core24', 'notes': ['base'], 'revision': 1643, 'source': 'snap-list'})
    +ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'gnome-46-2404', 'notes': [], 'revision': 153, 'source': 'snap-list'})
    +ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'gtk-common-themes', 'notes': [], 'revision': 1535, 'source': 'snap-list'})
    +ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'mesa-2404', 'notes': [], 'revision': 1165, 'source': 'snap-list'})
    +ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'onionshare', 'notes': [], 'revision': 212, 'source': 'snap-list'})
    +ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'snapd', 'notes': ['snapd'], 'revision': 26865, 'source': 'snap-list'})
    +            
    +
    + +

    Please be aware that you need version 13+ of the community.general collection for this to work properly. A requirements.yml gets created with your manifest to help you install it if necessary.

    + +

    Other smaller changes

    +
      +
    • sysctl runtime parameters are now detected and would be written to /etc/sysctl.d/99-enroll.conf. Not all runtime parameters are supported.
    • +
    • .bashrc and similar files are now only harvested from user directories when --dangerous is used, since this is a common place for sensitive environment variables to be set. As always, remember that --dangerous gives better harvest coverage, but you should use --sops or some other means of your own to encrypt the harvested data at rest safely!
    • +
    • Some output during an Ansible play is hidden with no_log to avoid potentially sensitive output, particularly of systemd unit state.
    • +
    • In case you missed it in version 0.6.0: Enroll now harvests runtime iptables and ipset rules!
    • +
    + +

    See you soon..

    +

    I'm off to try and write more tests - we're at 84% coverage in pytest, and we also run a stack of 'noop' executions for Ansible, Puppet and Salt too now, in CI.

    +

    Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports! You're helping make Enroll better for everyone.

    + + +
    +
    + +
    +
    +
    Targets
    +
      +
    • --target ansible (default)
    • +
    • --target puppet
    • +
    • --target salt
    • +
    +
    +
    +
    Useful links
    + +
    +
    +
    +
    +
    diff --git a/src/content/news/_index.html b/src/content/news/_index.html new file mode 100644 index 0000000..6958573 --- /dev/null +++ b/src/content/news/_index.html @@ -0,0 +1,12 @@ +--- +title: "News" +html_title: "Enroll News" +description: "Release notes and project updates for Enroll." +--- +
    +
    +
    News
    +

    What's new in Enroll

    +

    Release notes, tips and tricks, and whatever else we've been cooking up.

    +
    +
    diff --git a/src/content/security.html b/src/content/security.html index c2a812b..2218f29 100644 --- a/src/content/security.html +++ b/src/content/security.html @@ -40,12 +40,12 @@ description: "Security posture and safe workflows for Enroll outputs."
    $ enroll harvest --out /tmp/enroll-harvest --dangerous --sops <FINGERPRINT>
     $ enroll manifest --harvest /tmp/enroll-harvest/harvest.tar.gz.sops \
    -  --out /tmp/enroll-ansible --sops <FINGERPRINT>
    + --target ansible --out /tmp/enroll-ansible --sops <FINGERPRINT>
    Important
    -
    In manifest --sops mode, you'll need to decrypt and extract the bundle before running ansible-playbook.
    +
    In manifest --sops mode, you'll need to decrypt and extract the bundle before running ansible-playbook, puppet apply, or salt-call.
    @@ -92,7 +92,7 @@ description: "Security posture and safe workflows for Enroll outputs."
    From 83c30de44a9217ee9f4b45c04fdd9c48df89d340 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 17 Jun 2026 15:35:26 +1000 Subject: [PATCH 02/30] 0.7.0 --- src/hugo.toml | 2 +- src/static/schema/state.schema.json | 298 ++++++++++++++++++ .../enroll-theme/layouts/news/list.html | 19 ++ .../enroll-theme/layouts/news/single.html | 3 + .../enroll-theme/layouts/partials/footer.html | 15 +- .../enroll-theme/layouts/partials/head.html | 2 +- .../enroll-theme/layouts/partials/nav.html | 13 +- .../layouts/partials/scripts.html | 2 +- 8 files changed, 340 insertions(+), 14 deletions(-) create mode 100644 src/themes/enroll-theme/layouts/news/list.html create mode 100644 src/themes/enroll-theme/layouts/news/single.html diff --git a/src/hugo.toml b/src/hugo.toml index 53735bb..4172e70 100644 --- a/src/hugo.toml +++ b/src/hugo.toml @@ -6,4 +6,4 @@ uglyURLs = true disableKinds = ["taxonomy", "term", "RSS", "sitemap"] [params] - description = "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible roles/playbooks from what it finds. Harvest → Manifest → Manage." + description = "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible, Puppet, or Salt manifests from what it finds. Harvest → Manifest → Manage." diff --git a/src/static/schema/state.schema.json b/src/static/schema/state.schema.json index d0bde52..a67cf64 100644 --- a/src/static/schema/state.schema.json +++ b/src/static/schema/state.schema.json @@ -117,6 +117,14 @@ "minLength": 1, "type": "string" }, + "group": { + "minLength": 1, + "type": "string" + }, + "section": { + "minLength": 1, + "type": "string" + }, "version": { "minLength": 1, "type": "string" @@ -364,6 +372,12 @@ }, "type": "array" }, + "section": { + "type": [ + "string", + "null" + ] + }, "version": { "type": [ "string", @@ -390,6 +404,16 @@ "package": { "minLength": 1, "type": "string" + }, + "has_config": { + "type": "boolean", + "default": true + }, + "section": { + "type": [ + "string", + "null" + ] } }, "required": [ @@ -571,6 +595,21 @@ "$ref": "#/$defs/UserEntry" }, "type": "array" + }, + "user_flatpaks": { + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/FlatpakInstall" + } + }, + "type": "object" + }, + "user_flatpak_remotes": { + "type": "array", + "items": { + "$ref": "#/$defs/FlatpakRemote" + } } }, "required": [ @@ -652,6 +691,256 @@ "notes" ], "type": "object" + }, + "SysctlSnapshot": { + "additionalProperties": false, + "properties": { + "role_name": { + "const": "sysctl" + }, + "managed_files": { + "items": { + "$ref": "#/$defs/ManagedFile" + }, + "type": "array" + }, + "parameters": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "notes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "role_name", + "managed_files", + "parameters", + "notes" + ], + "type": "object" + }, + "FlatpakInstall": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "method": { + "type": "string", + "enum": [ + "system", + "user" + ] + }, + "remote": { + "type": [ + "string", + "null" + ] + }, + "branch": { + "type": [ + "string", + "null" + ] + }, + "arch": { + "type": [ + "string", + "null" + ] + }, + "kind": { + "type": [ + "string", + "null" + ], + "enum": [ + "app", + "runtime", + null + ] + }, + "ref": { + "type": [ + "string", + "null" + ] + }, + "user": { + "type": [ + "string", + "null" + ] + }, + "home": { + "type": [ + "string", + "null" + ] + }, + "source": { + "type": "string" + }, + "from_url": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "name", + "method" + ], + "type": "object" + }, + "FlatpakRemote": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "method": { + "type": "string", + "enum": [ + "system", + "user" + ] + }, + "url": { + "type": "string", + "minLength": 1 + }, + "user": { + "type": [ + "string", + "null" + ] + }, + "home": { + "type": [ + "string", + "null" + ] + }, + "source": { + "type": "string" + } + }, + "required": [ + "name", + "method", + "url" + ], + "type": "object" + }, + "SnapInstall": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "channel": { + "type": [ + "string", + "null" + ] + }, + "revision": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "classic": { + "type": "boolean" + }, + "devmode": { + "type": "boolean" + }, + "dangerous": { + "type": "boolean" + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "install_revision": { + "type": "boolean" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "FlatpakSnapshot": { + "additionalProperties": false, + "properties": { + "role_name": { + "const": "flatpak" + }, + "system_flatpaks": { + "type": "array", + "items": { + "$ref": "#/$defs/FlatpakInstall" + } + }, + "remotes": { + "type": "array", + "items": { + "$ref": "#/$defs/FlatpakRemote" + } + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "role_name" + ], + "type": "object" + }, + "SnapSnapshot": { + "additionalProperties": false, + "properties": { + "role_name": { + "const": "snap" + }, + "system_snaps": { + "type": "array", + "items": { + "$ref": "#/$defs/SnapInstall" + } + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "role_name" + ], + "type": "object" } }, "$id": "https://enroll.sh/schema/state.schema.json", @@ -762,6 +1051,15 @@ }, "firewall_runtime": { "$ref": "#/$defs/FirewallRuntimeSnapshot" + }, + "sysctl": { + "$ref": "#/$defs/SysctlSnapshot" + }, + "flatpak": { + "$ref": "#/$defs/FlatpakSnapshot" + }, + "snap": { + "$ref": "#/$defs/SnapSnapshot" } }, "required": [ diff --git a/src/themes/enroll-theme/layouts/news/list.html b/src/themes/enroll-theme/layouts/news/list.html new file mode 100644 index 0000000..6c0783c --- /dev/null +++ b/src/themes/enroll-theme/layouts/news/list.html @@ -0,0 +1,19 @@ +{{ define "main" }} +{{ .Content | safeHTML }} +
    +
    +
    + {{ range .Pages.ByDate.Reverse }} +
    +
    +
    {{ .Date.Format "2 January 2006" }}
    +

    {{ .Title }}

    + {{ with .Params.summary }}

    {{ . }}

    {{ end }} + Read more +
    +
    + {{ end }} +
    +
    +
    +{{ end }} diff --git a/src/themes/enroll-theme/layouts/news/single.html b/src/themes/enroll-theme/layouts/news/single.html new file mode 100644 index 0000000..3a342e5 --- /dev/null +++ b/src/themes/enroll-theme/layouts/news/single.html @@ -0,0 +1,3 @@ +{{ define "main" }} +{{ .Content | safeHTML }} +{{ end }} diff --git a/src/themes/enroll-theme/layouts/partials/footer.html b/src/themes/enroll-theme/layouts/partials/footer.html index 3ab6587..a8ab385 100644 --- a/src/themes/enroll-theme/layouts/partials/footer.html +++ b/src/themes/enroll-theme/layouts/partials/footer.html @@ -3,12 +3,14 @@
    - Enroll + Enroll
    Enroll (a mig5 project)
    CLI Ansible + Puppet + Salt
    -
    Reverse-engineering servers into Ansible.
    +
    Reverse-engineering servers into Ansible, Puppet, or Salt.
    @@ -26,10 +28,11 @@ diff --git a/src/themes/enroll-theme/layouts/partials/head.html b/src/themes/enroll-theme/layouts/partials/head.html index f185324..8b2bd48 100644 --- a/src/themes/enroll-theme/layouts/partials/head.html +++ b/src/themes/enroll-theme/layouts/partials/head.html @@ -21,4 +21,4 @@ - + diff --git a/src/themes/enroll-theme/layouts/partials/nav.html b/src/themes/enroll-theme/layouts/partials/nav.html index 3c03651..3812e8f 100644 --- a/src/themes/enroll-theme/layouts/partials/nav.html +++ b/src/themes/enroll-theme/layouts/partials/nav.html @@ -1,7 +1,7 @@
    - -
    -
    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
    -
    -
    diff --git a/src/content/docs.html b/src/content/docs.html index bd0523d..92f16ee 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -159,10 +159,6 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
    Does Enroll use community roles/modules?
    No, Enroll does not pull in Ansible Galaxy roles, Puppet Forge modules, or Salt formulas. It generates its own output from the harvest state. If you want to adopt community content later, Enroll can still help you discover and bootstrap the host-specific state you need to port.
    -
    -
    Keep in mind that a lot of software config files are also good candidates for being Jinja templates with abstracted vars for separate hosts.
    -
    -
    Enroll does use my companion tool JinjaTurtle if it's installed, but JinjaTurtle only recognises certain types of files (.ini style, .json, .xml, .yaml, .toml, but not special ones like Nginx or Apache conf files which have their own special syntax). When Enroll cannot turn a config file into a template, it copies the raw file instead. JinjaTurtle creates Jinja2 templates for both Ansible and Salt, and because it's a cheerful turtle, it generates .erb templates if you're manifesting Puppet code, too!
    @@ -265,7 +261,8 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."

    JinjaTurtle integration

    -

    If JinjaTurtle (one of my other projects) is installed, Enroll can also produce Jinja2 templates for ini/json/xml/toml-style config and extract variables cleanly into Ansible and Salt, and even erb files for Puppet (as of version 0.5.5).

    +

    If JinjaTurtle (one of my other projects) is installed, Enroll can also produce Jinja2 templates for ini/json/xml/toml-style config files (as well as SSH configs), and extract variables cleanly into Ansible and Salt, and even erb files for Puppet (as of version 0.5.5).

    +

    When Enroll cannot turn a config file into a template, it copies the raw file instead.

    @@ -282,7 +279,11 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
    Where variables land
    • Ansible single-site: roles/<role>/defaults/main.yml
    • -
    • Ansible multi-site: inventory/host_vars/<fqdn>/<role>.yml
    • +
    • Ansible multi-site (--fqdn): inventory/host_vars/<fqdn>/<role>.yml
    • +
    • Puppet single-site: modules/<role>/manifests/init.pp as generated class parameter defaults
    • +
    • Puppet multi-site: data/nodes/<fqdn>.yaml as Hiera class parameter data
    • +
    • Salt single-site: inline in states/roles/<role>/init.sls as file.managed context: data
    • +
    • Salt multi-site: pillar/nodes/<fqdn>.sls under enroll:roles:<role>:files:<path>:context
    @@ -316,6 +317,7 @@ exclude_path = /usr/local/bin/docker-*, /usr/local/bin/some-tool [manifest] no_jinjaturtle = true sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99 +target = ansible # is the default, otherwise salt, or puppet [diff] # ignore noisy drift From 493c6f5b8cc4cd70da0c7328046baf2534e7e0f1 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 21 Jun 2026 10:29:19 +1000 Subject: [PATCH 10/30] Fix icon for diff --- src/content/news/0-7-0.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/news/0-7-0.html b/src/content/news/0-7-0.html index 6051b50..5300397 100644 --- a/src/content/news/0-7-0.html +++ b/src/content/news/0-7-0.html @@ -160,7 +160,7 @@ localhost : ok=5 changed=0 unreachable=0 failed=0 s From 1d1316d89f19b6fc4b2134b0d12b7431cfed7c0f Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 21 Jun 2026 12:41:31 +1000 Subject: [PATCH 11/30] More updates --- src/content/_index.html | 2 +- src/content/docs.html | 3 ++- src/content/news/0-7-0.html | 10 +++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/content/_index.html b/src/content/_index.html index 98fe0aa..2e62fb4 100644 --- a/src/content/_index.html +++ b/src/content/_index.html @@ -257,7 +257,7 @@ enroll diff \ --exit-code # Ignore a path and changes to package versions, and optionally -# enforce the old state locally (requires ansible-playbook) +# enforce the old state locally enroll diff --old /path/to/harvestA --new /path/to/harvestB \ --exclude-path /var/anacron \ --ignore-package-versions \ diff --git a/src/content/docs.html b/src/content/docs.html index 92f16ee..0c64ab0 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -323,7 +323,8 @@ target = ansible # is the default, otherwise salt, or puppet # ignore noisy drift exclude_path = /var/anacron ignore_package_versions = true -# enforce = true # requires ansible-playbook on PATH +# enforce = true +# target = ansible # to run ansible-playbook to enforce the old harvest, else puppet, salt
    diff --git a/src/content/news/0-7-0.html b/src/content/news/0-7-0.html index 5300397..71a37d2 100644 --- a/src/content/news/0-7-0.html +++ b/src/content/news/0-7-0.html @@ -24,8 +24,8 @@ summary: "Enroll can now render Puppet and Salt manifests alongside Ansible, det
    • Puppet support! --target puppet renders Puppet module/control-repo style output., and in --fqdn mode, renders per-host Hiera data.
    • Salt Stack support! --target salt renders Salt state trees and, in --fqdn mode, Salt pillar data.
    • -
    • Ansible works basically as it always did, and is the default, but you can specify --target ansible too. As usual, in --fqdn mode, specific artifacts end up in host_vars inventory folders rather than polluting the 'golden' roles.
    • -
    • All three config management manifest renderers derive from the same harvest state as a single source of truth. Evaluating how different config managers work? You can rendered repeatedly into different config management tools without re-harvesting the host!
    • +
    • Ansible works basically as it always did, and is the default, but you can specify --target ansible too.
    • +
    • Evaluating how different config managers work? You can rendered repeatedly into different config management tools without re-harvesting the host, because they all use the same harvest state!
    • Single-site output tries to combine package/service data by their package manager's Section (or equivalent metadata), to reduce role/module/state sprawl and speed up execution.
    • Flatpak and Snap detection!
    • Docker image detection!
    • @@ -91,7 +91,7 @@ ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangero

      The harvest now detects the presence of container images, if the user has permission to call Docker or Podman. In particular, it detects the SHA256 of the image instead of relying on floating tags.

      All three renderers (Ansible, Salt and Puppet) will attempt to enforce the presence of those Docker images per their precise SHA256 hash, if they were present in the harvest but not on the machine upon applying a manifest.

      For Ansible, you may need the community.docker collection, but on Debian 13 I found that it was already present by default in the official ansible Debian packages.

      -

      For Ansible, if using Podman, you'll need 1.20.0 or later. Enroll creates a requirements.yml to make it easy for you: its README.md will guide you to run ansible-galaxy collection install -r requirements.yml before running the playbook.

      +

      For Ansible, if using Podman, you'll need 1.20.0 or later of the community.podman collection. Enroll creates a requirements.yml to make it easy for you: its README.md will guide you to run ansible-galaxy collection install -r requirements.yml before running the playbook.

      $ ansible-playbook -i localhost, -c local playbook.yml --check --diff --tags role_container_images
      @@ -122,7 +122,7 @@ PLAY RECAP *********************************************************************
       localhost                  : ok=5    changed=0    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0   
                   
      - +

      I did not use community extensions/modules for Docker in the Salt and Puppet renderers, because, well, they are god-awful (the Salt one simply doesn't work in 3008.1, and the Puppet one is non-idempotent and I would argue cruder in its approach to image management than a guarded Exec call can be (and is).

      Other smaller changes

      @@ -134,7 +134,7 @@ localhost : ok=5 changed=0 unreachable=0 failed=0 s

    See you soon..

    -

    I'm off to try and write more tests - we're at about 85% coverage in pytest, and we also run a stack of 'noop' executions for Ansible, Puppet and Salt too now, in CI.

    +

    I'm off to try and write more tests - we're at about 86% coverage in pytest, and run a big suite unit tests for Ansible, Puppet and Salt too now, in CI. I'm always trying to catch any regressions given there are so many variations on how you can use this tool.

    Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports! You're helping make Enroll better for everyone.

    From b48666dc43d6450c76f06c72f7181b207d5c9eae Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 21 Jun 2026 12:45:20 +1000 Subject: [PATCH 12/30] more docs --- src/content/docs.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs.html b/src/content/docs.html index 0c64ab0..58af7fc 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -158,7 +158,7 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
    Does Enroll use community roles/modules?
    -
    No, Enroll does not pull in Ansible Galaxy roles, Puppet Forge modules, or Salt formulas. It generates its own output from the harvest state. If you want to adopt community content later, Enroll can still help you discover and bootstrap the host-specific state you need to port.
    +
    When using Ansible and any Docker/Podman images, Flatpak or Snaps are 'harvested' by the system, the manifested ansible playbooks will expect to use some of those community collections, but otherwise, everything is 'core' Ansible. Enroll generates a requirements.yml you can use with ansible-galaxy to install it, if needed, but many community collections may already be on your system, depending on your installation. For Puppet and Salt, no third-party extensions or modules are used.
    From 3e635c8e47a733ada21087980aa369bf641253ee Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 21 Jun 2026 12:45:53 +1000 Subject: [PATCH 13/30] Update schema --- src/static/schema/state.schema.json | 178 ++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/src/static/schema/state.schema.json b/src/static/schema/state.schema.json index a67cf64..8806b2e 100644 --- a/src/static/schema/state.schema.json +++ b/src/static/schema/state.schema.json @@ -16,6 +16,181 @@ ], "unevaluatedProperties": false }, + "ContainerImageTagAlias": { + "additionalProperties": false, + "properties": { + "ref": { + "minLength": 1, + "type": "string" + }, + "repository": { + "minLength": 1, + "type": "string" + }, + "tag": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "ref", + "repository", + "tag" + ], + "type": "object" + }, + "ContainerImage": { + "additionalProperties": false, + "properties": { + "architecture": { + "type": [ + "string", + "null" + ] + }, + "created": { + "type": [ + "string", + "null" + ] + }, + "engine": { + "enum": [ + "docker", + "podman" + ], + "type": "string" + }, + "home": { + "type": [ + "string", + "null" + ] + }, + "image_id": { + "type": [ + "string", + "null" + ] + }, + "notes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "os": { + "type": [ + "string", + "null" + ] + }, + "platform": { + "type": [ + "string", + "null" + ] + }, + "pull_ref": { + "type": [ + "string", + "null" + ] + }, + "repo_digests": { + "items": { + "type": "string" + }, + "type": "array" + }, + "repo_tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "scope": { + "enum": [ + "system", + "user" + ], + "type": "string" + }, + "size": { + "type": [ + "integer", + "null" + ] + }, + "source": { + "type": "string" + }, + "tag_aliases": { + "items": { + "$ref": "#/$defs/ContainerImageTagAlias" + }, + "type": "array" + }, + "user": { + "type": [ + "string", + "null" + ] + }, + "variant": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "engine", + "scope", + "user", + "home", + "image_id", + "repo_tags", + "repo_digests", + "pull_ref", + "tag_aliases", + "os", + "architecture", + "variant", + "platform", + "size", + "created", + "source", + "notes" + ], + "type": "object" + }, + "ContainerImagesSnapshot": { + "additionalProperties": false, + "properties": { + "images": { + "items": { + "$ref": "#/$defs/ContainerImage" + }, + "type": "array" + }, + "notes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "role_name": { + "const": "container_images" + } + }, + "required": [ + "role_name", + "images", + "notes" + ], + "type": "object" + }, "DnfConfigSnapshot": { "allOf": [ { @@ -1060,6 +1235,9 @@ }, "snap": { "$ref": "#/$defs/SnapSnapshot" + }, + "container_images": { + "$ref": "#/$defs/ContainerImagesSnapshot" } }, "required": [ From 89831d600bd96af19601b7327fce05760c948820 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 21 Jun 2026 12:49:20 +1000 Subject: [PATCH 14/30] enroll diff --enforce can take --target for specific config managers --- src/content/docs.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/docs.html b/src/content/docs.html index 58af7fc..7bea6e3 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -359,7 +359,7 @@ ignore_package_versions = true
    Optional: enforce the old harvest state (--enforce)
    -
    If drift exists and ansible-playbook is on PATH, Enroll can generate a manifest from the old harvest and apply it locally to restore expected state. It avoids package downgrades, and will often run Ansible with --tags role_... so only the roles implicated by the drift are applied. This is intentionally Ansible-based today, even when you also use Puppet or Salt rendering for normal manifests.
    +
    If drift exists and your desired config manager is on $PATH, Enroll can generate a manifest from the old harvest and apply it locally to restore expected state, if you pass the --enforce argument to enroll diff. By default, it will try to use Ansible for that, but you can pass --target [puppet|sops] just like with enroll manifest.
    From a0a2ed4e4e2619c97d1ca18374e1df1788216ab3 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 21 Jun 2026 13:26:54 +1000 Subject: [PATCH 15/30] Troubleshooting --- src/content/docs.html | 2 +- src/content/troubleshooting.html | 189 ++++++++++++++++++ .../enroll-theme/layouts/partials/nav.html | 7 +- 3 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 src/content/troubleshooting.html diff --git a/src/content/docs.html b/src/content/docs.html index 7bea6e3..69594c8 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -158,7 +158,7 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
    Does Enroll use community roles/modules?
    -
    When using Ansible and any Docker/Podman images, Flatpak or Snaps are 'harvested' by the system, the manifested ansible playbooks will expect to use some of those community collections, but otherwise, everything is 'core' Ansible. Enroll generates a requirements.yml you can use with ansible-galaxy to install it, if needed, but many community collections may already be on your system, depending on your installation. For Puppet and Salt, no third-party extensions or modules are used.
    +
    When using Ansible and any Docker/Podman images, Flatpak or Snaps are 'harvested' by the system, the manifested ansible playbooks will expect to use some of those community collections, but otherwise, everything is 'core' Ansible. Enroll generates a requirements.yml you can use with ansible-galaxy to install it, if needed, but many community collections may already be on your system, depending on your installation. For Puppet and Salt, no third-party extensions or modules are used. If you see Ansible errors about unsupported podman_image parameters or Flatpak from_url, see the Troubleshooting page.
    diff --git a/src/content/troubleshooting.html b/src/content/troubleshooting.html new file mode 100644 index 0000000..aa68499 --- /dev/null +++ b/src/content/troubleshooting.html @@ -0,0 +1,189 @@ +--- +title: "Troubleshooting" +html_title: "Enroll Troubleshooting" +description: "Common Enroll errors and fixes for generated Ansible, Puppet, and Salt manifests." +--- +
    +
    +
    Troubleshooting
    +

    Common errors and how to fix them

    +

    Most first-run problems are either missing runtime tools, old Ansible community collections, or files that Enroll intentionally skipped for safety.

    +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +

    Start here for Ansible module errors

    +

    Generated Ansible manifests may use community collections when a harvest contains Docker/Podman images, Flatpaks, or Snaps. Enroll writes a requirements.yml beside playbook.yml, and the generated manifest README.md shows the install command.

    +
    + +
    $ cd /path/to/generated/ansible-manifest
    +$ ansible-galaxy collection install -r requirements.yml
    +
    +

    Run this in the same Python virtualenv, container, CI job, or user account that will run ansible-playbook. If an older collection is already installed and Ansible keeps finding it first, retry with --force or adjust ANSIBLE_COLLECTIONS_PATH.

    +
    +
    +
    +
    + +
    +

    Podman error: unsupported platform parameter

    +

    If the host has an older containers.podman / community.podman Galaxy collection, an Ansible run may fail with a message like this:

    +
    +
    [ERROR]: Task failed: Module failed: Unsupported parameters for (containers.podman.podman_image) module: platform. Supported parameters include: arch, auth_file, build, ca_cert_dir, executable, force, name, password, path, pull, pull_extra_args, push, push_args, quadlet_dir, quadlet_file_mode, quadlet_filename, quadlet_options, state, tag, username, validate_certs (authfile, build_args, buildargs, tls_verify, tlsverify).
    +
    +
    +
    Cause
    +

    The generated task is using a newer module interface than the collection currently installed on the machine running Ansible.

    +
    +
    +
    Fix
    +

    Install the collection versions constrained by the generated manifest:

    +
    + +
    $ cd /path/to/generated/ansible-manifest
    +$ ansible-galaxy collection install -r requirements.yml
    +
    +

    Then re-run the playbook from the same environment. If Ansible still reports the older module, check ansible-galaxy collection list and remove or override the stale collection path.

    +
    +
    + +
    +

    Flatpak error: unsupported from_url parameter

    +

    Flatpak tasks can fail in the same way when the installed community.general collection is too old for the generated role.

    +
    +
    Fix
    +

    Use the generated Ansible requirements file before applying the manifest:

    +
    + +
    $ cd /path/to/generated/ansible-manifest
    +$ ansible-galaxy collection install -r requirements.yml
    +
    +
    +

    The same advice applies when Snap, Docker, Podman, or Flatpak roles complain about unknown module parameters: install from requirements.yml first, because Enroll generated that file for the manifest it just wrote.

    +
    + +
    +

    Salt error: Failed to import utils pycrypto

    +

    You run the salt-call command and see an error Failed to import utils pycrypto, this is due most likely to a syntax error with a Traceback that includes AttributeError: module 'crypt' has no attribute 'methods'.

    +
    +
    Fix
    +

    It's not an Enroll bug. Upgrade to Salt Stack 3008.1, in their Debian packages they use Python 3.14 and have solved the problem. The problem occurs with older Python, and is described in this upstream bug report.

    +
    +
    + +
    +

    JinjaTurtle is not found, or template generation was skipped

    +

    When --jinjaturtle auto is used, Enroll only templates recognised config files if the jinjaturtle executable is available on PATH. If it is missing, Enroll safely falls back to copying raw files. When --jinjaturtle on is used, missing JinjaTurtle is a hard error.

    +
    +
    +
    +
    Make templating optional (this is the default already)
    +
    $ enroll manifest --harvest ./harvest --target ansible --out ./ansible --jinjaturtle auto
    +
    +
    +
    +
    +
    Disable templating entirely
    +
    $ enroll manifest --harvest ./harvest --target puppet --out ./puppet --jinjaturtle off # or --no-jinjaturtle
    +
    +
    +
    +

    If a generated template would reference missing variables, Enroll should fall back to a raw file copy rather than producing a broken manifest.

    +
    + +
    +

    A file I expected was not harvested

    +

    The default harvest is intentionally conservative. A file may be skipped because it matched an exclude pattern, looked sensitive, looked binary, was too large, was already captured by another role, or was outside Enroll's standard scan paths.

    +
    +
    +
    +
    Explicitly include a path
    +
    $ enroll harvest --out ./harvest --include-path /opt/myapp/config.yml
    +
    +
    +
    +
    +
    Inspect what happened
    +
    $ enroll explain ./harvest
    +$ enroll validate ./harvest
    +
    +
    +
    +
    +
    Be careful with --dangerous
    +
    Use --dangerous only when you understand that it can harvest secrets such as private keys, tokens, credentials, and application config containing passwords.
    +
    +
    + +
    +

    Generated manifest references a missing artifact

    +

    If a manifest task points at a missing source file, validate the original harvest first. Validation checks state.json, referenced artifacts, generated firewall/sysctl files, and unreferenced artifact warnings.

    +
    + +
    $ enroll validate /path/to/harvest
    +
    +

    If validation passes, check whether you moved, edited, or partially copied the generated manifest after rendering it.

    +
    + +
    +

    Remote harvest cannot sudo or cannot unlock an SSH key

    +

    Remote mode can prompt for sudo and private-key passphrases, but CI and non-interactive shells should pass the required information up front. You can set the ENROLL_SSH_KEY_PASSPHRASE environment variable if needed, and then pass --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE.

    +
    +
    +
    +
    Prompt for sudo
    +
    $ enroll harvest --remote-host host.example.net --ask-become-pass --out ./harvest
    +
    +
    +
    +
    +
    Prompt for encrypted SSH key
    +
    $ enroll harvest --remote-host host.example.net --ask-key-passphrase --out ./harvest
    +
    +
    +
    +

    If host key verification fails, connect with normal ssh first so the expected key is in known_hosts, or use your normal SSH config with Enroll's remote SSH config support.

    +
    + +
    +

    enroll diff --enforce did not change everything

    +

    Enforcement is best-effort drift correction from the old harvest. It is intentionally conservative and does not try to do risky things such as arbitrary package downgrades.

    +
    +
    Choose the enforcement target explicitly
    +

    By default, enforcement uses Ansible. You can choose Puppet or Salt when those tools are available on the host running enforcement:

    +
    + +
    $ enroll diff --old ./golden --new ./current --enforce --target ansible
    +$ enroll diff --old ./golden --new ./current --enforce --target puppet
    +$ enroll diff --old ./golden --new ./current --enforce --target salt
    +
    +
    +

    For non-Ansible targets, make sure puppet or salt-call is installed and on PATH before running enforcement.

    +
    +
    +
    +
    +
    diff --git a/src/themes/enroll-theme/layouts/partials/nav.html b/src/themes/enroll-theme/layouts/partials/nav.html index 3812e8f..c49b719 100644 --- a/src/themes/enroll-theme/layouts/partials/nav.html +++ b/src/themes/enroll-theme/layouts/partials/nav.html @@ -12,6 +12,9 @@
    From 468a293f6177e8f4b4d110c789a44329a60a7d40 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 21 Jun 2026 15:23:32 +1000 Subject: [PATCH 17/30] Footer change --- src/themes/enroll-theme/layouts/partials/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/themes/enroll-theme/layouts/partials/footer.html b/src/themes/enroll-theme/layouts/partials/footer.html index a8ab385..1d378c2 100644 --- a/src/themes/enroll-theme/layouts/partials/footer.html +++ b/src/themes/enroll-theme/layouts/partials/footer.html @@ -37,7 +37,7 @@
    -
    Contact
    +
    Bugs or feature suggestions?

    Docker/Podman image detection

    -

    Because it works on your machine....

    The harvest now detects the presence of container images, if the user has permission to call Docker or Podman. In particular, it detects the SHA256 of the image instead of relying on floating tags.

    All three renderers (Ansible, Salt and Puppet) will attempt to enforce the presence of those Docker images per their precise SHA256 hash, if they were present in the harvest but not on the machine upon applying a manifest.

    For Ansible, you may need the community.docker collection, but on Debian 13 I found that it was already present by default in the official ansible Debian packages.

    @@ -133,11 +133,11 @@ localhost : ok=5 changed=0 unreachable=0 failed=0 s
  • In case you missed it in version 0.6.0: Enroll now harvests runtime iptables and ipset rules!
  • -

    See you soon..

    -

    I'm off to try and write more tests - we're at about 86% coverage in pytest, and run a big suite unit tests for Ansible, Puppet and Salt too now, in CI. I'm always trying to catch any regressions given there are so many variations on how you can use this tool.

    -

    Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports! You're helping make Enroll better for everyone.

    - +

    More coverage

    +

    With these changes comes a lot of new 'variance' and argument input to the app. Pytest coverage is now at about 86%, and there is a big suite unit tests for Ansible, Puppet and Salt too, in CI. I'm continuing to try and automate testing all the ways you can use this tool.

    +
    +

    Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports! If you think you know someone who would like Enroll, please tell them about it!

    From 7cda9a0519fa2d1282ab19979b11aea4646873e5 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 21 Jun 2026 15:32:49 +1000 Subject: [PATCH 19/30] paragraph change --- src/content/news/0-7-0.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/content/news/0-7-0.html b/src/content/news/0-7-0.html index 713899c..b19d784 100644 --- a/src/content/news/0-7-0.html +++ b/src/content/news/0-7-0.html @@ -137,7 +137,8 @@ localhost : ok=5 changed=0 unreachable=0 failed=0 s

    With these changes comes a lot of new 'variance' and argument input to the app. Pytest coverage is now at about 86%, and there is a big suite unit tests for Ansible, Puppet and Salt too, in CI. I'm continuing to try and automate testing all the ways you can use this tool.


    -

    Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports! If you think you know someone who would like Enroll, please tell them about it!

    +

    Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports!

    +

    If you think you know someone who would like Enroll, please tell them about it!

    From 277249c4c52d4fe9faf96240a7944a90a6fefe00 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 21 Jun 2026 15:36:33 +1000 Subject: [PATCH 20/30] news updates --- src/content/news/0-7-0.html | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/content/news/0-7-0.html b/src/content/news/0-7-0.html index b19d784..9fbb66f 100644 --- a/src/content/news/0-7-0.html +++ b/src/content/news/0-7-0.html @@ -24,14 +24,13 @@ summary: "Enroll can now render Puppet and Salt manifests alongside Ansible, det
    • Puppet support! --target puppet renders Puppet module/control-repo style output., and in --fqdn mode, renders per-host Hiera data.
    • Salt Stack support! --target salt renders Salt state trees and, in --fqdn mode, Salt pillar data.
    • -
    • Ansible works basically as it always did, and is the default, but you can specify --target ansible too.
    • -
    • Evaluating how different config managers work? You can rendered repeatedly into different config management tools without re-harvesting the host, because they all use the same harvest state!
    • -
    • Single-site output tries to combine package/service data by their package manager's Section (or equivalent metadata), to reduce role/module/state sprawl and speed up execution.
    • +
    • Single-site output tries to combine package/service data by their package manager's Section category, to reduce role sprawl and speed up execution.
    • Flatpak and Snap detection!
    • Docker image detection!

    Dry-run examples: choose your own config management adventure!

    +

    Feels too good to be true, but it isn't:

    $ enroll harvest --out ./harvest
    @@ -50,10 +49,12 @@ summary: "Enroll can now render Puppet and Salt manifests alongside Ansible, det
     
     	  

    I recommend using Salt 3008.1 - on Debian 13 I encountered annoying noisy Python errors with version 3007, which are unrelated to Enroll.

    -

    Bonus: since Salt uses Jinja2 templates, it will take advantage of my other tool JinjaTurtle, if it's on your PATH, just like Ansible does!

    +

    Since Salt uses Jinja2 templates, it will take advantage of my other tool JinjaTurtle, if it's on your PATH, just like Ansible does.

    And because I didn't want Puppet users to feel left out, version 0.5.5 of JinjaTurtle (despite its name) also supports erb templates as well. This means Puppet templates will be generated if you have JinjaTurtle and there are viable config files for it, too!

    +

    Have you used enroll diff --enforce to enforce reverting a system to an older harvest state via Ansible? Well, that works too for Puppet or Salt - just add --target [salt|puppet] to that command.

    +

    New grouping behaviour in roles/modules

    Did you find the number of manifested roles overwhelming?

    Previously, Enroll created an Ansible role (or, now, a Puppet module or Salt role) for pretty much every 'package' it found. In some cases this could result in hundreds of roles.

    From ff1ac0e01c9e333f8eb5b07cefbcb403d6f71912 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Mon, 22 Jun 2026 20:26:45 +1000 Subject: [PATCH 21/30] Fixes for a never-to-be-released, um, release --- src/content/docs.html | 4 +- src/content/news/0-7-0.html | 15 ++- src/content/security.html | 161 +++++++++++++++++++++++++++++-- src/content/troubleshooting.html | 59 +++++++++++ 4 files changed, 228 insertions(+), 11 deletions(-) diff --git a/src/content/docs.html b/src/content/docs.html index 69594c8..9477d27 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -292,11 +292,11 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."

    INI config file

    -

    If you're repeating flags (include/exclude patterns, SOPS settings, etc.), store defaults in enroll.ini and keep your muscle memory intact.

    +

    If you're repeating flags (include/exclude patterns, SOPS settings, etc.), store defaults in ~/config/enroll/enroll.ini and keep your muscle memory intact.

    Discovery order
    -
    You can pass -c/--config, set ENROLL_CONFIG, or let Enroll auto-discover ./enroll.ini, ./.enroll.ini, or ~/.config/enroll/enroll.ini.
    +
    You can pass -c/--config, set ENROLL_CONFIG, let Enroll auto-discover either your XDG config default directory, or finally ~/.config/enroll/enroll.ini.
    diff --git a/src/content/news/0-7-0.html b/src/content/news/0-7-0.html index 9fbb66f..096dcfe 100644 --- a/src/content/news/0-7-0.html +++ b/src/content/news/0-7-0.html @@ -132,10 +132,23 @@ localhost : ok=5 changed=0 unreachable=0 failed=0 s
  • .bashrc and similar files are now only harvested from user directories when --dangerous is used, since this is a common place for sensitive environment variables to be set. As always, remember that --dangerous gives better harvest coverage, but you should use --sops or some other means of your own to encrypt the harvested data at rest safely!
  • Some output during an Ansible play is hidden with no_log to avoid potentially sensitive output, particularly of systemd unit state.
  • In case you missed it in version 0.6.0: Enroll now harvests runtime iptables and ipset rules!
  • +
  • Enroll no longer reads from .enroll.ini or enroll.ini in the current working directory. Instead, it's recommended to put your config file in ~/.config/enroll/enroll.ini, or pass an explicit path with --config or through use of the ENROLL_CONFIG environment variable.
  • +
  • Lots of other hardening and safeguard improvements!
  • +
      +
    • enroll validate makes sure the harvest doesn't contain unsafe things such as symlinks traversing out of the artifacts tree.
    • +
    • enroll manifest takes an internal pass via validate to make sure the harvest validates ok before trying to render config management code. If you find that your harvest is not passing validation, and don't believe it's been tampered with, it may be that it was created on an older version of Enroll and so doesn't pass the current schema. Consider re-harvesting the host.
    • +
    • Other input validations, such as making sure the string passed to --fqdn is safe
    • +
    • Improving detection of sensitive strings in the IgnorePolicy
    • +
    • Safer quoting of command fragments when using remote mode
    • +
    • Warning if running as root and $PATH contains the cwd or a world-writable location (to resist attacks via malicious versions of binaries Enroll calls such as dpkg, rpm, systemctl etc)
    • +
    • TOCTOU avoidance when copying files to harvested artifacts
    • +
    • Permission hardening on manifested dir
    • +
    • "and more!" :)
    • +

    More coverage

    -

    With these changes comes a lot of new 'variance' and argument input to the app. Pytest coverage is now at about 86%, and there is a big suite unit tests for Ansible, Puppet and Salt too, in CI. I'm continuing to try and automate testing all the ways you can use this tool.

    +

    With these changes comes a lot of new 'variance' and argument input to the app. Pytest coverage is now at about 86%, and there is a big suite unit tests for Ansible, Puppet and Salt too, in CI, running across both Debian and AlmaLinux environments. I'm continuing to try and automate testing all the ways you can use this tool.


    Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports!

    diff --git a/src/content/security.html b/src/content/security.html index 2218f29..a56743a 100644 --- a/src/content/security.html +++ b/src/content/security.html @@ -48,6 +48,43 @@ description: "Security posture and safe workflows for Enroll outputs."
    In manifest --sops mode, you'll need to decrypt and extract the bundle before running ansible-playbook, puppet apply, or salt-call.
    + +
    +

    Filesystem hardening for root runs

    +

    Enroll is often run as root because it needs to inspect system state. Version 0.5.5 added stricter filesystem checks so root does not accidentally write harvests or generated output through an unsafe path.

    + +
    +
    +
    +
    Private output directories
    +

    Harvest and manifest outputs are created with restrictive permissions, and Enroll refuses to reuse an existing harvest directory unless that mode is explicitly expected for an internal workflow.

    +
    +
    +
    +
    +
    Symlink and parent checks
    +

    When running as root, Enroll checks path components and parent directories so output is not created below a parent controlled by another user. This reduces the risk of symlink and time-of-check/time-of-use path races.

    +
    +
    +
    +
    +
    Safer artifact handling
    +

    Captured files and generated artifacts are validated as regular files, with traversal, symlink, hardlink, and special-file cases rejected when bundles are validated or extracted.

    +
    +
    +
    +
    +
    Root PATH hygiene
    +

    For root runs, Enroll warns about unsafe PATH entries so helper commands are not resolved from relative, writable, or non-root-owned directories.

    +
    +
    +
    + +
    +
    Why this can feel strict
    +
    A command that writes under a user-owned directory may be convenient, but it can also be hard to prove safe when the process is running as root. Prefer a root-owned output area such as /var/tmp/enroll, /root, or a fresh directory directly under the normal sticky /tmp.
    +
    +
    @@ -85,22 +122,130 @@ description: "Security posture and safe workflows for Enroll outputs."
    -

    Threat model

    +

    Threat model and security scope

    +

    Enroll is a command-line systems administration tool. It is designed to be run intentionally by an administrator, often as root, to inspect a host, harvest selected system state, and optionally generate or apply configuration-management output.

    +

    That makes Enroll different from a web application, daemon, network service, or setuid helper. It is not intended to be a sandbox for hostile local users. Instead, it assumes an operator-controlled execution environment, while still adding defense-in-depth checks for common filesystem, path traversal, secret-handling, and command-resolution mistakes.

    + +
    +
    +
    +
    Trusted operator assumptions
    +
      +
    • If Enroll is run as root, the root user is assumed to control and understand the command line, environment, configuration file, and output location.
    • +
    • If an enroll.ini file is loaded, its location and contents are assumed to be owned, selected, and understood by the operator.
    • +
    • The operator is expected to understand the implications of options such as --dangerous, --assume-safe-path, --sops, --enforce, --remote-host, and --remote-ssh-config.
    • +
    • Harvest bundles used for manifest, diff, or diff --enforce are assumed to come from a trusted source unless the operator is deliberately inspecting them without applying them.
    • +
    +
    +
    +
    +
    +
    Trusted local tools
    +

    Enroll invokes ordinary administrative tools such as SSH, sudo, SOPS, package managers, Docker, Podman, Flatpak, Snap, Ansible, Puppet, Salt, and system utilities. These are assumed to be the trusted tools the operator intended to execute.

    +

    +

    If an attacker can replace or redirect those tools, the host already has a local trust-boundary problem outside Enroll's ability to fully solve.

    +
    +
    +
    +
    + +
    +

    What Enroll tries to protect against

    +

    Enroll still performs substantial hardening because privileged CLI tools can otherwise be easy to misuse. These controls are intended to protect careful administrators from common dangerous mistakes.

    -
    What Enroll tries to prevent
    • Accidentally copying obvious secrets in default mode
    • -
    • Harvesting huge/unbounded file sets by mistake
    • -
    • One host's difference causing problems for other hosts by keeping multi-site data in inventory, Hiera, or pillar
    • +
    • Harvesting huge or unbounded file sets by mistake
    • +
    • Writing root-run output through unsafe symlinks, hardlinks, special files, or path-race situations
    • +
    • Extracting harvest tar members outside the intended bundle directory
    • +
    • Copying unsafe harvested artifacts into generated manifests
    -
    What you still need to think about
      -
    • Where outputs are stored and who can access them
    • -
    • Reviewing what was captured before committing/sharing
    • -
    • Choosing encryption and secret-management strategy
    • +
    • Resolving helper commands from suspicious PATH entries during root runs
    • +
    • Generating manifest commands where ordinary harvested values could become shell injection
    • +
    • Accepting unknown SSH host keys during remote harvests
    • +
    • Confusing one host's data with another host's data in multi-site outputs
    • +
    • Applying structurally unsafe harvest bundles
    • +
    +
    +
    +
    + +
    +

    What is out of scope

    +

    The following are normally considered local compromise, operator-controlled behavior, or trust-boundary failures rather than Enroll vulnerabilities by themselves:

    +
    +
    +
      +
    • A malicious local user who can already control root's command line, shell environment, config file, working directory, PATH, or invoked binaries
    • +
    • A root user loading an enroll.ini file whose contents intentionally request dangerous behavior
    • +
    • A root user passing --dangerous and observing that Enroll may collect sensitive data
    • +
    • A root user passing --assume-safe-path and observing that Enroll does not prompt about PATH safety
    • +
    +
    +
    +
      +
    • A user applying generated Ansible, Puppet, or Salt output from a harvest bundle they do not trust
    • +
    • A user enforcing a malicious or manually edited harvest bundle with diff --enforce
    • +
    • A user configuring an untrusted webhook, SSH proxy command, SOPS binary, or configuration-management tool
    • +
    • Reports that amount to: root can run Enroll with malicious options and make the system do dangerous things
    • +
    +
    +
    +
    +
    Plain-language summary
    +
    Enroll is an administrator's tool, not a local privilege boundary. If the operator's root execution environment is already attacker-controlled, Enroll cannot make that safe. Enroll does, however, try to reduce the damage from common path, permission, traversal, secret-handling, and manifest-generation mistakes.
    +
    +
    + +
    +

    Trusted harvests and enforcement

    +

    Harvest bundles should be treated as sensitive administrative artifacts. A harvest may contain hostnames, usernames, package lists, service state, filesystem metadata, configuration files, firewall snapshots, container image references, Flatpak/Snap state, and other operational details. In --dangerous mode it may contain substantially more sensitive material.

    +

    Enroll validates harvest structure and artifact safety, but validation does not prove that the desired state represented by a harvest is safe to apply. Only run manifest, diff, or especially diff --enforce against bundles that came from hosts and people you trust.

    +
    + +
    +

    Security report scope

    +
    +
    +
    Useful reports
    +
      +
    • Enroll captures clearly sensitive default-denied files without --dangerous
    • +
    • Enroll follows a symlink or hardlink in a way that causes privileged file disclosure or overwrite
    • +
    • Enroll extracts a tar member outside the intended harvest directory
    • +
    • Enroll accepts an artifact path that escapes the artifact root
    • +
    • Ordinary harvested data can cause command injection in generated manifests
    • +
    • Enroll silently ignores a failed safety check and proceeds anyway
    • +
    +
    +
    +
    Normally out-of-scope reports
    +
      +
    • Root can configure Enroll to collect sensitive files
    • +
    • Root can pass --dangerous and collect dangerous data
    • +
    • Root can pass --assume-safe-path and bypass the root PATH warning
    • +
    • Root can point Enroll at a malicious config file
    • +
    • Root can enforce a malicious harvest bundle
    • +
    • A local attacker can influence Enroll after already controlling root's environment or binaries
    • +
    +
    +
    +
    + +
    + +
    +

    Security researchers

    +
    +
    +
    Found a vulnerability in Enroll?
    +
      +
    • Please contact me using the contact form or via Signal (mig5.55)
    • +
    • My GPG public key is here
    • +
    • Unfortunately, I cannot offer financial bounty rewards at this time, but you will receive full credit for valid security issues.
    diff --git a/src/content/troubleshooting.html b/src/content/troubleshooting.html index aa68499..ea70877 100644 --- a/src/content/troubleshooting.html +++ b/src/content/troubleshooting.html @@ -22,6 +22,7 @@ description: "Common Enroll errors and fixes for generated Ansible, Puppet, and Flatpak from_url error JinjaTurtle Missing files + Harvest output parent Validate artifacts Remote harvest Diff enforcement @@ -138,6 +139,49 @@ description: "Common Enroll errors and fixes for generated Ansible, Puppet, and
    +
    +

    Error: harvest output parent is owned by the wrong user

    +

    When Enroll is run as root, it deliberately refuses to create a harvest below a parent directory that is controlled by a non-root user. This is a safety check: a user-owned parent directory can be renamed or replaced while a root process is writing files, which can become a symlink or path-race problem.

    + +
    +
    error: harvest output parent is not owned by root; refusing root-run output: /home/alice
    +error: harvest output parent is not owned by root; refusing root-run output: /tmp/tmp.abcd1234
    +
    + +
    +
    What Enroll is protecting you from
    +

    Harvest output can contain detailed system state and, when --dangerous is used, may contain sensitive configuration material. If Enroll is running as root, it needs the output path to stay exactly where it was checked. Refusing user-owned parent directories helps avoid accidental writes through symlinks or paths that can be swapped underneath the process.

    +
    + +
    +
    +
    +
    Use a root-owned output area
    +

    Create a dedicated root-owned directory and place harvests underneath it:

    +
    $ sudo install -d -m 700 -o root -g root /var/tmp/enroll
    +$ sudo enroll harvest --out /var/tmp/enroll/host1
    +
    +
    +
    +
    +
    /tmp itself is okay
    +

    A fresh output directory directly under the normal root-owned sticky /tmp directory is accepted. The output path must not already exist.

    +
    $ sudo enroll harvest --out /tmp/host1-harvest
    +
    +
    +
    + +
    +
    Avoid root harvests into your home directory
    +
    A command such as sudo enroll harvest --out ~/harvest may expand to a path below a user-owned home directory. Use /var/tmp/enroll, /root, or a fresh directory directly under /tmp instead.
    +
    + +
    +
    Remote harvest note
    +

    In normal remote mode, Enroll uses sudo on the remote host so it can collect system-level state. Recent Enroll versions create a separate root-owned temporary directory for the remote bundle. If an older version reports this error for a path like /tmp/tmp.xxxxx during remote harvest, upgrade Enroll. Use --no-sudo only when you intentionally want a limited, non-root harvest.

    +
    +
    +

    Generated manifest references a missing artifact

    If a manifest task points at a missing source file, validate the original harvest first. Validation checks state.json, referenced artifacts, generated firewall/sysctl files, and unreferenced artifact warnings.

    @@ -183,6 +227,21 @@ description: "Common Enroll errors and fixes for generated Ansible, Puppet, and

    For non-Ansible targets, make sure puppet or salt-call is installed and on PATH before running enforcement.

    +
    +

    Error about schema when validating or manifesting a harvest

    +

    You see an error like error: harvest state does not match this Enroll version's schema; please re-harvest the host with this version of Enroll.

    +
    +
    +
    +
    Reharvest the host
    +

    The harvest has either been tampered with, become corrupt, or was made with an earlier version of Enroll, which means its layout is a different structure than what Enroll now expects.

    +

    The best thing to do is run the harvest again. Enroll will validate the harvest against the latest schema when you go to manifest it.

    +

    If you still experience the error, please report it as a bug!

    +
    +
    +
    +
    + From 42683598fa097c424dfa199ad740b0fc40124994 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 30 Jun 2026 15:51:33 +1000 Subject: [PATCH 22/30] New main --- src/content/_index.html | 392 ++-------- src/content/development.html | 49 ++ src/content/docs.html | 674 ++++-------------- src/content/examples.html | 187 ++--- src/content/news.html | 38 + src/content/schema.html | 66 +- src/content/security.html | 127 ++-- src/content/troubleshooting.html | 28 + src/hugo.toml | 2 +- src/static/schema/state.schema.json | 476 +++++++++++++ .../enroll-theme/layouts/partials/footer.html | 38 +- .../enroll-theme/layouts/partials/nav.html | 15 +- 12 files changed, 967 insertions(+), 1125 deletions(-) create mode 100644 src/content/development.html create mode 100644 src/content/news.html create mode 100644 src/content/troubleshooting.html diff --git a/src/content/_index.html b/src/content/_index.html index 253e406..ca1e6ce 100644 --- a/src/content/_index.html +++ b/src/content/_index.html @@ -1,385 +1,127 @@ --- title: "Enroll" -html_title: "Enroll - Reverse-engineering servers into Ansible" -description: "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible roles/playbooks from what it finds. Harvest → Manifest → Manage." -og_title: "Enroll - Reverse-engineering servers into Ansible" -og_description: "Harvest a host's real configuration and turn it into Ansible roles/playbooks. Safe-by-default, with optional SOPS encryption." +html_title: "Enroll - Turn existing Linux hosts into Ansible" +description: "Enroll harvests real Linux host state and renders Ansible configuration-management output. Documentation for current 0.7 beta behavior." +og_title: "Enroll - Turn existing Linux hosts into Ansible" +og_description: "Harvest real Linux host state and render reviewable Ansible roles/playbooks." og_type: "website" ---
    -
    Reverse-engineering servers into Ansible
    -

    Get an existing Linux host into Ansible in seconds.

    -

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

    +
    Current 0.7 beta documentation
    +

    Turn a real Linux host into reviewable Ansible.

    +

    Enroll inspects Debian-like and RedHat-like machines, writes a harvest bundle (state.json plus artifacts), then renders Ansible roles and playbooks from that snapshot.

    - Super fast - Optional SOPS encryption - Remote over SSH + Harvest → Manifest + Local or remote over SSH + Optional SOPS bundles + Ansible output
    -
    +
    -
    Config in the blink of an eye
    -
    single-shot → ansible-playbook
    +
    The core workflow
    +
    dry-run before applying
    -
    $ enroll single-shot --harvest ./harvest --out ./ansible
    -
    $ cd ./ansible && tree -L 2
    -
    .
    -├── ansible.cfg
    -├── playbook.yml
    -├── roles/
    -│   ├── cron/
    -│   ├── etc_custom/
    -│   ├── firewall/
    -│   ├── nginx/
    -│   ├── openssh-server/
    -│   ├── users/
    -└── README.md
    +
    $ enroll harvest --out ./harvest
    +$ enroll validate ./harvest
    +$ enroll manifest --harvest ./harvest --out ./ansible
    +$ cd ./ansible
    +$ ansible-galaxy collection install -r requirements.yml
    +$ ansible-playbook -i localhost, -c local playbook.yml --check --diff
    +
    +
    + Trust boundary: validation proves structure and artifact consistency, not semantic safety. Only apply manifests generated from harvests whose provenance you trust.
    -
    Tip: for multiple hosts, use --fqdn to generate inventory-driven, data-driven roles.
    -
    +
    -

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

    What Enroll does today

    +

    Enroll is a migration and recovery helper. It gives you a generated Ansible starting point from a machine that already exists; it does not prove that the machine is well designed, compliant, or safe to replicate.

    +
    +
    Current output target
    +

    The public CLI renders Ansible. The codebase has an internal renderer-neutral model, but the documented user-facing manifest output is Ansible roles, playbooks, inventory, variables, files, templates, handlers, and a generated README.md.

    -
    -
    -
    -
    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.
    -
    -
    -
    -
    -
    -
    -
    Multi-site without "shared role broke host2"
    -
    In --fqdn mode, roles are data-driven and host inventory decides what gets managed per host.
    -
    -
    -
    -
    -
    -
    -
    Remote over SSH
    -
    Harvest a remote host from your workstation, then manifest Ansible 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
    -
    +
    Harvest
    Collect package, service, user, file, runtime, Flatpak/Snap, and container-image evidence into a private bundle.
    +
    Validate
    Check state.json, the vendored JSON schema, referenced artifacts, and orphaned files before rendering or CI use.
    +
    Manifest
    Freeze the harvest, warn about semantic trust, then render Ansible. Use --fqdn for inventory-driven multi-host output.
    +
    Diff, explain, notify
    Compare harvests, suppress noisy package-version or path drift, produce text/Markdown/JSON, and optionally send webhooks or email.
    -
    +
    -
    -

    Quickstart

    -
    -
    - Full README +
    +

    Quickstart paths

    +

    Choose the shape that matches the job: one host, remote harvest, or a shared multi-host Ansible tree.

    +
    -
    -
    -
    -
    - -
    # Harvest → Manifest in one go
    -enroll single-shot --harvest ./harvest --out ./ansible
    -
    -# Then run Ansible locally
    -ansible-playbook -i "localhost," -c local ./ansible/playbook.yml
    -
    -
    -
    -
    -
    Good for
    -
    Disaster recovery snapshots, "make this one host reproducible", and carving a golden role set you'll refine over time.
    -
    -
    Want templates for structured configs? Install JinjaTurtle and use --jinjaturtle (or let it auto-detect).
    -
    -
    -
    +
    +
    enroll harvest --out ./harvest
    +enroll manifest --harvest ./harvest --out ./ansible
    +cd ./ansible
    +ansible-playbook -i localhost, -c local playbook.yml --check --diff
    - -
    -
    - -
    # Remote harvest over SSH, then manifest locally
    -enroll single-shot \
    -  --remote-host myhost.example.com \
    -  --remote-user myuser \
    -  --harvest /tmp/enroll-harvest \
    -  --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).
    +
    +
    enroll harvest --remote-host app01.example.net --remote-user mig --out ./harvest
    +# Use -K / --ask-become-pass when remote sudo needs a password.
    +enroll manifest --harvest ./harvest --out ./ansible
    - -
    -
    - -
    # Multi-site mode: shared roles, host-specific state in inventory
    -enroll harvest --out /tmp/enroll-harvest
    -enroll manifest --harvest /tmp/enroll-harvest --out ./ansible --fqdn "$(hostname -f)"
    -
    -# Run the per-host playbook
    -ansible-playbook ./ansible/playbooks/"$(hostname -f)".yml
    -
    -
    Rule of thumb: single-site for "one server, easy-to-read roles"; --fqdn for "many servers, high abstraction, fast adoption".
    +
    +
    host=app01.example.net
    +enroll single-shot --remote-host "$host" --remote-user mig \
    +  --harvest "./harvest-$host" --out ./ansible-site --fqdn "$host"
    +cd ./ansible-site
    +ansible-playbook -i inventory/hosts.ini "playbooks/${host}.yml" --check --diff
    - -
    -
    - -
    # 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!
    +
    +
    enroll diff --old ./harvest-golden --new ./harvest-current \
    +  --format markdown --ignore-package-versions --exclude-path /var/anacron
    -
    -
    - -
    # 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 confirms 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.
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    Single-shot
    -
    Harvest → Manifest in one command.
    -
    -
    -
    -
    -
    -
    -
    -
    Diff
    -
    Drift report + webhook/email notifications, or optionally enforce the previous state!
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -

    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
    -
    -
    +
    +

    Single-site by default

    Without --fqdn, Enroll renders self-contained roles where role files live under roles/<role>/files and variables live in defaults/main.yml.

    +

    Multi-site with --fqdn

    With --fqdn, shared role tasks stay reusable while host-specific files and variables live under inventory/host_vars/<fqdn>. --fqdn implies one-role-per-package/unit behavior.

    +

    Security-first rendering

    Directory harvests are frozen into private temp copies before use, artifact paths are no-follow validated, and harvested Jinja-looking values are emitted as Ansible !unsafe data.

    diff --git a/src/content/development.html b/src/content/development.html new file mode 100644 index 0000000..143fecf --- /dev/null +++ b/src/content/development.html @@ -0,0 +1,49 @@ +--- +title: "Development Notes" +html_title: "Enroll Development Notes" +description: "Developer-oriented overview of Enroll architecture, collectors, safety helpers, renderers, and tests." +--- +
    +
    +
    Development notes
    +

    How the codebase is structured

    +

    A compact web version of the current development guide for contributors and reviewers.

    +
    +
    + +
    +
    +
    +

    Runtime flow

    The CLI injects INI defaults, parses arguments, performs root PATH safety checks, then dispatches to harvest, manifest, diff, explain, or validate. Manifest and diff normalize directory/tar/SOPS inputs into private frozen working trees before consuming artifacts.

    +

    Harvest order

    Platform detection and package indexing run first. Collectors then process runtime state, cron/logrotate, services/packages, users plus Flatpak/Snap, container images, package-manager config, remaining /etc, /usr/local, and extra paths. Parent directories and package inventory are appended before writing state.json.

    +

    Global de-duplication

    captured_global keeps a destination path from being owned by two generated roles. New collectors should route file/link capture through the shared helpers rather than appending directly to snapshots.

    +

    Backends

    DpkgBackend and RpmBackend implement ownership lookup, manual package listing, installed inventory, /etc indexing, modified config detection, and package-manager config exclusions. Add new backends behind platform.get_backend().

    +

    Renderer-neutral model

    Harvest snapshots are converted into CMModule objects before Ansible rendering. Common role grouping can combine package and systemd modules by Debian Section/RPM Group unless --no-common-roles or --fqdn is used.

    +

    Ansible renderer

    The renderer writes scaffold, tasks, handlers, files/templates, variables, playbooks, inventory, requirements, and a generated README. It keeps harvested free text in data variables and only interpolates sanitized role/module tokens into task structure.

    +
    + +
    +

    Collector map

    +
    + + + + + + + + + + + + +
    CollectorPurposeTypical roles/snapshots
    RuntimeStateCollectorRoot-only runtime sysctl and firewall fallback snapshots.sysctl, firewall_runtime, enroll_runtime
    CronLogrotateCollectorCentralises cron and logrotate before package/service roles claim files.cron, logrotate
    ServicePackageCollectorEnabled systemd services, related packages, config, drop-ins, env files, and enablement symlinks.service and package roles
    UsersCollectorNon-system users, authorized keys, safe SSH material, shell dotfiles in dangerous mode, Flatpak/Snap discovery.users, flatpak, snap
    ContainerImagesCollectorDocker/Podman image metadata, digest-pinned pull references, tag aliases, notes for non-reproducible local tags.container_images
    PackageManagerConfigCollectorAPT/DNF/YUM repo and keyring configuration.apt_config, dnf_config
    UsrLocalCustomCollector/usr/local/etc and executable scripts under /usr/local/bin.usr_local_custom
    ExtraPathsCollectorUser requested include/exclude path capture.extra_paths
    +
    +
    + +
    +

    Testing focus

    +

    The test suite covers CLI parsing and config injection, package backends, collectors, path filtering, secret detection, filesystem safety, SOPS helpers, remote extraction, manifest rendering, JinjaTurtle integration, validation, diff formatting, and output hardening. New features should usually add focused unit tests plus an end-to-end render or validation check where generated Ansible changes.

    +
    +
    +
    diff --git a/src/content/docs.html b/src/content/docs.html index d6dbb48..ae977e7 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -1,578 +1,182 @@ --- -title: "Docs" -html_title: "Enroll Docs" -description: "How Enroll works: harvest, manifest, modes, and configuration." +title: "Documentation" +html_title: "Enroll Documentation" +description: "Current user documentation for Enroll harvest, manifest, diff, explain, validate, SOPS, remote mode, and Ansible output." ---
    -
    Documentation
    -

    How Enroll works

    -

    The mental model, output modes, and the knobs you'll actually use day-to-day.

    +
    Documentation
    +

    Current Enroll behavior

    +

    This page describes how the 0.7 beta CLI works today: what it captures, how it renders Ansible, and where the trust boundaries are.

    -
    - +
    -
    -

    Mental model

    -

    Enroll is intentionally simple: it collects facts first, then renders Ansible from those facts.

    - -
    -
    -
    -
    -
    -
    -
    1) Harvest
    -
    Snapshot state into a bundle
    -
    -
    -
      -
    • Detect installed packages and services
    • -
    • Collect config that deviates from packaged defaults (where possible)
    • -
    • Grab relevant custom/unowned files in service dirs
    • -
    • Capture non-system users & SSH public keys, .bashrc files etc
    • -
    -
    -
    -
    -
    -
    -
    -
    -
    2) Manifest
    -
    Generate an Ansible repo structure
    -
    -
    -
      -
    • Roles with files/templates and defaults
    • -
    • Playbooks to apply the captured state
    • -
    • Optional inventory structure for multi-host runs: each host gets its own playbook
    • -
    -
    -
    -
    - -
    -
    Typical flow
    -
    $ enroll harvest --out /tmp/enroll-harvest
    -$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
    -$ ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml
    +
    +
    +

    Mental model

    +

    Enroll has two main phases:

    +
      +
    1. Harvest collects host facts and selected files into a harvest bundle containing state.json and artifacts/.
    2. +
    3. Manifest converts that bundle into Ansible configuration-management output.
    4. +
    +

    Additional commands help you compare harvests (diff), explain why things were included or skipped (explain), and validate bundle structure (validate).

    -
    -

    How harvesting works

    -

    At a high level, this is what happens when enroll harvest runs on a host:

    - -
      -
    • Detects the OS and its package backend (e.g dpkg vs rpm)
    • -
    • Detects what packages are installed
    • -
    • For each package, it tries to detect files in /etc that have been modified from the default that get shipped with the package.
    • -
    • It detects running/enabled services and timers via systemd. For each of these, it looks for the unit files, any 'drop-in' files, environment variable files, etc, as well as what executable it executes, and tries to map those systemd services to the packages it's already learned about earlier (that way, those 'packages' or future Ansible roles, can also be associated with 'handlers' in Ansible, to handle restart of the services if/when the configs change)
    • -
    • Aside from known packages already learned, it optimistically tries to capture extra system configuration in /etc that is common for config management. This is stuff like the apt or dnf configuration, crons, logrotate configs, networking settings, hosts files, etc.
    • -
    • For applications that commonly make use of symlinks (think Apache2 or Nginx's sites-enabled or mods-enabled), it notes what symlinks exist so that it can capture those in Ansible
    • -
    • It also looks for other snowflake stuff in /etc not associated with packages/services or other typical system config, and will put these into an etc_custom role.
    • -
    • Likewise, it looks in /usr/local for stuff, on the assumption that this is an area that custom apps/configs might've been placed in. These go into a usr_local_custom role.
    • -
    • It captures non-system user accounts, their group memberships and files such as their .ssh/authorized_keys, and .bashrc, .profile, .bash_aliases, .bash_logout if these files differ from the skel defaults
    • -
    • It takes into account anything the user set with --exclude-path or --include-path. For anything extra that is included, it will put these into an 'extra_paths' role. The location could be anywhere e.g something in /opt, /srv, whatever you want.
    • -
    • It writes the state.json and captures the artifacts.
    • -
    -
    -

    Other things to be aware of:

    -
      -
    • You can use multiple invocations of --exclude-path to skip the bits you don't want. You also can always comment out from the playbook.yml or delete certain roles it generates once you've run the enroll manifest.
    • -
    • In terms of safety measures: it doesn't traverse into symlinks, and it has an 'IgnorePolicy' that makes it ignore most binary files (except GPG binary keys used with apt) - though if you specify certain paths with --include-path and use --dangerous, it will skip some policy statements such as what types of content to ignore.
    • -
    • It will skip files that are too large, and it also currently has a hardcoded cap of the number of files that it will harvest (4000 for /etc, /usr/local/etc and /usr/local/bin, and 500 files per 'role'), to avoid unintentional 'runaway' situations.
    • -
    • If you are using the 'remote' mode to harvest, and your remote user requires a password for sudo, you can pass in --ask-become-pass (or -K) and it will prompt for the password. If you forget, and remote requires password for sudo, it'll still fall back to prompting for a password, but will be a bit slower to do so.
    • -
    • If your SSH private key is encrypted, use --ask-key-passphrase to prompt up-front, or --ssh-key-passphrase-env ENV_VAR for non-interactive/CI runs. If neither is provided and Enroll detects an encrypted key in an interactive session, it will prompt on-demand. (The two key-passphrase flags are mutually exclusive.)
    • -
    - -
    -
    Does Enroll use Ansible community/galaxy roles?
    -
    No, Enroll doesn't have any knowledge of Ansible Galaxy roles or community plugins. It generates all the roles itself. If you really want to use roles from the community, Enroll may not be the tool for you, other than perhaps to help get you started.
    -
    -
    Keep in mind that a lot of software config files are also good candidates for being Jinja templates with abstracted vars for separate hosts.
    -
    -
    Enroll does use my companion tool JinjaTurtle if it's installed, but JinjaTurtle only recognises certain types of files (.ini style, .json, .xml, .yaml, .toml, but not special ones like Nginx or Apache conf files which have their own special syntax). When Enroll can't turn a config file into a template, it copies the raw file instead and uses it with ansible.builtin.copy in role tasks.
    +
    +
    +

    enroll harvest

    +

    Harvest writes a local bundle. If --out is omitted, Enroll creates a private randomized cache directory under the user’s Enroll cache. If --out is supplied for plaintext output, the final output directory must be new and safe; Enroll creates it with private permissions.

    +

    High-level capture scope

    +
    +
    • Manual packages and installed package inventory
    • Enabled systemd services and relevant package ownership
    • Modified package-owned config where the backend can prove drift
    • Service-relevant unowned files under matching /etc trees
    • Central cron and logrotate snippets
    +
    • Non-system users and SSH public/authorized key material
    • Package-manager config: apt_config or dnf_config
    • Miscellaneous /etc and /usr/local custom files
    • Runtime sysctl and firewall fallback state where appropriate
    • Flatpak, Snap, Docker, and Podman image evidence
    +
    +

    Path selection

    +

    --include-path adds extra files or directories to the extra_paths role. --exclude-path removes matching paths from all harvesting, including defaults. Excludes win over includes.

    +
    enroll harvest --out ./harvest \
    +  --include-path '/home/*/.profile' \
    +  --exclude-path '/home/*/.ssh/**' \
    +  --exclude-path 're:^/var/cache/myapp/.*$'
    +

    Pattern styles are plain paths, inferred or explicit globs such as glob:/path/**/*.conf, and regexes prefixed with re: or regex:. Include expansion ignores symlinks, respects caps, and reports unmatched/capped paths as notes.

    +
    +
    +

    Remote harvesting

    +

    Remote mode runs the harvest over SSH and stores the bundle locally. The remote host does not need Enroll installed in the same way you would for local operation.

    +
    enroll harvest --remote-host app01.example.net --remote-user mig --out ./harvest-app01
    +
      +
    • --remote-ssh-config [path] reads OpenSSH-style settings. With no value, it defaults to ~/.ssh/config.
    • +
    • Supported ssh config fields include HostName, User, Port, IdentityFile, ProxyCommand, and HostKeyAlias where supported.
    • +
    • -K / --ask-become-pass prompts for remote sudo. --no-sudo avoids sudo and produces a more limited harvest.
    • +
    • --ask-key-passphrase prompts for an encrypted SSH key. --ssh-key-passphrase-env ENV_VAR reads it from an environment variable for CI.
    • +
    +
    +
    -
    -

    State schema

    -

    Enroll writes a state.json file describing what was harvested. The canonical definition of that file format is the JSON Schema below.

    -

    You can also validate a harvest state file against the schema by using enroll validate /path/to/harvest.

    +
    +
    +

    enroll manifest

    +

    Manifest validates the harvest, freezes directory bundles into a private temporary copy, prints the semantic-safety warning, and writes Ansible output.

    +
    enroll validate ./harvest
    +enroll manifest --harvest ./harvest --out ./ansible
    +cd ./ansible
    +ansible-galaxy collection install -r requirements.yml
    +ansible-playbook -i localhost, -c local playbook.yml --check --diff
    +

    Output contents

    +
      +
    • playbook.yml in single-site mode, or playbooks/<fqdn>.yml plus inventory in multi-site mode.
    • +
    • roles/<role>/tasks/main.yml, with handlers where needed.
    • +
    • Harvested files in role files/, generated templates in role templates/, and variables in defaults/main.yml or host vars.
    • +
    • requirements.yml for Ansible collections such as container/Flatpak/Snap support when required.
    • +
    • A generated README.md summarising layout, roles, excluded paths, notes, and dry-run commands.
    • +
    +

    Every generated role has a tag named role_<role_name>. Use tags to dry-run or apply a narrow slice, for example --tags role_users,role_services.

    +
    +
    -
    -
    - View formatted schema - state.schema.json +
    +
    +

    Output modes

    +
    +

    Single-site: default

    Use this for one server or a “golden” role set. Roles are more self-contained; files live under the role; variables live in role defaults.

    +

    Multi-site: --fqdn

    Use this for several existing servers. Generic shared roles read host-specific variables and per-host files under inventory/host_vars/<fqdn>.

    +
    +

    By default, Enroll groups many package and systemd-unit roles into common Debian Section/RPM Group roles. --no-common-roles preserves one generated role per package/unit. --fqdn implies this behavior to avoid cross-host coupling.

    +
    +
    + +
    +
    +

    SOPS encrypted bundles

    +

    --sops <GPG_FINGERPRINT...> writes a single encrypted binary tarball instead of a plaintext directory. This is storage-at-rest encryption for harvest/manifest bundles, not an Ansible inventory-vault workflow.

    +
    enroll harvest --out ./harvest --dangerous --sops 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
    +enroll manifest --harvest ./harvest/harvest.tar.gz.sops \
    +  --out ./ansible-secure --sops 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
    +cd ./ansible-secure
    +sops -d manifest.tar.gz.sops | tar -xzvf -
    +cd manifest
    +

    When manifest output is encrypted, decrypt and extract it before running ansible-playbook.

    +
    +
    + +
    +
    +

    enroll diff

    +

    Diff compares two harvests and reports package, service, user, and managed-file drift. Inputs may be directories, state.json paths, tarballs, or SOPS bundles when --sops is set.

    +
    enroll diff --old ./harvest-a --new ./harvest-b --format markdown \
    +  --ignore-package-versions --exclude-path /var/anacron --exit-code
    +

    Notification options include --webhook, --webhook-format json|text|markdown, repeated --webhook-header 'K: V', repeated --email-to, SMTP settings, and --notify-always.

    +

    --exit-code returns status 2 when differences are detected, which is useful for cron, CI, or monitoring wrappers.

    +
    +
    + +
    +
    +

    Explain and validate

    +
    +

    enroll explain

    Summarises inventory, roles, included reasons, excluded reasons, and examples. Supports --format text|json, --max-examples, tarballs, directories, state.json, and SOPS bundles.

    enroll explain ./harvest --format json --max-examples 20
    +

    enroll validate

    Checks JSON validity, schema compliance, missing referenced artifacts, and orphaned artifact files. Supports --schema, --allow-remote-schema, --no-schema, --fail-on-warnings, JSON output, and report files.

    enroll validate ./harvest --format json --out validate.json
    -
    -

    Single-site vs multi-site

    -

    Manifest output has two styles. Choose based on how you'll use the result.

    - -
    -
    -
    -
    Single-site (default)
    -

    Best when you are enrolling one host, or you're producing a reusable "golden" role set that could be applied anywhere.

    -
      -
    • Roles are self-contained
    • -
    • Raw files live in each role's files/
    • -
    • Template variables live in defaults/main.yml
    • -
    -
    -
    -
    -
    -
    Multi-site (--fqdn)
    -

    Best when you want to enroll several existing servers quickly, especially if they differ.

    -
      -
    • Roles are shared; raw files live in host-specific inventory
    • -
    • Inventory decides what gets managed on each host (files/packages/services)
    • -
    • Non-templated files go under inventory/host_vars/<fqdn>/<role>/.files
    • -
    -
    -
    -
    - -
    -
    Multi-site example
    -
    $ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$(hostname -f)"
    -$ ansible-playbook /tmp/enroll-ansible/playbooks/"$(hostname -f)".yml
    -
    - -
    -
    Tip: role tags
    -
    Generated playbooks tag each role as role_<name> (e.g. role_users, role_services, role_other). You can target a subset with ansible-playbook ... --tags role_users.
    -
    -
    - -
    -

    Remote harvesting over SSH

    -

    Run Enroll on your workstation, harvest a remote host over SSH. The harvest is pulled locally.

    -
    -
    $ enroll harvest --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-harvest
    -$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-manifest
    -
    -# Alternatively, run both commands combined together with the 'single-shot' mode:
    -
    -$ enroll single-shot --remote-host myhost.example.com --remote-user myuser \
    -  --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible \
    -  --fqdn myhost.example.com
    -
    -# Encrypted SSH key examples:
    -$ enroll harvest --remote-host myhost.example.com --remote-user myuser \
    -  --ask-key-passphrase --out /tmp/enroll-harvest
    -
    -$ export ENROLL_SSH_KEY_PASSPHRASE='correct horse battery staple'
    -$ enroll harvest --remote-host myhost.example.com --remote-user myuser \
    -  --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE --out /tmp/enroll-harvest
    -
    -
    -
    Tip
    -
    If you don't want/need sudo on the remote side, add --no-sudo. However, be aware that you may get a more limited harvest depending on permissions.
    -
    -
    If your remote user requires a password for sudo, pass --ask-become-pass or -K and you'll be prompted to enter the password. If you forget, Enroll will still prompt for the password if it detects it's needed, but will be slightly slower to do so.
    -
    -
    If your SSH private key is encrypted, use --ask-key-passphrase to prompt up-front. For non-interactive/CI runs, use --ssh-key-passphrase-env ENV_VAR. If neither is set and Enroll detects an encrypted key in an interactive session, it'll still prompt on-demand.
    -
    -
    If your remote host requires additional SSH configuration that you've defined in your ~/.ssh/config, pass --remote-ssh-config ~/.ssh/config. Enroll will understand how to translate the Host alias, IdentityFile, ProxyCommand, ConnectTimeout and AddressFamily values. You must still pass a value for --remote-host that matches the Host value of the entry in the SSH config file.
    -
    -
    - -
    -

    JinjaTurtle integration

    -

    If JinjaTurtle (one of my other projects) is installed, Enroll can also produce Jinja2 templates for ini/json/xml/toml-style config and extract variables cleanly into Ansible, instead of just storing the 'raw' files.

    -
    -
    -
    -
    Modes
    -
      -
    • --jinjaturtle to force on
    • -
    • --no-jinjaturtle to force off
    • -
    • Default is auto
    • -
    -
    -
    -
    -
    -
    Where variables land
    -
      -
    • Single-site: roles/<role>/defaults/main.yml
    • -
    • Multi-site: inventory/host_vars/<fqdn>/<role>.yml
    • -
    -
    -
    -
    -
    - -
    -

    INI config file

    -

    If you're repeating flags (include/exclude patterns, SOPS settings, etc.), store defaults in enroll.ini and keep your muscle memory intact.

    - -
    -
    Discovery order
    -
    You can pass -c/--config, set ENROLL_CONFIG, or let Enroll auto-discover ./enroll.ini, ./.enroll.ini, or ~/.config/enroll/enroll.ini.
    -
    - -
    - -
    [enroll]
    -# (future global flags may live here)
    -
    -[harvest]
    -dangerous = false
    +        
    +
    +

    Configuration file

    +

    Enroll can read INI-style defaults from an explicit --config path, ENROLL_CONFIG, $XDG_CONFIG_HOME/enroll/enroll.ini, or ~/.config/enroll/enroll.ini. It no longer reads .enroll.ini from the current working directory.

    +

    Precedence is: explicit CLI flags, subcommand/global INI defaults, then argparse defaults. Use underscores for INI keys that are hyphenated on the CLI.

    +
    [harvest]
     include_path =
       /home/*/.bashrc
       /home/*/.profile
     exclude_path = /usr/local/bin/docker-*, /usr/local/bin/some-tool
    -# remote_host = yourserver.example.com
    -# remote_user = you
    -# remote_port = 2222
     
     [manifest]
     no_jinjaturtle = true
    -sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
     
     [diff]
    -# ignore noisy drift
     exclude_path = /var/anacron
    -ignore_package_versions = true
    -# enforce = true  # requires ansible-playbook on PATH
    -
    - -
    -
    Note
    -
    In INI sections, option names use underscores (e.g. include_path) even when the CLI flag uses hyphens (e.g. --include-path).
    +ignore_package_versions = true
    - -
    -

    Drift detection with enroll diff

    -

    One of the things I miss from my Puppet days, was the way the Puppet 'agent' would check in with the server and realign itself to the declared desired state. With Ansible, it's easy for systems to fall 'out of date', especially if someone is doing the wrong thing and changing things on-the-fly instead of via config management!

    -

    The purpose of enroll diff is to compare two 'harvests' and detect what has changed - be it adding/removing of programs, change to systemd unit state, modifications, addition or removal of files, and so on.

    - -
    -
    Notifications for diff
    -
    The enroll diff feature supports sending the difference to a webhook of your choosing, or by e-mail. The payload can be sent in json, plain text, or markdown.
    -
    - -
    -
    Noise suppression
    -
    Use --exclude-path to ignore file/dir drift under specific paths (e.g. /var/anacron). Use --ignore-package-versions to ignore routine package upgrades/downgrades while still reporting added/removed packages.
    -
    - -
    -
    $ enroll diff \
    ---old /path/to/harvestA \
    ---new /path/to/harvestB \
    ---exclude-path /var/spool/anacron \
    ---ignore-package-versions
    -
    - -
    -
    Optional: enforce the old harvest state (--enforce)
    -
    If drift exists and ansible-playbook is on PATH, Enroll can generate a manifest from the old harvest and apply it locally to restore expected state. It avoids package downgrades, and will often run Ansible with --tags role_... so only the roles implicated by the drift are applied. This is very much like a return to Puppet's agent mode!
    -
    - -
    -
    $ enroll diff \
    ---old /path/to/harvestA \
    ---new /path/to/harvestB \
    ---enforce
    -
    -
    - -

    How to run enroll diff automatically on a timer

    -

    A great way to use enroll diff is to run it periodically (e.g via cron or a systemd timer). Below is an example.

    -

    Store the below file at /usr/local/bin/enroll-harvest-diff.sh and make it executable.

    -
    - -
    #!/usr/bin/env bash
    -set -euo pipefail
    -
    -# Required env
    -: "${WEBHOOK_URL:?Set WEBHOOK_URL in /etc/enroll/enroll-harvest-diff}"
    -: "${ENROLL_SECRET:?Set ENROLL_SECRET in /etc/enroll/enroll-harvest-diff}"
    -
    -# Optional env
    -STATE_DIR="${ENROLL_STATE_DIR:-/var/lib/enroll}"
    -GOLDEN_DIR="${STATE_DIR}/golden"
    -PROMOTE_NEW="${PROMOTE_NEW:-1}"          # 1=promote new->golden; 0=keep golden fixed
    -KEEP_BACKUPS="${KEEP_BACKUPS:-7}"        # only used if PROMOTE_NEW=1
    -LOCKFILE="${STATE_DIR}/.enroll-harvest-diff.lock"
    -
    -mkdir -p "${STATE_DIR}"
    -chmod 700 "${STATE_DIR}" || true
    -
    -# single-instance lock (avoid overlapping timer runs)
    -exec 9>"${LOCKFILE}"
    -flock -n 9 || exit 0
    -
    -tmp_new=""
    -cleanup() {
    -  if [[ -n "${tmp_new}" && -d "${tmp_new}" ]]; then
    -    rm -rf "${tmp_new}"
    -  fi
    -}
    -trap cleanup EXIT
    -
    -make_tmp_dir() {
    -  mktemp -d "${STATE_DIR}/.harvest.XXXXXX"
    -}
    -
    -run_harvest() {
    -  local out_dir="$1"
    -  rm -rf "${out_dir}"
    -  mkdir -p "${out_dir}"
    -  chmod 700 "${out_dir}" || true
    -  enroll harvest --out "${out_dir}" >/dev/null
    -}
    -
    -# A) create golden if missing
    -if [[ ! -f "${GOLDEN_DIR}/state.json" ]]; then
    -  tmp="$(make_tmp_dir)"
    -  run_harvest "${tmp}"
    -  rm -rf "${GOLDEN_DIR}"
    -  mv "${tmp}" "${GOLDEN_DIR}"
    -  echo "Golden harvest created at ${GOLDEN_DIR}"
    -  exit 0
    -fi
    -
    -# B) create new harvest
    -tmp_new="$(make_tmp_dir)"
    -run_harvest "${tmp_new}"
    -
    -# C) diff + webhook notify
    -enroll diff \
    -  --old "${GOLDEN_DIR}" \
    -  --new "${tmp_new}" \
    -  --webhook "${WEBHOOK_URL}" \
    -  --webhook-format json \
    -  --webhook-header "X-Enroll-Secret: ${ENROLL_SECRET}" # You can send multiple --webhook-header params as you need
    -
    -# Promote or discard new harvest
    -if [[ "${PROMOTE_NEW}" == "1" || "${PROMOTE_NEW,,}" == "true" || "${PROMOTE_NEW}" == "yes" ]]; then
    -  ts="$(date -u +%Y%m%d-%H%M%S)"
    -  backup="${STATE_DIR}/golden.prev.${ts}"
    -  mv "${GOLDEN_DIR}" "${backup}"
    -  mv "${tmp_new}" "${GOLDEN_DIR}"
    -  tmp_new=""  # don't delete it in trap
    -
    -  # Keep only latest N backups
    -  if [[ "${KEEP_BACKUPS}" =~ ^[0-9]+$ ]] && (( KEEP_BACKUPS > 0 )); then
    -    ls -1dt "${STATE_DIR}"/golden.prev.* 2>/dev/null | tail -n +"$((KEEP_BACKUPS+1))" | xargs -r rm -rf
    -  fi
    -
    -  echo "Diff complete; baseline updated."
    -else
    -  # tmp_new will be deleted by trap
    -  echo "Diff complete; baseline unchanged (PROMOTE_NEW=${PROMOTE_NEW})."
    -fi
    -
    -
    -
    -

    Save these environment variables in /etc/enroll/enroll-harvest-diff

    -
    - -
    
    -# Where to store golden + temp harvests
    -ENROLL_STATE_DIR=/var/lib/enroll
    -
    -# 1 = each run becomes new baseline ("since last harvest")
    -# 0 = compare against a fixed baseline ("since golden")
    -PROMOTE_NEW=1
    -
    -# If PROMOTE_NEW=1, keep this many old baselines
    -KEEP_BACKUPS=7
    -
    -WEBHOOK_URL=https://example.com/webhook/xxxxxxxx
    -ENROLL_SECRET=xxxxxxxxxxxxxxxxxxxx
    -
    -
    - -
    -
    -
    Webhook headers
    -
    The --webhook-header parameter can be used multiple times. You can, for example, send X-Enroll-Secret and a secret value of your choice, to help secure your webhook endpoint.
    -
    - -

    Save this systemd unit file to /etc/systemd/system/enroll-harvest-diff.service

    -
    - -
    
    -[Unit]
    -Description=Enroll harvest + diff + webhook notify
    -Wants=network-online.target
    -After=network-online.target
    -ConditionPathExists=/etc/enroll/enroll-harvest-diff
    -
    -[Service]
    -Type=oneshot
    -EnvironmentFile=/etc/enroll/enroll-harvest-diff
    -Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    -UMask=0077
    -
    -# Create /var/lib/enroll automatically
    -StateDirectory=enroll
    -
    -ExecStart=/usr/local/bin/enroll-harvest-diff.sh
    -
    -
    - -
    -

    Save this systemd timer to /etc/systemd/system/enroll-harvest-diff.timer

    -
    - -
    
    -[Unit]
    -Description=Run Enroll harvest diff hourly
    -
    -[Timer]
    -OnCalendar=hourly
    -RandomizedDelaySec=10m
    -Persistent=true
    -
    -[Install]
    -WantedBy=timers.target
    -
    -
    - -
    -

    Now you can enable and test it!

    -
    - -
    
    -sudo systemctl daemon-reload
    -sudo systemctl enable --now enroll-harvest-diff.timer
    -
    -# run once now
    -sudo systemctl start enroll-harvest-diff.service
    -# watch it in the logs
    -sudo journalctl -u enroll-harvest-diff.service -n 200 --no-pager
    -
    -
    - -
    -
    -
    Need help with writing webhooks?
    -
    I use Node-RED. Here's a sample Node-RED flow that might help run your webhook, pre-configured to parse the enroll diff JSON payload!
    -
    - -
    - - -
    -

    Why did Enroll include/exclude something? enroll explain

    -

    When you run enroll harvest, Enroll records why it chose to include or exclude each path in state.json. The enroll explain subcommand summarizes that data so you can quickly sanity-check a harvest, tune include/exclude rules, and understand where packages/services came from.

    - -
    -
    What can it read?
    -
    enroll explain accepts a harvest bundle directory, a direct path to state.json, a .tar.gz/.tgz bundle, or an encrypted .tar.gz.sops bundle.
    -
    - -
    - -
    $ enroll explain /tmp/enroll-harvest
    -
    -# or point at the state.json path directly
    -$ enroll explain /tmp/enroll-harvest/state.json
    -
    - -
    -

    The default output is human-readable text. For scripting or deeper inspection, use JSON output:

    -
    - -
    $ enroll explain /tmp/enroll-harvest --format json | jq .
    -
    -# show more example paths per reason
    -$ enroll explain /tmp/enroll-harvest --max-examples 10
    -
    - -
    -

    If you stored a harvest as a single SOPS-encrypted bundle, enroll explain can decrypt it on the fly (it will also auto-detect files ending with .sops):

    -
    - -
    $ enroll explain /var/lib/enroll/harvest.tar.gz.sops --sops
    -
    - -
    -

    What you get back:

    -
      -
    • A summary of what roles were collected (users, services, package snapshots, etc_custom, usr_local_custom, etc.).
    • -
    • Why packages ended up in inventory (observed_via), e.g. user-installed vs referenced by a harvested systemd unit.
    • -
    • Breakdowns of managed_files.reason, managed_dirs.reason, and excluded.reason, with a few example paths for each reason.
    • -
    - -
    -
    Tip
    -
    Use enroll explain after a first harvest to decide what to exclude (noise) and what to include (snowflake app/config under /opt, /srv, etc.) before you generate a manifest.
    -
    -
    Security note: enroll explain doesn't print file contents, but it can print path names and unit/package names. Treat the output as sensitive if your environment uses revealing path conventions (and especially if you harvested with --dangerous).
    +
    +
    +

    Install

    +

    Enroll requires Python 3.10 or newer. The project’s README documents Debian/Ubuntu and Fedora repositories, AppImage, pip/pipx, and Poetry development installs.

    +
    +
    pip / pipx
    pip install enroll
    +# or
    +pipx install enroll
    +
    development checkout
    poetry install
    +poetry run enroll --help
    +
    +

    For a beta release, your package manager may require an explicit pre-release opt-in depending on how you install and what stable version is already available.

    - - -
    -

    Tips

    -
    -
    -
    -
    Start safe
    -

    Default harvesting tries to avoid likely secrets via path rules, content sniffing, and size caps. Use --dangerous only when you've planned where the output will live.

    -
    -
    -
    -
    -
    Encrypt at rest
    -

    If you plan to keep harvests/manifests long term (especially in git), use --sops to produce a single encrypted bundle file. Note: enroll diff can be passed --sops to decrypt and compare two harvests on-the-fly!

    -
    -
    -
    -
    -
    Multi-host safety
    -

    For fleets, prefer multi-site output so roles stay generic and host inventory controls what is applied per host - reducing "shared role breaks other host" surprises.

    -
    -
    -
    -
    -
    Keep it reproducible
    -

    Commit the manifest output, run it in CI, and use enroll diff as a drift alarm (webhook/email).

    -
    -
    -
    -
    -
    diff --git a/src/content/examples.html b/src/content/examples.html index 464eb1e..997da1b 100644 --- a/src/content/examples.html +++ b/src/content/examples.html @@ -1,142 +1,91 @@ --- title: "Examples" html_title: "Enroll Examples" -description: "Copy/paste recipes for Enroll: one host, fleets, drift detection, and safe storage." +description: "Copy/paste Enroll recipes for local harvests, remote harvests, multi-site manifests, SOPS, diff, validate, and explain." ---
    Examples
    -

    Copy/paste recipes

    -

    Practical flows you can adapt to your environment.

    +

    Copy/paste recipes that match the current CLI

    +

    Start with dry-runs and validate your harvest before manifesting.

    -
    -
    -
    -
    Enroll a single host (local)
    -
    - -
    $ enroll harvest --out /tmp/enroll-harvest
    -$ enroll manifest --harvest /tmp/enroll-harvest \
    -  --out /tmp/enroll-ansible
    -$ ansible-playbook -i "localhost," -c local \
    -  /tmp/enroll-ansible/playbook.yml --diff --check
    -
    -

    Great for "make this box reproducible" or building a golden role set.

    -
    -
    +

    Local single-host workflow

    enroll harvest --out /tmp/enroll-harvest
    +enroll validate /tmp/enroll-harvest
    +enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
    +cd /tmp/enroll-ansible
    +ansible-galaxy collection install -r requirements.yml
    +ansible-playbook -i localhost, -c local playbook.yml --check --diff

    Use this when the output is for one machine or a “golden” role set.

    -
    -
    -
    Enroll a remote host (over SSH)
    -
    - -
    $ enroll harvest \
    -  --remote-host myhost.example.com \
    -  --remote-user myuser \
    -  --out /tmp/enroll-harvest
    -$ enroll manifest \
    -  --harvest /tmp/enroll-harvest \
    -  --out /tmp/enroll-ansible
    -
    -

    No need to manually run commands on the server - your bundle lands locally. If your remote user needs a password for sudo, pass in --ask-become-pass or -K, just like in Ansible. If you don't want to use sudo, pass --no-sudo, but your harvest may contain less data.

    -
    -
    +

    Remote harvest over SSH

    enroll harvest \
    +  --remote-host app01.example.net \
    +  --remote-user mig \
    +  --out /tmp/enroll-app01-harvest
    +enroll manifest --harvest /tmp/enroll-app01-harvest --out /tmp/enroll-app01-ansible

    Add -K if sudo requires a password. Use --no-sudo only when a limited harvest is acceptable.

    -
    -
    -
    Fleets: multi-site output
    -
    - -
    $ fqdn="$(hostname -f)"
    -$ enroll single-shot --remote-host "$fqdn" \
    -  --remote-user myuser \
    -  --out /tmp/enroll-ansible \
    +      

    Remote host using ssh_config alias

    enroll harvest \
    +  --remote-host app01-alias \
    +  --remote-ssh-config ~/.ssh/config \
    +  --out /tmp/enroll-app01-harvest

    --remote-host is still required; it may be the Host alias in your ssh config.

    + +

    Encrypted SSH key in CI

    export ENROLL_SSH_KEY_PASSPHRASE='correct horse battery staple'
    +enroll single-shot \
    +  --remote-host app01.example.net \
    +  --remote-user mig \
    +  --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE \
    +  --harvest /tmp/enroll-app01-harvest \
    +  --out /tmp/enroll-app01-ansible \
    +  --fqdn app01.example.net

    --ask-key-passphrase is the interactive equivalent. The two key-passphrase options are mutually exclusive.

    + +

    Include and exclude paths

    enroll harvest --out /tmp/enroll-harvest \
    +  --include-path '/home/*/.profile' \
    +  --include-path 're:^/home/[^/]+/\.config/myapp/.*$' \
    +  --exclude-path '/home/*/.ssh/**'

    Included files are still secret-scanned unless --dangerous is also set.

    + +

    Dangerous mode, encrypted at rest

    enroll harvest \
    +  --out /tmp/enroll-secure-harvest \
    +  --dangerous \
    +  --sops 54A91143AE0AB4F7743B01FE888ED1B423A3BC99

    --dangerous disables likely-secret checks. Pair it with SOPS or equivalent storage controls.

    + +

    Manifest encrypted harvest, then encrypt manifest

    enroll manifest \
    +  --harvest /tmp/enroll-secure-harvest/harvest.tar.gz.sops \
    +  --out /tmp/enroll-secure-ansible \
    +  --sops 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
    +cd /tmp/enroll-secure-ansible
    +sops -d manifest.tar.gz.sops | tar -xzvf -
    +cd manifest

    Manifest SOPS mode outputs a single encrypted tarball. Extract it before using Ansible.

    + +

    Multi-site output

    fqdn=app01.example.net
    +enroll manifest \
    +  --harvest /tmp/enroll-app01-harvest \
    +  --out /srv/ansible-enroll \
       --fqdn "$fqdn"
    -$ ansible-playbook "/tmp/enroll-ansible/playbooks/${fqdn}.yml"
    -
    -

    Shared roles + host inventory keeps one host's differences from breaking another.

    -
    -
    +cd /srv/ansible-enroll +ansible-playbook -i inventory/hosts.ini "playbooks/${fqdn}.yml" --check --diff

    Multi-site mode stores host-specific variables and file artifacts in inventory.

    -
    -
    -
    Drift detection with enroll diff
    -
    - -
    $ enroll diff --old /path/to/harvestA --new /path/to/harvestB --format markdown --exclude-path /var/anacron --ignore-package-versions
    -$ enroll diff --old /path/to/golden --new /path/to/current \
    -  --webhook https://example.net/webhook  \
    +      

    Force or disable JinjaTurtle

    # Error if jinjaturtle is not on PATH
    +enroll manifest --harvest ./harvest --out ./ansible --jinjaturtle
    +
    +# Never use template integration even if it is installed
    +enroll manifest --harvest ./harvest --out ./ansible --no-jinjaturtle

    Without either flag, Enroll auto-detects integration availability.

    + +

    Compare harvests and notify a webhook

    enroll diff \
    +  --old /srv/enroll/golden \
    +  --new /srv/enroll/current \
    +  --webhook https://example.net/enroll/webhook \
       --webhook-format json \
    -  --webhook-header 'X-Enroll-Secret: ...'  \
    -   --ignore-package-versions --exit-code
    -            
    -
    -

    Use it in cron or CI to alert on change.

    -
    -
    + --webhook-header 'Authorization: Bearer ...' \ + --ignore-package-versions \ + --exit-code

    With --exit-code, detected drift exits 2 after producing the report and notifications.

    -
    -
    -
    Explain a harvest with enroll explain
    -
    - -
    $ enroll explain /tmp/enroll-harvest
    +      

    Validate and fail CI on warnings

    enroll validate ./harvest --format json --out validate.json --fail-on-warnings

    Use --allow-remote-schema only when you intentionally want validation to fetch an HTTP(S) schema.

    -# machine-readable (reasons, examples, inventory breakdown) -$ enroll explain /tmp/enroll-harvest --format json | jq . - -# encrypted bundle -$ enroll explain /var/lib/enroll/harvest.tar.gz.sops --sops
    -
    -

    Great for answering "why did it include/exclude that file?" before you generate a manifest.

    -
    -
    - -
    -
    -
    Enforce the previous state with enroll diff --enforce
    -
    - -
    $ enroll diff \
    -  --old /path/to/harvestA \
    -  --new /path/to/harvestB \
    -  --enforce \
    -  --format json
    -	    
    -
    -

    Enforcing the old harvest will restore its files/perms, missing packages, changed services or users, if ansible-playbook is on the PATH.

    -
    -
    +

    Explain what was harvested

    enroll explain ./harvest --max-examples 25
    +enroll explain ./harvest.tar.gz.sops --sops --format json

    Useful before committing generated Ansible or debugging why expected files were skipped.

    - -
    - -
    -
    -
    -
    Safe harvesting (default)
    -

    Enroll tries to avoid harvesting files that might contain secrets. If you need to capture "everything", pass --dangerous and treat the output as sensitive.

    -

    You can still control what gets collected and what doesn't by using --include and --exclude flags.

    -
    $ enroll harvest --dangerous --out /tmp/enroll-harvest
    -
    -
    -
    -
    -
    Encrypt bundles at rest (SOPS)
    -

    Produce a single encrypted file for harvest and/or manifest output (requires SOPS to be installed).

    -

    This is especially a good idea if you are using --dangerous, which might sweep up secrets (see above).

    -
    $ enroll harvest --dangerous --out /tmp/harvest \
    -  --sops <FINGERPRINT>
    -$ enroll manifest --harvest /tmp/harvest/harvest.tar.gz.sops \
    -  --out /tmp/enroll-ansible --sops <FINGERPRINT>
    -
    -
    -
    -
    diff --git a/src/content/news.html b/src/content/news.html new file mode 100644 index 0000000..72a6cf5 --- /dev/null +++ b/src/content/news.html @@ -0,0 +1,38 @@ +--- +title: "News" +html_title: "Enroll News" +description: "Project status and release notes for the current Enroll 0.7 beta documentation." +--- +
    +
    +
    News
    +

    Project notes

    +

    The site now reflects the current 0.7 beta behavior documented in the repository.

    +
    +
    + +
    +
    +
    +
    + 0.7 beta + current documentation refresh +
    +

    Documentation now matches the current CLI

    +

    The rewritten site documents the current harvest/manifest model, Ansible-only public output, common role grouping, SOPS bundle behavior, JinjaTurtle flags, root PATH safety, remote SSH passphrase handling, validation, explain, and diff notifications.

    +

    The security pages now emphasise the key trust boundary: Enroll can validate bundle structure, but it cannot prove that harvested host state is semantically safe to apply.

    +
    + +
    +

    Highlights in the current codebase

    +
      +
    • Harvest validation runs before manifest rendering.
    • +
    • Directory harvests are frozen before consumption to reduce race opportunities.
    • +
    • Plain output paths are private and hardened against unsafe parents/symlinks.
    • +
    • Root PATH checks can stop unsafe non-interactive runs unless --assume-safe-path is used intentionally.
    • +
    • User shell dotfiles are automatic only in --dangerous mode.
    • +
    • Container images are enforceable only when a digest-pinned registry reference exists.
    • +
    +
    +
    +
    diff --git a/src/content/schema.html b/src/content/schema.html index 49f4a8f..096ed7d 100644 --- a/src/content/schema.html +++ b/src/content/schema.html @@ -1,52 +1,46 @@ --- -title: "Schema" +title: "State Schema" html_title: "Enroll State Schema" -description: "JSON Schema describing the Enroll harvest state.json format." -layout: "schema" +description: "The current JSON schema for Enroll harvest state.json bundles." ---
    Schema

    Harvest state.json schema

    -

    enroll harvest generates a state file. This is its structure.

    +

    The schema below is served from /schema/state.schema.json and should match the schema vendored with the current Enroll codebase.

    -
    -
    -
    -
    Tips
    -
    -

    You can validate a harvest against the schema by running enroll validate /path/to/harvest .

    -
    -
    -
    -
    Links
    - -
    -
    -
    - -
    -
    -
    -

    state.schema.json

    - Open raw -
    - -
    - -
    Loading…
    -
    - -
    Tip: you can validate a harvest with python -m jsonschema -i state.json schema/state.schema.json
    -
    +
    +

    Validation behavior

    +

    enroll validate uses the vendored schema by default. You may pass a local schema path with --schema. HTTP(S) schemas are blocked unless --allow-remote-schema is explicitly set.

    +
    enroll validate ./harvest
    +enroll validate ./harvest --schema ./state.schema.json
    +enroll validate ./harvest --schema https://enroll.sh/schema/state.schema.json --allow-remote-schema
    +enroll validate ./harvest --no-schema
    +
    + +
    +
    +

    Current schema

    + Open JSON
    +
    Loading schema…
    + + diff --git a/src/content/security.html b/src/content/security.html index c2a812b..f8a7676 100644 --- a/src/content/security.html +++ b/src/content/security.html @@ -1,52 +1,60 @@ --- title: "Security Design" -html_title: "Enroll Security" -description: "Security posture and safe workflows for Enroll outputs." +html_title: "Enroll Security Design" +description: "Threat model, trust boundaries, dangerous mode, SOPS, root PATH safety, and manifest safety for Enroll." ---
    -
    Security
    -

    Safe by default. Powerful when you opt in.

    -

    Enroll can touch sensitive files. This page helps you use it confidently.

    +
    Security design
    +

    Safe-by-default collection, explicit trust at apply time.

    +

    Enroll tries hard not to accidentally copy secrets or follow hostile paths, but it cannot decide whether a structurally valid host snapshot is safe to reproduce.

    -
    -
    -

    Default behavior

    -

    In normal mode, Enroll attempts to avoid harvesting likely secrets using a combination of path deny-lists, content sniffing, and size caps. This means you may see some files intentionally skipped.

    -
    - -
    -
    -
    -
    -

    The --dangerous flag

    -

    This disables secret-safety checks. It can copy private keys, API tokens, DB passwords, TLS key material, etc.

    -

    Rule: if you use --dangerous, treat the output as sensitive data and plan secure storage before you run it. Don't store secrets in plaintext in a public place!

    -
    -
    +
    +

    The most important warning

    +

    When manifesting, Enroll prints:

    +
    This harvest is structurally valid, but Enroll cannot prove it is semantically safe.
    +Only apply manifests generated from harvests whose provenance you trust.
    +

    Validation checks shape and consistency. It does not prove the captured configuration is a good idea, that service files are benign, or that the host was not already compromised.

    -

    Encrypt bundles at rest with SOPS

    -

    You can install SOPS on your $PATH, then use --sops to write a single encrypted .tar.gz.sops file for harvests and/or manifests. This is meant for storage-at-rest and backups.

    +

    Default secret avoidance

    +

    In normal mode, Enroll uses path deny-lists, symlink-safe file opening, file size limits, binary detection, and sampled content sniffing to avoid likely secrets and noisy data.

    +

    Safe-mode scanning treats common credential-looking assignments as sensitive, including password/pass/passwd/pwd/pw forms, API keys, auth tokens, client secrets, secret keys, cloud access key names, service-account key names, and similar patterns.

    +
    Comment caveat: Enroll ignores comments during the sensitive-content scan. If a real secret is commented out, it may still be captured in safe mode. Use SOPS or your own encryption when in doubt.
    +
    -
    - -
    $ enroll harvest --out /tmp/enroll-harvest --dangerous --sops <FINGERPRINT>
    -$ enroll manifest --harvest /tmp/enroll-harvest/harvest.tar.gz.sops \
    -  --out /tmp/enroll-ansible --sops <FINGERPRINT>
    -
    +
    +

    --dangerous

    +

    --dangerous disables likely-secret checks. It may copy private keys, TLS key material, tokens, passwords, cloud credentials, and user shell dotfiles into the harvest in plaintext.

    +

    Use it only when you intentionally want an aggressive disaster-recovery snapshot and you already know how the output will be protected.

    +
    -
    -
    Important
    -
    In manifest --sops mode, you'll need to decrypt and extract the bundle before running ansible-playbook.
    -
    +
    +

    SOPS is storage-at-rest protection

    +

    --sops writes encrypted .tar.gz.sops bundles for harvest and manifest outputs. It is not the same as generating Ansible Vault or SOPS-managed inventory variables.

    +
    enroll harvest --out ./secure-harvest --dangerous --sops <GPG_FINGERPRINT>
    +enroll manifest --harvest ./secure-harvest/harvest.tar.gz.sops \
    +  --out ./secure-manifest --sops <GPG_FINGERPRINT>
    +

    Decrypt and extract an encrypted manifest before running Ansible.

    +
    + +
    +

    Filesystem and rendering hardening

    +
      +
    • Plaintext harvest/manifest output directories are created private and are refused when unsafe or unexpectedly pre-existing.
    • +
    • Root-run output refuses parent directories controlled by unprivileged users, except safe sticky boundaries such as /tmp with further child checks.
    • +
    • Source and artifact reads use no-follow semantics and reject symlinked components, non-regular files, hardlinks, and unexpected large inputs.
    • +
    • Directory harvests are frozen into a private temporary copy before validation and rendering to avoid post-validation swaps.
    • +
    • Tar/SOPS/remote inputs are extracted through safe extraction rules.
    • +
    • Harvested strings that look like Jinja are emitted as Ansible !unsafe data, while generated task scaffolding only accepts strict Enroll-controlled tokens.
    • +
    @@ -54,57 +62,30 @@ description: "Security posture and safe workflows for Enroll outputs."
    Recommended workflow
      -
    1. Start with default mode (no --dangerous).
    2. -
    3. Add --include-path for a small set of extra files you genuinely want managed.
    4. -
    5. If you must capture secrets, use --dangerous and --sops.
    6. -
    7. Keep outputs out of public repos; review before committing.
    8. -
    9. Rotate credentials if you ever suspect they were captured or exposed.
    10. +
    11. Harvest in normal mode first.
    12. +
    13. Run enroll explain and enroll validate.
    14. +
    15. Use targeted --include-path instead of broad --dangerous where possible.
    16. +
    17. Encrypt outputs if they may contain sensitive data or leave the host.
    18. +
    19. Run Ansible with --check --diff and review before applying.
    -
    Storage ideas
    -
      -
    • Encrypted SOPS bundle stored in a password manager vault
    • -
    • Private git repo with additional encryption at rest
    • -
    • Offline backup in an encrypted volume
    • -
    +
    Root PATH safety
    +

    When run as root, Enroll checks PATH for empty, relative, current-directory, or group/world-writable entries that could cause host tools to resolve to attacker-controlled binaries.

    +

    Use --assume-safe-path only in trusted automation where the PATH is intentional.

    -
    Scope control
    -

    You can explicitly include or exclude paths. Excludes take precedence over includes.

    -
    $ enroll harvest \
    -  --out /tmp/enroll-harvest \
    -  --include-path '/home/*/.profile' \
    -  --exclude-path '/home/*/.ssh/**'
    -
    -
    -
    - -
    - -
    -

    Threat model

    -
    -
    -
    What Enroll tries to prevent
    -
      -
    • Accidentally copying obvious secrets in default mode
    • -
    • Harvesting huge/unbounded file sets by mistake
    • -
    • One host's difference causing problems for other hosts in terms of Ansible task steps (multi-site mode)
    • -
    -
    -
    -
    What you still need to think about
    -
      -
    • Where outputs are stored and who can access them
    • -
    • Reviewing what was captured before committing/sharing
    • -
    • Choosing encryption and secret-management strategy
    • +
      Out of scope
      +
        +
      • Proving harvested config is semantically safe
      • +
      • Protecting against a fully compromised root account
      • +
      • Guaranteeing generated Ansible is an ideal long-term design
      • +
      • Secret management after you intentionally disable safety checks
    -
    diff --git a/src/content/troubleshooting.html b/src/content/troubleshooting.html new file mode 100644 index 0000000..3a249ad --- /dev/null +++ b/src/content/troubleshooting.html @@ -0,0 +1,28 @@ +--- +title: "Troubleshooting" +html_title: "Enroll Troubleshooting" +description: "Common Enroll errors and what they mean: unsafe PATH, sudo, SSH key passphrases, SOPS, validation, and Ansible runs." +--- +
    +
    +
    Troubleshooting
    +

    Common messages and fixes

    +

    Most Enroll failures are deliberate fail-closed checks. This page explains what to do next.

    +
    +
    + +
    +
    +
    +

    Re-run the remote harvest with -K or --ask-become-pass. Use --no-sudo only when you accept a limited harvest.

    enroll harvest --remote-host app01.example.net --remote-user mig -K --out ./harvest
    +

    Use --ask-key-passphrase interactively or --ssh-key-passphrase-env ENV_VAR in CI.

    export ENROLL_SSH_KEY_PASSPHRASE='...'
    +enroll harvest --remote-host app01 --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE --out ./harvest
    +

    When running as root, Enroll refuses or prompts if PATH could resolve host tools from an attacker-controlled location. Fix the PATH or parent directory permissions before running in automation. Use --assume-safe-path only when you intentionally trust the environment.

    +

    Plain manifest mode expects a directory harvest. For an encrypted harvest tarball, pass --sops <fingerprint> to manifest so Enroll decrypts and extracts it safely before rendering.

    enroll manifest --harvest ./harvest/harvest.tar.gz.sops --out ./ansible --sops <GPG_FINGERPRINT>
    +

    The bundle is internally inconsistent: state.json references an artifact that is absent, or artifacts/ contains files not referenced by state. Re-harvest from a trusted host, or inspect the bundle provenance before using it.

    +

    Install the generated requirements before running the playbook. Container, Podman, Flatpak, or Snap roles may add collection requirements.

    cd ./ansible
    +ansible-galaxy collection install -r requirements.yml
    +

    Automatic capture of .bashrc, .profile, .bash_logout, and .bash_aliases is disabled unless --dangerous is used. Prefer targeted --include-path first; normal secret checks still apply.

    +
    +
    +
    diff --git a/src/hugo.toml b/src/hugo.toml index 53735bb..9a37c63 100644 --- a/src/hugo.toml +++ b/src/hugo.toml @@ -6,4 +6,4 @@ uglyURLs = true disableKinds = ["taxonomy", "term", "RSS", "sitemap"] [params] - description = "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible roles/playbooks from what it finds. Harvest → Manifest → Manage." + description = "Enroll harvests real Linux host state and renders Ansible configuration-management output." diff --git a/src/static/schema/state.schema.json b/src/static/schema/state.schema.json index d0bde52..8806b2e 100644 --- a/src/static/schema/state.schema.json +++ b/src/static/schema/state.schema.json @@ -16,6 +16,181 @@ ], "unevaluatedProperties": false }, + "ContainerImageTagAlias": { + "additionalProperties": false, + "properties": { + "ref": { + "minLength": 1, + "type": "string" + }, + "repository": { + "minLength": 1, + "type": "string" + }, + "tag": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "ref", + "repository", + "tag" + ], + "type": "object" + }, + "ContainerImage": { + "additionalProperties": false, + "properties": { + "architecture": { + "type": [ + "string", + "null" + ] + }, + "created": { + "type": [ + "string", + "null" + ] + }, + "engine": { + "enum": [ + "docker", + "podman" + ], + "type": "string" + }, + "home": { + "type": [ + "string", + "null" + ] + }, + "image_id": { + "type": [ + "string", + "null" + ] + }, + "notes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "os": { + "type": [ + "string", + "null" + ] + }, + "platform": { + "type": [ + "string", + "null" + ] + }, + "pull_ref": { + "type": [ + "string", + "null" + ] + }, + "repo_digests": { + "items": { + "type": "string" + }, + "type": "array" + }, + "repo_tags": { + "items": { + "type": "string" + }, + "type": "array" + }, + "scope": { + "enum": [ + "system", + "user" + ], + "type": "string" + }, + "size": { + "type": [ + "integer", + "null" + ] + }, + "source": { + "type": "string" + }, + "tag_aliases": { + "items": { + "$ref": "#/$defs/ContainerImageTagAlias" + }, + "type": "array" + }, + "user": { + "type": [ + "string", + "null" + ] + }, + "variant": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "engine", + "scope", + "user", + "home", + "image_id", + "repo_tags", + "repo_digests", + "pull_ref", + "tag_aliases", + "os", + "architecture", + "variant", + "platform", + "size", + "created", + "source", + "notes" + ], + "type": "object" + }, + "ContainerImagesSnapshot": { + "additionalProperties": false, + "properties": { + "images": { + "items": { + "$ref": "#/$defs/ContainerImage" + }, + "type": "array" + }, + "notes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "role_name": { + "const": "container_images" + } + }, + "required": [ + "role_name", + "images", + "notes" + ], + "type": "object" + }, "DnfConfigSnapshot": { "allOf": [ { @@ -117,6 +292,14 @@ "minLength": 1, "type": "string" }, + "group": { + "minLength": 1, + "type": "string" + }, + "section": { + "minLength": 1, + "type": "string" + }, "version": { "minLength": 1, "type": "string" @@ -364,6 +547,12 @@ }, "type": "array" }, + "section": { + "type": [ + "string", + "null" + ] + }, "version": { "type": [ "string", @@ -390,6 +579,16 @@ "package": { "minLength": 1, "type": "string" + }, + "has_config": { + "type": "boolean", + "default": true + }, + "section": { + "type": [ + "string", + "null" + ] } }, "required": [ @@ -571,6 +770,21 @@ "$ref": "#/$defs/UserEntry" }, "type": "array" + }, + "user_flatpaks": { + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/$defs/FlatpakInstall" + } + }, + "type": "object" + }, + "user_flatpak_remotes": { + "type": "array", + "items": { + "$ref": "#/$defs/FlatpakRemote" + } } }, "required": [ @@ -652,6 +866,256 @@ "notes" ], "type": "object" + }, + "SysctlSnapshot": { + "additionalProperties": false, + "properties": { + "role_name": { + "const": "sysctl" + }, + "managed_files": { + "items": { + "$ref": "#/$defs/ManagedFile" + }, + "type": "array" + }, + "parameters": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "notes": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "role_name", + "managed_files", + "parameters", + "notes" + ], + "type": "object" + }, + "FlatpakInstall": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "method": { + "type": "string", + "enum": [ + "system", + "user" + ] + }, + "remote": { + "type": [ + "string", + "null" + ] + }, + "branch": { + "type": [ + "string", + "null" + ] + }, + "arch": { + "type": [ + "string", + "null" + ] + }, + "kind": { + "type": [ + "string", + "null" + ], + "enum": [ + "app", + "runtime", + null + ] + }, + "ref": { + "type": [ + "string", + "null" + ] + }, + "user": { + "type": [ + "string", + "null" + ] + }, + "home": { + "type": [ + "string", + "null" + ] + }, + "source": { + "type": "string" + }, + "from_url": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "name", + "method" + ], + "type": "object" + }, + "FlatpakRemote": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "method": { + "type": "string", + "enum": [ + "system", + "user" + ] + }, + "url": { + "type": "string", + "minLength": 1 + }, + "user": { + "type": [ + "string", + "null" + ] + }, + "home": { + "type": [ + "string", + "null" + ] + }, + "source": { + "type": "string" + } + }, + "required": [ + "name", + "method", + "url" + ], + "type": "object" + }, + "SnapInstall": { + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "channel": { + "type": [ + "string", + "null" + ] + }, + "revision": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "classic": { + "type": "boolean" + }, + "devmode": { + "type": "boolean" + }, + "dangerous": { + "type": "boolean" + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "string" + }, + "install_revision": { + "type": "boolean" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "FlatpakSnapshot": { + "additionalProperties": false, + "properties": { + "role_name": { + "const": "flatpak" + }, + "system_flatpaks": { + "type": "array", + "items": { + "$ref": "#/$defs/FlatpakInstall" + } + }, + "remotes": { + "type": "array", + "items": { + "$ref": "#/$defs/FlatpakRemote" + } + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "role_name" + ], + "type": "object" + }, + "SnapSnapshot": { + "additionalProperties": false, + "properties": { + "role_name": { + "const": "snap" + }, + "system_snaps": { + "type": "array", + "items": { + "$ref": "#/$defs/SnapInstall" + } + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "role_name" + ], + "type": "object" } }, "$id": "https://enroll.sh/schema/state.schema.json", @@ -762,6 +1226,18 @@ }, "firewall_runtime": { "$ref": "#/$defs/FirewallRuntimeSnapshot" + }, + "sysctl": { + "$ref": "#/$defs/SysctlSnapshot" + }, + "flatpak": { + "$ref": "#/$defs/FlatpakSnapshot" + }, + "snap": { + "$ref": "#/$defs/SnapSnapshot" + }, + "container_images": { + "$ref": "#/$defs/ContainerImagesSnapshot" } }, "required": [ diff --git a/src/themes/enroll-theme/layouts/partials/footer.html b/src/themes/enroll-theme/layouts/partials/footer.html index 3ab6587..512855b 100644 --- a/src/themes/enroll-theme/layouts/partials/footer.html +++ b/src/themes/enroll-theme/layouts/partials/footer.html @@ -7,19 +7,14 @@
    Enroll (a mig5 project)
    CLI Ansible + 0.7 beta docs
    -
    Reverse-engineering servers into Ansible.
    +
    Harvest existing Linux host state and render reviewable Ansible.
    @@ -28,36 +23,27 @@
    Contact

    - diff --git a/src/themes/enroll-theme/layouts/partials/nav.html b/src/themes/enroll-theme/layouts/partials/nav.html index 3c03651..3e3c0ad 100644 --- a/src/themes/enroll-theme/layouts/partials/nav.html +++ b/src/themes/enroll-theme/layouts/partials/nav.html @@ -12,16 +12,11 @@ - -
    -

    Filesystem hardening for root runs

    -

    Enroll is often run as root because it needs to inspect system state. Version 0.5.5 added stricter filesystem checks so root does not accidentally write harvests or generated output through an unsafe path.

    - -
    -
    -
    -
    Private output directories
    -

    Harvest and manifest outputs are created with restrictive permissions, and Enroll refuses to reuse an existing harvest directory unless that mode is explicitly expected for an internal workflow.

    -
    -
    -
    -
    -
    Symlink and parent checks
    -

    When running as root, Enroll checks path components and parent directories so output is not created below a parent controlled by another user. This reduces the risk of symlink and time-of-check/time-of-use path races.

    -
    -
    -
    -
    -
    Safer artifact handling
    -

    Captured files and generated artifacts are validated as regular files, with traversal, symlink, hardlink, and special-file cases rejected when bundles are validated or extracted.

    -
    -
    -
    -
    -
    Root PATH hygiene
    -

    For root runs, Enroll warns about unsafe PATH entries so helper commands are not resolved from relative, writable, or non-root-owned directories.

    -
    -
    -
    - -
    -
    Why this can feel strict
    -
    A command that writes under a user-owned directory may be convenient, but it can also be hard to prove safe when the process is running as root. Prefer a root-owned output area such as /var/tmp/enroll, /root, or a fresh directory directly under the normal sticky /tmp.
    -
    -
    @@ -122,130 +85,22 @@ description: "Security posture and safe workflows for Enroll outputs."
    -

    Threat model and security scope

    -

    Enroll is a command-line systems administration tool. It is designed to be run intentionally by an administrator, often as root, to inspect a host, harvest selected system state, and optionally generate or apply configuration-management output.

    -

    That makes Enroll different from a web application, daemon, network service, or setuid helper. It is not intended to be a sandbox for hostile local users. Instead, it assumes an operator-controlled execution environment, while still adding defense-in-depth checks for common filesystem, path traversal, secret-handling, and command-resolution mistakes.

    - -
    -
    -
    -
    Trusted operator assumptions
    -
      -
    • If Enroll is run as root, the root user is assumed to control and understand the command line, environment, configuration file, and output location.
    • -
    • If an enroll.ini file is loaded, its location and contents are assumed to be owned, selected, and understood by the operator.
    • -
    • The operator is expected to understand the implications of options such as --dangerous, --assume-safe-path, --sops, --enforce, --remote-host, and --remote-ssh-config.
    • -
    • Harvest bundles used for manifest, diff, or diff --enforce are assumed to come from a trusted source unless the operator is deliberately inspecting them without applying them.
    • -
    -
    -
    -
    -
    -
    Trusted local tools
    -

    Enroll invokes ordinary administrative tools such as SSH, sudo, SOPS, package managers, Docker, Podman, Flatpak, Snap, Ansible, Puppet, Salt, and system utilities. These are assumed to be the trusted tools the operator intended to execute.

    -

    -

    If an attacker can replace or redirect those tools, the host already has a local trust-boundary problem outside Enroll's ability to fully solve.

    -
    -
    -
    -
    - -
    -

    What Enroll tries to protect against

    -

    Enroll still performs substantial hardening because privileged CLI tools can otherwise be easy to misuse. These controls are intended to protect careful administrators from common dangerous mistakes.

    +

    Threat model

    +
    What Enroll tries to prevent
    • Accidentally copying obvious secrets in default mode
    • -
    • Harvesting huge or unbounded file sets by mistake
    • -
    • Writing root-run output through unsafe symlinks, hardlinks, special files, or path-race situations
    • -
    • Extracting harvest tar members outside the intended bundle directory
    • -
    • Copying unsafe harvested artifacts into generated manifests
    • +
    • Harvesting huge/unbounded file sets by mistake
    • +
    • One host's difference causing problems for other hosts by keeping multi-site data in inventory, Hiera, or pillar
    +
    What you still need to think about
      -
    • Resolving helper commands from suspicious PATH entries during root runs
    • -
    • Generating manifest commands where ordinary harvested values could become shell injection
    • -
    • Accepting unknown SSH host keys during remote harvests
    • -
    • Confusing one host's data with another host's data in multi-site outputs
    • -
    • Applying structurally unsafe harvest bundles
    • -
    -
    -
    -
    - -
    -

    What is out of scope

    -

    The following are normally considered local compromise, operator-controlled behavior, or trust-boundary failures rather than Enroll vulnerabilities by themselves:

    -
    -
    -
      -
    • A malicious local user who can already control root's command line, shell environment, config file, working directory, PATH, or invoked binaries
    • -
    • A root user loading an enroll.ini file whose contents intentionally request dangerous behavior
    • -
    • A root user passing --dangerous and observing that Enroll may collect sensitive data
    • -
    • A root user passing --assume-safe-path and observing that Enroll does not prompt about PATH safety
    • -
    -
    -
    -
      -
    • A user applying generated Ansible, Puppet, or Salt output from a harvest bundle they do not trust
    • -
    • A user enforcing a malicious or manually edited harvest bundle with diff --enforce
    • -
    • A user configuring an untrusted webhook, SSH proxy command, SOPS binary, or configuration-management tool
    • -
    • Reports that amount to: root can run Enroll with malicious options and make the system do dangerous things
    • -
    -
    -
    -
    -
    Plain-language summary
    -
    Enroll is an administrator's tool, not a local privilege boundary. If the operator's root execution environment is already attacker-controlled, Enroll cannot make that safe. Enroll does, however, try to reduce the damage from common path, permission, traversal, secret-handling, and manifest-generation mistakes.
    -
    -
    - -
    -

    Trusted harvests and enforcement

    -

    Harvest bundles should be treated as sensitive administrative artifacts. A harvest may contain hostnames, usernames, package lists, service state, filesystem metadata, configuration files, firewall snapshots, container image references, Flatpak/Snap state, and other operational details. In --dangerous mode it may contain substantially more sensitive material.

    -

    Enroll validates harvest structure and artifact safety, but validation does not prove that the desired state represented by a harvest is safe to apply. Only run manifest, diff, or especially diff --enforce against bundles that came from hosts and people you trust.

    -
    - -
    -

    Security report scope

    -
    -
    -
    Useful reports
    -
      -
    • Enroll captures clearly sensitive default-denied files without --dangerous
    • -
    • Enroll follows a symlink or hardlink in a way that causes privileged file disclosure or overwrite
    • -
    • Enroll extracts a tar member outside the intended harvest directory
    • -
    • Enroll accepts an artifact path that escapes the artifact root
    • -
    • Ordinary harvested data can cause command injection in generated manifests
    • -
    • Enroll silently ignores a failed safety check and proceeds anyway
    • -
    -
    -
    -
    Normally out-of-scope reports
    -
      -
    • Root can configure Enroll to collect sensitive files
    • -
    • Root can pass --dangerous and collect dangerous data
    • -
    • Root can pass --assume-safe-path and bypass the root PATH warning
    • -
    • Root can point Enroll at a malicious config file
    • -
    • Root can enforce a malicious harvest bundle
    • -
    • A local attacker can influence Enroll after already controlling root's environment or binaries
    • -
    -
    -
    -
    - -
    - -
    -

    Security researchers

    -
    -
    -
    Found a vulnerability in Enroll?
    -
      -
    • Please contact me using the contact form or via Signal (mig5.55)
    • -
    • My GPG public key is here
    • -
    • Unfortunately, I cannot offer financial bounty rewards at this time, but you will receive full credit for valid security issues.
    • +
    • Where outputs are stored and who can access them
    • +
    • Reviewing what was captured before committing/sharing
    • +
    • Choosing encryption and secret-management strategy
    diff --git a/src/content/troubleshooting.html b/src/content/troubleshooting.html index ea70877..aa68499 100644 --- a/src/content/troubleshooting.html +++ b/src/content/troubleshooting.html @@ -22,7 +22,6 @@ description: "Common Enroll errors and fixes for generated Ansible, Puppet, and Flatpak from_url error JinjaTurtle Missing files - Harvest output parent Validate artifacts Remote harvest Diff enforcement @@ -139,49 +138,6 @@ description: "Common Enroll errors and fixes for generated Ansible, Puppet, and
    -
    -

    Error: harvest output parent is owned by the wrong user

    -

    When Enroll is run as root, it deliberately refuses to create a harvest below a parent directory that is controlled by a non-root user. This is a safety check: a user-owned parent directory can be renamed or replaced while a root process is writing files, which can become a symlink or path-race problem.

    - -
    -
    error: harvest output parent is not owned by root; refusing root-run output: /home/alice
    -error: harvest output parent is not owned by root; refusing root-run output: /tmp/tmp.abcd1234
    -
    - -
    -
    What Enroll is protecting you from
    -

    Harvest output can contain detailed system state and, when --dangerous is used, may contain sensitive configuration material. If Enroll is running as root, it needs the output path to stay exactly where it was checked. Refusing user-owned parent directories helps avoid accidental writes through symlinks or paths that can be swapped underneath the process.

    -
    - -
    -
    -
    -
    Use a root-owned output area
    -

    Create a dedicated root-owned directory and place harvests underneath it:

    -
    $ sudo install -d -m 700 -o root -g root /var/tmp/enroll
    -$ sudo enroll harvest --out /var/tmp/enroll/host1
    -
    -
    -
    -
    -
    /tmp itself is okay
    -

    A fresh output directory directly under the normal root-owned sticky /tmp directory is accepted. The output path must not already exist.

    -
    $ sudo enroll harvest --out /tmp/host1-harvest
    -
    -
    -
    - -
    -
    Avoid root harvests into your home directory
    -
    A command such as sudo enroll harvest --out ~/harvest may expand to a path below a user-owned home directory. Use /var/tmp/enroll, /root, or a fresh directory directly under /tmp instead.
    -
    - -
    -
    Remote harvest note
    -

    In normal remote mode, Enroll uses sudo on the remote host so it can collect system-level state. Recent Enroll versions create a separate root-owned temporary directory for the remote bundle. If an older version reports this error for a path like /tmp/tmp.xxxxx during remote harvest, upgrade Enroll. Use --no-sudo only when you intentionally want a limited, non-root harvest.

    -
    -
    -

    Generated manifest references a missing artifact

    If a manifest task points at a missing source file, validate the original harvest first. Validation checks state.json, referenced artifacts, generated firewall/sysctl files, and unreferenced artifact warnings.

    @@ -227,21 +183,6 @@ error: harvest output parent is not owned by root; refusing root-run output: /tm

    For non-Ansible targets, make sure puppet or salt-call is installed and on PATH before running enforcement.

    -
    -

    Error about schema when validating or manifesting a harvest

    -

    You see an error like error: harvest state does not match this Enroll version's schema; please re-harvest the host with this version of Enroll.

    -
    -
    -
    -
    Reharvest the host
    -

    The harvest has either been tampered with, become corrupt, or was made with an earlier version of Enroll, which means its layout is a different structure than what Enroll now expects.

    -

    The best thing to do is run the harvest again. Enroll will validate the harvest against the latest schema when you go to manifest it.

    -

    If you still experience the error, please report it as a bug!

    -
    -
    -
    -
    - From c760c2ab0f9e348d05cc91a73a5e2129feade5ae Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 30 Jun 2026 16:18:14 +1000 Subject: [PATCH 24/30] Updates to reflect reality --- src/content/_index.html | 428 ++-------- src/content/docs.html | 736 ++++-------------- src/content/examples.html | 259 +++--- src/content/news/0-7-0.html | 203 ++--- src/content/news/_index.html | 15 +- src/content/schema.html | 54 +- src/content/security.html | 167 ++-- src/content/troubleshooting.html | 211 ++--- src/hugo.toml | 2 +- .../enroll-theme/layouts/partials/footer.html | 6 +- 10 files changed, 590 insertions(+), 1491 deletions(-) diff --git a/src/content/_index.html b/src/content/_index.html index 2e62fb4..c31bc8a 100644 --- a/src/content/_index.html +++ b/src/content/_index.html @@ -1,26 +1,26 @@ --- 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." +html_title: "Enroll - Reverse-engineering Linux hosts into Ansible" +description: "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible configuration-management code from what it finds. Harvest → Manifest → Manage." +og_title: "Enroll - Reverse-engineering Linux hosts into Ansible" +og_description: "Harvest a host's real configuration and turn it into Ansible roles and playbooks. 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.

    +
    Reverse-engineering Linux hosts into Ansible
    +

    Get an existing server into configuration management without starting from a blank file.

    +

    Enroll inspects a Debian-like or RedHat-like Linux machine, harvests the state that matters, and generates Ansible roles and playbooks from the captured bundle.

    - Super fast + Safe by default Optional SOPS encryption Remote over SSH
    @@ -29,19 +29,17 @@ og_type: "website"
    -
    Config in the blink of an eye
    -
    harvest once → render to your CM tool
    +
    Harvest once, render Ansible
    +
    state.json + artifacts → roles/playbooks
    $ 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/...
    +
    $ enroll manifest --harvest ./harvest --out ./ansible
    +
    $ ansible-galaxy collection install -r ./ansible/requirements.yml
    +
    ./ansible → playbook.yml, roles/, requirements.yml
    +./ansible + --fqdn → playbooks/<fqdn>.yml + inventory/host_vars/...
    -
    Tip: for multiple hosts, use --fqdn to generate target-native host data: Ansible inventory, Puppet Hiera, or Salt pillar.
    +
    Current Enroll output is Ansible-only. Older examples that mention --target puppet or --target salt are from an experimental branch and no longer match the CLI.
    @@ -52,96 +50,24 @@ og_type: "website"
    -

    A simple mental model

    -

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

    +

    The mental model

    +

    Enroll is built around a small pipeline: capture first, render later, validate before you trust the output.

    -
    -
    -
    -
    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.
    -
    -
    +
    Harvest
    Collect host facts and selected files into a bundle.
    +
    Manifest
    Turn that bundle into an Ansible repository-style output tree.
    +
    Diff
    Compare two harvests and optionally notify by webhook or email.
    +
    Explain
    Summarise what was included or excluded and why.
    +
    Validate
    Check schema and artifact consistency before using a bundle.
    -
    -
    -
    -
    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.
    -
    -
    -
    +
    Default secret avoidance
    Likely private keys, credential assignments, tokens, secret paths, oversized files, and binary-looking files are avoided unless you opt into --dangerous.
    +
    Ansible output
    Generated output includes roles, playbooks, requirements, defaults, templates, and host variables in --fqdn mode.
    +
    Grouped roles by default
    Package/service snapshots are grouped by Debian Section or RPM Group where possible. Use --no-common-roles to keep one generated role per package or unit.
    +
    Multi-site mode
    --fqdn moves host-specific state into Ansible inventory and implies --no-common-roles.
    +
    Remote over SSH
    Run the harvest on another host through Paramiko/OpenSSH config support, then render the manifest locally.
    +
    SOPS bundles
    --sops stores harvests and generated manifests as encrypted tarballs for safer at-rest storage.
    @@ -151,272 +77,78 @@ og_type: "website"
    -
    -

    Quickstart

    -
    - +

    Quickstart

    Start with a local safe-mode harvest, inspect the generated output, then run Ansible when you are ready.

    +
    -
    -
    -
    -
    -
    - -
    # 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).
    -
    -
    -
    +
    +
    $ enroll harvest --out /tmp/enroll-harvest
    +$ enroll validate /tmp/enroll-harvest
    +$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
    +$ cd /tmp/enroll-ansible
    +$ ansible-galaxy collection install -r requirements.yml
    +$ ansible-playbook -i "localhost," -c local playbook.yml --check
    - -
    -
    - -
    # 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).
    +
    +
    $ enroll harvest \
    +  --remote-host host.example.net \
    +  --remote-user admin \
    +  --remote-ssh-config ~/.ssh/config \
    +  --out /tmp/enroll-harvest
    +$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn host.example.net
    - -
    -
    - -
    # 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".
    +
    +
    $ enroll harvest --out /srv/enroll/baseline
    +$ enroll harvest --out /srv/enroll/current
    +$ enroll diff --old /srv/enroll/baseline --new /srv/enroll/current --format markdown --ignore-package-versions
    - -
    -
    - -
    # 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
    -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

    -
    -
    - +

    What Enroll tries to capture

    -
    -
    -
    -
    Harvest
    -
    Collect state into a bundle.
    -
    -
    -
    -
    -
    -
    -
    -
    Manifest
    -
    Render Ansible roles/playbooks, Puppet modules, or Salt states.
    -
    -
    -
    -
    +
    Packages and services

    Manual packages, service-linked packages, systemd enable/running state, and service-relevant changed or custom config.

    +
    Configuration files

    Changed package conffiles, unowned service files, miscellaneous /etc, APT/DNF/YUM config, selected symlinks, and explicit include paths.

    +
    Runtime and user state

    Non-system users, SSH public keys, Flatpak/Snap state, Docker/Podman images, writable sysctls, and fallback iptables/ipset state when persistent files are absent.

    +
    +
    + +
    +
    +

    Commands

    -
    -
    -
    -
    Single-shot
    -
    Harvest → Manifest in one command.
    -
    -
    -
    -
    -
    -
    -
    -
    Diff
    -
    Drift report + webhook/email notifications, or optionally enforce the previous state!
    -
    -
    -
    -
    +

    harvest

    Collect host state into state.json and artifacts/.
    Harvest docs
    +

    manifest

    Validate a harvest and render Ansible output.
    Manifest docs
    +

    single-shot

    Run harvest and manifest in one command.
    Single-shot docs
    +

    diff

    Compare harvests and send reports.
    Diff docs
    +

    explain

    Explain what a harvest contains.
    Explain docs
    +

    validate

    Check schema and bundle consistency.
    Validate docs
    -
    -
    +
    -
    -
    -
    -
    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
    +
    +
    +

    Safe by default, not magic

    +

    Enroll avoids obvious secrets by default, validates harvest structure, freezes directory bundles into private temp trees before manifesting, rejects unknown SSH host keys, and warns when a root PATH looks unsafe.

    +

    It cannot prove that a structurally valid harvest is semantically safe. Only apply manifests generated from harvests whose provenance you trust.

    +
    +
    +
    +
    Dangerous mode means dangerous
    +
    Use --dangerous only when you intentionally want to bypass likely-secret checks. Pair it with --sops or another appropriate at-rest encryption workflow whenever there is any doubt.
    diff --git a/src/content/docs.html b/src/content/docs.html index 69594c8..c8ad555 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -1,308 +1,182 @@ --- title: "Docs" -html_title: "Enroll Docs" -description: "How Enroll works: harvest, manifest, modes, and configuration." +html_title: "Enroll documentation - Ansible host harvesting and manifesting" +description: "Documentation for the current Enroll CLI: harvest Linux host state and generate Ansible configuration-management code." +og_title: "Enroll documentation" +og_description: "Current Enroll docs for harvest, manifest, single-shot, diff, explain, validate, security, SOPS, and Ansible output." +og_type: "article" --- -
    -
    -
    Documentation
    -

    How Enroll works

    -

    The mental model, output modes, and the knobs you'll actually use day-to-day.

    +
    +
    +
    +
    +
    Documentation
    +

    How Enroll works today

    +

    Enroll harvests selected Linux host state, validates it, and renders Ansible configuration-management output. This documentation reflects the current Ansible-only CLI.

    +
    + +
    -
    -
    - +
    -
    -

    Mental model

    -

    Enroll is intentionally simple: it collects facts first, then renders target-native configuration-management output from those facts.

    - -
    -
    -
    -
    -
    -
    -
    1) Harvest
    -
    Snapshot state into a bundle
    -
    -
    -
      -
    • Detect installed packages and services
    • -
    • Collect config that deviates from packaged defaults (where possible)
    • -
    • Grab relevant custom/unowned files in service dirs
    • -
    • Capture non-system users, group memberships, SSH public keys, and dangerous-mode shell dotfiles when requested
    • -
    -
    -
    -
    -
    -
    -
    -
    -
    2) Manifest
    -
    Generate Ansible, Puppet, or Salt output
    -
    -
    -
      -
    • Ansible roles/playbooks, Puppet modules/Hiera data, or Salt states/pillar
    • -
    • Noop-friendly output you can review before applying
    • -
    • Optional target-native host data for multi-host runs
    • -
    -
    -
    -
    - -
    -
    Typical flow
    -
    $ enroll harvest --out /tmp/enroll-harvest
    -$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible
    -$ ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml --check --diff
    -
    -$ enroll manifest --harvest /tmp/enroll-harvest --target puppet --out /tmp/enroll-puppet
    -$ puppet apply --modulepath /tmp/enroll-puppet/modules /tmp/enroll-puppet/manifests/site.pp --noop
    -
    -$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt
    -$ salt-call --local --file-root /tmp/enroll-salt/states state.apply test=True
    +
    +

    Install

    +

    Install from your preferred channel. The Python package requires Python 3.10 or newer.

    +
    +

    $ pip install enroll
    +$ pipx install enroll
    +

    $ 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
    +

    $ poetry install
    +$ poetry run enroll --help
    -
    -

    Targets and grouping

    -

    The harvest bundle is the contract. Manifesting is a separate step, so the same harvest can be rendered into Ansible, Puppet, or Salt without re-probing the host.

    +
    +

    Mental model

    +

    Enroll works in two main phases. Harvest collects host facts and relevant files into a bundle. Manifest turns that bundle into Ansible configuration-management code.

    +
    $ enroll harvest --out ./harvest
    +$ enroll validate ./harvest
    +$ enroll manifest --harvest ./harvest --out ./ansible
    +

    diff, explain, and validate all operate on harvest bundles too.

    +
    +
    +

    Output modes: single-site and multi-site

    -
    -
    -
    Ansible
    -

    Generates playbooks, roles, defaults, files/templates, inventory, and host vars.

    -
    $ enroll manifest --harvest ./harvest --target ansible --out ./ansible
    -$ ansible-playbook -i "localhost," -c local ./ansible/playbook.yml --check --diff
    -
    -
    -
    -
    -
    Puppet
    -

    Generates a control-repo-style tree with manifests/site.pp, modules, files, and Hiera data in --fqdn mode.

    -
    $ enroll manifest --harvest ./harvest --target puppet --out ./puppet
    -$ puppet apply --modulepath ./puppet/modules ./puppet/manifests/site.pp --noop
    -
    -
    -
    -
    -
    Salt
    -

    Generates Salt state trees under states/, with pillar data under pillar/ in --fqdn mode.

    -
    $ enroll manifest --harvest ./harvest --target salt --out ./salt
    -$ salt-call --local --file-root ./salt/states state.apply test=True
    -
    -
    -
    - -
    -
    Reducing role/module/state sprawl
    -

    When Enroll can see package metadata, it tries to combine related package/service snapshots by the package Section category, or equivalent backend metadata. On Debian-like systems this can turn many tiny package roles into broader groups such as admin, web, net, libs, or utils. On RPM-like systems it uses comparable package group metadata where available.

    -

    This grouping is enabled by default in single-site output. It is disabled by --no-common-roles when you want one role/module/state per discovered package/service, and it is also disabled by --fqdn because host-specific output should stay isolated in inventory/Hiera/pillar.

    +

    Single-site mode

    Default mode. Use it for one server or for a golden role set. Raw files live in each role's files/, and template variables live in defaults/main.yml.

    $ enroll manifest --harvest ./harvest --out ./ansible
    +

    Multi-site mode

    Enable with --fqdn. Roles are generic; host-specific state lives under Ansible inventory host_vars. This mode implies --no-common-roles.

    $ enroll manifest --harvest ./harvest --out ./ansible --fqdn host.example.net
    -

    How harvesting works

    -

    At a high level, this is what happens when enroll harvest runs on a host:

    - -
      -
    • Detects the OS and its package backend (e.g dpkg vs rpm)
    • -
    • Detects what packages are installed
    • -
    • For each package, it tries to detect files in /etc that have been modified from the default that get shipped with the package.
    • -
    • It detects running/enabled services and timers via systemd. For each of these, it looks for the unit files, any 'drop-in' files, environment variable files, etc, as well as what executable it executes, and tries to map those systemd services to the packages it's already learned about earlier (that way, those 'packages' or future manifest output can also be associated with service handling in the selected target, to handle restart of the services if/when the configs change)
    • -
    • Aside from known packages already learned, it optimistically tries to capture extra system configuration in /etc that is common for config management. This is stuff like the apt or dnf configuration, crons, logrotate configs, networking settings, hosts files, etc.
    • -
    • For applications that commonly make use of symlinks (think Apache2 or Nginx's sites-enabled or mods-enabled), it notes what symlinks exist so that it can capture those in the generated manifest
    • -
    • It also looks for other snowflake stuff in /etc not associated with packages/services or other typical system config, and will put these into an etc_custom role.
    • -
    • Likewise, it looks in /usr/local for stuff, on the assumption that this is an area that custom apps/configs might've been placed in. These go into a usr_local_custom role.
    • -
    • It captures non-system user accounts, group memberships, and files such as .ssh/authorized_keys. In --dangerous mode it can also capture shell dotfiles such as .bashrc, .profile, .bash_aliases, and .bash_logout when they differ from the skel defaults.
    • -
    • It takes into account anything the user set with --exclude-path or --include-path. For anything extra that is included, it will put these into an 'extra_paths' role. The location could be anywhere e.g something in /opt, /srv, whatever you want.
    • -
    • It writes the state.json and captures the artifacts.
    • +

      enroll harvest

      +

      Harvest writes a bundle containing state.json and captured artifacts. It detects packages, services, changed config, unowned service files, non-system users and SSH public keys, miscellaneous /etc, selected symlinks, Flatpak/Snap state, Docker/Podman image metadata, /usr/local scripts/config, writable sysctls, and fallback iptables/ipset runtime state when persistent files are absent.

      +
      $ enroll harvest --out /tmp/enroll-harvest
      +$ enroll harvest --remote-host host.example.net --remote-user admin --out /tmp/enroll-harvest
      +$ enroll harvest --out /tmp/enroll-harvest --include-path '/home/*/.bashrc' --exclude-path '/usr/local/bin/docker-*'
      +

      Common flags

      +
        +
      • --remote-host, --remote-user, --remote-port, --remote-ssh-config for SSH harvesting.
      • +
      • --ask-become-pass / -K for remote sudo prompts.
      • +
      • --ask-key-passphrase or --ssh-key-passphrase-env ENV_VAR for encrypted SSH private keys.
      • +
      • --no-sudo for a less complete remote harvest without sudo.
      • +
      • --include-path and --exclude-path for plain paths, globs, glob:, re:, or regex: patterns. Excludes win over includes.
      • +
      • --dangerous disables likely-secret filtering.
      • +
      • --sops GPG_FINGERPRINT... writes an encrypted harvest.tar.gz.sops.
      • +
      • --assume-safe-path skips the root unsafe-PATH prompt in trusted automation.
      -
      -

      Other things to be aware of:

      -
        -
      • You can use multiple invocations of --exclude-path to skip the bits you don't want. You can also comment out or delete generated playbooks, classes, states, roles, or modules after enroll manifest if you want to trim the first pass.
      • -
      • In terms of safety measures: it doesn't traverse into symlinks, and it has an 'IgnorePolicy' that makes it ignore most binary files (except GPG binary keys used with apt) - though if you specify certain paths with --include-path and use --dangerous, it will skip some policy statements such as what types of content to ignore.
      • -
      • It will skip files that are too large, and it also currently has a hardcoded cap of the number of files that it will harvest (4000 for /etc, /usr/local/etc and /usr/local/bin, and 500 files per 'role'), to avoid unintentional 'runaway' situations.
      • -
      • If you are using the 'remote' mode to harvest, and your remote user requires a password for sudo, you can pass in --ask-become-pass (or -K) and it will prompt for the password. If you forget, and remote requires password for sudo, it'll still fall back to prompting for a password, but will be a bit slower to do so.
      • -
      • If your SSH private key is encrypted, use --ask-key-passphrase to prompt up-front, or --ssh-key-passphrase-env ENV_VAR for non-interactive/CI runs. If neither is provided and Enroll detects an encrypted key in an interactive session, it will prompt on-demand. (The two key-passphrase flags are mutually exclusive.)
      • +
    + +
    +

    enroll manifest

    +

    Manifest validates a harvest and renders Ansible output. It does not support --target; current Enroll has no Puppet or Salt renderer.

    +
    $ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
    +$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn host.example.net
    +$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --no-common-roles
    +
    Trust warning: Enroll validates structure, but it cannot prove the desired state is safe. Only apply manifests generated from harvests whose provenance you trust.
    +

    Ansible output

    +
      +
    • Single-site output contains playbook.yml, roles/, role files, defaults, templates, and requirements.yml.
    • +
    • Multi-site output uses host-specific inventory under inventory/host_vars/<fqdn>/... and playbooks under playbooks/.
    • +
    • Playbooks tag roles as role_<role_name>, such as role_users or role_services.
    - -
    -
    Does Enroll use community roles/modules?
    -
    When using Ansible and any Docker/Podman images, Flatpak or Snaps are 'harvested' by the system, the manifested ansible playbooks will expect to use some of those community collections, but otherwise, everything is 'core' Ansible. Enroll generates a requirements.yml you can use with ansible-galaxy to install it, if needed, but many community collections may already be on your system, depending on your installation. For Puppet and Salt, no third-party extensions or modules are used. If you see Ansible errors about unsupported podman_image parameters or Flatpak from_url, see the Troubleshooting page.
    -
    - -
    -

    State schema

    -

    Enroll writes a state.json file describing what was harvested. The canonical definition of that file format is the JSON Schema below.

    -

    You can also validate a harvest state file against the schema by using enroll validate /path/to/harvest.

    - - +
    +

    enroll single-shot

    +

    Single-shot runs harvest then manifest in one command. It supports the relevant harvest and manifest flags, including remote SSH, include/exclude paths, --dangerous, --sops, --fqdn, --no-common-roles, and JinjaTurtle flags.

    +
    $ enroll single-shot --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$(hostname -f)"
    +$ enroll single-shot --remote-host host.example.net --remote-user admin --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn host.example.net
    -
    -

    Single-site vs multi-site

    -

    Manifest output has two styles. The exact files differ by target, but the intent is the same.

    - -
    -
    -
    -
    Single-site (default)
    -

    Best when you are enrolling one host, or producing a reusable baseline that you will review and refine.

    -
      -
    • Ansible: self-contained roles and playbook.yml
    • -
    • Puppet: manifests/site.pp plus modules under modules/
    • -
    • Salt: states/top.sls plus states under states/roles/
    • -
    • Common role grouping is enabled unless --no-common-roles is passed
    • -
    -
    -
    -
    -
    -
    Multi-site (--fqdn)
    -

    Best when you want to enroll several existing servers quickly, especially if they differ.

    -
      -
    • Ansible: host-specific state in inventory/host_vars/<fqdn>/
    • -
    • Puppet: host-specific data in Hiera under data/nodes/<fqdn>.yaml
    • -
    • Salt: host-specific data in pillar under pillar/nodes/
    • -
    • Common grouping is disabled so each host's harvested state remains isolated
    • -
    -
    -
    -
    - -
    -
    Multi-site noop examples
    -
    $ fqdn="$(hostname -f)"
    -$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible --fqdn "$fqdn"
    -$ ansible-playbook /tmp/enroll-ansible/playbooks/${fqdn}.yml -i /tmp/enroll-ansible/inventory/hosts.ini -c local --check --diff
    -
    -$ enroll manifest --harvest /tmp/enroll-harvest --target puppet --out /tmp/enroll-puppet --fqdn "$fqdn"
    -$ puppet apply --modulepath /tmp/enroll-puppet/modules --hiera_config /tmp/enroll-puppet/hiera.yaml --certname "$fqdn" /tmp/enroll-puppet/manifests/site.pp --noop
    -
    -$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt --fqdn "$fqdn"
    -$ salt-call --local --id "$fqdn" --file-root /tmp/enroll-salt/states --pillar-root /tmp/enroll-salt/pillar state.apply test=True
    -
    - -
    -
    Tip: targeting subsets
    -
    Ansible playbooks tag each role as role_<name>. Puppet and Salt output can be trimmed by editing generated class/state inclusion data. In every target, treat the first generated manifest as a reviewable bootstrap rather than magic.
    -
    +
    +

    enroll diff

    +

    Diff compares two harvest bundles and reports package, service, user, and managed-file drift. Inputs can be bundle directories, state.json paths, tarballs, or SOPS bundles when --sops is enabled.

    +
    $ enroll diff --old ./baseline --new ./current --format markdown
    +$ enroll diff --old ./baseline --new ./current --ignore-package-versions --exclude-path /var/anacron
    +$ enroll diff --old ./baseline --new ./current --webhook https://example.net/hook --webhook-format json --webhook-header 'Authorization: Bearer ...'
    +

    Use --exit-code to return status 2 when differences exist. Use --notify-always to send webhook/email even when there are no differences.

    -
    -

    Remote harvesting over SSH

    -

    Run Enroll on your workstation, harvest a remote host over SSH. The harvest is pulled locally.

    -
    -
    $ enroll harvest --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-harvest
    -$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible
    -
    -# Alternatively, run both commands combined together with the 'single-shot' mode:
    -
    -$ enroll single-shot --remote-host myhost.example.com --remote-user myuser \
    -  --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible \
    -  --fqdn myhost.example.com
    -
    -# Encrypted SSH key examples:
    -$ enroll harvest --remote-host myhost.example.com --remote-user myuser \
    -  --ask-key-passphrase --out /tmp/enroll-harvest
    -
    -$ export ENROLL_SSH_KEY_PASSPHRASE='correct horse battery staple'
    -$ enroll harvest --remote-host myhost.example.com --remote-user myuser \
    -  --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE --out /tmp/enroll-harvest
    -
    -
    -
    Tip
    -
    If you don't want/need sudo on the remote side, add --no-sudo. However, be aware that you may get a more limited harvest depending on permissions.
    -
    -
    If your remote user requires a password for sudo, pass --ask-become-pass or -K and you'll be prompted to enter the password. If you forget, Enroll will still prompt for the password if it detects it's needed, but will be slightly slower to do so.
    -
    -
    If your SSH private key is encrypted, use --ask-key-passphrase to prompt up-front. For non-interactive/CI runs, use --ssh-key-passphrase-env ENV_VAR. If neither is set and Enroll detects an encrypted key in an interactive session, it'll still prompt on-demand.
    -
    -
    If your remote host requires additional SSH configuration that you've defined in your ~/.ssh/config, pass --remote-ssh-config ~/.ssh/config. Enroll will understand how to translate the Host alias, IdentityFile, ProxyCommand, ConnectTimeout and AddressFamily values. You must still pass a value for --remote-host that matches the Host value of the entry in the SSH config file.
    -
    +
    +

    enroll explain

    +

    Explain produces a human or JSON summary of a harvest: package counts, roles, included reasons, excluded reasons, and example paths.

    +
    $ enroll explain ./harvest
    +$ enroll explain ./harvest --format json --max-examples 25
    +$ enroll explain ./harvest.tar.gz.sops --sops
    -
    +
    +

    enroll validate

    +

    Validate checks that state.json exists, is valid JSON, matches the vendored JSON Schema unless skipped, and references artifacts that actually exist. It also reports unreferenced files in artifacts/.

    +
    $ enroll validate ./harvest
    +$ enroll validate ./harvest --format json --out validate.json
    +$ enroll validate ./harvest --fail-on-warnings
    +$ enroll validate ./harvest --schema ./state.schema.json
    +$ enroll validate ./harvest --schema https://enroll.sh/schema/state.schema.json --allow-remote-schema
    +

    Remote schemas are not fetched unless --allow-remote-schema is explicitly supplied. Use --no-schema to skip schema checks while keeping consistency checks.

    +
    + +
    +

    Sensitive data

    +

    By default, Enroll tries not to harvest likely secrets. It denies known sensitive paths, private key material, common certificate/private-key locations, credential-looking assignments, credential-bearing URIs, authorization headers, service account key names, and other obvious sensitive values.

    +
    --dangerous disables those checks. It may copy private keys, API tokens, TLS key material, database passwords, and other secrets into the harvest output in plaintext. Use it only intentionally, and strongly consider --sops.
    +

    Value-less comment mentions such as # token are tolerated so stock config files do not become impossible to harvest. A commented-out populated credential assignment is still treated as sensitive.

    +
    + +

    JinjaTurtle integration

    -

    If JinjaTurtle (one of my other projects) is installed, Enroll can also produce Jinja2 templates for ini/json/xml/toml-style config files (as well as SSH configs), and extract variables cleanly into Ansible and Salt, and even erb files for Puppet (as of version 0.5.5).

    -

    When Enroll cannot turn a config file into a template, it copies the raw file instead.

    -
    -
    -
    -
    Modes
    -
      -
    • --jinjaturtle to force on
    • -
    • --no-jinjaturtle to force off
    • -
    • Default is auto
    • -
    -
    -
    -
    -
    -
    Where variables land
    -
      -
    • Ansible single-site: roles/<role>/defaults/main.yml
    • -
    • Ansible multi-site (--fqdn): inventory/host_vars/<fqdn>/<role>.yml
    • -
    • Puppet single-site: modules/<role>/manifests/init.pp as generated class parameter defaults
    • -
    • Puppet multi-site: data/nodes/<fqdn>.yaml as Hiera class parameter data
    • -
    • Salt single-site: inline in states/roles/<role>/init.sls as file.managed context: data
    • -
    • Salt multi-site: pillar/nodes/<fqdn>.sls under enroll:roles:<role>:files:<path>:context
    • -
    -
    -
    -
    +

    If the jinjaturtle executable is on PATH, Enroll can turn supported config files into Ansible templates. The default mode uses JinjaTurtle when available. --jinjaturtle makes it required; --no-jinjaturtle disables it.

    +
    $ enroll manifest --harvest ./harvest --out ./ansible --jinjaturtle
    +$ enroll manifest --harvest ./harvest --out ./ansible --no-jinjaturtle
    +

    Supported suffixes include INI, CFG, JSON, TOML, YAML, XML, repo files, and common systemd unit file suffixes. Templates live in role templates/; variables live in role defaults or host vars depending on output mode.

    -
    -

    INI config file

    -

    If you're repeating flags (include/exclude patterns, SOPS settings, etc.), store defaults in enroll.ini and keep your muscle memory intact.

    - -
    -
    Discovery order
    -
    You can pass -c/--config, set ENROLL_CONFIG, or let Enroll auto-discover ./enroll.ini, ./.enroll.ini, or ~/.config/enroll/enroll.ini.
    -
    - -
    - -
    [enroll]
    -# (future global flags may live here)
    +        
    +

    Configuration file

    +

    Enroll can load INI-style defaults from --config, ENROLL_CONFIG, $XDG_CONFIG_HOME/enroll/enroll.ini, or ~/.config/enroll/enroll.ini. Current-directory config files are not auto-loaded; pass them explicitly.

    +
    [enroll]
    +assume_safe_path = false
     
     [harvest]
     dangerous = false
    @@ -310,326 +184,36 @@ include_path =
       /home/*/.bashrc
       /home/*/.profile
     exclude_path = /usr/local/bin/docker-*, /usr/local/bin/some-tool
    -# remote_host = yourserver.example.com
    -# remote_user = you
    -# remote_port = 2222
    +# remote_host = host.example.net
    +# remote_user = admin
     
     [manifest]
     no_jinjaturtle = true
    -sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
    -target = ansible # is the default, otherwise salt, or puppet
    +# sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
     
     [diff]
    -# ignore noisy drift
     exclude_path = /var/anacron
     ignore_package_versions = true
    -# enforce = true
    -# target = ansible # to run ansible-playbook to enforce the old harvest, else puppet, salt
    -
    -
    -
    Note
    -
    In INI sections, option names use underscores (e.g. include_path) even when the CLI flag uses hyphens (e.g. --include-path).
    -
    +[single-shot] +include_path = re:^/home/[^/]+/\.config/myapp/.*$
    +

    Precedence is explicit CLI flags, then INI config, then argparse defaults. For hyphenated flags, use underscores in the INI file.

    +
    +

    Run generated Ansible

    +

    Install generated collection requirements first, then run in check mode before applying for real.

    +
    $ cd /tmp/enroll-ansible
    +$ ansible-galaxy collection install -r requirements.yml
    +$ ansible-playbook -i "localhost," -c local playbook.yml --check
    +$ ansible-playbook -i "localhost," -c local playbook.yml
     
    -        
    -

    Drift detection with enroll diff

    -

    One of the things I miss from my Puppet days, was the way the Puppet 'agent' would check in with the server and realign itself to the declared desired state. With any configuration-management system, it is easy for systems to fall out of date if someone changes things on-the-fly instead of via the desired-state workflow.

    -

    The purpose of enroll diff is to compare two 'harvests' and detect what has changed - be it adding/removing of programs, change to systemd unit state, modifications, addition or removal of files, and so on.

    - -
    -
    Notifications for diff
    -
    The enroll diff feature supports sending the difference to a webhook of your choosing, or by e-mail. The payload can be sent in json, plain text, or markdown.
    -
    - -
    -
    Noise suppression
    -
    Use --exclude-path to ignore file/dir drift under specific paths (e.g. /var/anacron). Use --ignore-package-versions to ignore routine package upgrades/downgrades while still reporting added/removed packages.
    -
    - -
    -
    $ enroll diff \
    ---old /path/to/harvestA \
    ---new /path/to/harvestB \
    ---exclude-path /var/spool/anacron \
    ---ignore-package-versions
    -
    - -
    -
    Optional: enforce the old harvest state (--enforce)
    -
    If drift exists and your desired config manager is on $PATH, Enroll can generate a manifest from the old harvest and apply it locally to restore expected state, if you pass the --enforce argument to enroll diff. By default, it will try to use Ansible for that, but you can pass --target [puppet|sops] just like with enroll manifest.
    -
    - -
    -
    $ enroll diff \
    ---old /path/to/harvestA \
    ---new /path/to/harvestB \
    ---enforce
    -
    -
    - -

    How to run enroll diff automatically on a timer

    -

    A great way to use enroll diff is to run it periodically (e.g via cron or a systemd timer). Below is an example.

    -

    Store the below file at /usr/local/bin/enroll-harvest-diff.sh and make it executable.

    -
    - -
    #!/usr/bin/env bash
    -set -euo pipefail
    -
    -# Required env
    -: "${WEBHOOK_URL:?Set WEBHOOK_URL in /etc/enroll/enroll-harvest-diff}"
    -: "${ENROLL_SECRET:?Set ENROLL_SECRET in /etc/enroll/enroll-harvest-diff}"
    -
    -# Optional env
    -STATE_DIR="${ENROLL_STATE_DIR:-/var/lib/enroll}"
    -GOLDEN_DIR="${STATE_DIR}/golden"
    -PROMOTE_NEW="${PROMOTE_NEW:-1}"          # 1=promote new->golden; 0=keep golden fixed
    -KEEP_BACKUPS="${KEEP_BACKUPS:-7}"        # only used if PROMOTE_NEW=1
    -LOCKFILE="${STATE_DIR}/.enroll-harvest-diff.lock"
    -
    -mkdir -p "${STATE_DIR}"
    -chmod 700 "${STATE_DIR}" || true
    -
    -# single-instance lock (avoid overlapping timer runs)
    -exec 9>"${LOCKFILE}"
    -flock -n 9 || exit 0
    -
    -tmp_new=""
    -cleanup() {
    -  if [[ -n "${tmp_new}" && -d "${tmp_new}" ]]; then
    -    rm -rf "${tmp_new}"
    -  fi
    -}
    -trap cleanup EXIT
    -
    -make_tmp_dir() {
    -  mktemp -d "${STATE_DIR}/.harvest.XXXXXX"
    -}
    -
    -run_harvest() {
    -  local out_dir="$1"
    -  rm -rf "${out_dir}"
    -  mkdir -p "${out_dir}"
    -  chmod 700 "${out_dir}" || true
    -  enroll harvest --out "${out_dir}" >/dev/null
    -}
    -
    -# A) create golden if missing
    -if [[ ! -f "${GOLDEN_DIR}/state.json" ]]; then
    -  tmp="$(make_tmp_dir)"
    -  run_harvest "${tmp}"
    -  rm -rf "${GOLDEN_DIR}"
    -  mv "${tmp}" "${GOLDEN_DIR}"
    -  echo "Golden harvest created at ${GOLDEN_DIR}"
    -  exit 0
    -fi
    -
    -# B) create new harvest
    -tmp_new="$(make_tmp_dir)"
    -run_harvest "${tmp_new}"
    -
    -# C) diff + webhook notify
    -enroll diff \
    -  --old "${GOLDEN_DIR}" \
    -  --new "${tmp_new}" \
    -  --webhook "${WEBHOOK_URL}" \
    -  --webhook-format json \
    -  --webhook-header "X-Enroll-Secret: ${ENROLL_SECRET}" # You can send multiple --webhook-header params as you need
    -
    -# Promote or discard new harvest
    -if [[ "${PROMOTE_NEW}" == "1" || "${PROMOTE_NEW,,}" == "true" || "${PROMOTE_NEW}" == "yes" ]]; then
    -  ts="$(date -u +%Y%m%d-%H%M%S)"
    -  backup="${STATE_DIR}/golden.prev.${ts}"
    -  mv "${GOLDEN_DIR}" "${backup}"
    -  mv "${tmp_new}" "${GOLDEN_DIR}"
    -  tmp_new=""  # don't delete it in trap
    -
    -  # Keep only latest N backups
    -  if [[ "${KEEP_BACKUPS}" =~ ^[0-9]+$ ]] && (( KEEP_BACKUPS > 0 )); then
    -    ls -1dt "${STATE_DIR}"/golden.prev.* 2>/dev/null | tail -n +"$((KEEP_BACKUPS+1))" | xargs -r rm -rf
    -  fi
    -
    -  echo "Diff complete; baseline updated."
    -else
    -  # tmp_new will be deleted by trap
    -  echo "Diff complete; baseline unchanged (PROMOTE_NEW=${PROMOTE_NEW})."
    -fi
    -
    -
    -
    -

    Save these environment variables in /etc/enroll/enroll-harvest-diff

    -
    - -
    
    -# Where to store golden + temp harvests
    -ENROLL_STATE_DIR=/var/lib/enroll
    -
    -# 1 = each run becomes new baseline ("since last harvest")
    -# 0 = compare against a fixed baseline ("since golden")
    -PROMOTE_NEW=1
    -
    -# If PROMOTE_NEW=1, keep this many old baselines
    -KEEP_BACKUPS=7
    -
    -WEBHOOK_URL=https://example.com/webhook/xxxxxxxx
    -ENROLL_SECRET=xxxxxxxxxxxxxxxxxxxx
    -
    -
    - -
    -
    -
    Webhook headers
    -
    The --webhook-header parameter can be used multiple times. You can, for example, send X-Enroll-Secret and a secret value of your choice, to help secure your webhook endpoint.
    -
    - -

    Save this systemd unit file to /etc/systemd/system/enroll-harvest-diff.service

    -
    - -
    
    -[Unit]
    -Description=Enroll harvest + diff + webhook notify
    -Wants=network-online.target
    -After=network-online.target
    -ConditionPathExists=/etc/enroll/enroll-harvest-diff
    -
    -[Service]
    -Type=oneshot
    -EnvironmentFile=/etc/enroll/enroll-harvest-diff
    -Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    -UMask=0077
    -
    -# Create /var/lib/enroll automatically
    -StateDirectory=enroll
    -
    -ExecStart=/usr/local/bin/enroll-harvest-diff.sh
    -
    -
    - -
    -

    Save this systemd timer to /etc/systemd/system/enroll-harvest-diff.timer

    -
    - -
    
    -[Unit]
    -Description=Run Enroll harvest diff hourly
    -
    -[Timer]
    -OnCalendar=hourly
    -RandomizedDelaySec=10m
    -Persistent=true
    -
    -[Install]
    -WantedBy=timers.target
    -
    -
    - -
    -

    Now you can enable and test it!

    -
    - -
    
    -sudo systemctl daemon-reload
    -sudo systemctl enable --now enroll-harvest-diff.timer
    -
    -# run once now
    -sudo systemctl start enroll-harvest-diff.service
    -# watch it in the logs
    -sudo journalctl -u enroll-harvest-diff.service -n 200 --no-pager
    -
    -
    - -
    -
    -
    Need help with writing webhooks?
    -
    I use Node-RED. Here's a sample Node-RED flow that might help run your webhook, pre-configured to parse the enroll diff JSON payload!
    -
    +# multi-site +$ ansible-playbook playbooks/host.example.net.yml --check +# targeted role tags +$ ansible-playbook -i "localhost," -c local playbook.yml --tags role_users,role_services
    - - -
    -

    Why did Enroll include/exclude something? enroll explain

    -

    When you run enroll harvest, Enroll records why it chose to include or exclude each path in state.json. The enroll explain subcommand summarizes that data so you can quickly sanity-check a harvest, tune include/exclude rules, and understand where packages/services came from.

    - -
    -
    What can it read?
    -
    enroll explain accepts a harvest bundle directory, a direct path to state.json, a .tar.gz/.tgz bundle, or an encrypted .tar.gz.sops bundle.
    -
    - -
    - -
    $ enroll explain /tmp/enroll-harvest
    -
    -# or point at the state.json path directly
    -$ enroll explain /tmp/enroll-harvest/state.json
    -
    - -
    -

    The default output is human-readable text. For scripting or deeper inspection, use JSON output:

    -
    - -
    $ enroll explain /tmp/enroll-harvest --format json | jq .
    -
    -# show more example paths per reason
    -$ enroll explain /tmp/enroll-harvest --max-examples 10
    -
    - -
    -

    If you stored a harvest as a single SOPS-encrypted bundle, enroll explain can decrypt it on the fly (it will also auto-detect files ending with .sops):

    -
    - -
    $ enroll explain /var/lib/enroll/harvest.tar.gz.sops --sops
    -
    - -
    -

    What you get back:

    -
      -
    • A summary of what roles were collected (users, services, package snapshots, etc_custom, usr_local_custom, etc.).
    • -
    • Why packages ended up in inventory (observed_via), e.g. user-installed vs referenced by a harvested systemd unit.
    • -
    • Breakdowns of managed_files.reason, managed_dirs.reason, and excluded.reason, with a few example paths for each reason.
    • -
    - -
    -
    Tip
    -
    Use enroll explain after a first harvest to decide what to exclude (noise) and what to include (snowflake app/config under /opt, /srv, etc.) before you generate a manifest.
    -
    -
    Security note: enroll explain doesn't print file contents, but it can print path names and unit/package names. Treat the output as sensitive if your environment uses revealing path conventions (and especially if you harvested with --dangerous).
    -
    -
    - - -
    -

    Tips

    -
    -
    -
    -
    Start safe
    -

    Default harvesting tries to avoid likely secrets via path rules, content sniffing, and size caps. Use --dangerous only when you've planned where the output will live.

    -
    -
    -
    -
    -
    Encrypt at rest
    -

    If you plan to keep harvests/manifests long term (especially in git), use --sops to produce a single encrypted bundle file. Note: enroll diff can be passed --sops to decrypt and compare two harvests on-the-fly!

    -
    -
    -
    -
    -
    Multi-host safety
    -

    For fleets, prefer multi-site output so target-native host data controls what is applied per host - reducing "shared role/module/state breaks another host" surprises.

    -
    -
    -
    -
    -
    Keep it reproducible
    -

    Commit the manifest output, run it in CI, and use enroll diff as a drift alarm (webhook/email).

    -
    -
    -
    -
    -
    diff --git a/src/content/examples.html b/src/content/examples.html index 58e5ca5..123042d 100644 --- a/src/content/examples.html +++ b/src/content/examples.html @@ -1,174 +1,165 @@ --- title: "Examples" -html_title: "Enroll Examples" -description: "Copy/paste recipes for Enroll: Ansible, Puppet, Salt, fleets, drift detection, and safe storage." +html_title: "Enroll examples - Ansible harvest and manifest recipes" +description: "Copy/paste recipes for current Enroll: Ansible output, remote harvests, drift detection, SOPS, and validation." +og_title: "Enroll examples" +og_description: "Practical Enroll recipes for local and remote harvests, Ansible manifesting, SOPS bundles, drift reports, and validation." +og_type: "article" --- -
    -
    +
    +
    Examples
    -

    Copy/paste recipes

    -

    Practical flows you can adapt to Ansible, Puppet, Salt, or all three.

    +

    Copy/paste Enroll recipes

    +

    Current examples for Ansible output, remote harvests, safe storage, drift reports, and troubleshooting-friendly validation.

    -
    -
    -
    Harvest once, try each renderer locally
    -
    - -
    $ enroll harvest --out /tmp/enroll-harvest
    -
    -# Ansible noop
    -$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible
    -$ ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml --diff --check
    -
    -# Puppet noop
    -$ enroll manifest --harvest /tmp/enroll-harvest --target puppet --out /tmp/enroll-puppet
    -$ puppet apply --modulepath /tmp/enroll-puppet/modules /tmp/enroll-puppet/manifests/site.pp --noop
    -
    -# Salt noop
    -$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt
    -$ salt-call --local --file-root /tmp/enroll-salt/states state.apply test=True
    -
    -

    Great for “make this box reproducible”, testing renderer output, or building a golden baseline you will refine.

    -
    +
    +

    Local safe-mode baseline

    +

    Harvest the current host, validate the bundle, generate Ansible, install requirements, and run a check-mode playbook.

    +
    $ enroll harvest --out /tmp/enroll-harvest
    +$ enroll validate /tmp/enroll-harvest
    +$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
    +$ cd /tmp/enroll-ansible
    +$ ansible-galaxy collection install -r requirements.yml
    +$ ansible-playbook -i "localhost," -c local playbook.yml --check
    +
    -
    -
    Enroll a remote host over SSH
    -
    - -
    $ enroll harvest \
    -  --remote-host myhost.example.com \
    -  --remote-user myuser \
    -  --out /tmp/enroll-harvest
    +        
    +

    Remote host through SSH config

    +

    Use an OpenSSH config alias while still passing --remote-host. The harvested bundle lands locally.

    +
    $ enroll harvest \
    +  --remote-host production-web-1 \
    +  --remote-ssh-config ~/.ssh/config \
    +  --out /tmp/enroll-web-1
    +$ enroll manifest --harvest /tmp/enroll-web-1 --out /tmp/enroll-web-1-ansible --fqdn web-1.example.net
    +
    +
    -$ enroll manifest \ +
    +
    +

    Encrypted SSH key in CI

    +

    Provide a key passphrase through an environment variable for non-interactive remote harvesting.

    +
    $ export ENROLL_SSH_KEY_PASSPHRASE='correct horse battery staple'
    +$ enroll single-shot \
    +  --remote-host host.example.net \
    +  --remote-user admin \
    +  --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE \
       --harvest /tmp/enroll-harvest \
    -  --target puppet \
    -  --out /tmp/enroll-puppet
    -
    -

    The bundle lands locally. Change --target puppet to ansible or salt without re-harvesting. For remote sudo prompts use --ask-become-pass/-K; use --no-sudo for a less complete non-root harvest.

    -
    + --out /tmp/enroll-ansible \ + --fqdn host.example.net
    +
    -
    -
    Fleets: --fqdn output
    -
    - -
    $ fqdn="$(hostname -f)"
    -$ enroll harvest --out /tmp/enroll-harvest
    -
    -# Ansible: inventory/host_vars and a per-host playbook
    -$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible --fqdn "$fqdn"
    -$ ansible-playbook /tmp/enroll-ansible/playbooks/${fqdn}.yml -i /tmp/enroll-ansible/inventory/hosts.ini -c local --check --diff
    -
    -# Puppet: Hiera data keyed by certname
    -$ enroll manifest --harvest /tmp/enroll-harvest --target puppet --out /tmp/enroll-puppet --fqdn "$fqdn"
    -$ puppet apply --modulepath /tmp/enroll-puppet/modules --hiera_config /tmp/enroll-puppet/hiera.yaml --certname "$fqdn" /tmp/enroll-puppet/manifests/site.pp --noop
    -
    -# Salt: pillar data keyed by minion id
    -$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt --fqdn "$fqdn"
    -$ salt-call --local --id "$fqdn" --file-root /tmp/enroll-salt/states --pillar-root /tmp/enroll-salt/pillar state.apply test=True
    -
    -

    --fqdn disables common grouping and moves host-specific state into the target’s native host-data layer: inventory, Hiera, or pillar.

    -
    +
    +

    Include and exclude paths

    +

    Add targeted files outside the standard scan paths while keeping safe-mode secret checks enabled.

    +
    $ enroll harvest --out /tmp/enroll-harvest \
    +  --include-path '/home/*/.bashrc' \
    +  --include-path 're:^/home/[^/]+/\.config/myapp/.*$' \
    +  --exclude-path '/home/*/.config/myapp/cache/**'
    +
    -
    -
    Control role/module/state grouping
    -
    - -
    # Default single-site mode: combine packages/services by package Section where possible
    -$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible
    -
    -# Keep output split more literally by discovered package/service role
    -$ enroll manifest --harvest /tmp/enroll-harvest --target ansible --out /tmp/enroll-ansible-raw --no-common-roles
    -
    -# --fqdn also disables common grouping because host data must remain isolated
    -$ enroll manifest --harvest /tmp/enroll-harvest --target salt --out /tmp/enroll-salt --fqdn "$(hostname -f)"
    -
    -

    Grouping keeps first-pass output manageable. Use --no-common-roles when you prefer more direct one-role-per-package/service output.

    -
    +
    +

    Dangerous harvest, encrypted at rest

    +

    When you deliberately need aggressive capture, encrypt the harvest bundle immediately.

    +
    $ enroll harvest \
    +  --out /srv/enroll/host-1 \
    +  --dangerous \
    +  --sops 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
    +

    This writes an encrypted harvest.tar.gz.sops rather than a plaintext bundle directory.

    +
    -
    -
    Drift detection with enroll diff
    -
    - -
    $ enroll diff --old /path/to/harvestA --new /path/to/harvestB \
    -  --format markdown \
    -  --exclude-path /var/anacron \
    -  --ignore-package-versions
    +        
    +

    Encrypted manifest bundle

    +

    Generate Ansible into a temporary directory, tar it, and encrypt it as one SOPS file.

    +
    $ enroll manifest \
    +  --harvest /srv/enroll/host-1/harvest.tar.gz.sops \
    +  --out /srv/enroll/host-1-manifest \
    +  --sops 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
     
    -$ enroll diff --old /path/to/golden --new /path/to/current \
    -  --webhook https://example.net/webhook \
    -  --webhook-format json \
    -  --webhook-header 'X-Enroll-Secret: ...' \
    +$ cd /srv/enroll/host-1-manifest
    +$ sops -d manifest.tar.gz.sops | tar -xzvf -
    +$ cd manifest
    +
    +
    + +
    +
    +

    Multi-site output

    +

    Use --fqdn when enrolling multiple already-running servers. Host-specific data goes to inventory host vars.

    +
    $ fqdn="$(hostname -f)"
    +$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$fqdn"
    +$ ansible-playbook /tmp/enroll-ansible/playbooks/"$fqdn".yml --check
    +
    +
    + +
    +
    +

    Targeted Ansible role runs

    +

    Generated playbooks tag roles as role_<name>, so you can run only a subset.

    +
    $ ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml --tags role_users,role_services
    +
    +
    + +
    +
    +

    Drift report for cron or CI

    +

    Ignore routine version churn while still catching package additions/removals, service changes, user changes, and file drift.

    +
    $ enroll diff \
    +  --old /srv/enroll/baseline \
    +  --new /srv/enroll/current \
    +  --format json \
       --ignore-package-versions \
    -  --exit-code
    -
    -

    Use it in cron, systemd timers, or CI to alert on change.

    -
    + --exclude-path /var/anacron \ + --exit-code
    +
    -
    -
    Explain a harvest with enroll explain
    -
    - -
    $ enroll explain /tmp/enroll-harvest
    -
    -# machine-readable reasons, examples, and inventory breakdown
    -$ enroll explain /tmp/enroll-harvest --format json | jq .
    -
    -# encrypted bundle
    -$ enroll explain /var/lib/enroll/harvest.tar.gz.sops --sops
    -
    -

    Great for answering “why did it include/exclude that file?” before you generate a manifest.

    -
    +
    +

    Webhook notification

    +

    Send the diff report when changes are detected. Use --notify-always when a heartbeat is useful.

    +
    $ enroll diff \
    +  --old /srv/enroll/baseline \
    +  --new /srv/enroll/current \
    +  --webhook https://example.net/hooks/enroll \
    +  --webhook-format json \
    +  --webhook-header 'X-Enroll-Secret: xxxx'
    +
    -
    -
    Enforce the previous state with enroll diff --enforce
    -
    - -
    $ enroll diff \
    -  --old /path/to/harvestA \
    -  --new /path/to/harvestB \
    -  --enforce \
    -  --format json
    -
    -

    Enforcement currently uses generated Ansible and ansible-playbook on PATH, even if you also use Puppet or Salt for normal manifest output.

    -
    +
    +

    Explain a harvest

    +

    Use explain when you want to understand why Enroll included or skipped things.

    +
    $ enroll explain /tmp/enroll-harvest
    +$ enroll explain /tmp/enroll-harvest --format json --max-examples 25
    +
    -
    -
    Encrypt bundles at rest with SOPS
    -
    - -
    $ enroll harvest --dangerous --out /tmp/harvest \
    -  --sops <FINGERPRINT>
    -
    -$ enroll manifest --harvest /tmp/harvest/harvest.tar.gz.sops \
    -  --target salt \
    -  --out /tmp/enroll-salt \
    -  --sops <FINGERPRINT>
    -
    -

    Use this if you intend to store harvests/manifests long term or if you harvested with --dangerous.

    -
    +
    +

    Validate with a pinned schema

    +

    CI can validate a harvested bundle against a local schema file and fail on warnings.

    +
    $ enroll validate /srv/enroll/current \
    +  --schema ./state.schema.json \
    +  --format json \
    +  --out validate.json \
    +  --fail-on-warnings
    +
    - diff --git a/src/content/news/0-7-0.html b/src/content/news/0-7-0.html index 9fbb66f..2f9ea69 100644 --- a/src/content/news/0-7-0.html +++ b/src/content/news/0-7-0.html @@ -1,172 +1,61 @@ --- -title: "Enroll 0.7.0: Puppet and Salt renderer support, Flatpak/Snap, and more" -html_title: "Enroll 0.7.0 News" -description: "Enroll 0.7.0 adds first-class Puppet and Salt manifest rendering alongside Ansible, along with other improvements." -date: 2026-06-17 -summary: "Enroll can now render Puppet and Salt manifests alongside Ansible, detect Flatpak and Snap packages, other obscure runtime state, and produce better organised roles/modules." +title: "Enroll 0.7.0 beta: Ansible-only again, with stronger safety tooling" +date: "2026-06-30" +description: "Current Enroll 0.7.0 beta notes: the Puppet and Salt experiment has been removed; Enroll now documents Ansible-only output and stronger safety tooling." +summary: "The current beta focuses Enroll back on Ansible-only output, removes stale Puppet/Salt documentation, and clarifies safety and provenance guidance." +html_title: "Enroll 0.7.0 beta - Ansible-only current documentation" +og_title: "Enroll 0.7.0 beta: Ansible-only current documentation" +og_description: "The current Enroll beta removes the old Puppet/Salt experiment from the docs and focuses on Ansible output, validation, SOPS, and safety." +og_type: "article" --- -
    -
    -
    Release notes
    -

    Enroll 0.7.0: Puppet and Salt join Ansible

    -

    A single harvest can now be rendered into Ansible, Puppet, or Salt output.

    +
    +
    +
    News
    +

    Enroll 0.7.0 beta: Ansible-only again, with stronger safety tooling

    +

    The current beta focuses Enroll back on one supported output path: harvest Linux hosts and render Ansible.

    -
    -
    -
    -

    Welcome to the first of Enroll's new, erm, news section! To celebrate, Enroll 0.7.0 has been released, and makes manifest rendering target-selectable based on your preferred config management tool! Ansible remains the default, but Puppet and Salt are now possible too.

    +
    +

    Earlier 0.7.0 development builds experimented with Puppet and Salt renderers. That experiment has been removed from the current codebase. The supported flow today is simpler and more honest: harvest a Linux host, validate the bundle, and generate Ansible roles and playbooks.

    -

    Highlights in 0.7.0

    -
      -
    • Puppet support! --target puppet renders Puppet module/control-repo style output., and in --fqdn mode, renders per-host Hiera data.
    • -
    • Salt Stack support! --target salt renders Salt state trees and, in --fqdn mode, Salt pillar data.
    • -
    • Single-site output tries to combine package/service data by their package manager's Section category, to reduce role sprawl and speed up execution.
    • -
    • Flatpak and Snap detection!
    • -
    • Docker image detection!
    • -
    +
    Documentation note: if you find an old command using --target ansible, --target puppet, or --target salt, it is stale. Current enroll manifest has no --target option.
    -

    Dry-run examples: choose your own config management adventure!

    -

    Feels too good to be true, but it isn't:

    -
    - -
    $ enroll harvest --out ./harvest
    +      

    What remains

    +
      +
    • Harvest bundles with state.json and captured artifacts.
    • +
    • Ansible manifesting in single-site mode or --fqdn multi-site mode.
    • +
    • Role grouping by package section/group where possible, with --no-common-roles available when you prefer one generated role per package or unit.
    • +
    • Remote harvesting over SSH, including OpenSSH config support, sudo prompts, and encrypted key passphrase handling.
    • +
    • Diff, explain, and validate workflows for drift reporting and bundle inspection.
    • +
    • SOPS at-rest encryption for harvest and manifest bundles.
    • +
    • JinjaTurtle integration for supported Ansible templates when the executable is available.
    • +
    -$ enroll manifest --harvest ./harvest --target ansible --out ./ansible -$ ansible-playbook -i "localhost," -c local ./ansible/playbook.yml --check --diff +

    A current workflow

    +
    $ enroll harvest --out ./harvest
    +$ enroll validate ./harvest
    +$ enroll manifest --harvest ./harvest --out ./ansible
    +$ cd ./ansible
    +$ ansible-galaxy collection install -r requirements.yml
    +$ ansible-playbook -i "localhost," -c local playbook.yml --check
    -$ enroll manifest --harvest ./harvest --target puppet --out ./puppet -$ puppet apply --modulepath ./puppet/modules ./puppet/manifests/site.pp --noop +

    Security and provenance changes

    +

    The current documentation is also more explicit about trust boundaries. Enroll validates structure and tries to avoid accidental secret capture, symlink traversal, unsafe artifact paths, unsafe root output paths, and root PATH foot-guns. It still cannot prove that a harvested desired state is semantically safe to apply.

    +
    This harvest is structurally valid, but Enroll cannot prove it is semantically safe. Only apply manifests generated from harvests whose provenance you trust.
    -$ enroll manifest --harvest ./harvest --target salt --out ./salt -$ salt-call --local --file-root ./salt/states state.apply test=True
    -
    +

    Other notable current behavior

    +
      +
    • Safe-mode content scanning tolerates value-less credential words in comments, but populated credential assignments are still treated as sensitive even if commented out.
    • +
    • Automatic per-user shell dotfile harvesting is only enabled in --dangerous mode.
    • +
    • Plain directory harvests are frozen into a private temp tree before manifesting, reducing TOCTOU exposure between validation and rendering.
    • +
    • Remote harvests reject unknown SSH host keys by default.
    • +
    • Root runs refuse unsafe non-interactive PATH setups unless --assume-safe-path is supplied.
    • +
    -
    - -

    I recommend using Salt 3008.1 - on Debian 13 I encountered annoying noisy Python errors with version 3007, which are unrelated to Enroll.

    - -

    Since Salt uses Jinja2 templates, it will take advantage of my other tool JinjaTurtle, if it's on your PATH, just like Ansible does.

    - -

    And because I didn't want Puppet users to feel left out, version 0.5.5 of JinjaTurtle (despite its name) also supports erb templates as well. This means Puppet templates will be generated if you have JinjaTurtle and there are viable config files for it, too!

    - -

    Have you used enroll diff --enforce to enforce reverting a system to an older harvest state via Ansible? Well, that works too for Puppet or Salt - just add --target [salt|puppet] to that command.

    - -

    New grouping behaviour in roles/modules

    -

    Did you find the number of manifested roles overwhelming?

    -

    Previously, Enroll created an Ansible role (or, now, a Puppet module or Salt role) for pretty much every 'package' it found. In some cases this could result in hundreds of roles.

    -

    Technically fine, but overwhelming to look at! It also made the playbooks a bit slow to run. If you have fewer roles for the same harvest, Ansible gets faster.

    - -

    As of 0.7.0, Enroll groups related package and service snapshots by the package manager's Section category (or comparable backend metadata).

    -

    For example, network-related packages and config files might end up in a role called net. Meanwhile, vim, nano might both appear in editors, and mutt and Thunderbird may be in mail. It's easier on the eye, and it's quicker to run!

    - -

    If you don't like this new layout, you can pass --no-common-roles to enforce the previous behaviour. Also, if you use --fqdn for host-specific data-driven output, 'common' roles are disabled automatically, to avoid 'bleed in' of unnecessary packages on other hosts that might share that role later.

    - -

    Flatpak and Snap detection

    -

    Enroll now attempts to detect Flatpak and Snaps present on the system. For Flatpaks, this includes user-specific Flatpaks as well as system-wide ones. Manifesting in Ansible will attempt to use the community.general collection to create Flatpak and Snap tasks to enforce the presence of those packages.

    -

    Flatpak/Snap manifesting is also available for Puppet and Snap, but it's slightly cruder through the use of guarded cmd/exec statements - I found this keeps things simpler than having to add third party modules/extensions (and the state of extensions in Salt Stack right now, is a bit of a mess, IMO).

    - -
    -
    $ sudo ansible-playbook playbook.yml -i localhost, -c local --tags role_snap --diff
    -
    -PLAY [Apply all roles on all hosts] *******************************************************************************************************************************************************************************
    -
    -TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
    -ok: [localhost]
    -
    -TASK [snap : Install system-wide snaps with full detected attributes] *********************************************************************************************************************************************
    -ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'bare', 'notes': ['base'], 'revision': 5, 'source': 'snap-list'})
    -ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'core24', 'notes': ['base'], 'revision': 1643, 'source': 'snap-list'})
    -ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'gnome-46-2404', 'notes': [], 'revision': 153, 'source': 'snap-list'})
    -ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'gtk-common-themes', 'notes': [], 'revision': 1535, 'source': 'snap-list'})
    -ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'mesa-2404', 'notes': [], 'revision': 1165, 'source': 'snap-list'})
    -ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'onionshare', 'notes': [], 'revision': 212, 'source': 'snap-list'})
    -ok: [localhost] => (item={'channel': 'latest/stable', 'classic': False, 'dangerous': False, 'devmode': False, 'install_revision': False, 'name': 'snapd', 'notes': ['snapd'], 'revision': 26865, 'source': 'snap-list'})
    -            
    -
    - -

    Docker/Podman image detection

    -

    The harvest now detects the presence of container images, if the user has permission to call Docker or Podman. In particular, it detects the SHA256 of the image instead of relying on floating tags.

    -

    All three renderers (Ansible, Salt and Puppet) will attempt to enforce the presence of those Docker images per their precise SHA256 hash, if they were present in the harvest but not on the machine upon applying a manifest.

    -

    For Ansible, you may need the community.docker collection, but on Debian 13 I found that it was already present by default in the official ansible Debian packages.

    -

    For Ansible, if using Podman, you'll need 1.20.0 or later of the community.podman collection. Enroll creates a requirements.yml to make it easy for you: its README.md will guide you to run ansible-galaxy collection install -r requirements.yml before running the playbook.

    - -
    -
    $ ansible-playbook -i localhost, -c local playbook.yml --check --diff --tags role_container_images
    -PLAY [Apply all roles on all hosts] *******************************************************************************************************************************************************************************
    -
    -TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
    -ok: [localhost]
    -
    -TASK [container_images : Pull Docker images by immutable registry digest] *****************************************************************************************************************************************
    -ok: [localhost] => (item={'architecture': 'amd64', 'created': '2026-06-17T22:44:41.625799723Z', 'engine': 'docker', 'home': None, 'image_id': 'sha256:eaf6f386053efdf0ad30236a394b4460e3669006afc9eb1220ae2047f330cc9c', 'notes': [], 'os': 'linux', 'platform': 'linux/amd64', 'pull_ref': 'nginx@sha256:42f2d24ae18df9b5251d1cc45548085656d2335e9338fd150a24e415462d151f', 'repo_digests': ['nginx@sha256:42f2d24ae18df9b5251d1cc45548085656d2335e9338fd150a24e415462d151f'], 'repo_tags': ['nginx:latest'], 'scope': 'system', 'size': 161308134, 'source': 'docker image inspect', 'tag_aliases': [{'ref': 'nginx:latest', 'repository': 'nginx', 'tag': 'latest'}], 'user': None, 'variant': None})
    -
    -TASK [container_images : Tag Docker images with harvested tag aliases] ********************************************************************************************************************************************
    -ok: [localhost] => (item=[{'architecture': 'amd64', 'created': '2026-06-17T22:44:41.625799723Z', 'engine': 'docker', 'home': None, 'image_id': 'sha256:eaf6f386053efdf0ad30236a394b4460e3669006afc9eb1220ae2047f330cc9c', 'notes': [], 'os': 'linux', 'platform': 'linux/amd64', 'pull_ref': 'nginx@sha256:42f2d24ae18df9b5251d1cc45548085656d2335e9338fd150a24e415462d151f', 'repo_digests': ['nginx@sha256:42f2d24ae18df9b5251d1cc45548085656d2335e9338fd150a24e415462d151f'], 'repo_tags': ['nginx:latest'], 'scope': 'system', 'size': 161308134, 'source': 'docker image inspect', 'user': None, 'variant': None}, {'ref': 'nginx:latest', 'repository': 'nginx', 'tag': 'latest'}])
    -
    -TASK [container_images : Pull system Podman images by immutable registry digest] **********************************************************************************************************************************
    -ok: [localhost] => (item={'architecture': 'amd64', 'created': '2026-06-16T00:01:29.967161902Z', 'engine': 'podman', 'home': None, 'image_id': 'sha256:d529dd0c6e5597ac7e4a3e2dea65c3fcc6173f4cae713c409265c1dd9914a11b', 'notes': [], 'os': 'linux', 'platform': 'linux/amd64', 'pull_ref': 'docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b', 'repo_digests': ['docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b', 'docker.io/library/alpine@sha256:79ff19e9084a00eece421b2523fb93e22d730e2c0e525905de047e848e56d95f'], 'repo_tags': ['docker.io/library/alpine:latest'], 'scope': 'system', 'size': 8709729, 'source': 'podman image inspect', 'tag_aliases': [{'ref': 'docker.io/library/alpine:latest', 'repository': 'docker.io/library/alpine', 'tag': 'latest'}], 'user': None, 'variant': None})
    -
    -TASK [container_images : Tag system Podman images with harvested tag aliases] *************************************************************************************************************************************
    -ok: [localhost] => (item=[{'architecture': 'amd64', 'created': '2026-06-16T00:01:29.967161902Z', 'engine': 'podman', 'home': None, 'image_id': 'sha256:d529dd0c6e5597ac7e4a3e2dea65c3fcc6173f4cae713c409265c1dd9914a11b', 'notes': [], 'os': 'linux', 'platform': 'linux/amd64', 'pull_ref': 'docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b', 'repo_digests': ['docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b', 'docker.io/library/alpine@sha256:79ff19e9084a00eece421b2523fb93e22d730e2c0e525905de047e848e56d95f'], 'repo_tags': ['docker.io/library/alpine:latest'], 'scope': 'system', 'size': 8709729, 'source': 'podman image inspect', 'user': None, 'variant': None}, {'ref': 'docker.io/library/alpine:latest', 'repository': 'docker.io/library/alpine', 'tag': 'latest'}])
    -
    -TASK [container_images : Pull user Podman images by immutable registry digest] ************************************************************************************************************************************
    -skipping: [localhost]
    -
    -TASK [container_images : Tag user Podman images with harvested tag aliases] ***************************************************************************************************************************************
    -skipping: [localhost]
    -
    -PLAY RECAP ********************************************************************************************************************************************************************************************************
    -localhost                  : ok=5    changed=0    unreachable=0    failed=0    skipped=2    rescued=0    ignored=0   
    -            
    -
    -

    -

    I did not use community extensions/modules for Docker in the Salt and Puppet renderers, because, well, they are god-awful (the Salt one simply doesn't work in 3008.1, and the Puppet one is non-idempotent and I would argue cruder in its approach to image management than a guarded Exec call can be (and is).

    - -

    Other smaller changes

    -
      -
    • sysctl runtime parameters are now detected and would be written to /etc/sysctl.d/99-enroll.conf. Not all runtime parameters are supported.
    • -
    • .bashrc and similar files are now only harvested from user directories when --dangerous is used, since this is a common place for sensitive environment variables to be set. As always, remember that --dangerous gives better harvest coverage, but you should use --sops or some other means of your own to encrypt the harvested data at rest safely!
    • -
    • Some output during an Ansible play is hidden with no_log to avoid potentially sensitive output, particularly of systemd unit state.
    • -
    • In case you missed it in version 0.6.0: Enroll now harvests runtime iptables and ipset rules!
    • -
    - -

    More coverage

    -

    With these changes comes a lot of new 'variance' and argument input to the app. Pytest coverage is now at about 86%, and there is a big suite unit tests for Ansible, Puppet and Salt too, in CI. I'm continuing to try and automate testing all the ways you can use this tool.

    - -
    -

    Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports!

    -

    If you think you know someone who would like Enroll, please tell them about it!

    -
    -
    - -
    -
    -
    Target your preferred config manager
    -
      -
    • --target ansible (default)
    • -
    • --target puppet
    • -
    • --target salt
    • -
    -
    -
    -
    Useful links
    - -
    -
    -
    There's more to Enroll!
    - -
    -
    -
    +

    The News and Troubleshooting sections are retained on the website, but they now document the current tool rather than the removed Puppet/Salt experiment.

    +
    diff --git a/src/content/news/_index.html b/src/content/news/_index.html index 82b6ad3..c3ff9a7 100644 --- a/src/content/news/_index.html +++ b/src/content/news/_index.html @@ -1,12 +1,15 @@ --- title: "News" -html_title: "Enroll News" -description: "Release notes and project updates for Enroll." +html_title: "Enroll news" +description: "News and release notes for Enroll." +og_title: "Enroll news" +og_description: "News and release notes for Enroll." +og_type: "website" --- -
    -
    +
    +
    News
    -

    What's new in Enroll

    -

    Release notes, important changes, tips and tricks.

    +

    Enroll news

    +

    Release notes and project notes for the current Enroll CLI.

    diff --git a/src/content/schema.html b/src/content/schema.html index 49f4a8f..fd3c265 100644 --- a/src/content/schema.html +++ b/src/content/schema.html @@ -1,14 +1,17 @@ --- title: "Schema" -html_title: "Enroll State Schema" -description: "JSON Schema describing the Enroll harvest state.json format." layout: "schema" +html_title: "Enroll state.json schema" +description: "The current JSON Schema for Enroll harvest state.json bundles." +og_title: "Enroll state.json schema" +og_description: "View the current Enroll JSON Schema and use it to validate harvest bundles." +og_type: "article" --- -
    -
    -
    Schema
    -

    Harvest state.json schema

    -

    enroll harvest generates a state file. This is its structure.

    +
    +
    +
    Harvest schema
    +

    Current state.json schema

    +

    Enroll validates harvest bundles against the vendored JSON Schema unless you pass --no-schema. The same schema is published here for inspection and CI pinning.

    @@ -16,35 +19,20 @@ layout: "schema"
    -
    -
    Tips
    -
    -

    You can validate a harvest against the schema by running enroll validate /path/to/harvest .

    -
    -
    -
    -
    Links
    - -
    +
    +

    Validate with the vendored schema

    +
    $ enroll validate ./harvest
    +

    Validate with this hosted schema

    +
    $ enroll validate ./harvest \
    +  --schema https://enroll.sh/schema/state.schema.json \
    +  --allow-remote-schema
    +

    Remote schema fetching is disabled by default and must be explicitly allowed.

    -
    -
    -
    -

    state.schema.json

    - Open raw -
    - -
    - -
    Loading…
    -
    - -
    Tip: you can validate a harvest with python -m jsonschema -i state.json schema/state.schema.json
    +
    + +
    Loading schema...
    diff --git a/src/content/security.html b/src/content/security.html index 2218f29..7c6ed8e 100644 --- a/src/content/security.html +++ b/src/content/security.html @@ -1,110 +1,91 @@ --- title: "Security Design" -html_title: "Enroll Security" -description: "Security posture and safe workflows for Enroll outputs." +html_title: "Enroll security design and threat model" +description: "Threat model and security scope for Enroll, a privileged Linux host harvesting CLI that generates Ansible output." +og_title: "Enroll security design" +og_description: "Enroll threat model, trusted harvest guidance, in-scope hardening, and vulnerability report guidance." +og_type: "article" --- -
    -
    -
    Security
    -

    Safe by default. Powerful when you opt in.

    -

    Enroll can touch sensitive files. This page helps you use it confidently.

    +
    +
    +
    Security design
    +

    Threat model and security scope

    +

    Enroll is a privileged systems-administration CLI, not a sandbox. It tries to protect careful administrators from common mistakes, while assuming the operator controls the environment they run it in.

    -
    -
    -
    -

    Default behavior

    -

    In normal mode, Enroll attempts to avoid harvesting likely secrets using a combination of path deny-lists, content sniffing, and size caps. This means you may see some files intentionally skipped.

    -
    - -
    -
    -
    -
    -

    The --dangerous flag

    -

    This disables secret-safety checks. It can copy private keys, API tokens, DB passwords, TLS key material, etc.

    -

    Rule: if you use --dangerous, treat the output as sensitive data and plan secure storage before you run it. Don't store secrets in plaintext in a public place!

    -
    +
    - -
    -
    -
    Recommended workflow
    -
      -
    1. Start with default mode (no --dangerous).
    2. -
    3. Add --include-path for a small set of extra files you genuinely want managed.
    4. -
    5. If you must capture secrets, use --dangerous and --sops.
    6. -
    7. Keep outputs out of public repos; review before committing.
    8. -
    9. Rotate credentials if you ever suspect they were captured or exposed.
    10. -
    -
    - -
    -
    Storage ideas
    -
      -
    • Encrypted SOPS bundle stored in a password manager vault
    • -
    • Private git repo with additional encryption at rest
    • -
    • Offline backup in an encrypted volume
    • + +
      +
      +

      Core assumptions

      +

      Enroll is designed to be executed intentionally by a system administrator, often as root, to inspect a host and generate Ansible output. If an attacker controls the administrator's command line, environment, config file, working directory, PATH, SSH config, SOPS binary, Ansible installation, or harvested input bundle, they may influence what Enroll does. That is considered a local trust-boundary failure outside Enroll's intended security model.

      +
        +
      • The operator understands options such as --dangerous, --assume-safe-path, --sops, --remote-host, and --remote-ssh-config.
      • +
      • Configuration files loaded from enroll.ini are selected and trusted by the operator.
      • +
      • Harvest bundles used for manifest or diff come from a trusted source unless the operator is deliberately inspecting untrusted input without applying it.
      • +
      • External tools invoked by Enroll, including Ansible, SOPS, SSH, sudo, Docker, Podman, Flatpak, Snap, package managers, and system utilities, are the trusted tools the operator intended to use.
      -
      +
    -
    -
    Scope control
    -

    You can explicitly include or exclude paths. Excludes take precedence over includes.

    -
    $ enroll harvest \
    -  --out /tmp/enroll-harvest \
    -  --include-path '/home/*/.profile' \
    -  --exclude-path '/home/*/.ssh/**'
    -
    +
    +

    What Enroll tries to defend against

    +
    +
    Accidental secret capture

    Default mode avoids obvious sensitive paths, private keys, token/password assignments, authorization headers, credential URIs, and similar material.

    +
    Unsafe filesystem behavior

    Enroll avoids symlink traversal, hardlinks, device nodes, tar path traversal, unsafe artifact paths, and common TOCTOU issues when copying and consuming artifacts.

    +
    Unsafe generated Ansible

    Harvested values are serialized as Ansible data, template-looking values are tagged !unsafe, and raw task scaffold identifiers are allowlisted.

    +
    Risky privileged automation

    Plain harvest outputs are private by default. Root-run output paths and unsafe root PATH entries are checked. Remote harvest rejects unknown SSH host keys by default.

    +
    +
    + +
    +

    What is out of scope

    +

    The following are normally operator trust failures, not Enroll vulnerabilities by themselves:

    +
      +
    • A malicious local user already controlling root's shell, environment, config files, PATH, binaries, SSH config, or working directory.
    • +
    • A root user intentionally loading a config file or passing options that request dangerous behavior.
    • +
    • Using --dangerous and observing that Enroll may collect sensitive information.
    • +
    • Using --assume-safe-path and observing that Enroll does not prompt about PATH.
    • +
    • Applying generated Ansible from an untrusted harvest.
    • +
    • Trusting an untrusted webhook, email target, SSH proxy command, SOPS binary, package manager, or Ansible toolchain.
    • +
    +
    + +
    +

    Trusted harvests

    +

    Harvest bundles are sensitive administrative artifacts. They may contain hostnames, usernames, package lists, service state, filesystem metadata, configuration files, firewall snapshots, container image references, Flatpak/Snap state, and other operational details. In --dangerous mode they may contain much more sensitive material.

    +
    Important: Enroll validates harvest structure and artifact safety. It cannot prove that the desired state represented by a harvest is safe to apply. Before running manifest, diff, or Ansible, be confident the harvest came from a trusted source and has not been tampered with.
    +
    $ enroll validate ./harvest
    +$ enroll manifest --harvest ./harvest --out ./ansible
    +
    + +
    +

    Useful security reports

    +

    Useful reports show Enroll behaving unsafely despite the documented trust model. Examples include:

    +
      +
    • Capturing a clearly sensitive default-denied file without --dangerous.
    • +
    • Following a symlink or hardlink in a way that causes privileged file disclosure or overwrite.
    • +
    • Extracting a tar member outside the intended harvest directory.
    • +
    • Accepting a malicious harvest artifact that escapes the artifact root.
    • +
    • Generating Ansible where ordinary harvested data can cause command injection or YAML/template structure injection.
    • +
    • Writing root-run output into an unsafe attacker-controlled path despite safety checks.
    • +
    • Accepting an unknown SSH host key unexpectedly.
    • +
    +

    Less useful reports are variations of “root deliberately asked Enroll to do dangerous things”. Those are expected consequences of running a privileged administration tool with trusted operator-controlled input.

    +
    - -
    - -
    -

    Threat model

    -
    -
    -
    What Enroll tries to prevent
    -
      -
    • Accidentally copying obvious secrets in default mode
    • -
    • Harvesting huge/unbounded file sets by mistake
    • -
    • One host's difference causing problems for other hosts by keeping multi-site data in inventory, Hiera, or pillar
    • -
    -
    -
    -
    What you still need to think about
    -
      -
    • Where outputs are stored and who can access them
    • -
    • Reviewing what was captured before committing/sharing
    • -
    • Choosing encryption and secret-management strategy
    • -
    -
    -
    -
    - diff --git a/src/content/troubleshooting.html b/src/content/troubleshooting.html index aa68499..c1538b2 100644 --- a/src/content/troubleshooting.html +++ b/src/content/troubleshooting.html @@ -1,187 +1,118 @@ --- title: "Troubleshooting" -html_title: "Enroll Troubleshooting" -description: "Common Enroll errors and fixes for generated Ansible, Puppet, and Salt manifests." +html_title: "Enroll troubleshooting - current Ansible CLI" +description: "Common Enroll errors and fixes for the current Ansible-only CLI." +og_title: "Enroll troubleshooting" +og_description: "Fix stale --target examples, unsafe root PATH, missing Ansible collections, JinjaTurtle, remote SSH auth, validation, and SOPS extraction." +og_type: "article" --- -
    -
    -
    Troubleshooting
    -

    Common errors and how to fix them

    -

    Most first-run problems are either missing runtime tools, old Ansible community collections, or files that Enroll intentionally skipped for safety.

    +
    +
    +
    Troubleshooting
    +

    Common Enroll problems and fixes

    +

    Current fixes for the Ansible-only Enroll CLI, including stale old examples, unsafe root PATH, JinjaTurtle, remote SSH, SOPS, and generated collection requirements.

    -
    -
    - +
    -
    -
    -
    -
    -
    -

    Start here for Ansible module errors

    -

    Generated Ansible manifests may use community collections when a harvest contains Docker/Podman images, Flatpaks, or Snaps. Enroll writes a requirements.yml beside playbook.yml, and the generated manifest README.md shows the install command.

    -
    - -
    $ cd /path/to/generated/ansible-manifest
    -$ ansible-galaxy collection install -r requirements.yml
    -
    -

    Run this in the same Python virtualenv, container, CI job, or user account that will run ansible-playbook. If an older collection is already installed and Ansible keeps finding it first, retry with --force or adjust ANSIBLE_COLLECTIONS_PATH.

    -
    -
    +
    +

    Error: unrecognized arguments: --target

    +

    You are following old documentation from a temporary version that experimented with Puppet and Salt renderers. Current Enroll only generates Ansible output, so there is no --target flag.

    +
    +
    Old command
    $ enroll manifest --harvest ./harvest --target ansible --out ./ansible
    +
    Current command
    $ enroll manifest --harvest ./harvest --out ./ansible
    -
    -

    Podman error: unsupported platform parameter

    -

    If the host has an older containers.podman / community.podman Galaxy collection, an Ansible run may fail with a message like this:

    -
    -
    [ERROR]: Task failed: Module failed: Unsupported parameters for (containers.podman.podman_image) module: platform. Supported parameters include: arch, auth_file, build, ca_cert_dir, executable, force, name, password, path, pull, pull_extra_args, push, push_args, quadlet_dir, quadlet_file_mode, quadlet_filename, quadlet_options, state, tag, username, validate_certs (authfile, build_args, buildargs, tls_verify, tlsverify).
    -
    -
    -
    Cause
    -

    The generated task is using a newer module interface than the collection currently installed on the machine running Ansible.

    -
    -
    -
    Fix
    -

    Install the collection versions constrained by the generated manifest:

    -
    - -
    $ cd /path/to/generated/ansible-manifest
    -$ ansible-galaxy collection install -r requirements.yml
    -
    -

    Then re-run the playbook from the same environment. If Ansible still reports the older module, check ansible-galaxy collection list and remove or override the stale collection path.

    -
    -
    - -
    -

    Flatpak error: unsupported from_url parameter

    -

    Flatpak tasks can fail in the same way when the installed community.general collection is too old for the generated role.

    +
    +

    Root run refuses to continue because PATH is unsafe

    +

    When running as root, Enroll refuses non-interactive execution if PATH contains ., an empty entry, a relative entry, a non-root-owned directory, or group/world-writable directories/parents that could let an untrusted binary be executed.

    -
    Fix
    -

    Use the generated Ansible requirements file before applying the manifest:

    -
    - -
    $ cd /path/to/generated/ansible-manifest
    -$ ansible-galaxy collection install -r requirements.yml
    -
    +
    Fix the environment, or explicitly accept it
    +
    $ export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    +$ enroll harvest --out ./harvest
    +
    +# only for trusted CI environments where the PATH is intentional
    +$ enroll harvest --out ./harvest --assume-safe-path
    -

    The same advice applies when Snap, Docker, Podman, or Flatpak roles complain about unknown module parameters: install from requirements.yml first, because Enroll generated that file for the manifest it just wrote.

    -
    -

    Salt error: Failed to import utils pycrypto

    -

    You run the salt-call command and see an error Failed to import utils pycrypto, this is due most likely to a syntax error with a Traceback that includes AttributeError: module 'crypt' has no attribute 'methods'.

    -
    -
    Fix
    -

    It's not an Enroll bug. Upgrade to Salt Stack 3008.1, in their Debian packages they use Python 3.14 and have solved the problem. The problem occurs with older Python, and is described in this upstream bug report.

    -
    +
    +

    Ansible complains about missing modules or unsupported parameters

    +

    Generated roles can use collections for Docker, Podman, Flatpak, Snap, or other resources. Install the generated requirements from the manifest output directory before running the playbook.

    +
    $ cd /tmp/enroll-ansible
    +$ ansible-galaxy collection install -r requirements.yml
    +$ ansible-galaxy collection list
    +

    If Ansible still uses an older collection, check your configured collection paths and remove or override stale copies.

    -

    JinjaTurtle is not found, or template generation was skipped

    -

    When --jinjaturtle auto is used, Enroll only templates recognised config files if the jinjaturtle executable is available on PATH. If it is missing, Enroll safely falls back to copying raw files. When --jinjaturtle on is used, missing JinjaTurtle is a hard error.

    +

    JinjaTurtle is missing or template generation was skipped

    +

    By default, Enroll uses JinjaTurtle if the jinjaturtle executable exists on PATH; otherwise it safely copies raw files. --jinjaturtle makes missing JinjaTurtle a hard error, and --no-jinjaturtle disables templating entirely.

    -
    -
    -
    Make templating optional (this is the default already)
    -
    $ enroll manifest --harvest ./harvest --target ansible --out ./ansible --jinjaturtle auto
    -
    -
    -
    -
    -
    Disable templating entirely
    -
    $ enroll manifest --harvest ./harvest --target puppet --out ./puppet --jinjaturtle off # or --no-jinjaturtle
    -
    -
    +
    Require JinjaTurtle
    $ enroll manifest --harvest ./harvest --out ./ansible --jinjaturtle
    +
    Disable templating
    $ enroll manifest --harvest ./harvest --out ./ansible --no-jinjaturtle
    -

    If a generated template would reference missing variables, Enroll should fall back to a raw file copy rather than producing a broken manifest.

    A file I expected was not harvested

    -

    The default harvest is intentionally conservative. A file may be skipped because it matched an exclude pattern, looked sensitive, looked binary, was too large, was already captured by another role, or was outside Enroll's standard scan paths.

    +

    Default harvesting is conservative. A file may be skipped because it matched an exclude pattern, looked sensitive, looked binary, was too large, was already captured elsewhere, was unreadable, or was outside standard scan paths.

    -
    -
    -
    Explicitly include a path
    -
    $ enroll harvest --out ./harvest --include-path /opt/myapp/config.yml
    -
    -
    -
    -
    -
    Inspect what happened
    -
    $ enroll explain ./harvest
    -$ enroll validate ./harvest
    -
    -
    -
    -
    -
    Be careful with --dangerous
    -
    Use --dangerous only when you understand that it can harvest secrets such as private keys, tokens, credentials, and application config containing passwords.
    +
    Explicitly include a path
    $ enroll harvest --out ./harvest --include-path /opt/myapp/config.yml
    +
    Inspect the result
    $ enroll explain ./harvest
    +$ enroll validate ./harvest
    +
    Be careful with --dangerous: it can harvest secrets such as private keys, tokens, credentials, and application config containing passwords.
    -

    Generated manifest references a missing artifact

    -

    If a manifest task points at a missing source file, validate the original harvest first. Validation checks state.json, referenced artifacts, generated firewall/sysctl files, and unreferenced artifact warnings.

    -
    - -
    $ enroll validate /path/to/harvest
    -
    -

    If validation passes, check whether you moved, edited, or partially copied the generated manifest after rendering it.

    +

    Validation fails, or a manifest references a missing artifact

    +

    Validate the original harvest. Validation checks the schema, referenced artifacts, artifact safety, and unreferenced artifact warnings.

    +
    $ enroll validate /path/to/harvest
    +$ enroll validate /path/to/harvest --format json --out validate.json
    +$ enroll validate /path/to/harvest --fail-on-warnings
    +

    If validation passes but Ansible cannot find a file, check whether the generated output was partially copied, edited, or moved after rendering.

    Remote harvest cannot sudo or cannot unlock an SSH key

    -

    Remote mode can prompt for sudo and private-key passphrases, but CI and non-interactive shells should pass the required information up front. You can set the ENROLL_SSH_KEY_PASSPHRASE environment variable if needed, and then pass --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE.

    +

    Remote mode can prompt for sudo and private-key passphrases. In non-interactive shells, provide the required inputs explicitly.

    -
    -
    -
    Prompt for sudo
    -
    $ enroll harvest --remote-host host.example.net --ask-become-pass --out ./harvest
    -
    -
    -
    -
    -
    Prompt for encrypted SSH key
    -
    $ enroll harvest --remote-host host.example.net --ask-key-passphrase --out ./harvest
    -
    -
    +
    Prompt for sudo
    $ enroll harvest --remote-host host.example.net --ask-become-pass --out ./harvest
    +
    Prompt for SSH key
    $ enroll harvest --remote-host host.example.net --ask-key-passphrase --out ./harvest
    +
    CI key passphrase
    $ export ENROLL_SSH_KEY_PASSPHRASE='...'
    +$ enroll harvest --remote-host host.example.net --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE --out ./harvest
    -

    If host key verification fails, connect with normal ssh first so the expected key is in known_hosts, or use your normal SSH config with Enroll's remote SSH config support.

    +

    If host key verification fails, connect with normal ssh first so the expected key is in known_hosts, or use --remote-ssh-config.

    -
    -

    enroll diff --enforce did not change everything

    -

    Enforcement is best-effort drift correction from the old harvest. It is intentionally conservative and does not try to do risky things such as arbitrary package downgrades.

    -
    -
    Choose the enforcement target explicitly
    -

    By default, enforcement uses Ansible. You can choose Puppet or Salt when those tools are available on the host running enforcement:

    -
    - -
    $ enroll diff --old ./golden --new ./current --enforce --target ansible
    -$ enroll diff --old ./golden --new ./current --enforce --target puppet
    -$ enroll diff --old ./golden --new ./current --enforce --target salt
    -
    -
    -

    For non-Ansible targets, make sure puppet or salt-call is installed and on PATH before running enforcement.

    +
    +

    I have a SOPS manifest bundle. How do I run it?

    +

    manifest --sops produces one encrypted manifest.tar.gz.sops file. It is an at-rest storage format, not something Ansible runs directly. Decrypt and extract it first.

    +
    $ cd /path/to/output
    +$ sops -d manifest.tar.gz.sops | tar -xzvf -
    +$ cd manifest
    +$ ansible-galaxy collection install -r requirements.yml
    +$ ansible-playbook -i "localhost," -c local playbook.yml --check
    diff --git a/src/hugo.toml b/src/hugo.toml index 4172e70..81d7d83 100644 --- a/src/hugo.toml +++ b/src/hugo.toml @@ -6,4 +6,4 @@ uglyURLs = true disableKinds = ["taxonomy", "term", "RSS", "sitemap"] [params] - description = "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible, Puppet, or Salt manifests from what it finds. Harvest → Manifest → Manage." + description = "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible configuration-management code from what it finds. Harvest → Manifest → Manage." diff --git a/src/themes/enroll-theme/layouts/partials/footer.html b/src/themes/enroll-theme/layouts/partials/footer.html index 1d378c2..c7e4d7e 100644 --- a/src/themes/enroll-theme/layouts/partials/footer.html +++ b/src/themes/enroll-theme/layouts/partials/footer.html @@ -7,10 +7,9 @@
    Enroll (a mig5 project)
    CLI Ansible - Puppet - Salt + SSH
    -
    Reverse-engineering servers into Ansible, Puppet, or Salt.
    +
    Reverse-engineering Linux hosts into Ansible.
    @@ -31,6 +30,7 @@
  • Docs
  • Examples
  • News
  • +
  • Troubleshooting
  • Schema
  • Security Design
  • From b4de16b73a74614479aefd95d4b7cf84e92a98a8 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 30 Jun 2026 16:21:27 +1000 Subject: [PATCH 25/30] Wording change --- src/content/_index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/_index.html b/src/content/_index.html index c31bc8a..1f5280d 100644 --- a/src/content/_index.html +++ b/src/content/_index.html @@ -11,7 +11,7 @@ og_type: "website"
    Reverse-engineering Linux hosts into Ansible
    -

    Get an existing server into configuration management without starting from a blank file.

    +

    Get an existing server into config management quickly.

    Enroll inspects a Debian-like or RedHat-like Linux machine, harvests the state that matters, and generates Ansible roles and playbooks from the captured bundle.

    Quickstart From 5319ac916169c667a6486ac821b645d7fbe3e52b Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 30 Jun 2026 16:36:26 +1000 Subject: [PATCH 26/30] Update news --- src/content/news/0-7-0.html | 33 ++++++++++++++++---------------- src/content/troubleshooting.html | 11 +---------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/content/news/0-7-0.html b/src/content/news/0-7-0.html index 2f9ea69..d4d37fc 100644 --- a/src/content/news/0-7-0.html +++ b/src/content/news/0-7-0.html @@ -1,29 +1,25 @@ --- -title: "Enroll 0.7.0 beta: Ansible-only again, with stronger safety tooling" +title: "Enroll 0.7.0: stronger safety tooling and new features" date: "2026-06-30" -description: "Current Enroll 0.7.0 beta notes: the Puppet and Salt experiment has been removed; Enroll now documents Ansible-only output and stronger safety tooling." -summary: "The current beta focuses Enroll back on Ansible-only output, removes stale Puppet/Salt documentation, and clarifies safety and provenance guidance." -html_title: "Enroll 0.7.0 beta - Ansible-only current documentation" -og_title: "Enroll 0.7.0 beta: Ansible-only current documentation" -og_description: "The current Enroll beta removes the old Puppet/Salt experiment from the docs and focuses on Ansible output, validation, SOPS, and safety." +description: "Enroll 0.7.0 has much more robust defenses against malicious harvested data and what gets manifested into Ansible." +summary: "Enroll 0.7.0 focuses on making Enroll very robust against many forms of attack, introduces 'combined' roles for faster and simpler execution, and adds Docker/Flatpak/Snap detection." +html_title: "Enroll 0.7.0 release notes" +og_title: "Enroll 0.7.0 release notes" +og_description: "0.7.0 focuses on making Enroll very robust against many forms of attack, introduces 'combined' roles for faster and simpler execution, and adds Docker/Flatpak/Snap detection." og_type: "article" ---
    News
    -

    Enroll 0.7.0 beta: Ansible-only again, with stronger safety tooling

    -

    The current beta focuses Enroll back on one supported output path: harvest Linux hosts and render Ansible.

    +

    Enroll 0.7.0: stronger safety tooling and other improvements

    +

    The new release focuses on hardening Enroll's defenses against various types of attack/content injection, along with some other new features and improvements.

    -

    Earlier 0.7.0 development builds experimented with Puppet and Salt renderers. That experiment has been removed from the current codebase. The supported flow today is simpler and more honest: harvest a Linux host, validate the bundle, and generate Ansible roles and playbooks.

    - -
    Documentation note: if you find an old command using --target ansible, --target puppet, or --target salt, it is stale. Current enroll manifest has no --target option.
    - -

    What remains

    +

    What Enroll is doing

    • Harvest bundles with state.json and captured artifacts.
    • Ansible manifesting in single-site mode or --fqdn multi-site mode.
    • @@ -43,19 +39,22 @@ og_type: "article" $ ansible-playbook -i "localhost," -c local playbook.yml --check

    Security and provenance changes

    -

    The current documentation is also more explicit about trust boundaries. Enroll validates structure and tries to avoid accidental secret capture, symlink traversal, unsafe artifact paths, unsafe root output paths, and root PATH foot-guns. It still cannot prove that a harvested desired state is semantically safe to apply.

    +

    Version 0.7.0 is also more explicit about trust boundaries. Enroll validates structure and tries to avoid accidental secret capture, symlink traversal, unsafe artifact paths, unsafe root output paths, and root PATH foot-guns. It still cannot prove that a harvested desired state is semantically safe to apply.

    +

    When manifesting from a harvest, you'll see a message like this:

    This harvest is structurally valid, but Enroll cannot prove it is semantically safe. Only apply manifests generated from harvests whose provenance you trust.
    -

    Other notable current behavior

    +

    Other notable changes

      +
    • In 'single site' mode, Enroll now combines packages/configs into 'common' roles where the 'Section' name of that software is common. This makes Ansible faster to run, and is easier to digest cognitively (you might now get 20 roles instead of 200!). This behaviour can be disabled with --no-common-roles or when using the --fqdn mode.
    • +
    • Enroll now tries to detect Docker images on your system. This requires the docker galaxy role in Ansible. It will enforce that the specific SHA256 of that image is present (not floating tagsZ), when running Ansible.
    • +
    • Enroll now tries to detect system and user-level Flatpaks, as well as Snaps.
    • Safe-mode content scanning tolerates value-less credential words in comments, but populated credential assignments are still treated as sensitive even if commented out.
    • Automatic per-user shell dotfile harvesting is only enabled in --dangerous mode.
    • Plain directory harvests are frozen into a private temp tree before manifesting, reducing TOCTOU exposure between validation and rendering.
    • Remote harvests reject unknown SSH host keys by default.
    • Root runs refuse unsafe non-interactive PATH setups unless --assume-safe-path is supplied.
    • +
    • Enroll will not want to harvest or manifest to a directory that is writable by others. It will try to make the harvest or manifest readable only by the user that is executing the command. This is in an effort to resist tampering attacks.
    - -

    The News and Troubleshooting sections are retained on the website, but they now document the current tool rather than the removed Puppet/Salt experiment.

    diff --git a/src/content/troubleshooting.html b/src/content/troubleshooting.html index c1538b2..2d6155e 100644 --- a/src/content/troubleshooting.html +++ b/src/content/troubleshooting.html @@ -3,7 +3,7 @@ title: "Troubleshooting" html_title: "Enroll troubleshooting - current Ansible CLI" description: "Common Enroll errors and fixes for the current Ansible-only CLI." og_title: "Enroll troubleshooting" -og_description: "Fix stale --target examples, unsafe root PATH, missing Ansible collections, JinjaTurtle, remote SSH auth, validation, and SOPS extraction." +og_description: "Fix unsafe root PATH, missing Ansible collections, JinjaTurtle, remote SSH auth, validation, and SOPS extraction." og_type: "article" ---
    @@ -33,15 +33,6 @@ og_type: "article"
    -
    -

    Error: unrecognized arguments: --target

    -

    You are following old documentation from a temporary version that experimented with Puppet and Salt renderers. Current Enroll only generates Ansible output, so there is no --target flag.

    -
    -
    Old command
    $ enroll manifest --harvest ./harvest --target ansible --out ./ansible
    -
    Current command
    $ enroll manifest --harvest ./harvest --out ./ansible
    -
    -
    -

    Root run refuses to continue because PATH is unsafe

    When running as root, Enroll refuses non-interactive execution if PATH contains ., an empty entry, a relative entry, a non-root-owned directory, or group/world-writable directories/parents that could let an untrusted binary be executed.

    From 97011f287f12ef23c0cda81b4923720d60bca26d Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 5 Jul 2026 11:00:58 +1000 Subject: [PATCH 27/30] Updates --- src/content/news/0-7-0.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/content/news/0-7-0.html b/src/content/news/0-7-0.html index d4d37fc..3dfdb26 100644 --- a/src/content/news/0-7-0.html +++ b/src/content/news/0-7-0.html @@ -52,8 +52,10 @@ og_type: "article"
  • Automatic per-user shell dotfile harvesting is only enabled in --dangerous mode.
  • Plain directory harvests are frozen into a private temp tree before manifesting, reducing TOCTOU exposure between validation and rendering.
  • Remote harvests reject unknown SSH host keys by default.
  • +
  • Enroll now implicitly 'validates' a harvest before trying to manifest it to Ansible code.
  • Root runs refuse unsafe non-interactive PATH setups unless --assume-safe-path is supplied.
  • Enroll will not want to harvest or manifest to a directory that is writable by others. It will try to make the harvest or manifest readable only by the user that is executing the command. This is in an effort to resist tampering attacks.
  • +
  • Enroll's --enforce argument to enroll diff has been removed. Non-interactive application of manifests is considered too dangerous by the maintainer, despite all the additional hardening of the harvesting described above. If you really want to enforce the previous harvest, you can still code up an enroll manifest [...] && ansible-playbook [....] yourself when detecting that enroll diff sees changes between two harvests.
  • From 41edfece4a535565f7f5891ef59f7a570b2d3309 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 5 Jul 2026 11:03:25 +1000 Subject: [PATCH 28/30] schema update --- src/static/schema/state.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/schema/state.schema.json b/src/static/schema/state.schema.json index 8806b2e..c1412fb 100644 --- a/src/static/schema/state.schema.json +++ b/src/static/schema/state.schema.json @@ -411,7 +411,7 @@ }, "src_rel": { "minLength": 1, - "pattern": "^[^/].*", + "pattern": "^(?!\\.\\.?(?:/|$))[^/\\u0000-\\u001f\\\\]+(?:/(?!\\.\\.?(?:/|$))[^/\\u0000-\\u001f\\\\]+)*$", "type": "string" } }, From 3253f6e16cd4cdc3f1c49ce1996448c837095feb Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 14 Jul 2026 17:36:59 +1000 Subject: [PATCH 29/30] Add 0.8.0 news --- src/content/news/0-8-0.html | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/content/news/0-8-0.html diff --git a/src/content/news/0-8-0.html b/src/content/news/0-8-0.html new file mode 100644 index 0000000..c4805c8 --- /dev/null +++ b/src/content/news/0-8-0.html @@ -0,0 +1,30 @@ +--- +title: "Enroll 0.8.0: more hardening" +date: "2026-07-14" +description: "Enroll 0.8.0 adds more hardening to prevent obscure issues." +summary: "Enroll 0.8.0 builds on the 0.7.0 release by adding several more hardening measures in an effort to keep it as safe as possible to use." +html_title: "Enroll 0.8.0 release notes" +og_title: "Enroll 0.8.0 release notes" +og_description: "Enroll 0.8.0 builds on the 0.7.0 release by adding several more hardening measures in an effort to keep it as safe as possible to use." +og_type: "article" +--- +
    +
    +
    News
    +

    Enroll 0.8.0: more hardening

    +

    0.8.0 builds on the 0.7.0 release by adding several more hardening measures in an effort to keep it as safe as possible to use.

    +
    +
    + +
    +
    +
    +

    Changelog

    +
      +
    • Keep sudo-created remote harvest bundles root-owned while root packages and hashes them, expose only the archive to the authenticated SSH uid, and verify the root-computed digest after download. This removes a tiny post-harvest tampering window created by recursively chowning the bundle before packaging, yet without making the plaintext archive world-readable.
    • +
    • Enforce tar member limits while lazily parsing untrusted archives rather than after `TarFile.getmembers()` has already indexed the entire archive; count repeated `.` entries and cap remote compressed downloads as well.
    • +
    • Apply aggregate byte and total filesystem-entry limits when freezing directory harvest bundles, reject symlinked bundle roots, and abort when files or discovered directories change during the copy, so direct directory inputs remain bounded and fail closed under mutation.
    • +
    +
    +
    +
    From 5f49bef2ad49362344583ab686fa22043c80ce36 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 24 Jul 2026 10:56:31 +1000 Subject: [PATCH 30/30] remove fedi link --- src/themes/enroll-theme/layouts/partials/footer.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/themes/enroll-theme/layouts/partials/footer.html b/src/themes/enroll-theme/layouts/partials/footer.html index c7e4d7e..fd4a981 100644 --- a/src/themes/enroll-theme/layouts/partials/footer.html +++ b/src/themes/enroll-theme/layouts/partials/footer.html @@ -44,10 +44,6 @@ Form -
  • - Fediverse: - @mig5 -