Compare commits
1 commit
main
...
mig/new-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 42683598fa |
21 changed files with 498 additions and 782 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,2 @@
|
|||
*.swp
|
||||
src/public
|
||||
src/.hugo_build.lock
|
||||
|
|
|
|||
0
src/.hugo_build.lock
Normal file
0
src/.hugo_build.lock
Normal file
|
|
@ -1,156 +1,127 @@
|
|||
---
|
||||
title: "Enroll"
|
||||
html_title: "Enroll - Reverse-engineering Linux hosts into Ansible"
|
||||
description: "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible configuration-management code from what it finds. Harvest → Manifest → Manage."
|
||||
og_title: "Enroll - Reverse-engineering Linux hosts into Ansible"
|
||||
og_description: "Harvest a host's real configuration and turn it into Ansible roles and playbooks. Safe-by-default, with optional SOPS encryption."
|
||||
html_title: "Enroll - Turn existing Linux hosts into Ansible"
|
||||
description: "Enroll harvests real Linux host state and renders Ansible configuration-management output. Documentation for current 0.7 beta behavior."
|
||||
og_title: "Enroll - Turn existing Linux hosts into Ansible"
|
||||
og_description: "Harvest real Linux host state and render reviewable Ansible roles/playbooks."
|
||||
og_type: "website"
|
||||
---
|
||||
<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 Linux hosts into Ansible</div>
|
||||
<h1 class="display-5 fw-800 lh-1 mb-3" style="letter-spacing:-0.03em;">Get an existing server into config management quickly.</h1>
|
||||
<p class="lead mb-4">Enroll inspects a Debian-like or RedHat-like Linux machine, harvests the state that matters, and generates Ansible roles and playbooks from the captured bundle.</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="docs.html"><i class="bi bi-journal-text"></i> Documentation</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-secondary btn-lg" href="news.html"><i class="bi bi-megaphone"></i> News</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-shield-check"></i> Safe by default</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> Harvest once, render Ansible</div>
|
||||
<div class="small text-muted">state.json + artifacts → roles/playbooks</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 harvest --out ./harvest</code></div>
|
||||
<div class="mb-2"><span class="prompt">$</span> <code style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;">enroll manifest --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;">ansible-galaxy collection install -r ./ansible/requirements.yml</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 → playbook.yml, roles/, requirements.yml
|
||||
./ansible + --fqdn → playbooks/<fqdn>.yml + inventory/host_vars/...</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">Current Enroll output is Ansible-only. Older examples that mention <code>--target puppet</code> or <code>--target salt</code> are from an experimental branch and no longer match the CLI.</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">The mental model</h2>
|
||||
<p class="mb-4">Enroll is built around a small pipeline: capture first, render later, validate before you trust the output.</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 and selected 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">Turn that bundle into an Ansible repository-style output tree.</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 optionally notify by webhook or 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">Summarise what was included or excluded 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">Check schema and artifact consistency before using a bundle.</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">Default secret avoidance</div><div class="text-muted">Likely private keys, credential assignments, tokens, secret paths, oversized files, and binary-looking files are avoided unless you opt into <code>--dangerous</code>.</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">Ansible output</div><div class="text-muted">Generated output includes roles, playbooks, requirements, defaults, templates, and host variables in <code>--fqdn</code> mode.</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">Grouped roles by default</div><div class="text-muted">Package/service snapshots are grouped by Debian Section or RPM Group where possible. Use <code>--no-common-roles</code> to keep one generated role per package or unit.</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 mode</div><div class="text-muted"><code>--fqdn</code> moves host-specific state into Ansible inventory and implies <code>--no-common-roles</code>.</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">Run the harvest on another host through Paramiko/OpenSSH config support, then render the manifest 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">SOPS bundles</div><div class="text-muted"><code>--sops</code> stores harvests and generated manifests as encrypted tarballs for safer at-rest storage.</div></div></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><p class="text-secondary mb-0">Start with a local safe-mode harvest, inspect the generated output, then run Ansible when you are ready.</p></div>
|
||||
<div class="col-lg-5 text-lg-end"><a class="btn btn-outline-dark" href="docs.html"><i class="bi bi-journal-text"></i> Full docs</a></div>
|
||||
<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="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="qs-local" role="tabpanel">
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#quick-local"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="quick-local"><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest
|
||||
<span class="prompt">$</span> enroll validate /tmp/enroll-harvest
|
||||
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
|
||||
<span class="prompt">$</span> cd /tmp/enroll-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</code></pre></div>
|
||||
<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="qs-remote" role="tabpanel">
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#quick-remote"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="quick-remote"><span class="prompt">$</span> enroll harvest \
|
||||
--remote-host host.example.net \
|
||||
--remote-user admin \
|
||||
--remote-ssh-config ~/.ssh/config \
|
||||
--out /tmp/enroll-harvest
|
||||
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn host.example.net</code></pre></div>
|
||||
<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="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="qs-drift" role="tabpanel">
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#quick-drift"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="quick-drift"><span class="prompt">$</span> enroll harvest --out /srv/enroll/baseline
|
||||
<span class="prompt">$</span> enroll harvest --out /srv/enroll/current
|
||||
<span class="prompt">$</span> enroll diff --old /srv/enroll/baseline --new /srv/enroll/current --format markdown --ignore-package-versions</code></pre></div>
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5" id="captures">
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<h2 class="section-title display-6 fw-bold mb-4">What Enroll tries to capture</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><div class="feature-card p-4 h-100"><div class="fw-semibold mb-2">Packages and services</div><p class="text-secondary mb-0">Manual packages, service-linked packages, systemd enable/running state, and service-relevant changed or custom config.</p></div></div>
|
||||
<div class="col-md-4"><div class="feature-card p-4 h-100"><div class="fw-semibold mb-2">Configuration files</div><p class="text-secondary mb-0">Changed package conffiles, unowned service files, miscellaneous <code>/etc</code>, APT/DNF/YUM config, selected symlinks, and explicit include paths.</p></div></div>
|
||||
<div class="col-md-4"><div class="feature-card p-4 h-100"><div class="fw-semibold mb-2">Runtime and user state</div><p class="text-secondary mb-0">Non-system users, SSH public keys, Flatpak/Snap state, Docker/Podman images, writable sysctls, and fallback iptables/ipset state when persistent files are absent.</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5 bg-light" id="commands">
|
||||
<div class="container">
|
||||
<h2 class="section-title display-6 fw-bold mb-4">Commands</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><div class="card feature-card h-100"><div class="card-body p-4"><h3 class="h5 fw-bold"><code>harvest</code></h3><div class="text-muted mb-3">Collect host state into <code>state.json</code> and <code>artifacts/</code>.</div><a href="docs.html#harvest" class="btn btn-sm btn-outline-dark">Harvest docs</a></div></div></div>
|
||||
<div class="col-md-4"><div class="card feature-card h-100"><div class="card-body p-4"><h3 class="h5 fw-bold"><code>manifest</code></h3><div class="text-muted mb-3">Validate a harvest and render Ansible output.</div><a href="docs.html#manifest" class="btn btn-sm btn-outline-dark">Manifest docs</a></div></div></div>
|
||||
<div class="col-md-4"><div class="card feature-card h-100"><div class="card-body p-4"><h3 class="h5 fw-bold"><code>single-shot</code></h3><div class="text-muted mb-3">Run harvest and manifest in one command.</div><a href="docs.html#single-shot" class="btn btn-sm btn-outline-dark">Single-shot docs</a></div></div></div>
|
||||
<div class="col-md-4"><div class="card feature-card h-100"><div class="card-body p-4"><h3 class="h5 fw-bold"><code>diff</code></h3><div class="text-muted mb-3">Compare harvests and send reports.</div><a href="docs.html#diff" class="btn btn-sm btn-outline-dark">Diff docs</a></div></div></div>
|
||||
<div class="col-md-4"><div class="card feature-card h-100"><div class="card-body p-4"><h3 class="h5 fw-bold"><code>explain</code></h3><div class="text-muted mb-3">Explain what a harvest contains.</div><a href="docs.html#explain" class="btn btn-sm btn-outline-dark">Explain docs</a></div></div></div>
|
||||
<div class="col-md-4"><div class="card feature-card h-100"><div class="card-body p-4"><h3 class="h5 fw-bold"><code>validate</code></h3><div class="text-muted mb-3">Check schema and bundle consistency.</div><a href="docs.html#validate" class="btn btn-sm btn-outline-dark">Validate docs</a></div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5" id="safety">
|
||||
<div class="container">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-6">
|
||||
<h2 class="section-title display-6 fw-bold mb-3">Safe by default, not magic</h2>
|
||||
<p class="text-secondary">Enroll avoids obvious secrets by default, validates harvest structure, freezes directory bundles into private temp trees before manifesting, rejects unknown SSH host keys, and warns when a root <code>PATH</code> looks unsafe.</p>
|
||||
<p class="text-secondary mb-0">It cannot prove that a structurally valid harvest is semantically safe. Only apply manifests generated from harvests whose provenance you trust.</p>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="alert alert-warning mb-0">
|
||||
<div class="fw-semibold mb-2">Dangerous mode means dangerous</div>
|
||||
<div>Use <code>--dangerous</code> only when you intentionally want to bypass likely-secret checks. Pair it with <code>--sops</code> or another appropriate at-rest encryption workflow whenever there is any doubt.</div>
|
||||
</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/<role>/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/<fqdn></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>
|
||||
|
|
|
|||
49
src/content/development.html
Normal file
49
src/content/development.html
Normal 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>
|
||||
|
|
@ -1,21 +1,13 @@
|
|||
---
|
||||
title: "Docs"
|
||||
html_title: "Enroll documentation - Ansible host harvesting and manifesting"
|
||||
description: "Documentation for the current Enroll CLI: harvest Linux host state and generate Ansible configuration-management code."
|
||||
og_title: "Enroll documentation"
|
||||
og_description: "Current Enroll docs for harvest, manifest, single-shot, diff, explain, validate, security, SOPS, and Ansible output."
|
||||
og_type: "article"
|
||||
title: "Documentation"
|
||||
html_title: "Enroll Documentation"
|
||||
description: "Current user documentation for Enroll harvest, manifest, diff, explain, validate, SOPS, remote mode, and Ansible output."
|
||||
---
|
||||
<header class="py-5 bg-light border-bottom">
|
||||
<div class="container">
|
||||
<div class="row g-4 align-items-end">
|
||||
<div class="col-lg-8">
|
||||
<header class="py-5 hero">
|
||||
<div class="container py-3">
|
||||
<div class="kicker mb-3"><i class="bi bi-journal-text"></i> Documentation</div>
|
||||
<h1 class="display-6 fw-bold mb-2">How Enroll works today</h1>
|
||||
<p class="lead mb-0">Enroll harvests selected Linux host state, validates it, and renders Ansible configuration-management output. This documentation reflects the current Ansible-only CLI.</p>
|
||||
</div>
|
||||
<div class="col-lg-4 text-lg-end"><a class="btn btn-outline-dark" href="examples.html"><i class="bi bi-terminal"></i> Examples</a></div>
|
||||
</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>
|
||||
|
||||
|
|
@ -23,196 +15,167 @@ og_type: "article"
|
|||
<div class="container">
|
||||
<div class="row g-4">
|
||||
<aside class="col-lg-3">
|
||||
<div class="position-sticky" style="top: 6rem;">
|
||||
<div class="list-group small shadow-sm">
|
||||
<a class="list-group-item list-group-item-action" href="#install">Install</a>
|
||||
<a class="list-group-item list-group-item-action" href="#model">Mental model</a>
|
||||
<a class="list-group-item list-group-item-action" href="#modes">Output modes</a>
|
||||
<a class="list-group-item list-group-item-action" href="#harvest">Harvest</a>
|
||||
<a class="list-group-item list-group-item-action" href="#manifest">Manifest</a>
|
||||
<a class="list-group-item list-group-item-action" href="#single-shot">Single-shot</a>
|
||||
<a class="list-group-item list-group-item-action" href="#diff">Diff</a>
|
||||
<a class="list-group-item list-group-item-action" href="#explain">Explain</a>
|
||||
<a class="list-group-item list-group-item-action" href="#validate">Validate</a>
|
||||
<a class="list-group-item list-group-item-action" href="#sensitive-data">Sensitive data</a>
|
||||
<a class="list-group-item list-group-item-action" href="#jinjaturtle">JinjaTurtle</a>
|
||||
<a class="list-group-item list-group-item-action" href="#config-file">Config file</a>
|
||||
<a class="list-group-item list-group-item-action" href="#run-ansible">Run Ansible</a>
|
||||
</div>
|
||||
<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>
|
||||
</aside>
|
||||
|
||||
<div class="col-lg-9">
|
||||
<section id="install" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Install</h2>
|
||||
<p class="text-secondary">Install from your preferred channel. The Python package requires Python 3.10 or newer.</p>
|
||||
<div class="accordion" id="installAccordion">
|
||||
<div class="accordion-item"><h3 class="accordion-header"><button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#install-pip">Pip / PipX</button></h3><div id="install-pip" class="accordion-collapse collapse show" data-bs-parent="#installAccordion"><div class="accordion-body"><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> pip install enroll
|
||||
<span class="prompt">$</span> pipx install enroll</code></pre></div></div></div></div>
|
||||
<div class="accordion-item"><h3 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#install-deb">Ubuntu / Debian apt repository</button></h3><div id="install-deb" class="accordion-collapse collapse" data-bs-parent="#installAccordion"><div class="accordion-body"><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> sudo mkdir -p /usr/share/keyrings
|
||||
<span class="prompt">$</span> curl -fsSL https://mig5.net/static/mig5.asc | sudo gpg --dearmor -o /usr/share/keyrings/mig5.gpg
|
||||
<span class="prompt">$</span> 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
|
||||
<span class="prompt">$</span> sudo apt update
|
||||
<span class="prompt">$</span> sudo apt install enroll</code></pre></div></div></div></div>
|
||||
<div class="accordion-item"><h3 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#install-rpm">Fedora / RPM-style repository</button></h3><div id="install-rpm" class="accordion-collapse collapse" data-bs-parent="#installAccordion"><div class="accordion-body"><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> sudo rpm --import https://mig5.net/static/mig5.asc
|
||||
<span class="prompt">$</span> 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
|
||||
<span class="prompt">$</span> sudo dnf upgrade --refresh
|
||||
<span class="prompt">$</span> sudo dnf install enroll</code></pre></div></div></div></div>
|
||||
<div class="accordion-item"><h3 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#install-dev">Development checkout</button></h3><div id="install-dev" class="accordion-collapse collapse" data-bs-parent="#installAccordion"><div class="accordion-body"><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> poetry install
|
||||
<span class="prompt">$</span> poetry run enroll --help</code></pre></div></div></div></div>
|
||||
<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="model" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Mental model</h2>
|
||||
<p class="text-secondary">Enroll works in two main phases. <strong>Harvest</strong> collects host facts and relevant files into a bundle. <strong>Manifest</strong> turns that bundle into Ansible configuration-management code.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docs-model"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="docs-model"><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</code></pre></div>
|
||||
<p class="small text-secondary mt-3 mb-0"><code>diff</code>, <code>explain</code>, and <code>validate</code> all operate on harvest bundles too.</p>
|
||||
</section>
|
||||
|
||||
<section id="modes" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Output modes: single-site and multi-site</h2>
|
||||
<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 user’s 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"><div class="callout p-4 h-100"><h3 class="h5 fw-bold">Single-site mode</h3><p class="small text-secondary">Default mode. Use it for one server or for a golden role set. Raw files live in each role's <code>files/</code>, and template variables live in <code>defaults/main.yml</code>.</p><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll manifest --harvest ./harvest --out ./ansible</code></pre></div></div></div>
|
||||
<div class="col-md-6"><div class="callout p-4 h-100"><h3 class="h5 fw-bold">Multi-site mode</h3><p class="small text-secondary">Enable with <code>--fqdn</code>. Roles are generic; host-specific state lives under Ansible inventory <code>host_vars</code>. This mode implies <code>--no-common-roles</code>.</p><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll manifest --harvest ./harvest --out ./ansible --fqdn host.example.net</code></pre></div></div></div>
|
||||
<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="harvest" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold"><code>enroll harvest</code></h2>
|
||||
<p class="text-secondary">Harvest writes a bundle containing <code>state.json</code> and captured artifacts. It detects packages, services, changed config, unowned service files, non-system users and SSH public keys, miscellaneous <code>/etc</code>, selected symlinks, Flatpak/Snap state, Docker/Podman image metadata, <code>/usr/local</code> scripts/config, writable sysctls, and fallback iptables/ipset runtime state when persistent files are absent.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docs-harvest"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="docs-harvest"><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest
|
||||
<span class="prompt">$</span> enroll harvest --remote-host host.example.net --remote-user admin --out /tmp/enroll-harvest
|
||||
<span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest --include-path '/home/*/.bashrc' --exclude-path '/usr/local/bin/docker-*'</code></pre></div>
|
||||
<h3 class="h5 fw-bold mt-4">Common flags</h3>
|
||||
<ul class="text-secondary">
|
||||
<li><code>--remote-host</code>, <code>--remote-user</code>, <code>--remote-port</code>, <code>--remote-ssh-config</code> for SSH harvesting.</li>
|
||||
<li><code>--ask-become-pass</code> / <code>-K</code> for remote sudo prompts.</li>
|
||||
<li><code>--ask-key-passphrase</code> or <code>--ssh-key-passphrase-env ENV_VAR</code> for encrypted SSH private keys.</li>
|
||||
<li><code>--no-sudo</code> for a less complete remote harvest without sudo.</li>
|
||||
<li><code>--include-path</code> and <code>--exclude-path</code> for plain paths, globs, <code>glob:</code>, <code>re:</code>, or <code>regex:</code> patterns. Excludes win over includes.</li>
|
||||
<li><code>--dangerous</code> disables likely-secret filtering.</li>
|
||||
<li><code>--sops GPG_FINGERPRINT...</code> writes an encrypted <code>harvest.tar.gz.sops</code>.</li>
|
||||
<li><code>--assume-safe-path</code> skips the root unsafe-<code>PATH</code> prompt in trusted automation.</li>
|
||||
<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="manifest" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold"><code>enroll manifest</code></h2>
|
||||
<p class="text-secondary">Manifest validates a harvest and renders Ansible output. It does not support <code>--target</code>; current Enroll has no Puppet or Salt renderer.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docs-manifest"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="docs-manifest"><span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
|
||||
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn host.example.net
|
||||
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --no-common-roles</code></pre></div>
|
||||
<div class="alert alert-warning mt-3"><strong>Trust warning:</strong> Enroll validates structure, but it cannot prove the desired state is safe. Only apply manifests generated from harvests whose provenance you trust.</div>
|
||||
<h3 class="h5 fw-bold mt-4">Ansible output</h3>
|
||||
<ul class="text-secondary">
|
||||
<li>Single-site output contains <code>playbook.yml</code>, <code>roles/</code>, role files, defaults, templates, and <code>requirements.yml</code>.</li>
|
||||
<li>Multi-site output uses host-specific inventory under <code>inventory/host_vars/<fqdn>/...</code> and playbooks under <code>playbooks/</code>.</li>
|
||||
<li>Playbooks tag roles as <code>role_<role_name></code>, such as <code>role_users</code> or <code>role_services</code>.</li>
|
||||
<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/<fqdn>.yml</code> plus inventory in multi-site mode.</li>
|
||||
<li><code>roles/<role>/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_<role_name></code>. Use tags to dry-run or apply a narrow slice, for example <code>--tags role_users,role_services</code>.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="single-shot" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold"><code>enroll single-shot</code></h2>
|
||||
<p class="text-secondary">Single-shot runs harvest then manifest in one command. It supports the relevant harvest and manifest flags, including remote SSH, include/exclude paths, <code>--dangerous</code>, <code>--sops</code>, <code>--fqdn</code>, <code>--no-common-roles</code>, and JinjaTurtle flags.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docs-single"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="docs-single"><span class="prompt">$</span> enroll single-shot --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$(hostname -f)"
|
||||
<span class="prompt">$</span> enroll single-shot --remote-host host.example.net --remote-user admin --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn host.example.net</code></pre></div>
|
||||
<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/<fqdn></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="diff" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold"><code>enroll diff</code></h2>
|
||||
<p class="text-secondary">Diff compares two harvest bundles and reports package, service, user, and managed-file drift. Inputs can be bundle directories, <code>state.json</code> paths, tarballs, or SOPS bundles when <code>--sops</code> is enabled.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docs-diff"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="docs-diff"><span class="prompt">$</span> enroll diff --old ./baseline --new ./current --format markdown
|
||||
<span class="prompt">$</span> enroll diff --old ./baseline --new ./current --ignore-package-versions --exclude-path /var/anacron
|
||||
<span class="prompt">$</span> enroll diff --old ./baseline --new ./current --webhook https://example.net/hook --webhook-format json --webhook-header 'Authorization: Bearer ...'</code></pre></div>
|
||||
<p class="small text-secondary mt-3 mb-0">Use <code>--exit-code</code> to return status 2 when differences exist. Use <code>--notify-always</code> to send webhook/email even when there are no differences.</p>
|
||||
<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 <GPG_FINGERPRINT...></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="explain" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold"><code>enroll explain</code></h2>
|
||||
<p class="text-secondary">Explain produces a human or JSON summary of a harvest: package counts, roles, included reasons, excluded reasons, and example paths.</p>
|
||||
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll explain ./harvest
|
||||
<span class="prompt">$</span> enroll explain ./harvest --format json --max-examples 25
|
||||
<span class="prompt">$</span> enroll explain ./harvest.tar.gz.sops --sops</code></pre></div>
|
||||
<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="validate" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold"><code>enroll validate</code></h2>
|
||||
<p class="text-secondary">Validate checks that <code>state.json</code> exists, is valid JSON, matches the vendored JSON Schema unless skipped, and references artifacts that actually exist. It also reports unreferenced files in <code>artifacts/</code>.</p>
|
||||
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll validate ./harvest
|
||||
<span class="prompt">$</span> enroll validate ./harvest --format json --out validate.json
|
||||
<span class="prompt">$</span> enroll validate ./harvest --fail-on-warnings
|
||||
<span class="prompt">$</span> enroll validate ./harvest --schema ./state.schema.json
|
||||
<span class="prompt">$</span> enroll validate ./harvest --schema https://enroll.sh/schema/state.schema.json --allow-remote-schema</code></pre></div>
|
||||
<p class="small text-secondary mt-3 mb-0">Remote schemas are not fetched unless <code>--allow-remote-schema</code> is explicitly supplied. Use <code>--no-schema</code> to skip schema checks while keeping consistency checks.</p>
|
||||
<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="sensitive-data" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Sensitive data</h2>
|
||||
<p class="text-secondary">By default, Enroll tries not to harvest likely secrets. It denies known sensitive paths, private key material, common certificate/private-key locations, credential-looking assignments, credential-bearing URIs, authorization headers, service account key names, and other obvious sensitive values.</p>
|
||||
<div class="alert alert-danger"><strong><code>--dangerous</code> disables those checks.</strong> It may copy private keys, API tokens, TLS key material, database passwords, and other secrets into the harvest output in plaintext. Use it only intentionally, and strongly consider <code>--sops</code>.</div>
|
||||
<p class="text-secondary">Value-less comment mentions such as <code># token</code> are tolerated so stock config files do not become impossible to harvest. A commented-out populated credential assignment is still treated as sensitive.</p>
|
||||
</section>
|
||||
|
||||
<section id="jinjaturtle" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">JinjaTurtle integration</h2>
|
||||
<p class="text-secondary">If the <code>jinjaturtle</code> executable is on <code>PATH</code>, Enroll can turn supported config files into Ansible templates. The default mode uses JinjaTurtle when available. <code>--jinjaturtle</code> makes it required; <code>--no-jinjaturtle</code> disables it.</p>
|
||||
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll manifest --harvest ./harvest --out ./ansible --jinjaturtle
|
||||
<span class="prompt">$</span> enroll manifest --harvest ./harvest --out ./ansible --no-jinjaturtle</code></pre></div>
|
||||
<p class="small text-secondary mt-3 mb-0">Supported suffixes include INI, CFG, JSON, TOML, YAML, XML, repo files, and common systemd unit file suffixes. Templates live in role <code>templates/</code>; variables live in role defaults or host vars depending on output mode.</p>
|
||||
</section>
|
||||
|
||||
<section id="config-file" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Configuration file</h2>
|
||||
<p class="text-secondary">Enroll can load INI-style defaults from <code>--config</code>, <code>ENROLL_CONFIG</code>, <code>$XDG_CONFIG_HOME/enroll/enroll.ini</code>, or <code>~/.config/enroll/enroll.ini</code>. Current-directory config files are not auto-loaded; pass them explicitly.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docs-config"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="docs-config">[enroll]
|
||||
assume_safe_path = false
|
||||
|
||||
[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 = host.example.net
|
||||
# remote_user = admin
|
||||
|
||||
[manifest]
|
||||
no_jinjaturtle = true
|
||||
# sops = 54A91143AE0AB4F7743B01FE888ED1B423A3BC99
|
||||
|
||||
[diff]
|
||||
exclude_path = /var/anacron
|
||||
ignore_package_versions = true
|
||||
|
||||
[single-shot]
|
||||
include_path = re:^/home/[^/]+/\.config/myapp/.*$</code></pre></div>
|
||||
<p class="small text-secondary mt-3 mb-0">Precedence is explicit CLI flags, then INI config, then argparse defaults. For hyphenated flags, use underscores in the INI file.</p>
|
||||
ignore_package_versions = true</code></pre></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="run-ansible" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Run generated Ansible</h2>
|
||||
<p class="text-secondary">Install generated collection requirements first, then run in check mode before applying for real.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#docs-run-ansible"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="docs-run-ansible"><span class="prompt">$</span> cd /tmp/enroll-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
|
||||
<span class="prompt">$</span> ansible-playbook -i "localhost," -c local playbook.yml
|
||||
|
||||
# multi-site
|
||||
<span class="prompt">$</span> ansible-playbook playbooks/host.example.net.yml --check
|
||||
|
||||
# targeted role tags
|
||||
<span class="prompt">$</span> ansible-playbook -i "localhost," -c local playbook.yml --tags role_users,role_services</code></pre></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 project’s 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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,165 +1,91 @@
|
|||
---
|
||||
title: "Examples"
|
||||
html_title: "Enroll examples - Ansible harvest and manifest recipes"
|
||||
description: "Copy/paste recipes for current Enroll: Ansible output, remote harvests, drift detection, SOPS, and validation."
|
||||
og_title: "Enroll examples"
|
||||
og_description: "Practical Enroll recipes for local and remote harvests, Ansible manifesting, SOPS bundles, drift reports, and validation."
|
||||
og_type: "article"
|
||||
html_title: "Enroll Examples"
|
||||
description: "Copy/paste Enroll recipes for local harvests, remote harvests, multi-site manifests, SOPS, diff, validate, and explain."
|
||||
---
|
||||
<header class="py-5 bg-light border-bottom">
|
||||
<div class="container">
|
||||
<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 Enroll recipes</h1>
|
||||
<p class="lead mb-0">Current examples for Ansible output, remote harvests, safe storage, drift reports, and troubleshooting-friendly validation.</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">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Local safe-mode baseline</h2>
|
||||
<p class="text-secondary">Harvest the current host, validate the bundle, generate Ansible, install requirements, and run a check-mode playbook.</p>
|
||||
<div class="codeblock terminal"><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="mb-0"><code id="ex-local"><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest
|
||||
<span class="prompt">$</span> enroll validate /tmp/enroll-harvest
|
||||
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
|
||||
<span class="prompt">$</span> cd /tmp/enroll-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</code></pre></div>
|
||||
</section>
|
||||
</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">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Remote host through SSH config</h2>
|
||||
<p class="text-secondary">Use an OpenSSH config alias while still passing <code>--remote-host</code>. The harvested bundle lands locally.</p>
|
||||
<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 production-web-1 \
|
||||
<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"><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-web-1
|
||||
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-web-1 --out /tmp/enroll-web-1-ansible --fqdn web-1.example.net</code></pre></div>
|
||||
</section>
|
||||
</div>
|
||||
--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">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Encrypted SSH key in CI</h2>
|
||||
<p class="text-secondary">Provide a key passphrase through an environment variable for non-interactive remote harvesting.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-key"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="ex-key"><span class="prompt">$</span> export ENROLL_SSH_KEY_PASSPHRASE='correct horse battery staple'
|
||||
<span class="prompt">$</span> enroll single-shot \
|
||||
--remote-host host.example.net \
|
||||
--remote-user admin \
|
||||
<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-harvest \
|
||||
--out /tmp/enroll-ansible \
|
||||
--fqdn host.example.net</code></pre></div>
|
||||
</section>
|
||||
</div>
|
||||
--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">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Include and exclude paths</h2>
|
||||
<p class="text-secondary">Add targeted files outside the standard scan paths while keeping safe-mode secret checks enabled.</p>
|
||||
<div class="codeblock terminal"><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="mb-0"><code id="ex-paths"><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest \
|
||||
--include-path '/home/*/.bashrc' \
|
||||
<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/*/.config/myapp/cache/**'</code></pre></div>
|
||||
</section>
|
||||
</div>
|
||||
--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">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Dangerous harvest, encrypted at rest</h2>
|
||||
<p class="text-secondary">When you deliberately need aggressive capture, encrypt the harvest bundle immediately.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-sops-harvest"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="ex-sops-harvest"><span class="prompt">$</span> enroll harvest \
|
||||
--out /srv/enroll/host-1 \
|
||||
<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">This writes an encrypted <code>harvest.tar.gz.sops</code> rather than a plaintext bundle directory.</p>
|
||||
</section>
|
||||
</div>
|
||||
--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">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Encrypted manifest bundle</h2>
|
||||
<p class="text-secondary">Generate Ansible into a temporary directory, tar it, and encrypt it as one SOPS file.</p>
|
||||
<div class="codeblock terminal"><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="mb-0"><code id="ex-sops-manifest"><span class="prompt">$</span> enroll manifest \
|
||||
--harvest /srv/enroll/host-1/harvest.tar.gz.sops \
|
||||
--out /srv/enroll/host-1-manifest \
|
||||
<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>
|
||||
|
||||
<span class="prompt">$</span> cd /srv/enroll/host-1-manifest
|
||||
<span class="prompt">$</span> sops -d manifest.tar.gz.sops | tar -xzvf -
|
||||
<span class="prompt">$</span> cd manifest</code></pre></div>
|
||||
</section>
|
||||
</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"
|
||||
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">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Multi-site output</h2>
|
||||
<p class="text-secondary">Use <code>--fqdn</code> when enrolling multiple already-running servers. Host-specific data goes to inventory host vars.</p>
|
||||
<div class="codeblock terminal"><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="mb-0"><code id="ex-fqdn"><span class="prompt">$</span> fqdn="$(hostname -f)"
|
||||
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$fqdn"
|
||||
<span class="prompt">$</span> ansible-playbook /tmp/enroll-ansible/playbooks/"$fqdn".yml --check</code></pre></div>
|
||||
</section>
|
||||
</div>
|
||||
<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
|
||||
|
||||
<div class="col-lg-6">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Targeted Ansible role runs</h2>
|
||||
<p class="text-secondary">Generated playbooks tag roles as <code>role_<name></code>, so you can run only a subset.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-tags"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="ex-tags"><span class="prompt">$</span> ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml --tags role_users,role_services</code></pre></div>
|
||||
</section>
|
||||
</div>
|
||||
# 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">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Drift report for cron or CI</h2>
|
||||
<p class="text-secondary">Ignore routine version churn while still catching package additions/removals, service changes, user changes, and file drift.</p>
|
||||
<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 /srv/enroll/baseline \
|
||||
<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 \
|
||||
--format json \
|
||||
--ignore-package-versions \
|
||||
--exclude-path /var/anacron \
|
||||
--exit-code</code></pre></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Webhook notification</h2>
|
||||
<p class="text-secondary">Send the diff report when changes are detected. Use <code>--notify-always</code> when a heartbeat is useful.</p>
|
||||
<div class="codeblock terminal"><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="mb-0"><code id="ex-webhook"><span class="prompt">$</span> enroll diff \
|
||||
--old /srv/enroll/baseline \
|
||||
--new /srv/enroll/current \
|
||||
--webhook https://example.net/hooks/enroll \
|
||||
--webhook https://example.net/enroll/webhook \
|
||||
--webhook-format json \
|
||||
--webhook-header 'X-Enroll-Secret: xxxx'</code></pre></div>
|
||||
</section>
|
||||
</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">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Explain a harvest</h2>
|
||||
<p class="text-secondary">Use explain when you want to understand why Enroll included or skipped things.</p>
|
||||
<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
|
||||
<span class="prompt">$</span> enroll explain /tmp/enroll-harvest --format json --max-examples 25</code></pre></div>
|
||||
</section>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<section class="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Validate with a pinned schema</h2>
|
||||
<p class="text-secondary">CI can validate a harvested bundle against a local schema file and fail on warnings.</p>
|
||||
<div class="codeblock terminal"><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="mb-0"><code id="ex-validate"><span class="prompt">$</span> enroll validate /srv/enroll/current \
|
||||
--schema ./state.schema.json \
|
||||
--format json \
|
||||
--out validate.json \
|
||||
--fail-on-warnings</code></pre></div>
|
||||
</section>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
38
src/content/news.html
Normal file
38
src/content/news.html
Normal 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>
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
---
|
||||
title: "Enroll 0.7.0: stronger safety tooling and new features"
|
||||
date: "2026-06-30"
|
||||
description: "Enroll 0.7.0 has much more robust defenses against malicious harvested data and what gets manifested into Ansible."
|
||||
summary: "Enroll 0.7.0 focuses on making Enroll very robust against many forms of attack, introduces 'combined' roles for faster and simpler execution, and adds Docker/Flatpak/Snap detection."
|
||||
html_title: "Enroll 0.7.0 release notes"
|
||||
og_title: "Enroll 0.7.0 release notes"
|
||||
og_description: "0.7.0 focuses on making Enroll very robust against many forms of attack, introduces 'combined' roles for faster and simpler execution, and adds Docker/Flatpak/Snap detection."
|
||||
og_type: "article"
|
||||
---
|
||||
<header class="py-5 bg-light border-bottom">
|
||||
<div class="container">
|
||||
<div class="kicker mb-3"><i class="bi bi-megaphone"></i> News</div>
|
||||
<h1 class="display-6 fw-bold mb-2">Enroll 0.7.0: stronger safety tooling and other improvements</h1>
|
||||
<p class="lead mb-0">The new release focuses on hardening Enroll's defenses against various types of attack/content injection, along with some other new features and improvements.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="py-5">
|
||||
<div class="container">
|
||||
<article class="mx-auto" style="max-width: 900px;">
|
||||
<h2 class="h3 fw-bold mt-5">What Enroll is doing</h2>
|
||||
<ul class="text-secondary">
|
||||
<li><strong>Harvest bundles</strong> with <code>state.json</code> and captured artifacts.</li>
|
||||
<li><strong>Ansible manifesting</strong> in single-site mode or <code>--fqdn</code> multi-site mode.</li>
|
||||
<li><strong>Role grouping</strong> by package section/group where possible, with <code>--no-common-roles</code> available when you prefer one generated role per package or unit.</li>
|
||||
<li><strong>Remote harvesting</strong> over SSH, including OpenSSH config support, sudo prompts, and encrypted key passphrase handling.</li>
|
||||
<li><strong>Diff, explain, and validate</strong> workflows for drift reporting and bundle inspection.</li>
|
||||
<li><strong>SOPS at-rest encryption</strong> for harvest and manifest bundles.</li>
|
||||
<li><strong>JinjaTurtle integration</strong> for supported Ansible templates when the executable is available.</li>
|
||||
</ul>
|
||||
|
||||
<h2 class="h3 fw-bold mt-5">A current workflow</h2>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#news-workflow"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="news-workflow"><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</code></pre></div>
|
||||
|
||||
<h2 class="h3 fw-bold mt-5">Security and provenance changes</h2>
|
||||
<p class="text-secondary">Version 0.7.0 is also more explicit about trust boundaries. Enroll validates structure and tries to avoid accidental secret capture, symlink traversal, unsafe artifact paths, unsafe root output paths, and root <code>PATH</code> foot-guns. It still cannot prove that a harvested desired state is semantically safe to apply.</p>
|
||||
<p class="text-secondary">When manifesting from a harvest, you'll see a message like this:</p>
|
||||
<blockquote class="blockquote border-start ps-3 text-secondary">This harvest is structurally valid, but Enroll cannot prove it is semantically safe. Only apply manifests generated from harvests whose provenance you trust.</blockquote>
|
||||
|
||||
<h2 class="h3 fw-bold mt-5">Other notable changes</h2>
|
||||
<ul class="text-secondary">
|
||||
<li>In 'single site' mode, Enroll now combines packages/configs into 'common' roles where the 'Section' name of that software is common. This makes Ansible faster to run, and is easier to digest cognitively (you might now get 20 roles instead of 200!). This behaviour can be disabled with <code>--no-common-roles</code> or when using the <code>--fqdn</code> mode.</li>
|
||||
<li>Enroll now tries to detect Docker images on your system. This requires the docker galaxy role in Ansible. It will enforce that the specific SHA256 of that image is present (not floating tagsZ), when running Ansible.</li>
|
||||
<li>Enroll now tries to detect system and user-level Flatpaks, as well as Snaps.</li>
|
||||
<li>Safe-mode content scanning tolerates value-less credential words in comments, but populated credential assignments are still treated as sensitive even if commented out.</li>
|
||||
<li>Automatic per-user shell dotfile harvesting is only enabled in <code>--dangerous</code> mode.</li>
|
||||
<li>Plain directory harvests are frozen into a private temp tree before manifesting, reducing TOCTOU exposure between validation and rendering.</li>
|
||||
<li>Remote harvests reject unknown SSH host keys by default.</li>
|
||||
<li>Enroll now implicitly 'validates' a harvest before trying to manifest it to Ansible code.</li>
|
||||
<li>Root runs refuse unsafe non-interactive <code>PATH</code> setups unless <code>--assume-safe-path</code> is supplied.</li>
|
||||
<li>Enroll will not want to harvest or manifest to a directory that is writable by others. It will try to make the harvest or manifest readable only by the user that is executing the command. This is in an effort to resist tampering attacks.</li>
|
||||
<li>Enroll's <code>--enforce</code> argument to <code>enroll diff</code> has been removed. Non-interactive application of manifests is considered too dangerous by the maintainer, despite all the additional hardening of the harvesting described above. If you really want to enforce the previous harvest, you can still code up an <code>enroll manifest [...] && ansible-playbook [....]</code> yourself when detecting that <code>enroll diff</code> sees changes between two harvests.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
title: "Enroll 0.8.0: more hardening"
|
||||
date: "2026-07-14"
|
||||
description: "Enroll 0.8.0 adds more hardening to prevent obscure issues."
|
||||
summary: "Enroll 0.8.0 builds on the 0.7.0 release by adding several more hardening measures in an effort to keep it as safe as possible to use."
|
||||
html_title: "Enroll 0.8.0 release notes"
|
||||
og_title: "Enroll 0.8.0 release notes"
|
||||
og_description: "Enroll 0.8.0 builds on the 0.7.0 release by adding several more hardening measures in an effort to keep it as safe as possible to use."
|
||||
og_type: "article"
|
||||
---
|
||||
<header class="py-5 bg-light border-bottom">
|
||||
<div class="container">
|
||||
<div class="kicker mb-3"><i class="bi bi-megaphone"></i> News</div>
|
||||
<h1 class="display-6 fw-bold mb-2">Enroll 0.8.0: more hardening</h1>
|
||||
<p class="lead mb-0">0.8.0 builds on the 0.7.0 release by adding several more hardening measures in an effort to keep it as safe as possible to use.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="py-5">
|
||||
<div class="container">
|
||||
<article class="mx-auto" style="max-width: 900px;">
|
||||
<h2 class="h3 fw-bold mt-5">Changelog</h2>
|
||||
<ul class="text-secondary">
|
||||
<li>Keep sudo-created remote harvest bundles root-owned while root packages and hashes them, expose only the archive to the authenticated SSH uid, and verify the root-computed digest after download. This removes a tiny post-harvest tampering window created by recursively chowning the bundle before packaging, yet without making the plaintext archive world-readable.</li>
|
||||
<li>Enforce tar member limits while lazily parsing untrusted archives rather than after `TarFile.getmembers()` has already indexed the entire archive; count repeated `.` entries and cap remote compressed downloads as well.</li>
|
||||
<li>Apply aggregate byte and total filesystem-entry limits when freezing directory harvest bundles, reject symlinked bundle roots, and abort when files or discovered directories change during the copy, so direct directory inputs remain bounded and fail closed under mutation.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
title: "News"
|
||||
html_title: "Enroll news"
|
||||
description: "News and release notes for Enroll."
|
||||
og_title: "Enroll news"
|
||||
og_description: "News and release notes for Enroll."
|
||||
og_type: "website"
|
||||
---
|
||||
<header class="py-5 bg-light border-bottom">
|
||||
<div class="container">
|
||||
<div class="kicker mb-3"><i class="bi bi-megaphone"></i> News</div>
|
||||
<h1 class="display-6 fw-bold mb-2">Enroll news</h1>
|
||||
<p class="lead mb-0">Release notes and project notes for the current Enroll CLI.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -1,40 +1,46 @@
|
|||
---
|
||||
title: "Schema"
|
||||
layout: "schema"
|
||||
html_title: "Enroll state.json schema"
|
||||
description: "The current JSON Schema for Enroll harvest state.json bundles."
|
||||
og_title: "Enroll state.json schema"
|
||||
og_description: "View the current Enroll JSON Schema and use it to validate harvest bundles."
|
||||
og_type: "article"
|
||||
title: "State Schema"
|
||||
html_title: "Enroll State Schema"
|
||||
description: "The current JSON schema for Enroll harvest state.json bundles."
|
||||
---
|
||||
<header class="py-5 bg-light border-bottom">
|
||||
<div class="container">
|
||||
<div class="kicker mb-3"><i class="bi bi-braces"></i> Harvest schema</div>
|
||||
<h1 class="display-6 fw-bold mb-2">Current <code>state.json</code> schema</h1>
|
||||
<p class="lead mb-0">Enroll validates harvest bundles against the vendored JSON Schema unless you pass <code>--no-schema</code>. The same schema is published here for inspection and CI pinning.</p>
|
||||
<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">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="feature-card p-4 h-100">
|
||||
<h2 class="h4 fw-bold">Validate with the vendored schema</h2>
|
||||
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll validate ./harvest</code></pre></div>
|
||||
<h2 class="h4 fw-bold mt-4">Validate with this hosted schema</h2>
|
||||
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll validate ./harvest \
|
||||
--schema https://enroll.sh/schema/state.schema.json \
|
||||
--allow-remote-schema</code></pre></div>
|
||||
<p class="small text-secondary mt-3 mb-0">Remote schema fetching is disabled by default and must be explicitly allowed.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<div class="codeblock terminal">
|
||||
<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 schema...</code></pre>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,90 +1,90 @@
|
|||
---
|
||||
title: "Security Design"
|
||||
html_title: "Enroll security design and threat model"
|
||||
description: "Threat model and security scope for Enroll, a privileged Linux host harvesting CLI that generates Ansible output."
|
||||
og_title: "Enroll security design"
|
||||
og_description: "Enroll threat model, trusted harvest guidance, in-scope hardening, and vulnerability report guidance."
|
||||
og_type: "article"
|
||||
html_title: "Enroll Security Design"
|
||||
description: "Threat model, trust boundaries, dangerous mode, SOPS, root PATH safety, and manifest safety for Enroll."
|
||||
---
|
||||
<header class="py-5 bg-light border-bottom">
|
||||
<div class="container">
|
||||
<header class="py-5 hero">
|
||||
<div class="container py-3">
|
||||
<div class="kicker mb-3"><i class="bi bi-shield-lock"></i> Security design</div>
|
||||
<h1 class="display-6 fw-bold mb-2">Threat model and security scope</h1>
|
||||
<p class="lead mb-0">Enroll is a privileged systems-administration CLI, not a sandbox. It tries to protect careful administrators from common mistakes, while assuming the operator controls the environment they run it in.</p>
|
||||
<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">
|
||||
<aside class="col-lg-3">
|
||||
<div class="position-sticky" style="top: 6rem;">
|
||||
<div class="list-group small shadow-sm">
|
||||
<a class="list-group-item list-group-item-action" href="#assumptions">Core assumptions</a>
|
||||
<a class="list-group-item list-group-item-action" href="#in-scope">In scope</a>
|
||||
<a class="list-group-item list-group-item-action" href="#out-of-scope">Out of scope</a>
|
||||
<a class="list-group-item list-group-item-action" href="#trusted-harvests">Trusted harvests</a>
|
||||
<a class="list-group-item list-group-item-action" href="#reporting">Report guidance</a>
|
||||
<div class="col-lg-8">
|
||||
<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">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="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="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 <GPG_FINGERPRINT>
|
||||
enroll manifest --harvest ./secure-harvest/harvest.tar.gz.sops \
|
||||
--out ./secure-manifest --sops <GPG_FINGERPRINT></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>
|
||||
</aside>
|
||||
<div class="col-lg-9">
|
||||
<section id="assumptions" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Core assumptions</h2>
|
||||
<p class="text-secondary">Enroll is designed to be executed intentionally by a system administrator, often as root, to inspect a host and generate Ansible output. If an attacker controls the administrator's command line, environment, config file, working directory, <code>PATH</code>, SSH config, SOPS binary, Ansible installation, or harvested input bundle, they may influence what Enroll does. That is considered a local trust-boundary failure outside Enroll's intended security model.</p>
|
||||
<ul class="text-secondary">
|
||||
<li>The operator understands options such as <code>--dangerous</code>, <code>--assume-safe-path</code>, <code>--sops</code>, <code>--remote-host</code>, and <code>--remote-ssh-config</code>.</li>
|
||||
<li>Configuration files loaded from <code>enroll.ini</code> are selected and trusted by the operator.</li>
|
||||
<li>Harvest bundles used for <code>manifest</code> or <code>diff</code> come from a trusted source unless the operator is deliberately inspecting untrusted input without applying it.</li>
|
||||
<li>External tools invoked by Enroll, including Ansible, SOPS, SSH, sudo, Docker, Podman, Flatpak, Snap, package managers, and system utilities, are the trusted tools the operator intended to use.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="in-scope" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">What Enroll tries to defend against</h2>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Accidental secret capture</div><p class="small text-secondary mb-0">Default mode avoids obvious sensitive paths, private keys, token/password assignments, authorization headers, credential URIs, and similar material.</p></div></div>
|
||||
<div class="col-md-6"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Unsafe filesystem behavior</div><p class="small text-secondary mb-0">Enroll avoids symlink traversal, hardlinks, device nodes, tar path traversal, unsafe artifact paths, and common TOCTOU issues when copying and consuming artifacts.</p></div></div>
|
||||
<div class="col-md-6"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Unsafe generated Ansible</div><p class="small text-secondary mb-0">Harvested values are serialized as Ansible data, template-looking values are tagged <code>!unsafe</code>, and raw task scaffold identifiers are allowlisted.</p></div></div>
|
||||
<div class="col-md-6"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Risky privileged automation</div><p class="small text-secondary mb-0">Plain harvest outputs are private by default. Root-run output paths and unsafe root <code>PATH</code> entries are checked. Remote harvest rejects unknown SSH host keys by default.</p></div></div>
|
||||
<div class="col-lg-4">
|
||||
<div class="callout p-4">
|
||||
<div class="fw-semibold mb-2">Recommended workflow</div>
|
||||
<ol class="small mb-0">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section id="out-of-scope" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">What is out of scope</h2>
|
||||
<p class="text-secondary">The following are normally operator trust failures, not Enroll vulnerabilities by themselves:</p>
|
||||
<ul class="text-secondary">
|
||||
<li>A malicious local user already controlling root's shell, environment, config files, <code>PATH</code>, binaries, SSH config, or working directory.</li>
|
||||
<li>A root user intentionally loading a config file or passing options that request dangerous behavior.</li>
|
||||
<li>Using <code>--dangerous</code> and observing that Enroll may collect sensitive information.</li>
|
||||
<li>Using <code>--assume-safe-path</code> and observing that Enroll does not prompt about <code>PATH</code>.</li>
|
||||
<li>Applying generated Ansible from an untrusted harvest.</li>
|
||||
<li>Trusting an untrusted webhook, email target, SSH proxy command, SOPS binary, package manager, or Ansible toolchain.</li>
|
||||
<div class="callout p-4 mt-3">
|
||||
<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">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>
|
||||
</section>
|
||||
|
||||
<section id="trusted-harvests" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Trusted harvests</h2>
|
||||
<p class="text-secondary">Harvest bundles are sensitive administrative artifacts. They may contain hostnames, usernames, package lists, service state, filesystem metadata, configuration files, firewall snapshots, container image references, Flatpak/Snap state, and other operational details. In <code>--dangerous</code> mode they may contain much more sensitive material.</p>
|
||||
<div class="alert alert-warning"><strong>Important:</strong> Enroll validates harvest structure and artifact safety. It cannot prove that the desired state represented by a harvest is safe to apply. Before running <code>manifest</code>, <code>diff</code>, or Ansible, be confident the harvest came from a trusted source and has not been tampered with.</div>
|
||||
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll validate ./harvest
|
||||
<span class="prompt">$</span> enroll manifest --harvest ./harvest --out ./ansible</code></pre></div>
|
||||
</section>
|
||||
|
||||
<section id="reporting" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Useful security reports</h2>
|
||||
<p class="text-secondary">Useful reports show Enroll behaving unsafely despite the documented trust model. Examples include:</p>
|
||||
<ul class="text-secondary">
|
||||
<li>Capturing a clearly sensitive default-denied file without <code>--dangerous</code>.</li>
|
||||
<li>Following a symlink or hardlink in a way that causes privileged file disclosure or overwrite.</li>
|
||||
<li>Extracting a tar member outside the intended harvest directory.</li>
|
||||
<li>Accepting a malicious harvest artifact that escapes the artifact root.</li>
|
||||
<li>Generating Ansible where ordinary harvested data can cause command injection or YAML/template structure injection.</li>
|
||||
<li>Writing root-run output into an unsafe attacker-controlled path despite safety checks.</li>
|
||||
<li>Accepting an unknown SSH host key unexpectedly.</li>
|
||||
</ul>
|
||||
<p class="text-secondary mb-0">Less useful reports are variations of “root deliberately asked Enroll to do dangerous things”. Those are expected consequences of running a privileged administration tool with trusted operator-controlled input.</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,111 +1,28 @@
|
|||
---
|
||||
title: "Troubleshooting"
|
||||
html_title: "Enroll troubleshooting - current Ansible CLI"
|
||||
description: "Common Enroll errors and fixes for the current Ansible-only CLI."
|
||||
og_title: "Enroll troubleshooting"
|
||||
og_description: "Fix unsafe root PATH, missing Ansible collections, JinjaTurtle, remote SSH auth, validation, and SOPS extraction."
|
||||
og_type: "article"
|
||||
html_title: "Enroll Troubleshooting"
|
||||
description: "Common Enroll errors and what they mean: unsafe PATH, sudo, SSH key passphrases, SOPS, validation, and Ansible runs."
|
||||
---
|
||||
<header class="py-5 bg-light border-bottom">
|
||||
<div class="container">
|
||||
<div class="kicker mb-3"><i class="bi bi-life-preserver"></i> Troubleshooting</div>
|
||||
<h1 class="display-6 fw-bold mb-2">Common Enroll problems and fixes</h1>
|
||||
<p class="lead mb-0">Current fixes for the Ansible-only Enroll CLI, including stale old examples, unsafe root <code>PATH</code>, JinjaTurtle, remote SSH, SOPS, and generated collection requirements.</p>
|
||||
<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="row g-4">
|
||||
<aside class="col-lg-3">
|
||||
<div class="position-sticky" style="top: 6rem;">
|
||||
<div class="list-group small shadow-sm">
|
||||
<a class="list-group-item list-group-item-action" href="#no-target">Unknown <code>--target</code></a>
|
||||
<a class="list-group-item list-group-item-action" href="#root-path">Unsafe root PATH</a>
|
||||
<a class="list-group-item list-group-item-action" href="#collections">Ansible collections</a>
|
||||
<a class="list-group-item list-group-item-action" href="#jinjaturtle">JinjaTurtle</a>
|
||||
<a class="list-group-item list-group-item-action" href="#missing-files">Missing files</a>
|
||||
<a class="list-group-item list-group-item-action" href="#validate">Validation</a>
|
||||
<a class="list-group-item list-group-item-action" href="#remote">Remote auth</a>
|
||||
<a class="list-group-item list-group-item-action" href="#sops">SOPS bundles</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="col-lg-9">
|
||||
<section id="root-path" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Root run refuses to continue because <code>PATH</code> is unsafe</h2>
|
||||
<p class="text-secondary">When running as root, Enroll refuses non-interactive execution if <code>PATH</code> contains <code>.</code>, an empty entry, a relative entry, a non-root-owned directory, or group/world-writable directories/parents that could let an untrusted binary be executed.</p>
|
||||
<div class="callout p-4">
|
||||
<div class="fw-semibold mb-2">Fix the environment, or explicitly accept it</div>
|
||||
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
<span class="prompt">$</span> enroll harvest --out ./harvest
|
||||
|
||||
# only for trusted CI environments where the PATH is intentional
|
||||
<span class="prompt">$</span> enroll harvest --out ./harvest --assume-safe-path</code></pre></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="collections" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Ansible complains about missing modules or unsupported parameters</h2>
|
||||
<p class="text-secondary">Generated roles can use collections for Docker, Podman, Flatpak, Snap, or other resources. Install the generated requirements from the manifest output directory before running the playbook.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#trouble-collections"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="trouble-collections"><span class="prompt">$</span> cd /tmp/enroll-ansible
|
||||
<span class="prompt">$</span> ansible-galaxy collection install -r requirements.yml
|
||||
<span class="prompt">$</span> ansible-galaxy collection list</code></pre></div>
|
||||
<p class="small text-secondary mt-3 mb-0">If Ansible still uses an older collection, check your configured collection paths and remove or override stale copies.</p>
|
||||
</section>
|
||||
|
||||
<section id="jinjaturtle" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">JinjaTurtle is missing or template generation was skipped</h2>
|
||||
<p class="text-secondary">By default, Enroll uses JinjaTurtle if the <code>jinjaturtle</code> executable exists on <code>PATH</code>; otherwise it safely copies raw files. <code>--jinjaturtle</code> makes missing JinjaTurtle a hard error, and <code>--no-jinjaturtle</code> disables templating entirely.</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Require JinjaTurtle</div><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll manifest --harvest ./harvest --out ./ansible --jinjaturtle</code></pre></div></div></div>
|
||||
<div class="col-md-6"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Disable templating</div><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll manifest --harvest ./harvest --out ./ansible --no-jinjaturtle</code></pre></div></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="missing-files" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">A file I expected was not harvested</h2>
|
||||
<p class="text-secondary">Default harvesting is conservative. A file may be skipped because it matched an exclude pattern, looked sensitive, looked binary, was too large, was already captured elsewhere, was unreadable, or was outside standard scan paths.</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Explicitly include a path</div><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --out ./harvest --include-path /opt/myapp/config.yml</code></pre></div></div></div>
|
||||
<div class="col-md-6"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Inspect the result</div><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll explain ./harvest
|
||||
<span class="prompt">$</span> enroll validate ./harvest</code></pre></div></div></div>
|
||||
</div>
|
||||
<div class="alert alert-warning mt-3 mb-0"><strong>Be careful with <code>--dangerous</code>:</strong> it can harvest secrets such as private keys, tokens, credentials, and application config containing passwords.</div>
|
||||
</section>
|
||||
|
||||
<section id="validate" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Validation fails, or a manifest references a missing artifact</h2>
|
||||
<p class="text-secondary">Validate the original harvest. Validation checks the schema, referenced artifacts, artifact safety, and unreferenced artifact warnings.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#trouble-validate"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="trouble-validate"><span class="prompt">$</span> enroll validate /path/to/harvest
|
||||
<span class="prompt">$</span> enroll validate /path/to/harvest --format json --out validate.json
|
||||
<span class="prompt">$</span> enroll validate /path/to/harvest --fail-on-warnings</code></pre></div>
|
||||
<p class="small text-secondary mt-3 mb-0">If validation passes but Ansible cannot find a file, check whether the generated output was partially copied, edited, or moved after rendering.</p>
|
||||
</section>
|
||||
|
||||
<section id="remote" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">Remote harvest cannot sudo or cannot unlock an SSH key</h2>
|
||||
<p class="text-secondary">Remote mode can prompt for sudo and private-key passphrases. In non-interactive shells, provide the required inputs explicitly.</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Prompt for sudo</div><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --remote-host host.example.net --ask-become-pass --out ./harvest</code></pre></div></div></div>
|
||||
<div class="col-md-4"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">Prompt for SSH key</div><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --remote-host host.example.net --ask-key-passphrase --out ./harvest</code></pre></div></div></div>
|
||||
<div class="col-md-4"><div class="callout p-4 h-100"><div class="fw-semibold mb-2">CI key passphrase</div><div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> export ENROLL_SSH_KEY_PASSPHRASE='...'
|
||||
<span class="prompt">$</span> enroll harvest --remote-host host.example.net --ssh-key-passphrase-env ENROLL_SSH_KEY_PASSPHRASE --out ./harvest</code></pre></div></div></div>
|
||||
</div>
|
||||
<p class="small text-secondary mt-3 mb-0">If host key verification fails, connect with normal <code>ssh</code> first so the expected key is in <code>known_hosts</code>, or use <code>--remote-ssh-config</code>.</p>
|
||||
</section>
|
||||
|
||||
<section id="sops" class="scroll-mt-nav mb-5">
|
||||
<h2 class="section-title fw-bold">I have a SOPS manifest bundle. How do I run it?</h2>
|
||||
<p class="text-secondary"><code>manifest --sops</code> produces one encrypted <code>manifest.tar.gz.sops</code> file. It is an at-rest storage format, not something Ansible runs directly. Decrypt and extract it first.</p>
|
||||
<div class="codeblock terminal"><button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#trouble-sops"><i class="bi bi-clipboard"></i> Copy</button><pre class="mb-0"><code id="trouble-sops"><span class="prompt">$</span> cd /path/to/output
|
||||
<span class="prompt">$</span> sops -d manifest.tar.gz.sops | tar -xzvf -
|
||||
<span class="prompt">$</span> cd manifest
|
||||
<span class="prompt">$</span> ansible-galaxy collection install -r requirements.yml
|
||||
<span class="prompt">$</span> ansible-playbook -i "localhost," -c local playbook.yml --check</code></pre></div>
|
||||
</section>
|
||||
</div>
|
||||
<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 <fingerprint></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 <GPG_FINGERPRINT></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>
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ uglyURLs = true
|
|||
disableKinds = ["taxonomy", "term", "RSS", "sitemap"]
|
||||
|
||||
[params]
|
||||
description = "Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible configuration-management code from what it finds. Harvest → Manifest → Manage."
|
||||
description = "Enroll harvests real Linux host state and renders Ansible configuration-management output."
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@
|
|||
},
|
||||
"src_rel": {
|
||||
"minLength": 1,
|
||||
"pattern": "^(?!\\.\\.?(?:/|$))[^/\\u0000-\\u001f\\\\]+(?:/(?!\\.\\.?(?:/|$))[^/\\u0000-\\u001f\\\\]+)*$",
|
||||
"pattern": "^[^/].*",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
{{ define "main" }}
|
||||
{{ .Content | safeHTML }}
|
||||
<main class="py-5">
|
||||
<div class="container">
|
||||
<div class="row g-4">
|
||||
{{ range .Pages.ByDate.Reverse }}
|
||||
<div class="col-lg-6">
|
||||
<article class="feature-card p-4 h-100">
|
||||
<div class="small text-secondary mb-2">{{ .Date.Format "2 January 2006" }}</div>
|
||||
<h2 class="h4 fw-bold mb-2"><a class="link-dark text-decoration-none" href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
|
||||
{{ with .Params.summary }}<p class="text-secondary">{{ . }}</p>{{ end }}
|
||||
<a class="btn btn-sm btn-outline-dark" href="{{ .RelPermalink }}">Read more</a>
|
||||
</article>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{{ define "main" }}
|
||||
{{ .Content | safeHTML }}
|
||||
{{ end }}
|
||||
|
|
@ -3,60 +3,47 @@
|
|||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-6">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<img class="brand-mark" src="{{ "assets/img/enroll.svg" | relURL }}" alt="Enroll">
|
||||
<img class="brand-mark" src="assets/img/enroll.svg" alt="Enroll">
|
||||
<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">SSH</span>
|
||||
<span class="badge badge-soft rounded-pill">0.7 beta docs</span>
|
||||
</div>
|
||||
<div class="text-secondary mb-3">Reverse-engineering Linux hosts 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>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<div class="fw-semibold mb-2">Site</div>
|
||||
<ul class="list-unstyled small mb-0">
|
||||
<li><a class="link-secondary text-decoration-none" href="{{ "docs.html" | relURL }}">Docs</a></li>
|
||||
<li><a class="link-secondary text-decoration-none" href="{{ "examples.html" | relURL }}">Examples</a></li>
|
||||
<li><a class="link-secondary text-decoration-none" href="{{ "news.html" | relURL }}">News</a></li>
|
||||
<li><a class="link-secondary text-decoration-none" href="{{ "troubleshooting.html" | relURL }}">Troubleshooting</a></li>
|
||||
<li><a class="link-secondary text-decoration-none" href="{{ "schema.html" | relURL }}">Schema</a></li>
|
||||
<li><a class="link-secondary text-decoration-none" href="{{ "security.html" | relURL }}">Security Design</a></li>
|
||||
<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="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">Bugs or feature suggestions?</div>
|
||||
<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><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>
|
||||
|
|
|
|||
|
|
@ -21,4 +21,4 @@
|
|||
<!-- Bootstrap -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link href="{{ "assets/css/site.css" | relURL }}" rel="stylesheet">
|
||||
<link href="assets/css/site.css" rel="stylesheet">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<nav class="navbar navbar-expand-lg bg-white bg-opacity-75 sticky-top border-bottom" data-bs-theme="light">
|
||||
<div class="container py-1">
|
||||
<a class="navbar-brand fw-bold d-flex align-items-center gap-2" href="{{ "index.html" | relURL }}">
|
||||
<img class="brand-mark" src="{{ "assets/img/enroll.svg" | relURL }}" alt="Enroll">
|
||||
<a class="navbar-brand fw-bold d-flex align-items-center gap-2" href="index.html">
|
||||
<img class="brand-mark" src="assets/img/enroll.svg" alt="Enroll">
|
||||
<span>Enroll</span>
|
||||
</a>
|
||||
|
||||
|
|
@ -12,22 +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="{{ "news.html" | relURL }}">News</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ "docs.html" | relURL }}">Docs</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ "examples.html" | relURL }}">Examples</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ "security.html" | relURL }}">Security Design</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ "troubleshooting.html" | relURL }}">Troubleshooting</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
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@
|
|||
})();
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{ "assets/js/site.js" | relURL }}"></script>
|
||||
<script src="assets/js/site.js"></script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue