diff --git a/.gitignore b/.gitignore index 8d25cf9..c478f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ *.swp src/public -src/.hugo_build.lock diff --git a/src/.hugo_build.lock b/src/.hugo_build.lock new file mode 100644 index 0000000..e69de29 diff --git a/src/content/_index.html b/src/content/_index.html index 1f5280d..ca1e6ce 100644 --- a/src/content/_index.html +++ b/src/content/_index.html @@ -1,156 +1,127 @@ --- title: "Enroll" -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." +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 Linux hosts into Ansible
-

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.

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

- Safe by default - Optional SOPS encryption - Remote over SSH + Harvest → Manifest + Local or remote over SSH + Optional SOPS bundles + Ansible output
-
+
-
Harvest once, render Ansible
-
state.json + artifacts → roles/playbooks
+
The core workflow
+
dry-run before applying
-
$ enroll harvest --out ./harvest
-
$ 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/...
+
$ 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.
-
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.
-
+
-

The mental model

-

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

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

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.

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

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

- +
+

Quickstart paths

+

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

+
+
+
-
$ 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 harvest --out ./harvest
+enroll manifest --harvest ./harvest --out ./ansible
+cd ./ansible
+ansible-playbook -i localhost, -c local playbook.yml --check --diff
-
$ 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
+
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
+
+
+
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
-
$ 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
+
enroll diff --old ./harvest-golden --new ./harvest-current \
+  --format markdown --ignore-package-versions --exclude-path /var/anacron
-
+
-

What Enroll tries to capture

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

-
-

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

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

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 c8ad555..ae977e7 100644 --- a/src/content/docs.html +++ b/src/content/docs.html @@ -1,21 +1,13 @@ --- -title: "Docs" -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" +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 today

-

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

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

@@ -23,196 +15,167 @@ og_type: "article"
-
-

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

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

-
-

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

-
-

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

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.

-
-

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

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

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

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.

+
-
-

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

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.

+
-
-

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.

+
+
+

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

+
-
-

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.

+
+
+

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

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

-
- -
-

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

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 = host.example.net
-# remote_user = admin
 
 [manifest]
 no_jinjaturtle = true
-# sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
 
 [diff]
 exclude_path = /var/anacron
-ignore_package_versions = true
-
-[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.

+ignore_package_versions = true
+
-
-

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

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.

+
diff --git a/src/content/examples.html b/src/content/examples.html index 123042d..997da1b 100644 --- a/src/content/examples.html +++ b/src/content/examples.html @@ -1,165 +1,91 @@ --- title: "Examples" -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" +html_title: "Enroll Examples" +description: "Copy/paste Enroll recipes for local harvests, remote harvests, multi-site manifests, SOPS, diff, validate, and explain." --- -
-
+
+
Examples
-

Copy/paste Enroll recipes

-

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

+

Copy/paste recipes that match the current CLI

+

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

-
-
-

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

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.

-
-
-

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

+ +

Remote host using ssh_config alias

enroll harvest \
+  --remote-host app01-alias \
   --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
-
-
+ --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

-

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

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-harvest \
-  --out /tmp/enroll-ansible \
-  --fqdn host.example.net
-
-
+ --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

-

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 and exclude paths

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

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

-
-
-

Dangerous harvest, encrypted at rest

-

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

-
$ enroll harvest \
-  --out /srv/enroll/host-1 \
+      

Dangerous mode, encrypted at rest

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

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

-
-
+ --sops 54A91143AE0AB4F7743B01FE888ED1B423A3BC99

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

-
-
-

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

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.

-$ cd /srv/enroll/host-1-manifest -$ sops -d manifest.tar.gz.sops | tar -xzvf - -$ cd manifest
-
-
+

Multi-site output

fqdn=app01.example.net
+enroll manifest \
+  --harvest /tmp/enroll-app01-harvest \
+  --out /srv/ansible-enroll \
+  --fqdn "$fqdn"
+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.

-
-
-

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

Force or disable JinjaTurtle

# Error if jinjaturtle is not on PATH
+enroll manifest --harvest ./harvest --out ./ansible --jinjaturtle
 
-      
-
-

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

-
-
-

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

Compare harvests and notify a webhook

enroll diff \
+  --old /srv/enroll/golden \
   --new /srv/enroll/current \
-  --format json \
-  --ignore-package-versions \
-  --exclude-path /var/anacron \
-  --exit-code
-
-
- -
-
-

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 https://example.net/enroll/webhook \
   --webhook-format json \
-  --webhook-header 'X-Enroll-Secret: xxxx'
-
-
+ --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

-

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

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.

-
-
-

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

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.

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/news/0-7-0.html b/src/content/news/0-7-0.html deleted file mode 100644 index 3dfdb26..0000000 --- a/src/content/news/0-7-0.html +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: "Enroll 0.7.0: stronger safety tooling and new features" -date: "2026-06-30" -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: 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.

-
-
- -
-
-
-

What Enroll is doing

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

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

Security and provenance changes

-

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 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.
  • -
  • 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.
  • -
-
-
-
diff --git a/src/content/news/0-8-0.html b/src/content/news/0-8-0.html deleted file mode 100644 index c4805c8..0000000 --- a/src/content/news/0-8-0.html +++ /dev/null @@ -1,30 +0,0 @@ ---- -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.
  • -
-
-
-
diff --git a/src/content/news/_index.html b/src/content/news/_index.html deleted file mode 100644 index c3ff9a7..0000000 --- a/src/content/news/_index.html +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "News" -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
-

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 fd3c265..096ed7d 100644 --- a/src/content/schema.html +++ b/src/content/schema.html @@ -1,40 +1,46 @@ --- -title: "Schema" -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" +title: "State Schema" +html_title: "Enroll State Schema" +description: "The current JSON schema for Enroll harvest state.json bundles." --- -
-
-
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.

+
+
+
Schema
+

Harvest state.json schema

+

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

-
-
-
-

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.

-
-
-
-
- -
Loading schema...
-
+
+

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 7c6ed8e..f8a7676 100644 --- a/src/content/security.html +++ b/src/content/security.html @@ -1,90 +1,90 @@ --- title: "Security Design" -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" +html_title: "Enroll Security Design" +description: "Threat model, trust boundaries, dangerous mode, SOPS, root PATH safety, and manifest safety for Enroll." --- -
-
+
+
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.

+

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.

- -
-
-

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

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

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

    +
    + +
    +

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

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.

-
-
+
+
+
Recommended workflow
+
    +
  1. Harvest in normal mode first.
  2. +
  3. Run enroll explain and enroll validate.
  4. +
  5. Use targeted --include-path instead of broad --dangerous where possible.
  6. +
  7. Encrypt outputs if they may contain sensitive data or leave the host.
  8. +
  9. Run Ansible with --check --diff and review before applying.
  10. +
+
-
-

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

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

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.

-
+
diff --git a/src/content/troubleshooting.html b/src/content/troubleshooting.html index 2d6155e..3a249ad 100644 --- a/src/content/troubleshooting.html +++ b/src/content/troubleshooting.html @@ -1,111 +1,28 @@ --- 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 unsafe root PATH, missing Ansible collections, JinjaTurtle, remote SSH auth, validation, and SOPS extraction." -og_type: "article" +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 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.

+
+
+
Troubleshooting
+

Common messages and fixes

+

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

-
- - -
-
-

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

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

-
-
Require JinjaTurtle
$ enroll manifest --harvest ./harvest --out ./ansible --jinjaturtle
-
Disable templating
$ enroll manifest --harvest ./harvest --out ./ansible --no-jinjaturtle
-
-
- -
-

A file I expected was not harvested

-

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

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. 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 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 --remote-ssh-config.

-
- -
-

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

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 81d7d83..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 configuration-management code 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 c1412fb..8806b2e 100644 --- a/src/static/schema/state.schema.json +++ b/src/static/schema/state.schema.json @@ -411,7 +411,7 @@ }, "src_rel": { "minLength": 1, - "pattern": "^(?!\\.\\.?(?:/|$))[^/\\u0000-\\u001f\\\\]+(?:/(?!\\.\\.?(?:/|$))[^/\\u0000-\\u001f\\\\]+)*$", + "pattern": "^[^/].*", "type": "string" } }, diff --git a/src/themes/enroll-theme/layouts/news/list.html b/src/themes/enroll-theme/layouts/news/list.html deleted file mode 100644 index 6c0783c..0000000 --- a/src/themes/enroll-theme/layouts/news/list.html +++ /dev/null @@ -1,19 +0,0 @@ -{{ 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 deleted file mode 100644 index 3a342e5..0000000 --- a/src/themes/enroll-theme/layouts/news/single.html +++ /dev/null @@ -1,3 +0,0 @@ -{{ 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 fd4a981..512855b 100644 --- a/src/themes/enroll-theme/layouts/partials/footer.html +++ b/src/themes/enroll-theme/layouts/partials/footer.html @@ -3,60 +3,47 @@
- Enroll + Enroll
Enroll (a mig5 project)
CLI Ansible - SSH + 0.7 beta docs
-
Reverse-engineering Linux hosts into Ansible.
+
Harvest existing Linux host state and render reviewable Ansible.
-
Bugs or feature suggestions?
+
Contact

-
diff --git a/src/themes/enroll-theme/layouts/partials/head.html b/src/themes/enroll-theme/layouts/partials/head.html index 8b2bd48..f185324 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 c49b719..3e3c0ad 100644 --- a/src/themes/enroll-theme/layouts/partials/nav.html +++ b/src/themes/enroll-theme/layouts/partials/nav.html @@ -1,7 +1,7 @@