New main
All checks were successful
CI / test (push) Successful in 2m25s

This commit is contained in:
Miguel Jacq 2026-06-30 15:51:33 +10:00
parent 7e4f85b0fd
commit 42683598fa
Signed by: mig5
GPG key ID: 03906B4110AAD3B8
12 changed files with 967 additions and 1125 deletions

View file

@ -1,385 +1,127 @@
---
title: "Enroll"
html_title: "Enroll - Reverse-engineering servers into Ansible"
description: "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible roles/playbooks from what it finds. Harvest → Manifest → Manage."
og_title: "Enroll - Reverse-engineering servers into Ansible"
og_description: "Harvest a host's real configuration and turn it into Ansible roles/playbooks. Safe-by-default, with optional SOPS encryption."
html_title: "Enroll - Turn existing Linux hosts into Ansible"
description: "Enroll harvests real Linux host state and renders Ansible configuration-management output. Documentation for current 0.7 beta behavior."
og_title: "Enroll - Turn existing Linux hosts into Ansible"
og_description: "Harvest real Linux host state and render reviewable Ansible roles/playbooks."
og_type: "website"
---
<header class="hero py-5">
<div class="container py-4">
<div class="row align-items-center g-4">
<div class="col-lg-6">
<div class="kicker mb-3"><i class="bi bi-magic"></i> Reverse-engineering servers into Ansible</div>
<h1 class="display-5 fw-800 lh-1 mb-3" style="letter-spacing:-0.03em;">Get an existing Linux host into Ansible in seconds.</h1>
<p class="lead mb-4">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.</p>
<div class="kicker mb-3"><i class="bi bi-compass"></i> Current 0.7 beta documentation</div>
<h1 class="display-5 fw-800 lh-1 mb-3" style="letter-spacing:-0.03em;">Turn a real Linux host into reviewable Ansible.</h1>
<p class="lead mb-4">Enroll inspects Debian-like and RedHat-like machines, writes a harvest bundle (<code>state.json</code> plus artifacts), then renders Ansible roles and playbooks from that snapshot.</p>
<div class="d-flex flex-wrap gap-2 mb-4">
<a class="btn btn-dark btn-lg" href="#quickstart"><i class="bi bi-rocket-takeoff"></i> Quickstart</a>
<a class="btn btn-outline-dark btn-lg" href="#demos"><i class="bi bi-play-circle"></i> Watch demos</a>
<a class="btn btn-outline-secondary btn-lg" href="https://pypi.org/project/enroll/" target="_blank" rel="noreferrer"><i class="bi bi-box-seam"></i> PyPI</a>
<a class="btn btn-outline-dark btn-lg" href="docs.html"><i class="bi bi-journal-text"></i> Read docs</a>
<a class="btn btn-outline-secondary btn-lg" href="security.html"><i class="bi bi-shield-lock"></i> Security model</a>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge badge-soft rounded-pill px-3 py-2"><i class="bi bi-speedometer"></i> Super fast</span>
<span class="badge badge-soft rounded-pill px-3 py-2"><i class="bi bi-lock"></i> Optional SOPS encryption</span>
<span class="badge badge-soft rounded-pill px-3 py-2"><i class="bi bi-hdd-network"></i> Remote over SSH</span>
<span class="badge badge-soft rounded-pill px-3 py-2">Harvest → Manifest</span>
<span class="badge badge-soft rounded-pill px-3 py-2">Local or remote over SSH</span>
<span class="badge badge-soft rounded-pill px-3 py-2">Optional SOPS bundles</span>
<span class="badge badge-soft rounded-pill px-3 py-2">Ansible output</span>
</div>
</div>
<div class="col-lg-6">
<div class="hero-card p-4 p-lg-4">
<div class="hero-card p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="fw-semibold"><i class="bi bi-terminal"></i> Config in the blink of an eye</div>
<div class="small text-muted">single-shot → ansible-playbook</div>
<div class="fw-semibold"><i class="bi bi-terminal"></i> The core workflow</div>
<div class="small text-muted">dry-run before applying</div>
</div>
<div class="terminal">
<div class="mb-2"><span class="prompt">$</span> <code style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;">enroll single-shot --harvest ./harvest --out ./ansible</code></div>
<div class="mb-2"><span class="prompt">$</span> <code style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;">cd ./ansible && tree -L 2</code></div>
<pre class="mb-0" style="white-space:pre-wrap; font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.9rem;">.
├── ansible.cfg
├── playbook.yml
├── roles/
│ ├── cron/
│ ├── etc_custom/
│ ├── firewall/
│ ├── nginx/
│ ├── openssh-server/
│ ├── users/
└── README.md</pre>
<pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --out ./harvest
<span class="prompt">$</span> enroll validate ./harvest
<span class="prompt">$</span> enroll manifest --harvest ./harvest --out ./ansible
<span class="prompt">$</span> cd ./ansible
<span class="prompt">$</span> ansible-galaxy collection install -r requirements.yml
<span class="prompt">$</span> ansible-playbook -i localhost, -c local playbook.yml --check --diff</code></pre>
</div>
<div class="alert alert-warning mt-3 mb-0 small">
<strong>Trust boundary:</strong> validation proves structure and artifact consistency, not semantic safety. Only apply manifests generated from harvests whose provenance you trust.
</div>
<div class="smallprint mt-3">Tip: for multiple hosts, use <code>--fqdn</code> to generate inventory-driven, data-driven roles.</div>
</div>
</div>
</div>
</div>
</header>
<section class="py-5" id="how" aria-label="How it works">
<section class="py-5" id="how">
<div class="container">
<div class="row g-4 align-items-start">
<div class="col-lg-5">
<h2 class="section-title display-6 fw-bold mb-3">A simple mental model</h2>
<p class="mb-4">Enroll is built around two phases, plus optional drift and reporting tools:</p>
<div class="d-flex flex-column gap-3">
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-bucket"></i></div>
<div>
<div class="fw-semibold">Harvest</div>
<div class="text-muted">Collect host facts + relevant files into a bundle.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-diagram-3"></i></div>
<div>
<div class="fw-semibold">Manifest</div>
<div class="text-muted">Render Ansible roles &amp; playbooks from the harvest.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-plus-slash-minus"></i></div>
<div>
<div class="fw-semibold">Diff</div>
<div class="text-muted">Compare two harvests and notify via webhook/email.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-patch-question"></i></div>
<div>
<div class="fw-semibold">Explain</div>
<div class="text-muted">Analyze what's included/excluded in the harvest and why.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-check-square"></i></div>
<div>
<div class="fw-semibold">Validate</div>
<div class="text-muted">Confirm that a harvest isn't corrupt or lacking artifacts.</div>
</div>
</div>
<h2 class="display-6 fw-bold section-title mb-3">What Enroll does today</h2>
<p class="text-secondary">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.</p>
<div class="callout p-4 mt-4">
<div class="fw-semibold mb-2">Current output target</div>
<p class="small text-secondary mb-0">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 <code>README.md</code>.</p>
</div>
</div>
<div class="col-lg-7">
<div class="row g-3">
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Safe-by-default harvesting</div>
<div class="text-muted">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.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Multi-site without "shared role broke host2"</div>
<div class="text-muted">In <code>--fqdn</code> mode, roles are data-driven and host inventory decides what gets managed per host.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Remote over SSH</div>
<div class="text-muted">Harvest a remote host from your workstation, then manifest Ansible output locally.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Encrypt bundles at rest</div>
<div class="text-muted">Use <code>--sops</code> to store harvests/manifests as a single encrypted <code>.tar.gz.sops</code> file (GPG) for safer long-term storage as a DR strategy.</div>
</div>
</div>
</div>
</div>
<div class="callout p-4 mt-3">
<div class="fw-semibold mb-2"><i class="bi bi-lightning-charge"></i> Why sysadmins like it</div>
<div class="row g-3">
<div class="col-md-6 text-muted">• Rapid enrolling of existing infra into config management<br>• Tweak include/exclude paths as needed</div>
<div class="col-md-6 text-muted">• Capture what changed from package defaults<br><code>diff</code> mode detects and alerts about drift</div>
</div>
<div class="col-md-6"><div class="feature-card p-4 h-100"><div class="fw-semibold mb-1">Harvest</div><div class="text-muted">Collect package, service, user, file, runtime, Flatpak/Snap, and container-image evidence into a private bundle.</div></div></div>
<div class="col-md-6"><div class="feature-card p-4 h-100"><div class="fw-semibold mb-1">Validate</div><div class="text-muted">Check <code>state.json</code>, the vendored JSON schema, referenced artifacts, and orphaned files before rendering or CI use.</div></div></div>
<div class="col-md-6"><div class="feature-card p-4 h-100"><div class="fw-semibold mb-1">Manifest</div><div class="text-muted">Freeze the harvest, warn about semantic trust, then render Ansible. Use <code>--fqdn</code> for inventory-driven multi-host output.</div></div></div>
<div class="col-md-6"><div class="feature-card p-4 h-100"><div class="fw-semibold mb-1">Diff, explain, notify</div><div class="text-muted">Compare harvests, suppress noisy package-version or path drift, produce text/Markdown/JSON, and optionally send webhooks or email.</div></div></div>
</div>
</div>
</div>
</div>
</section>
<section class="py-5 bg-light" id="quickstart" aria-label="Quickstart">
<section class="py-5 bg-light" id="quickstart">
<div class="container">
<div class="row align-items-end g-3 mb-3">
<div class="col-lg-7">
<h2 class="section-title display-6 fw-bold mb-2">Quickstart</h2>
</div>
<div class="col-lg-5 text-lg-end">
<a class="btn btn-outline-dark" href="https://git.mig5.net/mig5/enroll#readme" target="_blank" rel="noreferrer"><i class="bi bi-journal-text"></i> Full README</a>
<div class="col-lg-8">
<h2 class="display-6 fw-bold section-title mb-2">Quickstart paths</h2>
<p class="text-secondary mb-0">Choose the shape that matches the job: one host, remote harvest, or a shared multi-host Ansible tree.</p>
</div>
<div class="col-lg-4 text-lg-end"><a class="btn btn-outline-dark" href="examples.html">More examples</a></div>
</div>
<ul class="nav nav-pills gap-2" id="qsTabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" id="qs1-tab" data-bs-toggle="tab" data-bs-target="#qs1" type="button" role="tab">Local</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs2-tab" data-bs-toggle="tab" data-bs-target="#qs2" type="button" role="tab">Remote</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs3-tab" data-bs-toggle="tab" data-bs-target="#qs3" type="button" role="tab">Multi-site</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs4-tab" data-bs-toggle="tab" data-bs-target="#qs4" type="button" role="tab">Diff</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs5-tab" data-bs-toggle="tab" data-bs-target="#qs5" type="button" role="tab">Explain</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs6-tab" data-bs-toggle="tab" data-bs-target="#qs6" type="button" role="tab">Validate</button></li>
<li class="nav-item" role="presentation"><button class="nav-link active" id="qs-local-tab" data-bs-toggle="tab" data-bs-target="#qs-local" type="button" role="tab">Local</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs-remote-tab" data-bs-toggle="tab" data-bs-target="#qs-remote" type="button" role="tab">Remote</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs-fqdn-tab" data-bs-toggle="tab" data-bs-target="#qs-fqdn" type="button" role="tab">Multi-site</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs-drift-tab" data-bs-toggle="tab" data-bs-target="#qs-drift" type="button" role="tab">Drift</button></li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="qs1" role="tabpanel" aria-labelledby="qs1-tab">
<div class="row g-3">
<div class="col-lg-6">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs1code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs1code"># 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</code></pre>
</div>
</div>
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Good for</div>
<div class="text-muted">Disaster recovery snapshots, "make this one host reproducible", and carving a golden role set you'll refine over time.</div>
<hr>
<div class="smallprint">Want templates for structured configs? Install <a href="https://git.mig5.net/mig5/jinjaturtle" target="_blank" rel="noopener noreferrer">JinjaTurtle</a> and use <code>--jinjaturtle</code> (or let it auto-detect).</div>
</div>
</div>
</div>
<div class="tab-pane fade show active" id="qs-local" role="tabpanel">
<div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs-local-code"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="qs-local-code">enroll harvest --out ./harvest
enroll manifest --harvest ./harvest --out ./ansible
cd ./ansible
ansible-playbook -i localhost, -c local playbook.yml --check --diff</code></pre></div>
</div>
<div class="tab-pane fade" id="qs2" role="tabpanel" aria-labelledby="qs2-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs2code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs2code"># 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</code></pre>
</div>
<div class="smallprint mt-3">If you don't want/need sudo on the remote host, add <code>--no-sudo</code> (expect a less complete harvest). For remote sudo prompts use <code>--ask-become-pass</code>/<code>-K</code>. If your SSH private key is encrypted, use <code>--ask-key-passphrase</code> (interactive) or <code>--ssh-key-passphrase-env ENV_VAR</code> (non-interactive/CI).</div>
<div class="tab-pane fade" id="qs-remote" role="tabpanel">
<div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs-remote-code"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="qs-remote-code">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</code></pre></div>
</div>
<div class="tab-pane fade" id="qs3" role="tabpanel" aria-labelledby="qs3-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs3code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs3code"># 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</code></pre>
</div>
<div class="smallprint mt-3">Rule of thumb: single-site for "one server, easy-to-read roles"; <code>--fqdn</code> for "many servers, high abstraction, fast adoption".</div>
<div class="tab-pane fade" id="qs-fqdn" role="tabpanel">
<div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs-fqdn-code"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="qs-fqdn-code">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</code></pre></div>
</div>
<div class="tab-pane fade" id="qs4" role="tabpanel" aria-labelledby="qs4-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs4code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs4code"># 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</code></pre>
</div>
<div class="smallprint mt-3">E-mail notifications are also supported. Run it on a systemd timer to alert to drift!</div>
<div class="tab-pane fade" id="qs-drift" role="tabpanel">
<div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs-drift-code"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="qs-drift-code">enroll diff --old ./harvest-golden --new ./harvest-current \
--format markdown --ignore-package-versions --exclude-path /var/anacron</code></pre></div>
</div>
<div class="tab-pane fade" id="qs5" role="tabpanel" aria-labelledby="qs5-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs5code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs5code"># 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
</code></pre>
</div>
<div class="smallprint mt-3">'explain' tells you why something was included, but also why something was <em>excluded</em>.</div>
</div>
<div class="tab-pane fade" id="qs6" role="tabpanel" aria-labelledby="qs6-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs6code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs5code"># 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
</code></pre>
</div>
<div class="smallprint mt-3">'validate' makes sure the harvest's state confirms to Enroll's <a href="/schema.html">state schema</a>, 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.</div>
</div>
</div>
</div>
</section>
<section class="py-5" id="demos" aria-label="Demos">
<section class="py-5">
<div class="container">
<div class="row g-3 align-items-end mb-3">
<div class="col-lg-8">
<h2 class="section-title display-6 fw-bold mb-2">Demonstrations</h2>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Harvest</div>
<div class="text-muted mb-3">Collect state into a bundle.</div>
<div class="asciicast" data-asciinema-id="765203"><script src="https://asciinema.org/a/765203.js" id="asciicast-765203" async="true"></script></div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Manifest</div>
<div class="text-muted mb-3">Render Ansible roles/playbooks.</div>
<div class="asciicast" data-asciinema-id="765204"><script src="https://asciinema.org/a/765204.js" id="asciicast-765204" async="true"></script></div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Single-shot</div>
<div class="text-muted mb-3">Harvest → Manifest in one command.</div>
<div class="asciicast" data-asciinema-id="765127"><script src="https://asciinema.org/a/765127.js" id="asciicast-765127" async="true"></script></div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Diff</div>
<div class="text-muted mb-3">Drift report + webhook/email notifications, or optionally <em>enforce</em> the previous state!</div>
<div class="asciicast" data-asciinema-id="767081"><script src="https://asciinema.org/a/767081.js" id="asciicast-767081" async="true"></script></div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="py-5 bg-light" id="install" aria-label="Install">
<div class="container">
<div class="row g-3 align-items-end mb-3">
<div class="col-lg-8">
<h2 class="section-title display-6 fw-bold mb-2">Install</h2>
<p class="text-muted mb-0">Use your preferred packaging. An AppImage is also available.</p>
</div>
<div class="col-lg-4 text-lg-end">
<a class="btn btn-outline-dark" href="https://git.mig5.net/mig5/enroll#install" target="_blank" rel="noreferrer"><i class="bi bi-link-45deg"></i>Other install steps</a>
</div>
</div>
<ul class="nav nav-pills gap-2" id="installTabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#inst1" type="button" role="tab">Debian/Ubuntu</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#inst2" type="button" role="tab">Fedora</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#inst3" type="button" role="tab">Pip</button></li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="inst1" role="tabpanel">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst1code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst1code">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</code></pre>
</div>
</div>
<div class="tab-pane fade" id="inst2" role="tabpanel">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst2code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst2code">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</code></pre>
</div>
</div>
<div class="tab-pane fade" id="inst3" role="tabpanel">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst3code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst3code">pip install enroll
# or: pipx install enroll</code></pre>
</div>
</div>
<div class="row g-4">
<div class="col-lg-4"><div class="feature-card p-4 h-100"><h3 class="h5 fw-bold">Single-site by default</h3><p class="text-secondary mb-0">Without <code>--fqdn</code>, Enroll renders self-contained roles where role files live under <code>roles/&lt;role&gt;/files</code> and variables live in <code>defaults/main.yml</code>.</p></div></div>
<div class="col-lg-4"><div class="feature-card p-4 h-100"><h3 class="h5 fw-bold">Multi-site with <code>--fqdn</code></h3><p class="text-secondary mb-0">With <code>--fqdn</code>, shared role tasks stay reusable while host-specific files and variables live under <code>inventory/host_vars/&lt;fqdn&gt;</code>. <code>--fqdn</code> implies one-role-per-package/unit behavior.</p></div></div>
<div class="col-lg-4"><div class="feature-card p-4 h-100"><h3 class="h5 fw-bold">Security-first rendering</h3><p class="text-secondary mb-0">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 <code>!unsafe</code> data.</p></div></div>
</div>
</div>
</section>

View file

@ -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."
---
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-diagram-3"></i> Development notes</div>
<h1 class="display-6 fw-bold mb-2">How the codebase is structured</h1>
<p class="lead mb-0">A compact web version of the current development guide for contributors and reviewers.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Runtime flow</h2><p class="text-secondary">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.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Harvest order</h2><p class="text-secondary mb-0">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 <code>/etc</code>, <code>/usr/local</code>, and extra paths. Parent directories and package inventory are appended before writing <code>state.json</code>.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Global de-duplication</h2><p class="text-secondary mb-0"><code>captured_global</code> 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.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Backends</h2><p class="text-secondary mb-0"><code>DpkgBackend</code> and <code>RpmBackend</code> implement ownership lookup, manual package listing, installed inventory, <code>/etc</code> indexing, modified config detection, and package-manager config exclusions. Add new backends behind <code>platform.get_backend()</code>.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Renderer-neutral model</h2><p class="text-secondary mb-0">Harvest snapshots are converted into <code>CMModule</code> objects before Ansible rendering. Common role grouping can combine package and systemd modules by Debian Section/RPM Group unless <code>--no-common-roles</code> or <code>--fqdn</code> is used.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Ansible renderer</h2><p class="text-secondary mb-0">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.</p></div></div>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-3">Collector map</h2>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead><tr><th>Collector</th><th>Purpose</th><th>Typical roles/snapshots</th></tr></thead>
<tbody>
<tr><td><code>RuntimeStateCollector</code></td><td>Root-only runtime sysctl and firewall fallback snapshots.</td><td><code>sysctl</code>, <code>firewall_runtime</code>, <code>enroll_runtime</code></td></tr>
<tr><td><code>CronLogrotateCollector</code></td><td>Centralises cron and logrotate before package/service roles claim files.</td><td><code>cron</code>, <code>logrotate</code></td></tr>
<tr><td><code>ServicePackageCollector</code></td><td>Enabled systemd services, related packages, config, drop-ins, env files, and enablement symlinks.</td><td>service and package roles</td></tr>
<tr><td><code>UsersCollector</code></td><td>Non-system users, authorized keys, safe SSH material, shell dotfiles in dangerous mode, Flatpak/Snap discovery.</td><td><code>users</code>, <code>flatpak</code>, <code>snap</code></td></tr>
<tr><td><code>ContainerImagesCollector</code></td><td>Docker/Podman image metadata, digest-pinned pull references, tag aliases, notes for non-reproducible local tags.</td><td><code>container_images</code></td></tr>
<tr><td><code>PackageManagerConfigCollector</code></td><td>APT/DNF/YUM repo and keyring configuration.</td><td><code>apt_config</code>, <code>dnf_config</code></td></tr>
<tr><td><code>UsrLocalCustomCollector</code></td><td><code>/usr/local/etc</code> and executable scripts under <code>/usr/local/bin</code>.</td><td><code>usr_local_custom</code></td></tr>
<tr><td><code>ExtraPathsCollector</code></td><td>User requested include/exclude path capture.</td><td><code>extra_paths</code></td></tr>
</tbody>
</table>
</div>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-3">Testing focus</h2>
<p class="text-secondary mb-0">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.</p>
</div>
</div>
</main>

View file

@ -1,578 +1,182 @@
---
title: "Docs"
html_title: "Enroll Docs"
description: "How Enroll works: harvest, manifest, modes, and configuration."
title: "Documentation"
html_title: "Enroll Documentation"
description: "Current user documentation for Enroll harvest, manifest, diff, explain, validate, SOPS, remote mode, and Ansible output."
---
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-book"></i> Documentation</div>
<h1 class="display-6 fw-bold mb-2">How Enroll works</h1>
<p class="lead mb-0">The mental model, output modes, and the knobs you'll actually use day-to-day.</p>
<div class="kicker mb-3"><i class="bi bi-journal-text"></i> Documentation</div>
<h1 class="display-6 fw-bold mb-2">Current Enroll behavior</h1>
<p class="lead mb-0">This page describes how the 0.7 beta CLI works today: what it captures, how it renders Ansible, and where the trust boundaries are.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-3">
<div class="position-sticky" style="top: 96px;">
<div class="list-group">
<a class="list-group-item list-group-item-action" href="#model">Mental model</a>
<a class="list-group-item list-group-item-action" href="#harvest">How harvesting works</a>
<a class="list-group-item list-group-item-action" href="#schema">State schema</a>
<a class="list-group-item list-group-item-action" href="#modes">Single-site vs multi-site</a>
<a class="list-group-item list-group-item-action" href="#remote">Remote harvesting</a>
<a class="list-group-item list-group-item-action" href="#templates">JinjaTurtle templates</a>
<a class="list-group-item list-group-item-action" href="#config">Config file</a>
<a class="list-group-item list-group-item-action" href="#diff">Drift detection with <code>enroll diff</code></a>
<a class="list-group-item list-group-item-action" href="#explain">Why did Enroll include/exclude something? <code>enroll explain</code></a>
<a class="list-group-item list-group-item-action" href="#tips">Tips</a>
</div>
<div class="mt-3 small text-secondary">
Prefer the canonical docs?
<a class="link-secondary" href="https://git.mig5.net/mig5/enroll#readme" target="_blank" rel="noreferrer">README on Forgejo</a>.
</div>
<aside class="col-lg-3">
<div class="callout p-4 position-sticky" style="top: 96px;">
<div class="fw-semibold mb-2">On this page</div>
<ul class="small mb-0">
<li><a href="#mental-model">Mental model</a></li>
<li><a href="#harvest">Harvest</a></li>
<li><a href="#manifest">Manifest</a></li>
<li><a href="#modes">Single-site vs multi-site</a></li>
<li><a href="#diff">Diff</a></li>
<li><a href="#explain-validate">Explain and validate</a></li>
<li><a href="#config">Configuration file</a></li>
<li><a href="#install">Install</a></li>
</ul>
</div>
</div>
</aside>
<div class="col-lg-9">
<section id="model" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Mental model</h2>
<p class="text-secondary">Enroll is intentionally simple: it collects facts first, then renders Ansible from those facts.</p>
<div class="row g-3">
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="d-flex align-items-center gap-3 mb-2">
<div class="icon-pill"><i class="bi bi-archive"></i></div>
<div>
<div class="fw-semibold">1) Harvest</div>
<div class="small text-secondary">Snapshot state into a bundle</div>
</div>
</div>
<ul class="mb-0 small">
<li>Detect installed packages and services</li>
<li>Collect config that deviates from packaged defaults (where possible)</li>
<li>Grab relevant custom/unowned files in service dirs</li>
<li>Capture non-system users & SSH public keys, .bashrc files etc</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="d-flex align-items-center gap-3 mb-2">
<div class="icon-pill"><i class="bi bi-boxes"></i></div>
<div>
<div class="fw-semibold">2) Manifest</div>
<div class="small text-secondary">Generate an Ansible repo structure</div>
</div>
</div>
<ul class="mb-0 small">
<li>Roles with files/templates and defaults</li>
<li>Playbooks to apply the captured state</li>
<li>Optional inventory structure for multi-host runs: each host gets its own playbook</li>
</ul>
</div>
</div>
</div>
<div class="terminal mt-4">
<div class="small text-secondary mb-2">Typical flow</div>
<pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
<span class="prompt">$</span> ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml</code></pre>
<section id="mental-model" class="scroll-mt-nav">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3">Mental model</h2>
<p>Enroll has two main phases:</p>
<ol>
<li><strong>Harvest</strong> collects host facts and selected files into a harvest bundle containing <code>state.json</code> and <code>artifacts/</code>.</li>
<li><strong>Manifest</strong> converts that bundle into Ansible configuration-management output.</li>
</ol>
<p class="mb-0">Additional commands help you compare harvests (<code>diff</code>), explain why things were included or skipped (<code>explain</code>), and validate bundle structure (<code>validate</code>).</p>
</div>
</section>
<section id="harvest" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">How harvesting works</h2>
<p class="text-secondary">At a high level, this is what happens when <code>enroll harvest</code> runs on a host:</p>
<ul class="mb-0 small">
<li>Detects the OS and its package backend (e.g dpkg vs rpm)</li>
<li>Detects what packages are installed</li>
<li>For each package, it tries to detect files in <code>/etc</code> that have been modified from the default that get shipped with the package.</li>
<li>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)</li>
<li>Aside from known packages already learned, it optimistically tries to capture extra system configuration in <code>/etc</code> that is common for config management. This is stuff like the apt or dnf configuration, crons, logrotate configs, networking settings, hosts files, etc.</li>
<li>For applications that commonly make use of symlinks (think Apache2 or Nginx's <code>sites-enabled</code> or <code>mods-enabled</code>), it notes what symlinks exist so that it can capture those in Ansible</li>
<li>It also looks for other snowflake stuff in <code>/etc</code> not associated with packages/services or other typical system config, and will put these into an <code>etc_custom</code> role.</li>
<li>Likewise, it looks in <code>/usr/local</code> for stuff, on the assumption that this is an area that custom apps/configs might've been placed in. These go into a <code>usr_local_custom</code> role.</li>
<li>It captures non-system user accounts, their group memberships and files such as their <code>.ssh/authorized_keys</code>, and <code>.bashrc</code>, <code>.profile</code>, <code>.bash_aliases</code>, <code>.bash_logout</code> if these files differ from the <code>skel</code> defaults</li>
<li>It takes into account anything the user set with <code>--exclude-path</code> or <code>--include-path</code>. For anything extra that is included, it will put these into an '<code>extra_paths</code>' role. The location could be anywhere e.g something in <code>/opt</code>, <code>/srv</code>, whatever you want.</li>
<li>It writes the state.json and captures the artifacts.</li>
</ul>
<br />
<p class="text-secondary">Other things to be aware of:</p>
<ul class="mb-0 small">
<li>You can use multiple invocations of <code>--exclude-path</code> 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 <code>enroll manifest</code>.</li>
<li>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 <code>--include-path</code> and use <code>--dangerous</code>, it will skip some policy statements such as what types of content to ignore.</li>
<li>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 <code>/etc</code>, <code>/usr/local/etc</code> and <code>/usr/local/bin</code>, and 500 files per 'role'), to avoid unintentional 'runaway' situations.</li>
<li>If you are using the 'remote' mode to harvest, and your remote user requires a password for sudo, you can pass in <code>--ask-become-pass</code> (or <code>-K</code>) 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.</li>
<li>If your SSH private key is encrypted, use <code>--ask-key-passphrase</code> to prompt up-front, or <code>--ssh-key-passphrase-env ENV_VAR</code> 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.)</li>
</ul>
<div class="alert alert-secondary mt-3 mb-0">
<div class="fw-semibold">Does Enroll use Ansible community/galaxy roles?</div>
<div class="small mb-0">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.</div>
<br />
<div class="small mb-0">Keep in mind that a lot of software config files are also good candidates for being Jinja templates with abstracted vars for separate hosts.</div>
<br />
<div class="small mb-0">Enroll does use my companion tool <a href="https://git.mig5.net/mig5/jinjaturtle" target="_blank" rel="noopener noreferrer">JinjaTurtle</a> 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 <code>ansible.builtin.copy</code> in role tasks.</div>
<section id="harvest" class="scroll-mt-nav mt-4">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3"><code>enroll harvest</code></h2>
<p>Harvest writes a local bundle. If <code>--out</code> is omitted, Enroll creates a private randomized cache directory under the users Enroll cache. If <code>--out</code> is supplied for plaintext output, the final output directory must be new and safe; Enroll creates it with private permissions.</p>
<h3 class="h5 fw-bold mt-4">High-level capture scope</h3>
<div class="row g-3">
<div class="col-md-6"><ul class="mb-0"><li>Manual packages and installed package inventory</li><li>Enabled systemd services and relevant package ownership</li><li>Modified package-owned config where the backend can prove drift</li><li>Service-relevant unowned files under matching <code>/etc</code> trees</li><li>Central cron and logrotate snippets</li></ul></div>
<div class="col-md-6"><ul class="mb-0"><li>Non-system users and SSH public/authorized key material</li><li>Package-manager config: <code>apt_config</code> or <code>dnf_config</code></li><li>Miscellaneous <code>/etc</code> and <code>/usr/local</code> custom files</li><li>Runtime sysctl and firewall fallback state where appropriate</li><li>Flatpak, Snap, Docker, and Podman image evidence</li></ul></div>
</div>
<h3 class="h5 fw-bold mt-4">Path selection</h3>
<p><code>--include-path</code> adds extra files or directories to the <code>extra_paths</code> role. <code>--exclude-path</code> removes matching paths from all harvesting, including defaults. Excludes win over includes.</p>
<div class="terminal"><pre class="mb-0"><code>enroll harvest --out ./harvest \
--include-path '/home/*/.profile' \
--exclude-path '/home/*/.ssh/**' \
--exclude-path 're:^/var/cache/myapp/.*$'</code></pre></div>
<p class="small text-secondary mt-3 mb-0">Pattern styles are plain paths, inferred or explicit globs such as <code>glob:/path/**/*.conf</code>, and regexes prefixed with <code>re:</code> or <code>regex:</code>. Include expansion ignores symlinks, respects caps, and reports unmatched/capped paths as notes.</p>
</div>
</section>
<section id="remote" class="scroll-mt-nav mt-4">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3">Remote harvesting</h2>
<p>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.</p>
<div class="terminal"><pre class="mb-0"><code>enroll harvest --remote-host app01.example.net --remote-user mig --out ./harvest-app01</code></pre></div>
<ul class="mt-3 mb-0">
<li><code>--remote-ssh-config [path]</code> reads OpenSSH-style settings. With no value, it defaults to <code>~/.ssh/config</code>.</li>
<li>Supported ssh config fields include <code>HostName</code>, <code>User</code>, <code>Port</code>, <code>IdentityFile</code>, <code>ProxyCommand</code>, and <code>HostKeyAlias</code> where supported.</li>
<li><code>-K</code> / <code>--ask-become-pass</code> prompts for remote sudo. <code>--no-sudo</code> avoids sudo and produces a more limited harvest.</li>
<li><code>--ask-key-passphrase</code> prompts for an encrypted SSH key. <code>--ssh-key-passphrase-env ENV_VAR</code> reads it from an environment variable for CI.</li>
</ul>
</div>
</section>
<section id="schema" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">State schema</h2>
<p class="text-secondary">Enroll writes a <code>state.json</code> file describing what was harvested. The canonical definition of that file format is the JSON Schema below.</p>
<p class="text secondary">You can also validate a harvest state file against the schema by using <code>enroll validate /path/to/harvest</code>.</p>
<section id="manifest" class="scroll-mt-nav mt-4">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3"><code>enroll manifest</code></h2>
<p>Manifest validates the harvest, freezes directory bundles into a private temporary copy, prints the semantic-safety warning, and writes Ansible output.</p>
<div class="terminal"><pre class="mb-0"><code>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</code></pre></div>
<h3 class="h5 fw-bold mt-4">Output contents</h3>
<ul>
<li><code>playbook.yml</code> in single-site mode, or <code>playbooks/&lt;fqdn&gt;.yml</code> plus inventory in multi-site mode.</li>
<li><code>roles/&lt;role&gt;/tasks/main.yml</code>, with handlers where needed.</li>
<li>Harvested files in role <code>files/</code>, generated templates in role <code>templates/</code>, and variables in <code>defaults/main.yml</code> or host vars.</li>
<li><code>requirements.yml</code> for Ansible collections such as container/Flatpak/Snap support when required.</li>
<li>A generated <code>README.md</code> summarising layout, roles, excluded paths, notes, and dry-run commands.</li>
</ul>
<p class="mb-0">Every generated role has a tag named <code>role_&lt;role_name&gt;</code>. Use tags to dry-run or apply a narrow slice, for example <code>--tags role_users,role_services</code>.</p>
</div>
</section>
<div class="callout p-4">
<div class="d-flex flex-wrap gap-2 align-items-center">
<a class="btn btn-sm btn-dark" href="schema.html"><i class="bi bi-eye"></i> View formatted schema</a>
<a class="btn btn-sm btn-outline-dark" href="schema/state.schema.json"><i class="bi bi-braces"></i> state.schema.json</a>
<section id="modes" class="scroll-mt-nav mt-4">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3">Output modes</h2>
<div class="row g-3">
<div class="col-md-6"><div class="callout p-3 h-100"><h3 class="h5 fw-bold">Single-site: default</h3><p class="small text-secondary mb-0">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.</p></div></div>
<div class="col-md-6"><div class="callout p-3 h-100"><h3 class="h5 fw-bold">Multi-site: <code>--fqdn</code></h3><p class="small text-secondary mb-0">Use this for several existing servers. Generic shared roles read host-specific variables and per-host files under <code>inventory/host_vars/&lt;fqdn&gt;</code>.</p></div></div>
</div>
<p class="mt-3 mb-0">By default, Enroll groups many package and systemd-unit roles into common Debian Section/RPM Group roles. <code>--no-common-roles</code> preserves one generated role per package/unit. <code>--fqdn</code> implies this behavior to avoid cross-host coupling.</p>
</div>
</section>
<section id="sops" class="scroll-mt-nav mt-4">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3">SOPS encrypted bundles</h2>
<p><code>--sops &lt;GPG_FINGERPRINT...&gt;</code> 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.</p>
<div class="terminal"><pre class="mb-0"><code>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</code></pre></div>
<p class="small text-secondary mt-3 mb-0">When manifest output is encrypted, decrypt and extract it before running <code>ansible-playbook</code>.</p>
</div>
</section>
<section id="diff" class="scroll-mt-nav mt-4">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3"><code>enroll diff</code></h2>
<p>Diff compares two harvests and reports package, service, user, and managed-file drift. Inputs may be directories, <code>state.json</code> paths, tarballs, or SOPS bundles when <code>--sops</code> is set.</p>
<div class="terminal"><pre class="mb-0"><code>enroll diff --old ./harvest-a --new ./harvest-b --format markdown \
--ignore-package-versions --exclude-path /var/anacron --exit-code</code></pre></div>
<p class="mt-3">Notification options include <code>--webhook</code>, <code>--webhook-format json|text|markdown</code>, repeated <code>--webhook-header 'K: V'</code>, repeated <code>--email-to</code>, SMTP settings, and <code>--notify-always</code>.</p>
<p class="mb-0"><code>--exit-code</code> returns status 2 when differences are detected, which is useful for cron, CI, or monitoring wrappers.</p>
</div>
</section>
<section id="explain-validate" class="scroll-mt-nav mt-4">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3">Explain and validate</h2>
<div class="row g-3">
<div class="col-md-6"><div class="callout p-3 h-100"><h3 class="h5 fw-bold"><code>enroll explain</code></h3><p class="small text-secondary">Summarises inventory, roles, included reasons, excluded reasons, and examples. Supports <code>--format text|json</code>, <code>--max-examples</code>, tarballs, directories, <code>state.json</code>, and SOPS bundles.</p><pre class="terminal small mb-0"><code>enroll explain ./harvest --format json --max-examples 20</code></pre></div></div>
<div class="col-md-6"><div class="callout p-3 h-100"><h3 class="h5 fw-bold"><code>enroll validate</code></h3><p class="small text-secondary">Checks JSON validity, schema compliance, missing referenced artifacts, and orphaned artifact files. Supports <code>--schema</code>, <code>--allow-remote-schema</code>, <code>--no-schema</code>, <code>--fail-on-warnings</code>, JSON output, and report files.</p><pre class="terminal small mb-0"><code>enroll validate ./harvest --format json --out validate.json</code></pre></div></div>
</div>
</div>
</section>
<section id="modes" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Single-site vs multi-site</h2>
<p class="text-secondary">Manifest output has two styles. Choose based on how you'll use the result.</p>
<div class="row g-3">
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-house"></i> Single-site (default)</div>
<p class="small mb-2">Best when you are enrolling one host, or you're producing a reusable "golden" role set that could be applied anywhere.</p>
<ul class="small mb-0">
<li>Roles are self-contained</li>
<li>Raw files live in each role's <code>files/</code></li>
<li>Template variables live in <code>defaults/main.yml</code></li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-diagram-3"></i> Multi-site (<code>--fqdn</code>)</div>
<p class="small mb-2">Best when you want to enroll several existing servers quickly, especially if they differ.</p>
<ul class="small mb-0">
<li>Roles are shared; raw files live in host-specific inventory</li>
<li>Inventory decides what gets managed on each host (files/packages/services)</li>
<li>Non-templated files go under <code>inventory/host_vars/&lt;fqdn&gt;/&lt;role&gt;/.files</code></li>
</ul>
</div>
</div>
</div>
<div class="terminal mt-4">
<div class="small text-secondary mb-2">Multi-site example</div>
<pre class="mb-0"><code><span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$(hostname -f)"
<span class="prompt">$</span> ansible-playbook /tmp/enroll-ansible/playbooks/"$(hostname -f)".yml</code></pre>
</div>
<div class="alert alert-secondary mt-3 mb-0">
<div class="fw-semibold">Tip: role tags</div>
<div class="small mb-0">Generated playbooks tag each role as <code>role_&lt;name&gt;</code> (e.g. <code>role_users</code>, <code>role_services</code>, <code>role_other</code>). You can target a subset with <code>ansible-playbook ... --tags role_users</code>.</div>
</div>
</section>
<section id="remote" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Remote harvesting over SSH</h2>
<p class="text-secondary">Run Enroll on your workstation, harvest a remote host over SSH. The harvest is pulled locally.</p>
<div class="terminal">
<pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-harvest
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-manifest
# Alternatively, run both commands combined together with the 'single-shot' mode:
<span class="prompt">$</span> 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:
<span class="prompt">$</span> enroll harvest --remote-host myhost.example.com --remote-user myuser \
--ask-key-passphrase --out /tmp/enroll-harvest
<span class="prompt">$</span> export ENROLL_SSH_KEY_PASSPHRASE='correct horse battery staple'
<span class="prompt">$</span> enroll harvest --remote-host myhost.example.com --remote-user myuser \
--ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE --out /tmp/enroll-harvest</code></pre>
</div>
<div class="alert alert-secondary mt-3">
<div class="fw-semibold">Tip</div>
<div class="small mb-0">If you don't want/need sudo on the remote side, add <code>--no-sudo</code>. However, be aware that you may get a more limited harvest depending on permissions.</div>
<br />
<div class="small mb-0">If your remote user requires a password for sudo, pass <code>--ask-become-pass</code> or <code>-K</code> 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.</div>
<br />
<div class="small mb-0">If your SSH private key is encrypted, use <code>--ask-key-passphrase</code> to prompt up-front. For non-interactive/CI runs, use <code>--ssh-key-passphrase-env ENV_VAR</code>. If neither is set and Enroll detects an encrypted key in an interactive session, it'll still prompt on-demand.</div>
<br />
<div class="small mb-0">If your remote host requires additional SSH configuration that you've defined in your <code>~/.ssh/config</code>, pass <code>--remote-ssh-config ~/.ssh/config</code>. Enroll will understand how to translate the Host alias, IdentityFile, ProxyCommand, ConnectTimeout and AddressFamily values. You must still pass a value for <code>--remote-host</code> that matches the <code>Host</code> value of the entry in the SSH config file.</div>
</div>
</section>
<section id="templates" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">JinjaTurtle integration</h2>
<p class="text-secondary">If <a href="https://git.mig5.net/mig5/jinjaturtle" target="_blank" rel="noopener noreferrer">JinjaTurtle</a> (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.</p>
<div class="row g-3">
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Modes</div>
<ul class="small mb-0">
<li><code>--jinjaturtle</code> to force on</li>
<li><code>--no-jinjaturtle</code> to force off</li>
<li>Default is auto</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Where variables land</div>
<ul class="small mb-0">
<li>Single-site: <code>roles/&lt;role&gt;/defaults/main.yml</code></li>
<li>Multi-site: <code>inventory/host_vars/&lt;fqdn&gt;/&lt;role&gt;.yml</code></li>
</ul>
</div>
</div>
</div>
</section>
<section id="config" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">INI config file</h2>
<p class="text-secondary">If you're repeating flags (include/exclude patterns, SOPS settings, etc.), store defaults in <code>enroll.ini</code> and keep your muscle memory intact.</p>
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Discovery order</div>
<div class="small text-secondary mb-0">You can pass <code>-c/--config</code>, set <code>ENROLL_CONFIG</code>, or let Enroll auto-discover <code>./enroll.ini</code>, <code>./.enroll.ini</code>, or <code>~/.config/enroll/enroll.ini</code>.</div>
</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ini-example"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ini-example">[enroll]
# (future global flags may live here)
[harvest]
dangerous = false
<section id="config" class="scroll-mt-nav mt-4">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3">Configuration file</h2>
<p>Enroll can read INI-style defaults from an explicit <code>--config</code> path, <code>ENROLL_CONFIG</code>, <code>$XDG_CONFIG_HOME/enroll/enroll.ini</code>, or <code>~/.config/enroll/enroll.ini</code>. It no longer reads <code>.enroll.ini</code> from the current working directory.</p>
<p>Precedence is: explicit CLI flags, subcommand/global INI defaults, then argparse defaults. Use underscores for INI keys that are hyphenated on the CLI.</p>
<div class="terminal"><pre class="mb-0"><code>[harvest]
include_path =
/home/*/.bashrc
/home/*/.profile
exclude_path = /usr/local/bin/docker-*, /usr/local/bin/some-tool
# remote_host = yourserver.example.com
# remote_user = you
# remote_port = 2222
[manifest]
no_jinjaturtle = true
sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
[diff]
# ignore noisy drift
exclude_path = /var/anacron
ignore_package_versions = true
# enforce = true # requires ansible-playbook on PATH</code></pre>
</div>
<div class="alert alert-secondary mt-3 mb-0">
<div class="fw-semibold">Note</div>
<div class="small mb-0">In INI sections, option names use underscores (e.g. <code>include_path</code>) even when the CLI flag uses hyphens (e.g. <code>--include-path</code>).</div>
ignore_package_versions = true</code></pre></div>
</div>
</section>
<section id="diff" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Drift detection with <code>enroll diff</code></h2>
<p class="text-secondary">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!</p>
<p class="text-secondary">The purpose of <code>enroll diff</code> 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.</p>
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Notifications for diff</div>
<div class="small text-secondary mb-0">The <code>enroll diff</code> 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.</div>
</div>
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Noise suppression</div>
<div class="small text-secondary mb-0">Use <code>--exclude-path</code> to ignore file/dir drift under specific paths (e.g. <code>/var/anacron</code>). Use <code>--ignore-package-versions</code> to ignore routine package upgrades/downgrades while still reporting added/removed packages.</div>
</div>
<div class="terminal mb-4">
<pre class="mb-0"><code><span class="prompt">$</span> enroll diff \
--old /path/to/harvestA \
--new /path/to/harvestB \
--exclude-path /var/spool/anacron \
--ignore-package-versions</code></pre>
</div>
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Optional: enforce the old harvest state (<code>--enforce</code>)</div>
<div class="small text-secondary mb-0">If drift exists and <code>ansible-playbook</code> is on <code>PATH</code>, Enroll can generate a manifest from the <em>old</em> harvest and apply it locally to restore expected state. It avoids package downgrades, and will often run Ansible with <code>--tags role_...</code> so only the roles implicated by the drift are applied. This is very much like a return to Puppet's agent mode!</div>
</div>
<div class="terminal mb-4">
<pre class="mb-0"><code><span class="prompt">$</span> enroll diff \
--old /path/to/harvestA \
--new /path/to/harvestB \
--enforce
</code></pre>
</div>
<h3 class="section-title fw-bold">How to run <code>enroll diff</code> automatically on a timer</h3>
<p class="text-secondary">A great way to use <code>enroll diff</code> is to run it periodically (e.g via cron or a systemd timer). Below is an example.</p>
<p class="text-secondary">Store the below file at <code>/usr/local/bin/enroll-harvest-diff.sh</code> and make it executable.</p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-harvest-diff">#!/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
</code></pre>
</div>
<br />
<p class="text-secondary">Save these environment variables in <code>/etc/enroll/enroll-harvest-diff</code></p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff-env"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-harvest-diff-env">
# 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
</code></pre>
</div>
<br />
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Webhook headers</div>
<div class="small text-secondary mb-0">The <code>--webhook-header</code> parameter can be used multiple times. You can, for example, send <code>X-Enroll-Secret</code> and a secret value of your choice, to help secure your webhook endpoint.</div>
</div>
<p class="text-secondary">Save this systemd unit file to <code>/etc/systemd/system/enroll-harvest-diff.service</code></p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff-service"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="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
</code></pre>
</div>
<br />
<p class="text-secondary">Save this systemd timer to <code>/etc/systemd/system/enroll-harvest-diff.timer</code></p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff-timer"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-harvest-diff-timer">
[Unit]
Description=Run Enroll harvest diff hourly
[Timer]
OnCalendar=hourly
RandomizedDelaySec=10m
Persistent=true
[Install]
WantedBy=timers.target
</code></pre>
</div>
<br />
<p class="text-secondary">Now you can enable and test it!</p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff-enable"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-harvest-diff-enable">
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
</code></pre>
</div>
<br />
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Need help with writing webhooks?</div>
<div class="small text-secondary mb-0">I use Node-RED. Here's a <a href="https://git.mig5.net/mig5/enroll/wiki/enroll-diff#example-node-red-webhook-receiver-flow" target="_blank" rel="noopener noreferrer">sample Node-RED flow</a> that might help run your webhook, pre-configured to parse the <code>enroll diff</code> JSON payload!</div>
</div>
</section>
<section id="explain" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Why did Enroll include/exclude something? <code>enroll explain</code></h2>
<p class="text-secondary">When you run <code>enroll harvest</code>, Enroll records <em>why</em> it chose to include or exclude each path in <code>state.json</code>. The <code>enroll explain</code> subcommand summarizes that data so you can quickly sanity-check a harvest, tune include/exclude rules, and understand where packages/services came from.</p>
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">What can it read?</div>
<div class="small text-secondary mb-0"><code>enroll explain</code> accepts a harvest bundle directory, a direct path to <code>state.json</code>, a <code>.tar.gz</code>/<code>.tgz</code> bundle, or an encrypted <code>.tar.gz.sops</code> bundle.</div>
</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-explain-basic"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-explain-basic"><span class="prompt">$</span> enroll explain /tmp/enroll-harvest
# or point at the state.json path directly
<span class="prompt">$</span> enroll explain /tmp/enroll-harvest/state.json</code></pre>
</div>
<br />
<p class="text-secondary">The default output is human-readable text. For scripting or deeper inspection, use JSON output:</p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-explain-json"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-explain-json"><span class="prompt">$</span> enroll explain /tmp/enroll-harvest --format json | jq .
# show more example paths per reason
<span class="prompt">$</span> enroll explain /tmp/enroll-harvest --max-examples 10</code></pre>
</div>
<br />
<p class="text-secondary">If you stored a harvest as a single SOPS-encrypted bundle, <code>enroll explain</code> can decrypt it on the fly (it will also auto-detect files ending with <code>.sops</code>):</p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-explain-sops"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-explain-sops"><span class="prompt">$</span> enroll explain /var/lib/enroll/harvest.tar.gz.sops --sops</code></pre>
</div>
<br />
<p class="text-secondary">What you get back:</p>
<ul class="mb-0 small">
<li>A summary of what roles were collected (users, services, package snapshots, <code>etc_custom</code>, <code>usr_local_custom</code>, etc.).</li>
<li>Why packages ended up in inventory (<code>observed_via</code>), e.g. user-installed vs referenced by a harvested systemd unit.</li>
<li>Breakdowns of <code>managed_files.reason</code>, <code>managed_dirs.reason</code>, and <code>excluded.reason</code>, with a few example paths for each reason.</li>
</ul>
<div class="alert alert-secondary mt-3 mb-0">
<div class="fw-semibold">Tip</div>
<div class="small mb-0">Use <code>enroll explain</code> after a first harvest to decide what to exclude (noise) and what to include (snowflake app/config under <code>/opt</code>, <code>/srv</code>, etc.) before you generate a manifest.</div>
<br />
<div class="small mb-0"><strong>Security note:</strong> <code>enroll explain</code> 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 <code>--dangerous</code>).</div>
<section id="install" class="scroll-mt-nav mt-4">
<div class="feature-card p-4">
<h2 class="h3 fw-bold mb-3">Install</h2>
<p>Enroll requires Python 3.10 or newer. The projects README documents Debian/Ubuntu and Fedora repositories, AppImage, pip/pipx, and Poetry development installs.</p>
<div class="row g-3">
<div class="col-md-6"><div class="callout p-3 h-100"><div class="fw-semibold mb-2">pip / pipx</div><pre class="terminal small mb-0"><code>pip install enroll
# or
pipx install enroll</code></pre></div></div>
<div class="col-md-6"><div class="callout p-3 h-100"><div class="fw-semibold mb-2">development checkout</div><pre class="terminal small mb-0"><code>poetry install
poetry run enroll --help</code></pre></div></div>
</div>
<p class="small text-secondary mt-3 mb-0">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.</p>
</div>
</section>
<section id="tips" class="scroll-mt-nav">
<h2 class="section-title fw-bold">Tips</h2>
<div class="row g-3">
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-shield-check"></i> Start safe</div>
<p class="small text-secondary mb-0">Default harvesting tries to avoid likely secrets via path rules, content sniffing, and size caps. Use <code>--dangerous</code> only when you've planned where the output will live.</p>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-lock"></i> Encrypt at rest</div>
<p class="small text-secondary mb-0">If you plan to keep harvests/manifests long term (especially in git), use <code>--sops</code> to produce a single encrypted bundle file. <strong>Note:</strong> <code>enroll diff</code> can be passed <code>--sops</code> to decrypt and compare two harvests on-the-fly!</p>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-diagram-2"></i> Multi-host safety</div>
<p class="small text-secondary mb-0">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.</p>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-gear"></i> Keep it reproducible</div>
<p class="small text-secondary mb-0">Commit the manifest output, run it in CI, and use <code>enroll diff</code> as a drift alarm (webhook/email).</p>
</div>
</div>
</div>
</section>
</div>
</div>
</div>

View file

@ -1,142 +1,91 @@
---
title: "Examples"
html_title: "Enroll Examples"
description: "Copy/paste recipes for Enroll: one host, fleets, drift detection, and safe storage."
description: "Copy/paste Enroll recipes for local harvests, remote harvests, multi-site manifests, SOPS, diff, validate, and explain."
---
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-terminal"></i> Examples</div>
<h1 class="display-6 fw-bold mb-2">Copy/paste recipes</h1>
<p class="lead mb-0">Practical flows you can adapt to your environment.</p>
<h1 class="display-6 fw-bold mb-2">Copy/paste recipes that match the current CLI</h1>
<p class="lead mb-0">Start with dry-runs and validate your harvest before manifesting.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Enroll a single host (local)</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-single-local"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-single-local"><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest \
--out /tmp/enroll-ansible
<span class="prompt">$</span> ansible-playbook -i "localhost," -c local \
/tmp/enroll-ansible/playbook.yml --diff --check</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">Great for "make this box reproducible" or building a golden role set.</p>
</div>
</div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Local single-host workflow</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-local"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-local">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</code></pre></div><p class="small text-secondary mt-2 mb-0">Use this when the output is for one machine or a “golden” role set.</p></div></div>
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Enroll a remote host (over SSH)</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-remote"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-remote"><span class="prompt">$</span> enroll harvest \
--remote-host myhost.example.com \
--remote-user myuser \
--out /tmp/enroll-harvest
<span class="prompt">$</span> enroll manifest \
--harvest /tmp/enroll-harvest \
--out /tmp/enroll-ansible</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">No need to manually run commands on the server - your bundle lands locally. If your remote user needs a password for sudo, pass in <code>--ask-become-pass</code> or <code>-K</code>, just like in Ansible. If you don't want to use sudo, pass <code>--no-sudo</code>, but your harvest may contain less data.</p>
</div>
</div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Remote harvest over SSH</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-remote"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-remote">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</code></pre></div><p class="small text-secondary mt-2 mb-0">Add <code>-K</code> if sudo requires a password. Use <code>--no-sudo</code> only when a limited harvest is acceptable.</p></div></div>
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Fleets: multi-site output</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-multisite"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-multisite"><span class="prompt">$</span> fqdn="$(hostname -f)"
<span class="prompt">$</span> enroll single-shot --remote-host "$fqdn" \
--remote-user myuser \
--out /tmp/enroll-ansible \
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Remote host using ssh_config alias</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-sshconfig"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-sshconfig">enroll harvest \
--remote-host app01-alias \
--remote-ssh-config ~/.ssh/config \
--out /tmp/enroll-app01-harvest</code></pre></div><p class="small text-secondary mt-2 mb-0"><code>--remote-host</code> is still required; it may be the <code>Host</code> alias in your ssh config.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Encrypted SSH key in CI</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-keypass"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-keypass">export ENROLL_SSH_KEY_PASSPHRASE='correct horse battery staple'
enroll single-shot \
--remote-host app01.example.net \
--remote-user mig \
--ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE \
--harvest /tmp/enroll-app01-harvest \
--out /tmp/enroll-app01-ansible \
--fqdn app01.example.net</code></pre></div><p class="small text-secondary mt-2 mb-0"><code>--ask-key-passphrase</code> is the interactive equivalent. The two key-passphrase options are mutually exclusive.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Include and exclude paths</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-paths"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-paths">enroll harvest --out /tmp/enroll-harvest \
--include-path '/home/*/.profile' \
--include-path 're:^/home/[^/]+/\.config/myapp/.*$' \
--exclude-path '/home/*/.ssh/**'</code></pre></div><p class="small text-secondary mt-2 mb-0">Included files are still secret-scanned unless <code>--dangerous</code> is also set.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Dangerous mode, encrypted at rest</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-danger"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-danger">enroll harvest \
--out /tmp/enroll-secure-harvest \
--dangerous \
--sops 54A91143AE0AB4F7743B01FE888ED1B423A3BC99</code></pre></div><p class="small text-secondary mt-2 mb-0"><code>--dangerous</code> disables likely-secret checks. Pair it with SOPS or equivalent storage controls.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Manifest encrypted harvest, then encrypt manifest</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-sops-manifest"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-sops-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</code></pre></div><p class="small text-secondary mt-2 mb-0">Manifest SOPS mode outputs a single encrypted tarball. Extract it before using Ansible.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Multi-site output</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-fqdn"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-fqdn">fqdn=app01.example.net
enroll manifest \
--harvest /tmp/enroll-app01-harvest \
--out /srv/ansible-enroll \
--fqdn "$fqdn"
<span class="prompt">$</span> ansible-playbook "/tmp/enroll-ansible/playbooks/${fqdn}.yml"</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">Shared roles + host inventory keeps one host's differences from breaking another.</p>
</div>
</div>
cd /srv/ansible-enroll
ansible-playbook -i inventory/hosts.ini "playbooks/${fqdn}.yml" --check --diff</code></pre></div><p class="small text-secondary mt-2 mb-0">Multi-site mode stores host-specific variables and file artifacts in inventory.</p></div></div>
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Drift detection with <code>enroll diff</code></div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-diff"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-diff"><span class="prompt">$</span> enroll diff --old /path/to/harvestA --new /path/to/harvestB --format markdown --exclude-path /var/anacron --ignore-package-versions
<span class="prompt">$</span> enroll diff --old /path/to/golden --new /path/to/current \
--webhook https://example.net/webhook \
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Force or disable JinjaTurtle</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-jt"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-jt"># Error if jinjaturtle is not on PATH
enroll manifest --harvest ./harvest --out ./ansible --jinjaturtle
# Never use template integration even if it is installed
enroll manifest --harvest ./harvest --out ./ansible --no-jinjaturtle</code></pre></div><p class="small text-secondary mt-2 mb-0">Without either flag, Enroll auto-detects integration availability.</p></div></div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Compare harvests and notify a webhook</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-webhook"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-webhook">enroll diff \
--old /srv/enroll/golden \
--new /srv/enroll/current \
--webhook https://example.net/enroll/webhook \
--webhook-format json \
--webhook-header 'X-Enroll-Secret: ...' \
--ignore-package-versions --exit-code
</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">Use it in cron or CI to alert on change.</p>
</div>
</div>
--webhook-header 'Authorization: Bearer ...' \
--ignore-package-versions \
--exit-code</code></pre></div><p class="small text-secondary mt-2 mb-0">With <code>--exit-code</code>, detected drift exits 2 after producing the report and notifications.</p></div></div>
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Explain a harvest with <code>enroll explain</code></div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-explain"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-explain"><span class="prompt">$</span> enroll explain /tmp/enroll-harvest
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Validate and fail CI on warnings</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-validate"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-validate">enroll validate ./harvest --format json --out validate.json --fail-on-warnings</code></pre></div><p class="small text-secondary mt-2 mb-0">Use <code>--allow-remote-schema</code> only when you intentionally want validation to fetch an HTTP(S) schema.</p></div></div>
# machine-readable (reasons, examples, inventory breakdown)
<span class="prompt">$</span> enroll explain /tmp/enroll-harvest --format json | jq .
# encrypted bundle
<span class="prompt">$</span> enroll explain /var/lib/enroll/harvest.tar.gz.sops --sops</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">Great for answering "why did it include/exclude that file?" before you generate a manifest.</p>
</div>
</div>
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Enforce the previous state with <code>enroll diff --enforce</code></div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-diff"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-diff"><span class="prompt">$</span> enroll diff \
--old /path/to/harvestA \
--new /path/to/harvestB \
--enforce \
--format json
</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">Enforcing the old harvest will restore its files/perms, missing packages, changed services or users, if <code>ansible-playbook</code> is on the PATH.</p>
</div>
</div>
<div class="col-lg-6"><div class="feature-card p-4 h-100"><h2 class="h5 fw-bold">Explain what was harvested</h2><div class="codeblock"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-explain"><i class="bi bi-clipboard"></i> Copy</button><pre class="terminal mb-0"><code id="ex-explain">enroll explain ./harvest --max-examples 25
enroll explain ./harvest.tar.gz.sops --sops --format json</code></pre></div><p class="small text-secondary mt-2 mb-0">Useful before committing generated Ansible or debugging why expected files were skipped.</p></div></div>
</div>
<hr class="my-5">
<div class="row g-4">
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-shield-check"></i> Safe harvesting (default)</div>
<p class="small text-secondary mb-3">Enroll tries to avoid harvesting files that might contain secrets. If you need to capture "everything", pass <code>--dangerous</code> and treat the output as sensitive.</p>
<p class="small text-secondary mb-3">You can still control what gets collected and what doesn't by using <code>--include</code> and <code>--exclude</code> flags.</p>
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --dangerous --out /tmp/enroll-harvest</code></pre></div>
</div>
</div>
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-lock"></i> Encrypt bundles at rest (SOPS)</div>
<p class="small text-secondary mb-3">Produce a single encrypted file for harvest and/or manifest output (requires SOPS to be installed).</p>
<p class="small text-secondary mb-3">This is especially a good idea if you are using <code>--dangerous</code>, which might sweep up secrets (see above).</p>
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --dangerous --out /tmp/harvest \
--sops &lt;FINGERPRINT&gt;
<span class="prompt">$</span> enroll manifest --harvest /tmp/harvest/harvest.tar.gz.sops \
--out /tmp/enroll-ansible --sops &lt;FINGERPRINT&gt;</code></pre></div>
</div>
</div>
</div>
</div>
</main>

38
src/content/news.html Normal file
View file

@ -0,0 +1,38 @@
---
title: "News"
html_title: "Enroll News"
description: "Project status and release notes for the current Enroll 0.7 beta documentation."
---
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-newspaper"></i> News</div>
<h1 class="display-6 fw-bold mb-2">Project notes</h1>
<p class="lead mb-0">The site now reflects the current 0.7 beta behavior documented in the repository.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="feature-card p-4 mb-4">
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
<span class="badge badge-soft rounded-pill">0.7 beta</span>
<span class="small text-secondary">current documentation refresh</span>
</div>
<h2 class="h4 fw-bold">Documentation now matches the current CLI</h2>
<p>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.</p>
<p class="mb-0">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.</p>
</div>
<div class="feature-card p-4">
<h2 class="h4 fw-bold">Highlights in the current codebase</h2>
<ul class="mb-0">
<li>Harvest validation runs before manifest rendering.</li>
<li>Directory harvests are frozen before consumption to reduce race opportunities.</li>
<li>Plain output paths are private and hardened against unsafe parents/symlinks.</li>
<li>Root PATH checks can stop unsafe non-interactive runs unless <code>--assume-safe-path</code> is used intentionally.</li>
<li>User shell dotfiles are automatic only in <code>--dangerous</code> mode.</li>
<li>Container images are enforceable only when a digest-pinned registry reference exists.</li>
</ul>
</div>
</div>
</main>

View file

@ -1,52 +1,46 @@
---
title: "Schema"
title: "State Schema"
html_title: "Enroll State Schema"
description: "JSON Schema describing the Enroll harvest state.json format."
layout: "schema"
description: "The current JSON schema for Enroll harvest state.json bundles."
---
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-braces"></i> Schema</div>
<h1 class="display-6 fw-bold mb-2">Harvest <code>state.json</code> schema</h1>
<p class="lead mb-0"><code>enroll harvest</code> generates a state file. This is its structure.</p>
<p class="lead mb-0">The schema below is served from <code>/schema/state.schema.json</code> and should match the schema vendored with the current Enroll codebase.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-4">
<div class="callout p-4 mb-2">
<div class="fw-semibold mb-2">Tips</div>
<div class="d-grid gap-2">
<p class="secondary-text">You can validate a harvest against the schema by running <code>enroll validate /path/to/harvest</code> .</p>
</div>
</div>
<div class="callout p-4">
<div class="fw-semibold mb-2">Links</div>
<div class="d-grid gap-2">
<a class="btn btn-sm btn-outline-dark" href="schema/state.schema.json"><i class="bi bi-download"></i> Raw JSON Schema</a>
<a class="btn btn-sm btn-outline-dark" href="docs.html#schema"><i class="bi bi-book"></i> Docs section</a>
</div>
<hr class="my-3">
</div>
</div>
<div class="col-lg-8">
<div class="feature-card p-4">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2">
<h2 class="h4 fw-bold mb-0">state.schema.json</h2>
<a class="btn btn-sm btn-outline-secondary" href="schema/state.schema.json" target="_blank" rel="noreferrer"><i class="bi bi-box-arrow-up-right"></i> Open raw</a>
</div>
<div class="codeblock terminal mt-3">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#schema-code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="schema-code">Loading…</code></pre>
</div>
<div class="small text-secondary mt-3">Tip: you can validate a harvest with <code>python -m jsonschema -i state.json schema/state.schema.json</code></div>
</div>
<div class="feature-card p-4 mb-4">
<h2 class="h4 fw-bold mb-2">Validation behavior</h2>
<p><code>enroll validate</code> uses the vendored schema by default. You may pass a local schema path with <code>--schema</code>. HTTP(S) schemas are blocked unless <code>--allow-remote-schema</code> is explicitly set.</p>
<div class="terminal"><pre class="mb-0"><code>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</code></pre></div>
</div>
<div class="feature-card p-4">
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
<h2 class="h4 fw-bold mb-0">Current schema</h2>
<a class="btn btn-sm btn-outline-dark" href="schema/state.schema.json"><i class="bi bi-download"></i> Open JSON</a>
</div>
<pre class="terminal mb-0" style="max-height: 70vh; overflow:auto;"><code id="schema-code">Loading schema…</code></pre>
</div>
</div>
</main>
<script>
(async () => {
const el = document.getElementById('schema-code');
try {
const res = await fetch('schema/state.schema.json', {cache: 'no-store'});
const obj = await res.json();
el.textContent = JSON.stringify(obj, null, 2);
} catch (e) {
el.textContent = 'Failed to load schema: ' + (e && e.message ? e.message : String(e));
}
})();
</script>

View file

@ -1,52 +1,60 @@
---
title: "Security Design"
html_title: "Enroll Security"
description: "Security posture and safe workflows for Enroll outputs."
html_title: "Enroll Security Design"
description: "Threat model, trust boundaries, dangerous mode, SOPS, root PATH safety, and manifest safety for Enroll."
---
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-shield-lock"></i> Security</div>
<h1 class="display-6 fw-bold mb-2">Safe by default. Powerful when you opt in.</h1>
<p class="lead mb-0">Enroll can touch sensitive files. This page helps you use it confidently.</p>
<div class="kicker mb-3"><i class="bi bi-shield-lock"></i> Security design</div>
<h1 class="display-6 fw-bold mb-2">Safe-by-default collection, explicit trust at apply time.</h1>
<p class="lead mb-0">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.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-8">
<div class="feature-card p-4">
<h2 class="h4 fw-bold mb-2">Default behavior</h2>
<p class="text-secondary mb-0">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.</p>
</div>
<div class="feature-card p-4 mt-4 border border-warning">
<div class="d-flex align-items-start gap-3">
<div class="icon-pill" style="background: rgba(255,193,7,0.20); border-color: rgba(255,193,7,0.45);"><i class="bi bi-exclamation-triangle"></i></div>
<div>
<h2 class="h4 fw-bold mb-2">The <code>--dangerous</code> flag</h2>
<p class="mb-2">This disables secret-safety checks. It can copy private keys, API tokens, DB passwords, TLS key material, etc.</p>
<p class="small text-secondary mb-0"><strong>Rule:</strong> if you use <code>--dangerous</code>, treat the output as sensitive data and plan secure storage before you run it. Don't store secrets in plaintext in a public place!</p>
</div>
</div>
<div class="feature-card p-4 border border-warning">
<h2 class="h4 fw-bold mb-2">The most important warning</h2>
<p>When manifesting, Enroll prints:</p>
<pre class="terminal mb-3"><code>This harvest is structurally valid, but Enroll cannot prove it is semantically safe.
Only apply manifests generated from harvests whose provenance you trust.</code></pre>
<p class="mb-0">Validation checks shape and consistency. It does not prove the captured configuration is a good idea, that service files are benign, or that the host was not already compromised.</p>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2">Encrypt bundles at rest with SOPS</h2>
<p class="text-secondary">You can install <a href="https://github.com/getsops/sops" target="_blank" rel="noopener noreferrer">SOPS</a> on your <code>$PATH</code>, then use <code>--sops</code> to write a single encrypted <code>.tar.gz.sops</code> file for harvests and/or manifests. This is meant for storage-at-rest and backups.</p>
<h2 class="h4 fw-bold mb-2">Default secret avoidance</h2>
<p>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.</p>
<p>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.</p>
<div class="alert alert-secondary mb-0 small"><strong>Comment caveat:</strong> 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.</div>
</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#sec-sops"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="sec-sops"><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest --dangerous --sops &lt;FINGERPRINT&gt;
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest/harvest.tar.gz.sops \
--out /tmp/enroll-ansible --sops &lt;FINGERPRINT&gt;</code></pre>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2"><code>--dangerous</code></h2>
<p><code>--dangerous</code> 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.</p>
<p class="mb-0">Use it only when you intentionally want an aggressive disaster-recovery snapshot and you already know how the output will be protected.</p>
</div>
<div class="alert alert-secondary mt-3 mb-0">
<div class="fw-semibold">Important</div>
<div class="small mb-0">In manifest <code>--sops</code> mode, you'll need to decrypt and extract the bundle before running <code>ansible-playbook</code>.</div>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2">SOPS is storage-at-rest protection</h2>
<p><code>--sops</code> writes encrypted <code>.tar.gz.sops</code> bundles for harvest and manifest outputs. It is not the same as generating Ansible Vault or SOPS-managed inventory variables.</p>
<div class="terminal"><pre class="mb-0"><code>enroll harvest --out ./secure-harvest --dangerous --sops &lt;GPG_FINGERPRINT&gt;
enroll manifest --harvest ./secure-harvest/harvest.tar.gz.sops \
--out ./secure-manifest --sops &lt;GPG_FINGERPRINT&gt;</code></pre></div>
<p class="small text-secondary mt-3 mb-0">Decrypt and extract an encrypted manifest before running Ansible.</p>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2">Filesystem and rendering hardening</h2>
<ul class="mb-0">
<li>Plaintext harvest/manifest output directories are created private and are refused when unsafe or unexpectedly pre-existing.</li>
<li>Root-run output refuses parent directories controlled by unprivileged users, except safe sticky boundaries such as <code>/tmp</code> with further child checks.</li>
<li>Source and artifact reads use no-follow semantics and reject symlinked components, non-regular files, hardlinks, and unexpected large inputs.</li>
<li>Directory harvests are frozen into a private temporary copy before validation and rendering to avoid post-validation swaps.</li>
<li>Tar/SOPS/remote inputs are extracted through safe extraction rules.</li>
<li>Harvested strings that look like Jinja are emitted as Ansible <code>!unsafe</code> data, while generated task scaffolding only accepts strict Enroll-controlled tokens.</li>
</ul>
</div>
</div>
@ -54,57 +62,30 @@ description: "Security posture and safe workflows for Enroll outputs."
<div class="callout p-4">
<div class="fw-semibold mb-2">Recommended workflow</div>
<ol class="small mb-0">
<li>Start with default mode (no <code>--dangerous</code>).</li>
<li>Add <code>--include-path</code> for a small set of extra files you genuinely want managed.</li>
<li>If you must capture secrets, use <code>--dangerous</code> <strong>and</strong> <code>--sops</code>.</li>
<li>Keep outputs out of public repos; review before committing.</li>
<li>Rotate credentials if you ever suspect they were captured or exposed.</li>
<li>Harvest in normal mode first.</li>
<li>Run <code>enroll explain</code> and <code>enroll validate</code>.</li>
<li>Use targeted <code>--include-path</code> instead of broad <code>--dangerous</code> where possible.</li>
<li>Encrypt outputs if they may contain sensitive data or leave the host.</li>
<li>Run Ansible with <code>--check --diff</code> and review before applying.</li>
</ol>
</div>
<div class="callout p-4 mt-3">
<div class="fw-semibold mb-2">Storage ideas</div>
<ul class="small mb-0">
<li>Encrypted SOPS bundle stored in a password manager vault</li>
<li>Private git repo with additional encryption at rest</li>
<li>Offline backup in an encrypted volume</li>
</ul>
<div class="fw-semibold mb-2">Root PATH safety</div>
<p class="small text-secondary mb-2">When run as root, Enroll checks <code>PATH</code> for empty, relative, current-directory, or group/world-writable entries that could cause host tools to resolve to attacker-controlled binaries.</p>
<p class="small mb-0">Use <code>--assume-safe-path</code> only in trusted automation where the PATH is intentional.</p>
</div>
<div class="callout p-4 mt-3">
<div class="fw-semibold mb-2">Scope control</div>
<p class="small text-secondary mb-3">You can explicitly include or exclude paths. Excludes take precedence over includes.</p>
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll harvest \
--out /tmp/enroll-harvest \
--include-path '/home/*/.profile' \
--exclude-path '/home/*/.ssh/**'</code></pre></div>
</div>
</div>
</div>
<hr class="my-5">
<div class="feature-card p-4">
<h2 class="h4 fw-bold mb-2">Threat model</h2>
<div class="row g-3">
<div class="col-md-6">
<div class="fw-semibold">What Enroll tries to prevent</div>
<ul class="small mb-0">
<li>Accidentally copying obvious secrets in default mode</li>
<li>Harvesting huge/unbounded file sets by mistake</li>
<li>One host's difference causing problems for other hosts in terms of Ansible task steps (multi-site mode)</li>
</ul>
</div>
<div class="col-md-6">
<div class="fw-semibold">What you still need to think about</div>
<ul class="small mb-0">
<li>Where outputs are stored and who can access them</li>
<li>Reviewing what was captured before committing/sharing</li>
<li>Choosing encryption and secret-management strategy</li>
<div class="fw-semibold mb-2">Out of scope</div>
<ul class="small text-secondary mb-0">
<li>Proving harvested config is semantically safe</li>
<li>Protecting against a fully compromised root account</li>
<li>Guaranteeing generated Ansible is an ideal long-term design</li>
<li>Secret management after you intentionally disable safety checks</li>
</ul>
</div>
</div>
</div>
</div>
</main>

View file

@ -0,0 +1,28 @@
---
title: "Troubleshooting"
html_title: "Enroll Troubleshooting"
description: "Common Enroll errors and what they mean: unsafe PATH, sudo, SSH key passphrases, SOPS, validation, and Ansible runs."
---
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-tools"></i> Troubleshooting</div>
<h1 class="display-6 fw-bold mb-2">Common messages and fixes</h1>
<p class="lead mb-0">Most Enroll failures are deliberate fail-closed checks. This page explains what to do next.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="accordion" id="troubleshootingAccordion">
<div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#t1">“remote sudo requires a password”</button></h2><div id="t1" class="accordion-collapse collapse show" data-bs-parent="#troubleshootingAccordion"><div class="accordion-body"><p>Re-run the remote harvest with <code>-K</code> or <code>--ask-become-pass</code>. Use <code>--no-sudo</code> only when you accept a limited harvest.</p><pre class="terminal"><code>enroll harvest --remote-host app01.example.net --remote-user mig -K --out ./harvest</code></pre></div></div></div>
<div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#t2">“SSH private key passphrase is required”</button></h2><div id="t2" class="accordion-collapse collapse" data-bs-parent="#troubleshootingAccordion"><div class="accordion-body"><p>Use <code>--ask-key-passphrase</code> interactively or <code>--ssh-key-passphrase-env ENV_VAR</code> in CI.</p><pre class="terminal"><code>export ENROLL_SSH_KEY_PASSPHRASE='...'
enroll harvest --remote-host app01 --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE --out ./harvest</code></pre></div></div></div>
<div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#t3">Unsafe root <code>PATH</code> warning</button></h2><div id="t3" class="accordion-collapse collapse" data-bs-parent="#troubleshootingAccordion"><div class="accordion-body"><p>When running as root, Enroll refuses or prompts if <code>PATH</code> could resolve host tools from an attacker-controlled location. Fix the PATH or parent directory permissions before running in automation. Use <code>--assume-safe-path</code> only when you intentionally trust the environment.</p></div></div></div>
<div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#t4">“Harvest path is not a directory” during manifest</button></h2><div id="t4" class="accordion-collapse collapse" data-bs-parent="#troubleshootingAccordion"><div class="accordion-body"><p>Plain manifest mode expects a directory harvest. For an encrypted harvest tarball, pass <code>--sops &lt;fingerprint&gt;</code> to manifest so Enroll decrypts and extracts it safely before rendering.</p><pre class="terminal"><code>enroll manifest --harvest ./harvest/harvest.tar.gz.sops --out ./ansible --sops &lt;GPG_FINGERPRINT&gt;</code></pre></div></div></div>
<div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#t5">Validation errors about missing or orphaned artifacts</button></h2><div id="t5" class="accordion-collapse collapse" data-bs-parent="#troubleshootingAccordion"><div class="accordion-body"><p>The bundle is internally inconsistent: <code>state.json</code> references an artifact that is absent, or <code>artifacts/</code> contains files not referenced by state. Re-harvest from a trusted host, or inspect the bundle provenance before using it.</p></div></div></div>
<div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#t6">Generated Ansible says a collection/module is missing</button></h2><div id="t6" class="accordion-collapse collapse" data-bs-parent="#troubleshootingAccordion"><div class="accordion-body"><p>Install the generated requirements before running the playbook. Container, Podman, Flatpak, or Snap roles may add collection requirements.</p><pre class="terminal"><code>cd ./ansible
ansible-galaxy collection install -r requirements.yml</code></pre></div></div></div>
<div class="accordion-item"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#t7">Expected shell dotfiles were not captured</button></h2><div id="t7" class="accordion-collapse collapse" data-bs-parent="#troubleshootingAccordion"><div class="accordion-body"><p>Automatic capture of <code>.bashrc</code>, <code>.profile</code>, <code>.bash_logout</code>, and <code>.bash_aliases</code> is disabled unless <code>--dangerous</code> is used. Prefer targeted <code>--include-path</code> first; normal secret checks still apply.</p></div></div></div>
</div>
</div>
</main>

View file

@ -6,4 +6,4 @@ uglyURLs = true
disableKinds = ["taxonomy", "term", "RSS", "sitemap"]
[params]
description = "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible roles/playbooks from what it finds. Harvest → Manifest → Manage."
description = "Enroll harvests real Linux host state and renders Ansible configuration-management output."

View file

@ -16,6 +16,181 @@
],
"unevaluatedProperties": false
},
"ContainerImageTagAlias": {
"additionalProperties": false,
"properties": {
"ref": {
"minLength": 1,
"type": "string"
},
"repository": {
"minLength": 1,
"type": "string"
},
"tag": {
"minLength": 1,
"type": "string"
}
},
"required": [
"ref",
"repository",
"tag"
],
"type": "object"
},
"ContainerImage": {
"additionalProperties": false,
"properties": {
"architecture": {
"type": [
"string",
"null"
]
},
"created": {
"type": [
"string",
"null"
]
},
"engine": {
"enum": [
"docker",
"podman"
],
"type": "string"
},
"home": {
"type": [
"string",
"null"
]
},
"image_id": {
"type": [
"string",
"null"
]
},
"notes": {
"items": {
"type": "string"
},
"type": "array"
},
"os": {
"type": [
"string",
"null"
]
},
"platform": {
"type": [
"string",
"null"
]
},
"pull_ref": {
"type": [
"string",
"null"
]
},
"repo_digests": {
"items": {
"type": "string"
},
"type": "array"
},
"repo_tags": {
"items": {
"type": "string"
},
"type": "array"
},
"scope": {
"enum": [
"system",
"user"
],
"type": "string"
},
"size": {
"type": [
"integer",
"null"
]
},
"source": {
"type": "string"
},
"tag_aliases": {
"items": {
"$ref": "#/$defs/ContainerImageTagAlias"
},
"type": "array"
},
"user": {
"type": [
"string",
"null"
]
},
"variant": {
"type": [
"string",
"null"
]
}
},
"required": [
"engine",
"scope",
"user",
"home",
"image_id",
"repo_tags",
"repo_digests",
"pull_ref",
"tag_aliases",
"os",
"architecture",
"variant",
"platform",
"size",
"created",
"source",
"notes"
],
"type": "object"
},
"ContainerImagesSnapshot": {
"additionalProperties": false,
"properties": {
"images": {
"items": {
"$ref": "#/$defs/ContainerImage"
},
"type": "array"
},
"notes": {
"items": {
"type": "string"
},
"type": "array"
},
"role_name": {
"const": "container_images"
}
},
"required": [
"role_name",
"images",
"notes"
],
"type": "object"
},
"DnfConfigSnapshot": {
"allOf": [
{
@ -117,6 +292,14 @@
"minLength": 1,
"type": "string"
},
"group": {
"minLength": 1,
"type": "string"
},
"section": {
"minLength": 1,
"type": "string"
},
"version": {
"minLength": 1,
"type": "string"
@ -364,6 +547,12 @@
},
"type": "array"
},
"section": {
"type": [
"string",
"null"
]
},
"version": {
"type": [
"string",
@ -390,6 +579,16 @@
"package": {
"minLength": 1,
"type": "string"
},
"has_config": {
"type": "boolean",
"default": true
},
"section": {
"type": [
"string",
"null"
]
}
},
"required": [
@ -571,6 +770,21 @@
"$ref": "#/$defs/UserEntry"
},
"type": "array"
},
"user_flatpaks": {
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/$defs/FlatpakInstall"
}
},
"type": "object"
},
"user_flatpak_remotes": {
"type": "array",
"items": {
"$ref": "#/$defs/FlatpakRemote"
}
}
},
"required": [
@ -652,6 +866,256 @@
"notes"
],
"type": "object"
},
"SysctlSnapshot": {
"additionalProperties": false,
"properties": {
"role_name": {
"const": "sysctl"
},
"managed_files": {
"items": {
"$ref": "#/$defs/ManagedFile"
},
"type": "array"
},
"parameters": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"notes": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"role_name",
"managed_files",
"parameters",
"notes"
],
"type": "object"
},
"FlatpakInstall": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"method": {
"type": "string",
"enum": [
"system",
"user"
]
},
"remote": {
"type": [
"string",
"null"
]
},
"branch": {
"type": [
"string",
"null"
]
},
"arch": {
"type": [
"string",
"null"
]
},
"kind": {
"type": [
"string",
"null"
],
"enum": [
"app",
"runtime",
null
]
},
"ref": {
"type": [
"string",
"null"
]
},
"user": {
"type": [
"string",
"null"
]
},
"home": {
"type": [
"string",
"null"
]
},
"source": {
"type": "string"
},
"from_url": {
"type": "string",
"minLength": 1
}
},
"required": [
"name",
"method"
],
"type": "object"
},
"FlatpakRemote": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"method": {
"type": "string",
"enum": [
"system",
"user"
]
},
"url": {
"type": "string",
"minLength": 1
},
"user": {
"type": [
"string",
"null"
]
},
"home": {
"type": [
"string",
"null"
]
},
"source": {
"type": "string"
}
},
"required": [
"name",
"method",
"url"
],
"type": "object"
},
"SnapInstall": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"channel": {
"type": [
"string",
"null"
]
},
"revision": {
"type": [
"integer",
"null"
],
"minimum": 0
},
"classic": {
"type": "boolean"
},
"devmode": {
"type": "boolean"
},
"dangerous": {
"type": "boolean"
},
"notes": {
"type": "array",
"items": {
"type": "string"
}
},
"source": {
"type": "string"
},
"install_revision": {
"type": "boolean"
}
},
"required": [
"name"
],
"type": "object"
},
"FlatpakSnapshot": {
"additionalProperties": false,
"properties": {
"role_name": {
"const": "flatpak"
},
"system_flatpaks": {
"type": "array",
"items": {
"$ref": "#/$defs/FlatpakInstall"
}
},
"remotes": {
"type": "array",
"items": {
"$ref": "#/$defs/FlatpakRemote"
}
},
"notes": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"role_name"
],
"type": "object"
},
"SnapSnapshot": {
"additionalProperties": false,
"properties": {
"role_name": {
"const": "snap"
},
"system_snaps": {
"type": "array",
"items": {
"$ref": "#/$defs/SnapInstall"
}
},
"notes": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"role_name"
],
"type": "object"
}
},
"$id": "https://enroll.sh/schema/state.schema.json",
@ -762,6 +1226,18 @@
},
"firewall_runtime": {
"$ref": "#/$defs/FirewallRuntimeSnapshot"
},
"sysctl": {
"$ref": "#/$defs/SysctlSnapshot"
},
"flatpak": {
"$ref": "#/$defs/FlatpakSnapshot"
},
"snap": {
"$ref": "#/$defs/SnapSnapshot"
},
"container_images": {
"$ref": "#/$defs/ContainerImagesSnapshot"
}
},
"required": [

View file

@ -7,19 +7,14 @@
<div class="fw-bold">Enroll (a mig5 project)</div>
<span class="badge badge-soft rounded-pill">CLI</span>
<span class="badge badge-soft rounded-pill">Ansible</span>
<span class="badge badge-soft rounded-pill">0.7 beta docs</span>
</div>
<div class="text-secondary mb-3">Reverse-engineering servers into Ansible.</div>
<div class="text-secondary mb-3">Harvest existing Linux host state and render reviewable Ansible.</div>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/enroll" target="_blank" rel="noreferrer">
<i class="bi bi-git"></i> Repo
</a>
<a class="btn btn-sm btn-outline-dark" href="https://pypi.org/project/enroll/" target="_blank" rel="noreferrer">
<i class="bi bi-box"></i> PyPI
</a>
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/enroll/releases" target="_blank" rel="noreferrer">
<i class="bi bi-tags"></i> Releases
</a>
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/enroll" target="_blank" rel="noreferrer"><i class="bi bi-git"></i> Repo</a>
<a class="btn btn-sm btn-outline-dark" href="https://pypi.org/project/enroll/" target="_blank" rel="noreferrer"><i class="bi bi-box"></i> PyPI</a>
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/enroll/releases" target="_blank" rel="noreferrer"><i class="bi bi-tags"></i> Releases</a>
</div>
</div>
@ -28,36 +23,27 @@
<ul class="list-unstyled small mb-0">
<li><a class="link-secondary text-decoration-none" href="docs.html">Docs</a></li>
<li><a class="link-secondary text-decoration-none" href="examples.html">Examples</a></li>
<li><a class="link-secondary text-decoration-none" href="security.html">Security</a></li>
<li><a class="link-secondary text-decoration-none" href="development.html">Development</a></li>
<li><a class="link-secondary text-decoration-none" href="schema.html">Schema</a></li>
<li><a class="link-secondary text-decoration-none" href="security.html">Security Design</a></li>
<li><a class="link-secondary text-decoration-none" href="troubleshooting.html">Troubleshooting</a></li>
<li><a class="link-secondary text-decoration-none" href="news.html">News</a></li>
</ul>
</div>
<div class="col-lg-3">
<div class="fw-semibold mb-2">Contact</div>
<ul class="list-unstyled small mb-0">
<li>
<a class="link-secondary text-decoration-none" href="https://nr.mig5.net/forms/mig5/contact" target="_blank" rel="noreferrer">
<i class="bi bi-envelope"></i> Form
</a>
</li>
<li class="mt-1">
<span class="text-secondary">Fediverse:</span>
<a class="link-secondary text-decoration-none ms-1" href="https://goto.mig5.net/@mig5" target="_blank" rel="noreferrer">@mig5</a>
</li>
<li><a class="link-secondary text-decoration-none" href="https://nr.mig5.net/forms/mig5/contact" target="_blank" rel="noreferrer"><i class="bi bi-envelope"></i> Form</a></li>
<li class="mt-1"><span class="text-secondary">Fediverse:</span><a class="link-secondary text-decoration-none ms-1" href="https://goto.mig5.net/@mig5" target="_blank" rel="noreferrer">@mig5</a></li>
</ul>
</div>
</div>
<hr class="my-4">
<div class="small text-secondary d-flex flex-wrap justify-content-between gap-2">
<div>© <span id="year"></span></div>
<div>
<a class="link-secondary text-decoration-none" href="https://mig5.net" target="_blank" rel="noreferrer">
mig5 system administration
</a>
</div>
<div><a class="link-secondary text-decoration-none" href="https://mig5.net" target="_blank" rel="noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>

View file

@ -12,16 +12,11 @@
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item">
<a class="nav-link" href="docs.html">Docs</a>
</li>
<li class="nav-item">
<a class="nav-link" href="examples.html">Examples</a>
</li>
<li class="nav-item">
<a class="nav-link" href="security.html">Security Design</a>
</li>
<li class="nav-item"><a class="nav-link" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="examples.html">Examples</a></li>
<li class="nav-item"><a class="nav-link" href="security.html">Security</a></li>
<li class="nav-item"><a class="nav-link" href="development.html">Development</a></li>
<li class="nav-item"><a class="nav-link" href="troubleshooting.html">Troubleshooting</a></li>
<li class="nav-item ms-lg-2">
<a class="btn btn-sm btn-outline-dark" href="https://git.mig5.net/mig5/enroll" target="_blank" rel="noreferrer">
<i class="bi bi-git"></i> Repo