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