Fixes for a never-to-be-released, um, release
All checks were successful
CI / test (push) Successful in 2m27s

This commit is contained in:
Miguel Jacq 2026-06-22 20:26:45 +10:00
parent 277249c4c5
commit ff1ac0e01c
Signed by: mig5
GPG key ID: 03906B4110AAD3B8
4 changed files with 228 additions and 11 deletions

View file

@ -292,11 +292,11 @@ description: "How Enroll works: harvest, manifest, modes, and configuration."
<section id="config" class="scroll-mt-nav mb-5"> <section id="config" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">INI config file</h2> <h2 class="section-title fw-bold">INI config file</h2>
<p class="text-secondary">If you're repeating flags (include/exclude patterns, SOPS settings, etc.), store defaults in <code>enroll.ini</code> and keep your muscle memory intact.</p> <p class="text-secondary">If you're repeating flags (include/exclude patterns, SOPS settings, etc.), store defaults in <code>~/config/enroll/enroll.ini</code> and keep your muscle memory intact.</p>
<div class="callout p-4 mb-3"> <div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Discovery order</div> <div class="fw-semibold mb-1">Discovery order</div>
<div class="small text-secondary mb-0">You can pass <code>-c/--config</code>, set <code>ENROLL_CONFIG</code>, or let Enroll auto-discover <code>./enroll.ini</code>, <code>./.enroll.ini</code>, or <code>~/.config/enroll/enroll.ini</code>.</div> <div class="small text-secondary mb-0">You can pass <code>-c/--config</code>, set <code>ENROLL_CONFIG</code>, let Enroll auto-discover either your XDG config default directory, or finally <code>~/.config/enroll/enroll.ini</code>.</div>
</div> </div>
<div class="codeblock terminal"> <div class="codeblock terminal">

View file

@ -132,10 +132,23 @@ localhost : ok=5 changed=0 unreachable=0 failed=0 s
<li><code>.bashrc</code> and similar files are now only harvested from user directories when <code>--dangerous</code> is used, since this is a common place for sensitive environment variables to be set. As always, remember that <code>--dangerous</code> gives better harvest coverage, but you should use <code>--sops</code> or some other means of your own to encrypt the harvested data at rest safely!</li> <li><code>.bashrc</code> and similar files are now only harvested from user directories when <code>--dangerous</code> is used, since this is a common place for sensitive environment variables to be set. As always, remember that <code>--dangerous</code> gives better harvest coverage, but you should use <code>--sops</code> or some other means of your own to encrypt the harvested data at rest safely!</li>
<li>Some output during an Ansible play is hidden with <code>no_log</code> to avoid potentially sensitive output, particularly of systemd unit state.</li> <li>Some output during an Ansible play is hidden with <code>no_log</code> to avoid potentially sensitive output, particularly of systemd unit state.</li>
<li>In case you missed it in version 0.6.0: Enroll now harvests runtime <code>iptables</code> and <code>ipset</code> rules!</li> <li>In case you missed it in version 0.6.0: Enroll now harvests runtime <code>iptables</code> and <code>ipset</code> rules!</li>
<li>Enroll no longer reads from <code>.enroll.ini</code> or <code>enroll.ini</code> in the current working directory. Instead, it's recommended to put your config file in <code>~/.config/enroll/enroll.ini</code>, or pass an explicit path with <code>--config</code> or through use of the <code>ENROLL_CONFIG</code> environment variable.</li>
<li>Lots of other hardening and safeguard improvements!</li>
<ul>
<li><code>enroll validate</code> makes sure the harvest doesn't contain unsafe things such as symlinks traversing out of the artifacts tree.</li>
<li><code>enroll manifest</code> takes an internal pass via <code>validate</code> to make sure the harvest validates ok before trying to render config management code. If you find that your harvest is not passing validation, and don't believe it's been tampered with, it may be that it was created on an older version of Enroll and so doesn't pass the current schema. Consider re-harvesting the host.</li>
<li>Other input validations, such as making sure the string passed to <code>--fqdn</code> is safe</li>
<li>Improving detection of sensitive strings in the IgnorePolicy</li>
<li>Safer quoting of command fragments when using remote mode</li>
<li>Warning if running as root and <code>$PATH</code> contains the cwd or a world-writable location (to resist attacks via malicious versions of binaries Enroll calls such as dpkg, rpm, systemctl etc)</li>
<li>TOCTOU avoidance when copying files to harvested artifacts</li>
<li>Permission hardening on manifested dir</li>
<li>"and more!" :) </li>
</ul>
</ul> </ul>
<h2 class="h4 fw-bold mt-4">More coverage</h2> <h2 class="h4 fw-bold mt-4">More coverage</h2>
<p>With these changes comes a lot of new 'variance' and argument input to the app. Pytest coverage is now at about 86%, and there is a big suite unit tests for Ansible, Puppet and Salt too, <a href="https://git.mig5.net/mig5/enroll/actions/runs/592" target="_blank" rel="noopener noreferrer">in CI</a>. I'm continuing to try and automate testing all the ways you can use this tool.</p> <p>With these changes comes a lot of new 'variance' and argument input to the app. Pytest coverage is now at about 86%, and there is a big suite unit tests for Ansible, Puppet and Salt too, <a href="https://git.mig5.net/mig5/enroll/actions/runs/688" target="_blank" rel="noopener noreferrer">in CI</a>, running across both Debian and AlmaLinux environments. I'm continuing to try and automate testing all the ways you can use this tool.</p>
<hr> <hr>
<p>Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports!</p> <p>Thanks to everyone who has reached out with suggestions, constructive criticism, and bug reports!</p>

View file

@ -48,6 +48,43 @@ description: "Security posture and safe workflows for Enroll outputs."
<div class="small mb-0">In manifest <code>--sops</code> mode, you'll need to decrypt and extract the bundle before running <code>ansible-playbook</code>, <code>puppet apply</code>, or <code>salt-call</code>.</div> <div class="small mb-0">In manifest <code>--sops</code> mode, you'll need to decrypt and extract the bundle before running <code>ansible-playbook</code>, <code>puppet apply</code>, or <code>salt-call</code>.</div>
</div> </div>
</div> </div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2">Filesystem hardening for root runs</h2>
<p class="text-secondary">Enroll is often run as root because it needs to inspect system state. Version 0.5.5 added stricter filesystem checks so root does not accidentally write harvests or generated output through an unsafe path.</p>
<div class="row g-3">
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Private output directories</div>
<p class="small text-secondary mb-0">Harvest and manifest outputs are created with restrictive permissions, and Enroll refuses to reuse an existing harvest directory unless that mode is explicitly expected for an internal workflow.</p>
</div>
</div>
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Symlink and parent checks</div>
<p class="small text-secondary mb-0">When running as root, Enroll checks path components and parent directories so output is not created below a parent controlled by another user. This reduces the risk of symlink and time-of-check/time-of-use path races.</p>
</div>
</div>
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Safer artifact handling</div>
<p class="small text-secondary mb-0">Captured files and generated artifacts are validated as regular files, with traversal, symlink, hardlink, and special-file cases rejected when bundles are validated or extracted.</p>
</div>
</div>
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Root <code>PATH</code> hygiene</div>
<p class="small text-secondary mb-0">For root runs, Enroll warns about unsafe <code>PATH</code> entries so helper commands are not resolved from relative, writable, or non-root-owned directories.</p>
</div>
</div>
</div>
<div class="alert alert-info mt-3 mb-0">
<div class="fw-semibold">Why this can feel strict</div>
<div class="small mb-0">A command that writes under a user-owned directory may be convenient, but it can also be hard to prove safe when the process is running as root. Prefer a root-owned output area such as <code>/var/tmp/enroll</code>, <code>/root</code>, or a fresh directory directly under the normal sticky <code>/tmp</code>.</div>
</div>
</div>
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
@ -85,22 +122,130 @@ description: "Security posture and safe workflows for Enroll outputs."
<hr class="my-5"> <hr class="my-5">
<div class="feature-card p-4"> <div class="feature-card p-4">
<h2 class="h4 fw-bold mb-2">Threat model</h2> <h2 class="h4 fw-bold mb-2">Threat model and security scope</h2>
<p class="text-secondary">Enroll is a command-line systems administration tool. It is designed to be run intentionally by an administrator, often as root, to inspect a host, harvest selected system state, and optionally generate or apply configuration-management output.</p>
<p class="text-secondary mb-0">That makes Enroll different from a web application, daemon, network service, or setuid helper. It is not intended to be a sandbox for hostile local users. Instead, it assumes an operator-controlled execution environment, while still adding defense-in-depth checks for common filesystem, path traversal, secret-handling, and command-resolution mistakes.</p>
<div class="row g-3 mt-2">
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Trusted operator assumptions</div>
<ul class="small text-secondary mb-0">
<li>If Enroll is run as root, the root user is assumed to control and understand the command line, environment, configuration file, and output location.</li>
<li>If an <code>enroll.ini</code> file is loaded, its location and contents are assumed to be owned, selected, and understood by the operator.</li>
<li>The operator is expected to understand the implications of options such as <code>--dangerous</code>, <code>--assume-safe-path</code>, <code>--sops</code>, <code>--enforce</code>, <code>--remote-host</code>, and <code>--remote-ssh-config</code>.</li>
<li>Harvest bundles used for <code>manifest</code>, <code>diff</code>, or <code>diff --enforce</code> are assumed to come from a trusted source unless the operator is deliberately inspecting them without applying them.</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Trusted local tools</div>
<p class="small text-secondary mb-0">Enroll invokes ordinary administrative tools such as SSH, <code>sudo</code>, SOPS, package managers, Docker, Podman, Flatpak, Snap, Ansible, Puppet, Salt, and system utilities. These are assumed to be the trusted tools the operator intended to execute.</p>
<p></p>
<p class="small text-secondary mb-0">If an attacker can replace or redirect those tools, the host already has a local trust-boundary problem outside Enroll's ability to fully solve.</p>
</div>
</div>
</div>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2">What Enroll tries to protect against</h2>
<p class="text-secondary">Enroll still performs substantial hardening because privileged CLI tools can otherwise be easy to misuse. These controls are intended to protect careful administrators from common dangerous mistakes.</p>
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<div class="fw-semibold">What Enroll tries to prevent</div>
<ul class="small mb-0"> <ul class="small mb-0">
<li>Accidentally copying obvious secrets in default mode</li> <li>Accidentally copying obvious secrets in default mode</li>
<li>Harvesting huge/unbounded file sets by mistake</li> <li>Harvesting huge or unbounded file sets by mistake</li>
<li>One host's difference causing problems for other hosts by keeping multi-site data in inventory, Hiera, or pillar</li> <li>Writing root-run output through unsafe symlinks, hardlinks, special files, or path-race situations</li>
<li>Extracting harvest tar members outside the intended bundle directory</li>
<li>Copying unsafe harvested artifacts into generated manifests</li>
</ul> </ul>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="fw-semibold">What you still need to think about</div>
<ul class="small mb-0"> <ul class="small mb-0">
<li>Where outputs are stored and who can access them</li> <li>Resolving helper commands from suspicious <code>PATH</code> entries during root runs</li>
<li>Reviewing what was captured before committing/sharing</li> <li>Generating manifest commands where ordinary harvested values could become shell injection</li>
<li>Choosing encryption and secret-management strategy</li> <li>Accepting unknown SSH host keys during remote harvests</li>
<li>Confusing one host's data with another host's data in multi-site outputs</li>
<li>Applying structurally unsafe harvest bundles</li>
</ul>
</div>
</div>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2">What is out of scope</h2>
<p class="text-secondary">The following are normally considered local compromise, operator-controlled behavior, or trust-boundary failures rather than Enroll vulnerabilities by themselves:</p>
<div class="row g-3">
<div class="col-md-6">
<ul class="small mb-0">
<li>A malicious local user who can already control root's command line, shell environment, config file, working directory, <code>PATH</code>, or invoked binaries</li>
<li>A root user loading an <code>enroll.ini</code> file whose contents intentionally request dangerous behavior</li>
<li>A root user passing <code>--dangerous</code> and observing that Enroll may collect sensitive data</li>
<li>A root user passing <code>--assume-safe-path</code> and observing that Enroll does not prompt about <code>PATH</code> safety</li>
</ul>
</div>
<div class="col-md-6">
<ul class="small mb-0">
<li>A user applying generated Ansible, Puppet, or Salt output from a harvest bundle they do not trust</li>
<li>A user enforcing a malicious or manually edited harvest bundle with <code>diff --enforce</code></li>
<li>A user configuring an untrusted webhook, SSH proxy command, SOPS binary, or configuration-management tool</li>
<li>Reports that amount to: root can run Enroll with malicious options and make the system do dangerous things</li>
</ul>
</div>
</div>
<div class="alert alert-secondary mt-3 mb-0">
<div class="fw-semibold">Plain-language summary</div>
<div class="small mb-0">Enroll is an administrator's tool, not a local privilege boundary. If the operator's root execution environment is already attacker-controlled, Enroll cannot make that safe. Enroll does, however, try to reduce the damage from common path, permission, traversal, secret-handling, and manifest-generation mistakes.</div>
</div>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2">Trusted harvests and enforcement</h2>
<p class="text-secondary">Harvest bundles should be treated as sensitive administrative artifacts. A harvest 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 it may contain substantially more sensitive material.</p>
<p class="text-secondary mb-0">Enroll validates harvest structure and artifact safety, but validation does not prove that the desired state represented by a harvest is safe to apply. Only run <code>manifest</code>, <code>diff</code>, or especially <code>diff --enforce</code> against bundles that came from hosts and people you trust.</p>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2">Security report scope</h2>
<div class="row g-3">
<div class="col-md-6">
<div class="fw-semibold mb-2">Useful reports</div>
<ul class="small mb-0">
<li>Enroll captures clearly sensitive default-denied files without <code>--dangerous</code></li>
<li>Enroll follows a symlink or hardlink in a way that causes privileged file disclosure or overwrite</li>
<li>Enroll extracts a tar member outside the intended harvest directory</li>
<li>Enroll accepts an artifact path that escapes the artifact root</li>
<li>Ordinary harvested data can cause command injection in generated manifests</li>
<li>Enroll silently ignores a failed safety check and proceeds anyway</li>
</ul>
</div>
<div class="col-md-6">
<div class="fw-semibold mb-2">Normally out-of-scope reports</div>
<ul class="small mb-0">
<li>Root can configure Enroll to collect sensitive files</li>
<li>Root can pass <code>--dangerous</code> and collect dangerous data</li>
<li>Root can pass <code>--assume-safe-path</code> and bypass the root <code>PATH</code> warning</li>
<li>Root can point Enroll at a malicious config file</li>
<li>Root can enforce a malicious harvest bundle</li>
<li>A local attacker can influence Enroll after already controlling root's environment or binaries</li>
</ul>
</div>
</div>
</div>
<hr class="my-5">
<div class="feature-card p-4">
<h2 class="h4 fw-bold mb-2">Security researchers</h2>
<div class="row g-3">
<div class="col-md-12">
<div class="fw-semibold">Found a vulnerability in Enroll?</div>
<ul class="small mb-0">
<li>Please contact me using the <a href="https://nr.mig5.net/forms/mig5/contact" target="_blank" rel="noopener noreferrer">contact form</a> or via Signal (<code>mig5.55</code>)</li>
<li>My GPG public key is <a href="https://mig5.net/static/mig5.asc" target="_blank" rel="noopener noreferrer">here</a></li>
<li>Unfortunately, I cannot offer financial bounty rewards at this time, but you will receive full credit for valid security issues.</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -22,6 +22,7 @@ description: "Common Enroll errors and fixes for generated Ansible, Puppet, and
<a class="list-group-item list-group-item-action" href="#flatpak-from-url">Flatpak from_url error</a> <a class="list-group-item list-group-item-action" href="#flatpak-from-url">Flatpak from_url error</a>
<a class="list-group-item list-group-item-action" href="#jinjaturtle">JinjaTurtle</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="#missing-files">Missing files</a>
<a class="list-group-item list-group-item-action" href="#harvest-output-parent">Harvest output parent</a>
<a class="list-group-item list-group-item-action" href="#validate">Validate artifacts</a> <a class="list-group-item list-group-item-action" href="#validate">Validate artifacts</a>
<a class="list-group-item list-group-item-action" href="#remote">Remote harvest</a> <a class="list-group-item list-group-item-action" href="#remote">Remote harvest</a>
<a class="list-group-item list-group-item-action" href="#enforce">Diff enforcement</a> <a class="list-group-item list-group-item-action" href="#enforce">Diff enforcement</a>
@ -138,6 +139,49 @@ description: "Common Enroll errors and fixes for generated Ansible, Puppet, and
</div> </div>
</section> </section>
<section id="harvest-output-parent" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Error: harvest output parent is owned by the wrong user</h2>
<p class="text-secondary">When Enroll is run as root, it deliberately refuses to create a harvest below a parent directory that is controlled by a non-root user. This is a safety check: a user-owned parent directory can be renamed or replaced while a root process is writing files, which can become a symlink or path-race problem.</p>
<div class="codeblock terminal mb-3">
<pre class="mb-0"><code>error: harvest output parent is not owned by root; refusing root-run output: /home/alice
error: harvest output parent is not owned by root; refusing root-run output: /tmp/tmp.abcd1234</code></pre>
</div>
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-2">What Enroll is protecting you from</div>
<p class="small text-secondary mb-0">Harvest output can contain detailed system state and, when <code>--dangerous</code> is used, may contain sensitive configuration material. If Enroll is running as root, it needs the output path to stay exactly where it was checked. Refusing user-owned parent directories helps avoid accidental writes through symlinks or paths that can be swapped underneath the process.</p>
</div>
<div class="row g-3">
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Use a root-owned output area</div>
<p class="small text-secondary mb-3">Create a dedicated root-owned directory and place harvests underneath it:</p>
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> sudo install -d -m 700 -o root -g root /var/tmp/enroll
<span class="prompt">$</span> sudo enroll harvest --out /var/tmp/enroll/host1</code></pre></div>
</div>
</div>
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2"><code>/tmp</code> itself is okay</div>
<p class="small text-secondary mb-3">A fresh output directory directly under the normal root-owned sticky <code>/tmp</code> directory is accepted. The output path must not already exist.</p>
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> sudo enroll harvest --out /tmp/host1-harvest</code></pre></div>
</div>
</div>
</div>
<div class="alert alert-warning mt-3">
<div class="fw-semibold">Avoid root harvests into your home directory</div>
<div class="small">A command such as <code>sudo enroll harvest --out ~/harvest</code> may expand to a path below a user-owned home directory. Use <code>/var/tmp/enroll</code>, <code>/root</code>, or a fresh directory directly under <code>/tmp</code> instead.</div>
</div>
<div class="callout p-4 mt-3">
<div class="fw-semibold mb-2">Remote harvest note</div>
<p class="small text-secondary mb-0">In normal remote mode, Enroll uses sudo on the remote host so it can collect system-level state. Recent Enroll versions create a separate root-owned temporary directory for the remote bundle. If an older version reports this error for a path like <code>/tmp/tmp.xxxxx</code> during remote harvest, upgrade Enroll. Use <code>--no-sudo</code> only when you intentionally want a limited, non-root harvest.</p>
</div>
</section>
<section id="validate" class="scroll-mt-nav mb-5"> <section id="validate" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Generated manifest references a missing artifact</h2> <h2 class="section-title fw-bold">Generated manifest references a missing artifact</h2>
<p class="text-secondary">If a manifest task points at a missing source file, validate the original harvest first. Validation checks <code>state.json</code>, referenced artifacts, generated firewall/sysctl files, and unreferenced artifact warnings.</p> <p class="text-secondary">If a manifest task points at a missing source file, validate the original harvest first. Validation checks <code>state.json</code>, referenced artifacts, generated firewall/sysctl files, and unreferenced artifact warnings.</p>
@ -183,6 +227,21 @@ description: "Common Enroll errors and fixes for generated Ansible, Puppet, and
</div> </div>
<p class="small text-secondary mt-3 mb-0">For non-Ansible targets, make sure <code>puppet</code> or <code>salt-call</code> is installed and on <code>PATH</code> before running enforcement.</p> <p class="small text-secondary mt-3 mb-0">For non-Ansible targets, make sure <code>puppet</code> or <code>salt-call</code> is installed and on <code>PATH</code> before running enforcement.</p>
</section> </section>
<section id="enforce" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Error about schema when validating or manifesting a harvest</h2>
<p class="text-secondary">You see an error like <code>error: harvest state does not match this Enroll version's schema; please re-harvest the host with this version of Enroll.</code></p>
<div class="row g-3">
<div class="col-md-12">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Reharvest the host</div>
<p>The harvest has either been tampered with, become corrupt, or was made with an earlier version of Enroll, which means its layout is a different structure than what Enroll now expects.</p>
<p>The best thing to do is run the harvest again. Enroll will validate the harvest against the latest schema when you go to manifest it.</p>
<p>If you still experience the error, please report it as a bug!</p>
</div>
</div>
</div>
</section>
</div> </div>
</div> </div>
</div> </div>