diff --git a/.gitignore b/.gitignore index c478f8c..8d25cf9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.swp src/public +src/.hugo_build.lock diff --git a/src/.hugo_build.lock b/src/.hugo_build.lock deleted file mode 100644 index e69de29..0000000 diff --git a/src/content/_index.html b/src/content/_index.html index 253e406..1f5280d 100644 --- a/src/content/_index.html +++ b/src/content/_index.html @@ -1,25 +1,26 @@ --- title: "Enroll" -html_title: "Enroll - Reverse-engineering servers into Ansible" -description: "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible roles/playbooks from what it finds. Harvest → Manifest → Manage." -og_title: "Enroll - Reverse-engineering servers into Ansible" -og_description: "Harvest a host's real configuration and turn it into Ansible roles/playbooks. Safe-by-default, with optional SOPS encryption." +html_title: "Enroll - Reverse-engineering Linux hosts into Ansible" +description: "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible configuration-management code from what it finds. Harvest → Manifest → Manage." +og_title: "Enroll - Reverse-engineering Linux hosts into Ansible" +og_description: "Harvest a host's real configuration and turn it into Ansible roles and playbooks. Safe-by-default, with optional SOPS encryption." og_type: "website" ---
-
Reverse-engineering servers into Ansible
-

Get an existing Linux host into Ansible in seconds.

-

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

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

- Super fast + Safe by default Optional SOPS encryption Remote over SSH
@@ -28,25 +29,17 @@ og_type: "website"
-
Config in the blink of an eye
-
single-shot → ansible-playbook
+
Harvest once, render Ansible
+
state.json + artifacts → roles/playbooks
-
$ enroll single-shot --harvest ./harvest --out ./ansible
-
$ cd ./ansible && tree -L 2
-
.
-├── ansible.cfg
-├── playbook.yml
-├── roles/
-│   ├── cron/
-│   ├── etc_custom/
-│   ├── firewall/
-│   ├── nginx/
-│   ├── openssh-server/
-│   ├── users/
-└── README.md
+
$ enroll harvest --out ./harvest
+
$ enroll manifest --harvest ./harvest --out ./ansible
+
$ ansible-galaxy collection install -r ./ansible/requirements.yml
+
./ansible → playbook.yml, roles/, requirements.yml
+./ansible + --fqdn → playbooks/<fqdn>.yml + inventory/host_vars/...
-
Tip: for multiple hosts, use --fqdn to generate inventory-driven, data-driven roles.
+
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.
@@ -57,88 +50,24 @@ og_type: "website"
-

A simple mental model

-

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

+

The mental model

+

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

-
-
-
-
Harvest
-
Collect host facts + relevant files into a bundle.
-
-
-
-
-
-
Manifest
-
Render Ansible roles & playbooks from the harvest.
-
-
-
-
-
-
Diff
-
Compare two harvests and notify via webhook/email.
-
-
-
-
-
-
Explain
-
Analyze what's included/excluded in the harvest and why.
-
-
-
-
-
-
Validate
-
Confirm that a harvest isn't corrupt or lacking artifacts.
-
-
+
Harvest
Collect host facts and selected files into a bundle.
+
Manifest
Turn that bundle into an Ansible repository-style output tree.
+
Diff
Compare two harvests and optionally notify by webhook or email.
+
Explain
Summarise what was included or excluded and why.
+
Validate
Check schema and artifact consistency before using a bundle.
-
-
-
-
Safe-by-default harvesting
-
Enroll avoids likely secrets with a path denylist, content sniffing, and size caps - then lets you opt in to more aggressive collection when you're ready.
-
-
-
-
-
-
-
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
-
+
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.
@@ -148,236 +77,78 @@ og_type: "website"
-
-

Quickstart

-
- +

Quickstart

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

+
-
-
-
-
-
- -
# Harvest → 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 /tmp/enroll-harvest
+$ enroll validate /tmp/enroll-harvest
+$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
+$ cd /tmp/enroll-ansible
+$ ansible-galaxy collection install -r requirements.yml
+$ ansible-playbook -i "localhost," -c local playbook.yml --check
- -
-
- -
# Remote harvest over SSH, then manifest locally
-enroll single-shot \
-  --remote-host myhost.example.com \
-  --remote-user myuser \
-  --harvest /tmp/enroll-harvest \
-  --out ./ansible \
-  --fqdn myhost.example.com
-
-
If you don't want/need sudo on the remote host, add --no-sudo (expect a less complete harvest). For remote sudo prompts use --ask-become-pass/-K. If your SSH private key is encrypted, use --ask-key-passphrase (interactive) or --ssh-key-passphrase-env ENV_VAR (non-interactive/CI).
+
+
$ enroll harvest \
+  --remote-host host.example.net \
+  --remote-user admin \
+  --remote-ssh-config ~/.ssh/config \
+  --out /tmp/enroll-harvest
+$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn host.example.net
- -
-
- -
# Multi-site mode: 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".
+
+
$ enroll harvest --out /srv/enroll/baseline
+$ enroll harvest --out /srv/enroll/current
+$ enroll diff --old /srv/enroll/baseline --new /srv/enroll/current --format markdown --ignore-package-versions
- -
-
- -
# Compare two harvests and get a human-friendly report (ignoring noise)
-enroll diff --old /path/to/harvestA --new /path/to/harvestB --format markdown \
-  --exclude-path /var/anacron \
-  --ignore-package-versions
-
-# Send a webhook when differences are detected
-enroll diff \
-  --old /path/to/harvestA \
-  --new /path/to/harvestB \
-  --webhook https://example.net/webhook \
-  --webhook-format json \
-  --webhook-header 'X-Enroll-Secret: ...' \
-  --ignore-package-versions \
-  --exit-code
-
-# Ignore a path and changes to package versions, and optionally
-# enforce the old state locally (requires ansible-playbook)
-enroll diff --old /path/to/harvestA --new /path/to/harvestB \
-  --exclude-path /var/anacron \
-  --ignore-package-versions \
-  --enforce
-
-
E-mail notifications are also supported. Run it on a systemd timer to alert to drift!
-
-
-
- -
# Explain what's in a harvest
-enroll explain /path/to/harvest
-
-# JSON format, and using a SOPS-encrypted harvest
-enroll explain /path/to/harvest.sops \
-  --sops \
-  --format json
-
-
-
'explain' tells you why something was included, but also why something was excluded.
-
-
-
- -
# Validate a harvest is correct.
-enroll validate /path/to/harvest
-
-# Check against the latest published version of the state schema specification
-enroll validate /path/to/harvest --schema https://enroll.sh/schema/state.schema.json
-
-
-
'validate' makes sure the harvest's state 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

-
-
- +

What Enroll tries to capture

-
-
-
-
Harvest
-
Collect state into a bundle.
-
-
-
-
-
-
-
-
Manifest
-
Render Ansible roles/playbooks.
-
-
-
-
+
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.

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

-
- +

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

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.

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

How Enroll works

-

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

+
+
+
+
+
Documentation
+

How Enroll works today

+

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

+
+ +
-
-
- +
+
+

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 is intentionally simple: it collects facts first, then renders Ansible from those facts.

+

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

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

Single-site mode

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

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

Multi-site mode

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

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

How harvesting works

-

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

- -
    -
  • Detects the OS and its package backend (e.g dpkg vs rpm)
  • -
  • Detects what packages are installed
  • -
  • For each package, it tries to detect files in /etc that have been modified from the default that get shipped with the package.
  • -
  • It detects running/enabled services and timers via systemd. For each of these, it looks for the unit files, any 'drop-in' files, environment variable files, etc, as well as what executable it executes, and tries to map those systemd services to the packages it's already learned about earlier (that way, those 'packages' or future 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.
  • +

    enroll harvest

    +

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

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

    Common flags

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

    Other things to be aware of:

    -
      -
    • You can use multiple invocations of --exclude-path to skip the bits you don't want. You 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.)
    • +
+ +
+

enroll manifest

+

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

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

Ansible output

+
    +
  • Single-site output contains playbook.yml, roles/, role files, defaults, templates, and requirements.yml.
  • +
  • Multi-site output uses host-specific inventory under inventory/host_vars/<fqdn>/... and playbooks under playbooks/.
  • +
  • Playbooks tag roles as role_<role_name>, such as role_users or role_services.
- -
-
Does Enroll use 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.
-
- -
-

State schema

-

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

-

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

- - +
+

enroll single-shot

+

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

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

Single-site vs multi-site

-

Manifest output has two styles. 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.
-
+
+

enroll diff

+

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

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

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

-
-

Remote harvesting over SSH

-

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

-
-
$ enroll harvest --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-harvest
-$ enroll manifest --harvest /tmp/enroll-harvest --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.
-
+
+

enroll explain

+

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

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

enroll validate

+

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

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

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

+
+ +
+

Sensitive data

+

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

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

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

+
+ +

JinjaTurtle integration

-

If JinjaTurtle (one of my other projects) is installed, Enroll can also produce Jinja2 templates for ini/json/xml/toml-style config 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
  • -
-
-
-
+

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

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

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

-
-

INI config file

-

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

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

Configuration file

+

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

+
[enroll]
+assume_safe_path = false
 
 [harvest]
 dangerous = false
@@ -255,324 +184,36 @@ include_path =
   /home/*/.bashrc
   /home/*/.profile
 exclude_path = /usr/local/bin/docker-*, /usr/local/bin/some-tool
-# remote_host = yourserver.example.com
-# remote_user = you
-# remote_port = 2222
+# remote_host = host.example.net
+# remote_user = admin
 
 [manifest]
 no_jinjaturtle = true
-sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
+# 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).
-
+[single-shot] +include_path = re:^/home/[^/]+/\.config/myapp/.*$
+

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

+
+

Run generated Ansible

+

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

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

Drift detection with enroll diff

-

One of the things I miss from my Puppet days, was the way the Puppet 'agent' would check in with the server and realign itself to the declared desired state. With 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!
-
+# multi-site +$ ansible-playbook playbooks/host.example.net.yml --check +# targeted role tags +$ ansible-playbook -i "localhost," -c local playbook.yml --tags role_users,role_services
- - -
-

Why did Enroll include/exclude something? enroll explain

-

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

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

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

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

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

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

What you get back:

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

Tips

-
-
-
-
Start safe
-

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

-
-
-
-
-
Encrypt at rest
-

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

-
-
-
-
-
Multi-host safety
-

For fleets, prefer multi-site output so 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..123042d 100644 --- a/src/content/examples.html +++ b/src/content/examples.html @@ -1,142 +1,165 @@ --- title: "Examples" -html_title: "Enroll Examples" -description: "Copy/paste recipes for Enroll: one host, fleets, drift detection, and safe storage." +html_title: "Enroll examples - Ansible harvest and manifest recipes" +description: "Copy/paste recipes for current Enroll: Ansible output, remote harvests, drift detection, SOPS, and validation." +og_title: "Enroll examples" +og_description: "Practical Enroll recipes for local and remote harvests, Ansible manifesting, SOPS bundles, drift reports, and validation." +og_type: "article" --- -
-
+
+
Examples
-

Copy/paste recipes

-

Practical flows you can adapt to your environment.

+

Copy/paste Enroll recipes

+

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

-
-
-
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 safe-mode baseline

+

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

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

Remote host through SSH config

+

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

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

Encrypted SSH key in CI

+

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

+
$ export ENROLL_SSH_KEY_PASSPHRASE='correct horse battery staple'
+$ enroll single-shot \
+  --remote-host host.example.net \
+  --remote-user admin \
+  --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE \
   --harvest /tmp/enroll-harvest \
-  --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.

-
-
- -
-
-
Fleets: multi-site output
-
- -
$ fqdn="$(hostname -f)"
-$ enroll single-shot --remote-host "$fqdn" \
-  --remote-user myuser \
   --out /tmp/enroll-ansible \
-  --fqdn "$fqdn"
-$ ansible-playbook "/tmp/enroll-ansible/playbooks/${fqdn}.yml"
-
-

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

-
+ --fqdn host.example.net
+
-
-
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  \
+        
+

Include and exclude paths

+

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

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

Dangerous harvest, encrypted at rest

+

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

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

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

+
+
+ +
+
+

Encrypted manifest bundle

+

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

+
$ enroll manifest \
+  --harvest /srv/enroll/host-1/harvest.tar.gz.sops \
+  --out /srv/enroll/host-1-manifest \
+  --sops 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
+
+$ cd /srv/enroll/host-1-manifest
+$ sops -d manifest.tar.gz.sops | tar -xzvf -
+$ cd manifest
+
+
+ +
+
+

Multi-site output

+

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

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

Targeted Ansible role runs

+

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

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

Drift report for cron or CI

+

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

+
$ enroll diff \
+  --old /srv/enroll/baseline \
+  --new /srv/enroll/current \
+  --format json \
+  --ignore-package-versions \
+  --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-format json \
-  --webhook-header 'X-Enroll-Secret: ...'  \
-   --ignore-package-versions --exit-code
-            
-
-

Use it in cron or CI to alert on change.

-
+ --webhook-header 'X-Enroll-Secret: xxxx'
+
-
-
Explain a harvest with enroll explain
-
- -
$ enroll explain /tmp/enroll-harvest
-
-# 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.

-
+
+

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

-
+
+

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
+
- -
- -
-
-
-
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/0-7-0.html b/src/content/news/0-7-0.html new file mode 100644 index 0000000..3dfdb26 --- /dev/null +++ b/src/content/news/0-7-0.html @@ -0,0 +1,62 @@ +--- +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 new file mode 100644 index 0000000..c4805c8 --- /dev/null +++ b/src/content/news/0-8-0.html @@ -0,0 +1,30 @@ +--- +title: "Enroll 0.8.0: more hardening" +date: "2026-07-14" +description: "Enroll 0.8.0 adds more hardening to prevent obscure issues." +summary: "Enroll 0.8.0 builds on the 0.7.0 release by adding several more hardening measures in an effort to keep it as safe as possible to use." +html_title: "Enroll 0.8.0 release notes" +og_title: "Enroll 0.8.0 release notes" +og_description: "Enroll 0.8.0 builds on the 0.7.0 release by adding several more hardening measures in an effort to keep it as safe as possible to use." +og_type: "article" +--- +
+
+
News
+

Enroll 0.8.0: more hardening

+

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

+
+
+ +
+
+
+

Changelog

+
    +
  • Keep sudo-created remote harvest bundles root-owned while root packages and hashes them, expose only the archive to the authenticated SSH uid, and verify the root-computed digest after download. This removes a tiny post-harvest tampering window created by recursively chowning the bundle before packaging, yet without making the plaintext archive world-readable.
  • +
  • Enforce tar member limits while lazily parsing untrusted archives rather than after `TarFile.getmembers()` has already indexed the entire archive; count repeated `.` entries and cap remote compressed downloads as well.
  • +
  • Apply aggregate byte and total filesystem-entry limits when freezing directory harvest bundles, reject symlinked bundle roots, and abort when files or discovered directories change during the copy, so direct directory inputs remain bounded and fail closed under mutation.
  • +
+
+
+
diff --git a/src/content/news/_index.html b/src/content/news/_index.html new file mode 100644 index 0000000..c3ff9a7 --- /dev/null +++ b/src/content/news/_index.html @@ -0,0 +1,15 @@ +--- +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 49f4a8f..fd3c265 100644 --- a/src/content/schema.html +++ b/src/content/schema.html @@ -1,14 +1,17 @@ --- title: "Schema" -html_title: "Enroll State Schema" -description: "JSON Schema describing the Enroll harvest state.json format." layout: "schema" +html_title: "Enroll state.json schema" +description: "The current JSON Schema for Enroll harvest state.json bundles." +og_title: "Enroll state.json schema" +og_description: "View the current Enroll JSON Schema and use it to validate harvest bundles." +og_type: "article" --- -
-
-
Schema
-

Harvest state.json schema

-

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

+
+
+
Harvest schema
+

Current state.json schema

+

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

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

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

-
-
-
-
Links
- -
+
+

Validate with the vendored schema

+
$ enroll validate ./harvest
+

Validate with this hosted schema

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

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

-
-
-
-

state.schema.json

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

Safe by default. Powerful when you opt in.

-

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

+
+
+
Security design
+

Threat model and security scope

+

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

-
-
-
-

Default behavior

-

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

-
- -
-
-
-
-

The --dangerous flag

-

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

-

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

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

    Core assumptions

    +

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

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

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

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

    What Enroll tries to defend against

    +
    +
    Accidental secret capture

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

    +
    Unsafe filesystem behavior

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

    +
    Unsafe generated Ansible

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

    +
    Risky privileged automation

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

    +
    +
    + +
    +

    What is out of scope

    +

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

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

    Trusted harvests

    +

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

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

    Useful security reports

    +

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

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

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

    +
- -
- -
-

Threat model

-
-
-
What Enroll tries to prevent
-
    -
  • Accidentally copying obvious secrets in default mode
  • -
  • Harvesting huge/unbounded file sets by mistake
  • -
  • One host's difference causing problems for other hosts 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
  • -
-
-
-
-
diff --git a/src/content/troubleshooting.html b/src/content/troubleshooting.html new file mode 100644 index 0000000..2d6155e --- /dev/null +++ b/src/content/troubleshooting.html @@ -0,0 +1,111 @@ +--- +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" +--- +
+
+
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.

+
+
+ +
+
+
+ + +
+
+

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
+
+
+
+
+
diff --git a/src/hugo.toml b/src/hugo.toml index 53735bb..81d7d83 100644 --- a/src/hugo.toml +++ b/src/hugo.toml @@ -6,4 +6,4 @@ uglyURLs = true disableKinds = ["taxonomy", "term", "RSS", "sitemap"] [params] - description = "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible roles/playbooks from what it finds. Harvest → Manifest → Manage." + description = "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible configuration-management code from what it finds. Harvest → Manifest → Manage." diff --git a/src/static/schema/state.schema.json b/src/static/schema/state.schema.json index d0bde52..c1412fb 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" @@ -228,7 +411,7 @@ }, "src_rel": { "minLength": 1, - "pattern": "^[^/].*", + "pattern": "^(?!\\.\\.?(?:/|$))[^/\\u0000-\\u001f\\\\]+(?:/(?!\\.\\.?(?:/|$))[^/\\u0000-\\u001f\\\\]+)*$", "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/news/list.html b/src/themes/enroll-theme/layouts/news/list.html new file mode 100644 index 0000000..6c0783c --- /dev/null +++ b/src/themes/enroll-theme/layouts/news/list.html @@ -0,0 +1,19 @@ +{{ define "main" }} +{{ .Content | safeHTML }} +
+
+
+ {{ range .Pages.ByDate.Reverse }} +
+
+
{{ .Date.Format "2 January 2006" }}
+

{{ .Title }}

+ {{ with .Params.summary }}

{{ . }}

{{ end }} + Read more +
+
+ {{ end }} +
+
+
+{{ end }} diff --git a/src/themes/enroll-theme/layouts/news/single.html b/src/themes/enroll-theme/layouts/news/single.html new file mode 100644 index 0000000..3a342e5 --- /dev/null +++ b/src/themes/enroll-theme/layouts/news/single.html @@ -0,0 +1,3 @@ +{{ define "main" }} +{{ .Content | safeHTML }} +{{ end }} diff --git a/src/themes/enroll-theme/layouts/partials/footer.html b/src/themes/enroll-theme/layouts/partials/footer.html index 3ab6587..fd4a981 100644 --- a/src/themes/enroll-theme/layouts/partials/footer.html +++ b/src/themes/enroll-theme/layouts/partials/footer.html @@ -3,12 +3,13 @@
- Enroll + Enroll
Enroll (a mig5 project)
CLI Ansible + SSH
-
Reverse-engineering servers into Ansible.
+
Reverse-engineering Linux hosts into Ansible.
@@ -26,25 +27,23 @@
-
Contact
+
Bugs or feature suggestions?
diff --git a/src/themes/enroll-theme/layouts/partials/head.html b/src/themes/enroll-theme/layouts/partials/head.html index f185324..8b2bd48 100644 --- a/src/themes/enroll-theme/layouts/partials/head.html +++ b/src/themes/enroll-theme/layouts/partials/head.html @@ -21,4 +21,4 @@ - + diff --git a/src/themes/enroll-theme/layouts/partials/nav.html b/src/themes/enroll-theme/layouts/partials/nav.html index 3c03651..c49b719 100644 --- a/src/themes/enroll-theme/layouts/partials/nav.html +++ b/src/themes/enroll-theme/layouts/partials/nav.html @@ -1,7 +1,7 @@