Initial commit

This commit is contained in:
Miguel Jacq 2026-01-02 08:23:28 +11:00
commit 09943a6439
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
10 changed files with 1659 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.swp

149
src/assets/css/site.css Normal file
View file

@ -0,0 +1,149 @@
:root{
--enroll-amber:#E8B35E;
--enroll-amber-2:#F7D58A;
--enroll-brown:#5A3415;
--enroll-brown-2:#8A5A2D;
--enroll-ink:#0f172a;
--bs-link-color: var(--enroll-brown);
--bs-link-hover-color: var(--enroll-brown-2);
--bs-link-color-rgb: 90, 52, 21; /* #5A3415 */
--bs-link-hover-color-rgb: 138, 90, 45; /* #8A5A2D */
--bs-nav-pills-link-active-bg: var(--enroll-amber);
--bs-nav-pills-link-active-color: var(--enroll-brown);
}
html{scroll-behavior:smooth;}
body{
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
color: var(--enroll-ink);
}
.navbar{
backdrop-filter: blur(10px);
}
.navbar-toggler-icon{
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(90,52,21,0.85)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
.brand-mark{width:84px;height:84px;}
.hero{
background:
radial-gradient(1200px circle at 15% 10%, rgba(247,213,138,0.60), transparent 55%),
radial-gradient(900px circle at 85% 20%, rgba(232,179,94,0.55), transparent 60%),
linear-gradient(135deg, #fff7e8 0%, #ffffff 55%, #fffaf0 100%);
}
.hero .lead{
color: rgba(15,23,42,0.78);
}
.hero-card{
background: rgba(255,255,255,0.70);
border: 1px solid rgba(15,23,42,0.08);
box-shadow: 0 18px 48px rgba(15,23,42,0.10);
border-radius: 1.25rem;
}
.kicker{
display: inline-flex;
align-items: center;
gap: .5rem;
font-weight: 600;
color: var(--enroll-brown);
background: rgba(232,179,94,0.18);
border: 1px solid rgba(232,179,94,0.35);
padding: .35rem .65rem;
border-radius: 999px;
}
.section-title{
letter-spacing: -0.02em;
}
.icon-pill{
width: 44px;
height: 44px;
border-radius: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(232,179,94,0.20);
border: 1px solid rgba(232,179,94,0.35);
color: var(--enroll-brown);
}
.feature-card{
border: 1px solid rgba(15,23,42,0.08);
border-radius: 1.25rem;
box-shadow: 0 12px 30px rgba(15,23,42,0.06);
}
.terminal{
background: #0b1220;
color: #e5e7eb;
border-radius: 1rem;
padding: 1.25rem;
border: 1px solid rgba(255,255,255,0.06);
box-shadow: 0 18px 52px rgba(11,18,32,0.35);
}
.terminal .prompt{ color: #93c5fd; }
.terminal code{ color: #e5e7eb; }
.codeblock{
position: relative;
}
.copy-btn{
position: absolute;
top: .75rem;
right: .75rem;
}
.badge-soft{
background: rgba(15,23,42,0.06);
border: 1px solid rgba(15,23,42,0.10);
color: rgba(15,23,42,0.85);
}
.callout{
border: 1px solid rgba(15,23,42,0.10);
border-radius: 1rem;
background: rgba(255,255,255,0.78);
}
footer{
background: linear-gradient(180deg, #ffffff 0%, #fff7e8 100%);
border-top: 1px solid rgba(15,23,42,0.06);
}
.smallprint{ color: rgba(15,23,42,0.65); }
/* Make anchor scrolling nicer under sticky nav */
.scroll-mt-nav{ scroll-margin-top: 90px; }
a{ text-decoration-color: rgba(232,179,94,0.55); }
a:hover{ text-decoration-color: rgba(232,179,94,0.85); }
/* Quickstart pills */
.nav-pills .nav-link{
border: 1px solid rgba(232,179,94,0.35);
color: var(--enroll-brown);
background: rgba(232,179,94,0.12);
border-radius: 999px;
}
.nav-pills .nav-link:hover{
background: rgba(232,179,94,0.18);
border-color: rgba(232,179,94,0.55);
}
.nav-pills .nav-link.active,
.nav-pills .show > .nav-link{
background: var(--enroll-amber);
color: var(--enroll-brown);
border-color: rgba(90,52,21,0.25);
}

107
src/assets/img/enroll.svg Normal file
View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="720" viewBox="0 0 720 720" role="img" aria-label="enroll logo">
<defs>
<linearGradient id="bgGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#F7D58A"/>
<stop offset="1" stop-color="#E8B35E"/>
</linearGradient>
<linearGradient id="crumbGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#FFE7BD"/>
<stop offset="1" stop-color="#E6A44A"/>
</linearGradient>
<linearGradient id="shineGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#FFF2D6" stop-opacity="0.85"/>
<stop offset="1" stop-color="#FFF2D6" stop-opacity="0"/>
</linearGradient>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceAlpha" stdDeviation="6" result="blur"/>
<feOffset dx="0" dy="10" result="off"/>
<feColorMatrix in="off" type="matrix"
values="0 0 0 0 0.15 0 0 0 0 0.08 0 0 0 0 0.02 0 0 0 0.35 0" result="shadow"/>
<feMerge>
<feMergeNode in="shadow"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<style>
.stroke { stroke:#5A3415; stroke-width:10; stroke-linecap:round; stroke-linejoin:round; }
.thin { stroke:#5A3415; stroke-width:7; stroke-linecap:round; stroke-linejoin:round; }
.text-dark { fill:#5A3415; font-family: ui-rounded, "Arial Rounded MT Bold", system-ui, -apple-system, "Segoe UI", Arial, sans-serif; font-weight: 800; }
.text-light { fill:#8A5A2D; font-family: ui-rounded, "Arial Rounded MT Bold", system-ui, -apple-system, "Segoe UI", Arial, sans-serif; font-weight: 800; }
.sesame { fill:#F8E6C8; opacity:0.92; }
.crumb { fill:#E9B56B; opacity:0.30; }
</style>
</defs>
<!-- NOTE: background removed for transparency -->
<!-- Roll group -->
<g filter="url(#softShadow)" transform="translate(-120,-70)">
<path class="stroke" fill="url(#crumbGrad)"
d="M220 450
C220 320, 320 235, 480 235
C640 235, 740 320, 740 450
C740 565, 640 610, 480 610
C320 610, 220 565, 220 450 Z"/>
<path d="M300 320
C340 285, 410 265, 480 265
C550 265, 620 285, 660 320
C610 305, 545 298, 480 298
C415 298, 350 305, 300 320 Z"
fill="url(#shineGrad)"/>
<g class="thin" opacity="0.85" fill="none">
<path d="M330 320 C350 285, 400 270, 430 315"/>
<path d="M420 305 C450 268, 510 268, 540 305"/>
<path d="M530 315 C560 270, 610 285, 630 320"/>
</g>
<g>
<ellipse class="sesame" cx="335" cy="368" rx="10" ry="6"/>
<ellipse class="sesame" cx="625" cy="368" rx="10" ry="6"/>
<ellipse class="sesame" cx="385" cy="345" rx="10" ry="6"/>
<ellipse class="sesame" cx="575" cy="345" rx="10" ry="6"/>
<ellipse class="sesame" cx="400" cy="330" rx="10" ry="6"/>
<ellipse class="sesame" cx="560" cy="330" rx="10" ry="6"/>
</g>
<g>
<circle class="crumb" cx="310" cy="430" r="6"/>
<circle class="crumb" cx="650" cy="430" r="6"/>
<circle class="crumb" cx="350" cy="470" r="5"/>
<circle class="crumb" cx="610" cy="470" r="5"/>
<circle class="crumb" cx="420" cy="520" r="6"/>
<circle class="crumb" cx="540" cy="520" r="6"/>
</g>
<g>
<circle cx="445" cy="365" r="12" fill="#5A3415"/>
<circle cx="515" cy="365" r="12" fill="#5A3415"/>
<path d="M445 405 C468 430, 492 430, 515 405"
fill="none" stroke="#5A3415" stroke-width="12" stroke-linecap="round"/>
</g>
<g transform="translate(330,470)">
<rect x="0" y="0" width="300" height="92" rx="26" fill="#F7C879" class="thin"/>
<g class="thin" fill="none" opacity="0.95">
<rect x="18" y="16" width="190" height="24" rx="11" fill="#FFE7BD"/>
<rect x="18" y="50" width="190" height="24" rx="11" fill="#FFE7BD"/>
<path d="M18 82 H232"/>
</g>
<g>
<circle cx="245" cy="28" r="6.5" fill="#5A3415"/>
<circle cx="268" cy="28" r="6.5" fill="#5A3415"/>
<circle cx="291" cy="28" r="6.5" fill="#5A3415"/>
<rect x="238" y="52" width="56" height="22" rx="9" fill="#FFE7BD" stroke="#5A3415" stroke-width="7"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

60
src/assets/js/site.js Normal file
View file

@ -0,0 +1,60 @@
(function(){
// Copy-to-clipboard for code blocks
function setupCopyButtons(){
document.querySelectorAll('[data-copy-target]').forEach(function(btn){
btn.addEventListener('click', async function(){
var sel = btn.getAttribute('data-copy-target');
var el = document.querySelector(sel);
if(!el) return;
var text = el.innerText || el.textContent || '';
try{
await navigator.clipboard.writeText(text.trim());
var old = btn.innerHTML;
btn.innerHTML = 'Copied';
btn.classList.add('btn-success');
btn.classList.remove('btn-outline-secondary');
setTimeout(function(){
btn.innerHTML = old;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-secondary');
}, 1200);
}catch(e){
// Fallback
var ta = document.createElement('textarea');
ta.value = text.trim();
document.body.appendChild(ta);
ta.select();
try{ document.execCommand('copy'); }catch(_){}
document.body.removeChild(ta);
}
});
});
}
// Asciinema embed helper:
// Put <div class="asciicast" data-asciinema-id="12345"></div>
// Or provide a self-hosted player by swapping the script URL.
function setupAsciinema(){
document.querySelectorAll('.asciicast[data-asciinema-id]').forEach(function(el){
var id = el.getAttribute('data-asciinema-id');
if(!id || id === 'REPLACE_ME'){
el.innerHTML = '<div class="alert alert-warning mb-0">Add your asciinema id here: <code>data-asciinema-id</code>.</div>';
return;
}
// Avoid injecting twice
if(document.getElementById('asciinema-embed-'+id)) return;
var s = document.createElement('script');
s.src = 'https://asciinema.org/a/' + encodeURIComponent(id) + '.js';
s.id = 'asciinema-embed-'+id;
s.async = true;
// The script replaces a placeholder div with the player if it's directly after it.
el.appendChild(s);
});
}
document.addEventListener('DOMContentLoaded', function(){
setupCopyButtons();
setupAsciinema();
});
})();

506
src/docs.html Normal file
View file

@ -0,0 +1,506 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Enroll Docs</title>
<meta name="description" content="How Enroll works: harvest, manifest, modes, and configuration.">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- 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" rel="stylesheet">
</head>
<body>
<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">
<img class="brand-mark" src="assets/img/enroll.svg" alt="Enroll">
<span>Enroll</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="examples.html">Examples</a></li>
<li class="nav-item"><a class="nav-link" href="security.html">Security Design</a></li>
<li class="nav-item 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
</a>
</li>
</ul>
</div>
</div>
</nav>
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-book"></i> Documentation</div>
<h1 class="display-6 fw-bold mb-2">How Enroll works</h1>
<p class="lead mb-0">The mental model, output modes, and the knobs you'll actually use day-to-day.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-3">
<div class="position-sticky" style="top: 96px;">
<div class="list-group">
<a class="list-group-item list-group-item-action" href="#model">Mental model</a>
<a class="list-group-item list-group-item-action" href="#modes">Single-site vs multi-site</a>
<a class="list-group-item list-group-item-action" href="#remote">Remote harvesting</a>
<a class="list-group-item list-group-item-action" href="#templates">JinjaTurtle templates</a>
<a class="list-group-item list-group-item-action" href="#config">Config file</a>
<a class="list-group-item list-group-item-action" href="#diff">Drift detection with <code>enroll diff</code></a>
<a class="list-group-item list-group-item-action" href="#tips">Tips</a>
</div>
<div class="mt-3 small text-secondary">
Prefer the canonical docs?
<a class="link-secondary" href="https://git.mig5.net/mig5/enroll" target="_blank" rel="noreferrer">README on Forgejo</a>.
</div>
</div>
</div>
<div class="col-lg-9">
<section id="model" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Mental model</h2>
<p class="text-secondary">Enroll is intentionally simple: it collects facts first, then renders Ansible from those facts.</p>
<div class="row g-3">
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="d-flex align-items-center gap-3 mb-2">
<div class="icon-pill"><i class="bi bi-archive"></i></div>
<div>
<div class="fw-semibold">1) Harvest</div>
<div class="small text-secondary">Snapshot state into a bundle</div>
</div>
</div>
<ul class="mb-0 small">
<li>Detect installed packages and services</li>
<li>Collect config that deviates from packaged defaults (where possible)</li>
<li>Grab relevant custom/unowned files in service dirs</li>
<li>Capture non-system users & SSH public keys</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="d-flex align-items-center gap-3 mb-2">
<div class="icon-pill"><i class="bi bi-boxes"></i></div>
<div>
<div class="fw-semibold">2) Manifest</div>
<div class="small text-secondary">Generate an Ansible repo structure</div>
</div>
</div>
<ul class="mb-0 small">
<li>Roles with files/templates and defaults</li>
<li>Playbooks to apply the captured state</li>
<li>Optional inventory structure for multi-host runs: each host gets its own playbook</li>
</ul>
</div>
</div>
</div>
<div class="terminal mt-4">
<div class="small text-secondary mb-2">Typical flow</div>
<pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
<span class="prompt">$</span> ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml</code></pre>
</div>
</section>
<section id="modes" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Single-site vs multi-site</h2>
<p class="text-secondary">Manifest output has two styles. Choose based on how you'll use the result.</p>
<div class="row g-3">
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-house"></i> Single-site (default)</div>
<p class="small mb-2">Best when you are enrolling one host, or you're producing a reusable "golden" role set that could be applied anywhere.</p>
<ul class="small mb-0">
<li>Roles are self-contained</li>
<li>Raw files live in each role's <code>files/</code></li>
<li>Template variables live in <code>defaults/main.yml</code></li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-diagram-3"></i> Multi-site (<code>--fqdn</code>)</div>
<p class="small mb-2">Best when you want to enroll several existing servers quickly, especially if they differ.</p>
<ul class="small mb-0">
<li>Roles are shared; raw files live in host-specific inventory</li>
<li>Inventory decides what gets managed on each host (files/packages/services)</li>
<li>Non-templated files go under <code>inventory/host_vars/&lt;fqdn&gt;/&lt;role&gt;/.files</code></li>
</ul>
</div>
</div>
</div>
<div class="terminal mt-4">
<div class="small text-secondary mb-2">Multi-site example</div>
<pre class="mb-0"><code><span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$(hostname -f)"
<span class="prompt">$</span> ansible-playbook /tmp/enroll-ansible/playbooks/"$(hostname -f)".yml</code></pre>
</div>
</section>
<section id="remote" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Remote harvesting over SSH</h2>
<p class="text-secondary">Run Enroll on your workstation, harvest a remote host over SSH. The harvest is pulled locally.</p>
<div class="terminal">
<pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-harvest
<span class="prompt">$</span> enroll single-shot --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-ansible --fqdn myhost.example.com</code></pre>
</div>
<div class="alert alert-secondary mt-3">
<div class="fw-semibold">Tip</div>
<div class="small mb-0">If you don't want/need sudo on the remote side, add <code>--no-sudo</code>. However, be aware that you may get a more limited harvest depending on permissions.</div>
</div>
</section>
<section id="templates" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">JinjaTurtle integration</h2>
<p class="text-secondary">If <a href="https://git.mig5.net/mig5/jinjaturtle" target="_blank" rel="noopener noreferrer">JinjaTurtle</a> (one of my other projects) is installed, Enroll can also produce Jinja2 templates for ini/json/xml/toml-style config and extract variables cleanly into Ansible, instead of just storing the 'raw' files.</p>
<div class="row g-3">
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Modes</div>
<ul class="small mb-0">
<li><code>--jinjaturtle</code> to force on</li>
<li><code>--no-jinjaturtle</code> to force off</li>
<li>Default is auto</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Where variables land</div>
<ul class="small mb-0">
<li>Single-site: <code>roles/&lt;role&gt;/defaults/main.yml</code></li>
<li>Multi-site: <code>inventory/host_vars/&lt;fqdn&gt;/&lt;role&gt;.yml</code></li>
</ul>
</div>
</div>
</div>
</section>
<section id="config" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">INI config file</h2>
<p class="text-secondary">If you're repeating flags (include/exclude patterns, SOPS settings, etc.), store defaults in <code>enroll.ini</code> and keep your muscle memory intact.</p>
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Discovery order</div>
<div class="small text-secondary mb-0">You can pass <code>-c/--config</code>, set <code>ENROLL_CONFIG</code>, or let Enroll auto-discover <code>./enroll.ini</code>, <code>./.enroll.ini</code>, or <code>~/.config/enroll/enroll.ini</code>.</div>
</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ini-example"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ini-example">[enroll]
# (future global flags may live here)
[harvest]
dangerous = false
include_path =
/home/*/.bashrc
/home/*/.profile
exclude_path = /usr/local/bin/docker-*, /usr/local/bin/some-tool
# remote_host = yourserver.example.com
# remote_user = you
# remote_port = 2222
[manifest]
no_jinjaturtle = true
sops = 00AE817C24A10C2540461A9C1D7CDE0234DB458D</code></pre>
</div>
<div class="alert alert-secondary mt-3 mb-0">
<div class="fw-semibold">Note</div>
<div class="small mb-0">In INI sections, option names use underscores (e.g. <code>include_path</code>) even when the CLI flag uses hyphens (e.g. <code>--include-path</code>).</div>
</div>
</section>
<section id="diff" class="scroll-mt-nav mb-5">
<h2 class="section-title fw-bold">Drift detection with <code>enroll diff</code></h2>
<p class="text-secondary">One of the things I miss from my Puppet days, was the way the Puppet 'agent' would check in with the server and realign itself to the declared desired state. With Ansible, it's easy for systems to fall 'out of date', especially if someone is doing the wrong thing and changing things on-the-fly instead of via config management!</p>
<p class="text-secondary">The purpose of <code>enroll diff</code> is to compare two 'harvests' and detect what has changed - be it adding/removing of programs, change to systemd unit state, modifications, addition or removal of files, and so on.</p>
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Notifications for diff</div>
<div class="small text-secondary mb-0">The <code>enroll diff</code> feature supports sending the difference to a webhook of your choosing, or by e-mail. The payload can be sent in json, plain text, or markdown.</div>
</div>
<p class="text-secondary">A great way to use <code>enroll diff</code> is to run it periodically (e.g via cron or a systemd timer). Below is an example.</p>
<p class="text-secondary">Store the below file at <code>/usr/local/bin/enroll-harvest-diff.sh</code> and make it executable.</p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-harvest-diff">#!/usr/bin/env bash
set -euo pipefail
# Required env
: "${WEBHOOK_URL:?Set WEBHOOK_URL in /etc/enroll/enroll-harvest-diff}"
: "${ENROLL_SECRET:?Set ENROLL_SECRET in /etc/enroll/enroll-harvest-diff}"
# Optional env
STATE_DIR="${ENROLL_STATE_DIR:-/var/lib/enroll}"
GOLDEN_DIR="${STATE_DIR}/golden"
PROMOTE_NEW="${PROMOTE_NEW:-1}" # 1=promote new->golden; 0=keep golden fixed
KEEP_BACKUPS="${KEEP_BACKUPS:-7}" # only used if PROMOTE_NEW=1
LOCKFILE="${STATE_DIR}/.enroll-harvest-diff.lock"
mkdir -p "${STATE_DIR}"
chmod 700 "${STATE_DIR}" || true
# single-instance lock (avoid overlapping timer runs)
exec 9>"${LOCKFILE}"
flock -n 9 || exit 0
tmp_new=""
cleanup() {
if [[ -n "${tmp_new}" && -d "${tmp_new}" ]]; then
rm -rf "${tmp_new}"
fi
}
trap cleanup EXIT
make_tmp_dir() {
mktemp -d "${STATE_DIR}/.harvest.XXXXXX"
}
run_harvest() {
local out_dir="$1"
rm -rf "${out_dir}"
mkdir -p "${out_dir}"
chmod 700 "${out_dir}" || true
enroll harvest --out "${out_dir}" >/dev/null
}
# A) create golden if missing
if [[ ! -f "${GOLDEN_DIR}/state.json" ]]; then
tmp="$(make_tmp_dir)"
run_harvest "${tmp}"
rm -rf "${GOLDEN_DIR}"
mv "${tmp}" "${GOLDEN_DIR}"
echo "Golden harvest created at ${GOLDEN_DIR}"
exit 0
fi
# B) create new harvest
tmp_new="$(make_tmp_dir)"
run_harvest "${tmp_new}"
# C) diff + webhook notify
enroll diff \
--old "${GOLDEN_DIR}" \
--new "${tmp_new}" \
--webhook "${WEBHOOK_URL}" \
--webhook-format json \
--webhook-header "X-Enroll-Secret: ${ENROLL_SECRET}" # You can send multiple --webhook-header params as you need
# Promote or discard new harvest
if [[ "${PROMOTE_NEW}" == "1" || "${PROMOTE_NEW,,}" == "true" || "${PROMOTE_NEW}" == "yes" ]]; then
ts="$(date -u +%Y%m%d-%H%M%S)"
backup="${STATE_DIR}/golden.prev.${ts}"
mv "${GOLDEN_DIR}" "${backup}"
mv "${tmp_new}" "${GOLDEN_DIR}"
tmp_new="" # don't delete it in trap
# Keep only latest N backups
if [[ "${KEEP_BACKUPS}" =~ ^[0-9]+$ ]] && (( KEEP_BACKUPS > 0 )); then
ls -1dt "${STATE_DIR}"/golden.prev.* 2>/dev/null | tail -n +"$((KEEP_BACKUPS+1))" | xargs -r rm -rf
fi
echo "Diff complete; baseline updated."
else
# tmp_new will be deleted by trap
echo "Diff complete; baseline unchanged (PROMOTE_NEW=${PROMOTE_NEW})."
fi
</code></pre>
</div>
<br />
<p class="text-secondary">Save these environment variables in <code>/etc/enroll/enroll-harvest-diff</code></p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff-env"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-harvest-diff-env">
# Where to store golden + temp harvests
ENROLL_STATE_DIR=/var/lib/enroll
# 1 = each run becomes new baseline ("since last harvest")
# 0 = compare against a fixed baseline ("since golden")
PROMOTE_NEW=1
# If PROMOTE_NEW=1, keep this many old baselines
KEEP_BACKUPS=7
WEBHOOK_URL=https://example.com/webhook/xxxxxxxx
ENROLL_SECRET=xxxxxxxxxxxxxxxxxxxx
</code></pre>
</div>
<br />
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Webhook headers</div>
<div class="small text-secondary mb-0">The <code>--webhook-header</code> parameter can be used multiple times. You can, for example, send <code>X-Enroll-Secret</code> and a secret value of your choice, to help secure your webhook endpoint.</div>
</div>
<p class="text-secondary">Save this systemd unit file to <code>/etc/systemd/system/enroll-harvest-diff.service</code></p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff-service"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-harvest-diff-service">
[Unit]
Description=Enroll harvest + diff + webhook notify
Wants=network-online.target
After=network-online.target
ConditionPathExists=/etc/enroll/enroll-harvest-diff
[Service]
Type=oneshot
EnvironmentFile=/etc/enroll/enroll-harvest-diff
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
UMask=0077
# Create /var/lib/enroll automatically
StateDirectory=enroll
ExecStart=/usr/local/bin/enroll-harvest-diff.sh
</code></pre>
</div>
<br />
<p class="text-secondary">Save this systemd timer to <code>/etc/systemd/system/enroll-harvest-diff.timer</code></p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff-timer"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-harvest-diff-timer">
[Unit]
Description=Run Enroll harvest diff hourly
[Timer]
OnCalendar=hourly
RandomizedDelaySec=10m
Persistent=true
[Install]
WantedBy=timers.target
</code></pre>
</div>
<br />
<p class="text-secondary">Now you can enable and test it!</p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#enroll-harvest-diff-enable"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="enroll-harvest-diff-enable">
sudo systemctl daemon-reload
sudo systemctl enable --now enroll-harvest-diff.timer
# run once now
sudo systemctl start enroll-harvest-diff.service
# watch it in the logs
sudo journalctl -u enroll-harvest-diff.service -n 200 --no-pager
</code></pre>
</div>
<br />
<div class="callout p-4 mb-3">
<div class="fw-semibold mb-1">Need help with writing webhooks?</div>
<div class="small text-secondary mb-0">I use Node-RED. Here's a <a href="https://git.mig5.net/mig5/enroll/wiki/enroll-diff#example-node-red-webhook-receiver-flow" target="_blank" rel="noopener noreferrer">sample Node-RED flow</a> that might help run your webhook, pre-configured to parse the <code>enroll diff</code> JSON payload!</div>
</div>
</section>
<section id="tips" class="scroll-mt-nav">
<h2 class="section-title fw-bold">Tips</h2>
<div class="row g-3">
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-shield-check"></i> Start safe</div>
<p class="small text-secondary mb-0">Default harvesting tries to avoid likely secrets via path rules, content sniffing, and size caps. Use <code>--dangerous</code> only when you've planned where the output will live.</p>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-lock"></i> Encrypt at rest</div>
<p class="small text-secondary mb-0">If you plan to keep harvests/manifests long term (especially in git), use <code>--sops</code> to produce a single encrypted bundle file. <strong>Note:</strong> <code>enroll diff</code> can be passed <code>--sops</code> to decrypt and compare two harvests on-the-fly!</p>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-diagram-2"></i> Multi-host safety</div>
<p class="small text-secondary mb-0">For fleets, prefer multi-site output so roles stay generic and host inventory controls what is applied per host - reducing "shared role breaks other host" surprises.</p>
</div>
</div>
<div class="col-md-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-gear"></i> Keep it reproducible</div>
<p class="small text-secondary mb-0">Commit the manifest output, run it in CI, and use <code>enroll diff</code> as a drift alarm (webhook/email).</p>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</main>
<footer class="py-5">
<div class="container">
<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" 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>
</div>
<p class="smallprint mb-3">Reverse-engineering servers into Ansible.</p>
<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>
</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">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 Design</a></li>
</ul>
</div>
<div class="col-lg-3">
<div class="fw-semibold mb-2">Contact</div>
<ul class="list-unstyled small mb-0">
<li><a class="link-secondary text-decoration-none" href="https://nr.mig5.net/forms/mig5/contact" target="_blank" rel="noreferrer">Form</a></li>
<li><span class="text-secondary">Fediverse:</span> <a class="link-secondary text-decoration-none" href="https://goto.mig5.net/@mig5" target="_blank" rel="noreferrer">@mig5</a></li>
</ul>
</div>
</div>
<hr class="my-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 small">
<div class="text-secondary">© <span id="year"></span> <a href="https://mig5.net" target="_blank" rel="noopener noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/site.js"></script>
<script>document.getElementById('year').textContent = new Date().getFullYear();</script>
</body>
</html>

200
src/examples.html Normal file
View file

@ -0,0 +1,200 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Enroll Examples</title>
<meta name="description" content="Copy/paste recipes for Enroll: one host, fleets, drift detection, and safe storage.">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- 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" rel="stylesheet">
</head>
<body>
<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">
<img class="brand-mark" src="assets/img/enroll.svg" alt="Enroll">
<span>Enroll</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="examples.html">Examples</a></li>
<li class="nav-item"><a class="nav-link" href="security.html">Security Design</a></li>
<li class="nav-item 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
</a>
</li>
</ul>
</div>
</div>
</nav>
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-terminal"></i> Examples</div>
<h1 class="display-6 fw-bold mb-2">Copy/paste recipes</h1>
<p class="lead mb-0">Practical flows you can adapt to your environment.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Enroll a single host (local)</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-single-local"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-single-local"><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest \
--out /tmp/enroll-ansible
<span class="prompt">$</span> ansible-playbook -i "localhost," -c local \
/tmp/enroll-ansible/playbook.yml --diff --check</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">Great for "make this box reproducible" or building a golden role set.</p>
</div>
</div>
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Enroll a remote host (over SSH)</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-remote"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-remote"><span class="prompt">$</span> enroll harvest \
--remote-host myhost.example.com \
--remote-user myuser \
--out /tmp/enroll-harvest
<span class="prompt">$</span> enroll manifest \
--harvest /tmp/enroll-harvest \
--out /tmp/enroll-ansible</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">No need to manually run commands on the server - your bundle lands locally.</p>
</div>
</div>
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Fleets: multi-site output</div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-multisite"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-multisite"><span class="prompt">$</span> fqdn="$(hostname -f)"
<span class="prompt">$</span> enroll single-shot --remote-host "$fqdn" \
--remote-user myuser \
--out /tmp/enroll-ansible \
--fqdn "$fqdn"
<span class="prompt">$</span> ansible-playbook "/tmp/enroll-ansible/playbooks/${fqdn}.yml"</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">Shared roles + host inventory keeps one host's differences from breaking another.</p>
</div>
</div>
<div class="col-lg-6">
<div class="feature-card p-4 h-100">
<div class="fw-semibold mb-2">Drift detection with <code>enroll diff</code></div>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#ex-diff"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="ex-diff"><span class="prompt">$</span> enroll diff \
--old /path/to/harvestA \
--new /path/to/harvestB \
--format markdown
<span class="prompt">$</span> enroll diff --old /path/to/golden --new /path/to/current \
--webhook https://example.net/webhook \
--webhook-format json \
--webhook-header 'X-Enroll-Secret: ...' \
--exit-code</code></pre>
</div>
<p class="small text-secondary mt-2 mb-0">Use it in cron or CI to alert on change.</p>
</div>
</div>
</div>
<hr class="my-5">
<div class="row g-4">
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-shield-check"></i> Safe harvesting (default)</div>
<p class="small text-secondary mb-3">Enroll tries to avoid harvesting files that might contain secrets. If you need to capture "everything", pass <code>--dangerous</code> and treat the output as sensitive.</p>
<p class="small text-secondary mb-3">You can still control what gets collected and what doesn't by using <code>--include</code> and <code>--exclude</code> flags.</p>
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --dangerous --out /tmp/enroll-harvest</code></pre></div>
</div>
</div>
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2"><i class="bi bi-lock"></i> Encrypt bundles at rest (SOPS)</div>
<p class="small text-secondary mb-3">Produce a single encrypted file for harvest and/or manifest output (requires SOPS to be installed).</p>
<p class="small text-secondary mb-3">This is especially a good idea if you are using <code>--dangerous</code>, which might sweep up secrets (see above).</p>
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll harvest --dangerous --out /tmp/harvest \
--sops &lt;FINGERPRINT&gt;
<span class="prompt">$</span> enroll manifest --harvest /tmp/harvest/harvest.tar.gz.sops \
--out /tmp/enroll-ansible --sops &lt;FINGERPRINT&gt;</code></pre></div>
</div>
</div>
</div>
</div>
</main>
<footer class="py-5">
<div class="container">
<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" 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>
</div>
<p class="smallprint mb-3">Reverse-engineering servers into Ansible.</p>
<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>
</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">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 Design</a></li>
</ul>
</div>
<div class="col-lg-3">
<div class="fw-semibold mb-2">Contact</div>
<ul class="list-unstyled small mb-0">
<li><a class="link-secondary text-decoration-none" href="https://nr.mig5.net/forms/mig5/contact" target="_blank" rel="noreferrer">Form</a></li>
<li><span class="text-secondary">Fediverse:</span> <a class="link-secondary text-decoration-none" href="https://goto.mig5.net/@mig5" target="_blank" rel="noreferrer">@mig5</a></li>
</ul>
</div>
</div>
<hr class="my-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 small">
<div class="text-secondary">© <span id="year"></span> <a href="https://mig5.net" target="_blank" rel="noopener noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/site.js"></script>
<script>document.getElementById('year').textContent = new Date().getFullYear();</script>
</body>
</html>

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

427
src/index.html Normal file
View file

@ -0,0 +1,427 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Enroll - Reverse-engineering servers into Ansible</title>
<meta name="description" content="Enroll inspects Debian-like and RedHat-like Linux hosts and generates Ansible roles/playbooks from what it finds. Harvest → Manifest → Manage.">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- 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" rel="stylesheet">
<!-- OpenGraph -->
<meta property="og:title" content="Enroll - Reverse-engineering servers into Ansible">
<meta property="og:description" content="Harvest a host's real configuration and turn it into Ansible roles/playbooks. Safe-by-default, with optional SOPS encryption.">
<meta property="og:type" content="website">
</head>
<body>
<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">
<img class="brand-mark" src="assets/img/enroll.svg" alt="Enroll">
<span>Enroll</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="examples.html">Examples</a></li>
<li class="nav-item"><a class="nav-link" href="security.html">Security Design</a></li>
<li class="nav-item 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
</a>
</li>
</ul>
</div>
</div>
</nav>
<header class="hero py-5">
<div class="container py-4">
<div class="row align-items-center g-4">
<div class="col-lg-6">
<div class="kicker mb-3"><i class="bi bi-magic"></i> Reverse-engineering servers into Ansible</div>
<h1 class="display-5 fw-800 lh-1 mb-3" style="letter-spacing:-0.03em;">Get an existing Linux host into Ansible in seconds.</h1>
<p class="lead mb-4">Enroll inspects a Debian-like or RedHat-like system, harvests the state that matters, and generates Ansible roles/playbooks so you can bring snowflakes under management fast.</p>
<div class="d-flex flex-wrap gap-2 mb-4">
<a class="btn btn-dark btn-lg" href="#quickstart"><i class="bi bi-rocket-takeoff"></i> Quickstart</a>
<a class="btn btn-outline-dark btn-lg" href="#demos"><i class="bi bi-play-circle"></i> Watch demos</a>
<a class="btn btn-outline-secondary btn-lg" href="https://pypi.org/project/enroll/" target="_blank" rel="noreferrer"><i class="bi bi-box-seam"></i> PyPI</a>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge badge-soft rounded-pill px-3 py-2"><i class="bi bi-speedometer"></i> Super fast</span>
<span class="badge badge-soft rounded-pill px-3 py-2"><i class="bi bi-lock"></i> Optional SOPS encryption</span>
<span class="badge badge-soft rounded-pill px-3 py-2"><i class="bi bi-hdd-network"></i> Remote over SSH</span>
</div>
</div>
<div class="col-lg-6">
<div class="hero-card p-4 p-lg-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="fw-semibold"><i class="bi bi-terminal"></i> Config in the blink of an eye</div>
<div class="small text-muted">single-shot → ansible-playbook</div>
</div>
<div class="terminal">
<div class="mb-2"><span class="prompt">$</span> <code style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;">enroll single-shot --out ./ansible</code></div>
<div class="mb-2"><span class="prompt">$</span> <code style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;">cd ./ansible && tree -L 2</code></div>
<pre class="mb-0" style="white-space:pre-wrap; font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; font-size:.9rem;">.
├── ansible.cfg
├── playbook.yml
├── roles/
│ ├── cron/
│ ├── firewall/
│ ├── nginx/
│ ├── openssh-server/
│ ├── users/
│ ├── etc_custom/
└── README.md</pre>
</div>
<div class="smallprint mt-3">Tip: for multiple hosts, use <code>--fqdn</code> to generate inventory-driven, data-driven roles.</div>
</div>
</div>
</div>
</div>
</header>
<section class="py-5" id="how" aria-label="How it works">
<div class="container">
<div class="row g-4 align-items-start">
<div class="col-lg-5">
<h2 class="section-title display-6 fw-bold mb-3">A simple mental model</h2>
<p class="mb-4">Enroll is built around two phases, plus an optional drift report:</p>
<div class="d-flex flex-column gap-3">
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-bucket"></i></div>
<div>
<div class="fw-semibold">Harvest</div>
<div class="text-muted">Collect host facts + relevant files into a bundle.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-diagram-3"></i></div>
<div>
<div class="fw-semibold">Manifest</div>
<div class="text-muted">Render Ansible roles &amp; playbooks from the harvest.</div>
</div>
</div>
<div class="d-flex gap-3">
<div class="icon-pill"><i class="bi bi-plus-slash-minus"></i></div>
<div>
<div class="fw-semibold">Diff</div>
<div class="text-muted">Compare two harvests and notify via webhook/email.</div>
</div>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="row g-3">
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Safe-by-default harvesting</div>
<div class="text-muted">Enroll avoids likely secrets with a path denylist, content sniffing, and size caps - then lets you opt in to more aggressive collection when you're ready.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Multi-site without "shared role broke host2"</div>
<div class="text-muted">In <code>--fqdn</code> mode, roles are data-driven and host inventory decides what gets managed per host.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Remote over SSH</div>
<div class="text-muted">Harvest a remote host from your workstation, then manifest Ansible output locally.</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Encrypt bundles at rest</div>
<div class="text-muted">Use <code>--sops</code> to store harvests/manifests as a single encrypted <code>.tar.gz.sops</code> file (GPG) for safer long-term storage as a DR strategy.</div>
</div>
</div>
</div>
</div>
<div class="callout p-4 mt-3">
<div class="fw-semibold mb-2"><i class="bi bi-lightning-charge"></i> Why sysadmins like it</div>
<div class="row g-3">
<div class="col-md-6 text-muted">• Rapid enrolling of existing infra into config management<br>• Tweak include/exclude paths as needed</div>
<div class="col-md-6 text-muted">• Capture what changed from package defaults<br><code>diff</code> mode detects and alerts about drift</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="py-5 bg-light" id="quickstart" aria-label="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-muted mb-0">Copy, paste, iterate.</p>
</div>
<div class="col-lg-5 text-lg-end">
<a class="btn btn-outline-dark" href="https://git.mig5.net/mig5/enroll" target="_blank" rel="noreferrer"><i class="bi bi-journal-text"></i> Full README</a>
</div>
</div>
<ul class="nav nav-pills gap-2" id="qsTabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" id="qs1-tab" data-bs-toggle="tab" data-bs-target="#qs1" type="button" role="tab">Local</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs2-tab" data-bs-toggle="tab" data-bs-target="#qs2" type="button" role="tab">Remote</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs3-tab" data-bs-toggle="tab" data-bs-target="#qs3" type="button" role="tab">Multi-site</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="qs4-tab" data-bs-toggle="tab" data-bs-target="#qs4" type="button" role="tab">Diff</button></li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="qs1" role="tabpanel" aria-labelledby="qs1-tab">
<div class="row g-3">
<div class="col-lg-6">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs1code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs1code"># Harvest → Manifest in one go
enroll single-shot --out ./ansible
# Then run Ansible locally
ansible-playbook -i "localhost," -c local ./ansible/playbook.yml</code></pre>
</div>
</div>
<div class="col-lg-6">
<div class="callout p-4 h-100">
<div class="fw-semibold mb-2">Good for</div>
<div class="text-muted">Disaster recovery snapshots, "make this one host reproducible", and carving a golden role set you'll refine over time.</div>
<hr>
<div class="smallprint">Want templates for structured configs? Install <a href="https://git.mig5.net/mig5/jinjaturtle" target="_blank" rel="noopener noreferrer">JinjaTurtle</a> and use <code>--jinjaturtle</code> (or let it auto-detect).</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="qs2" role="tabpanel" aria-labelledby="qs2-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs2code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs2code"># Remote harvest over SSH, then manifest locally
enroll single-shot \
--remote-host myhost.example.com \
--remote-user myuser \
--harvest /tmp/enroll-harvest \
--out ./ansible \
--fqdn myhost.example.com</code></pre>
</div>
<div class="smallprint mt-3">If you don't want/need sudo on the remote host, add <code>--no-sudo</code> (expect a less complete harvest).</div>
</div>
<div class="tab-pane fade" id="qs3" role="tabpanel" aria-labelledby="qs3-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs3code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs3code"># Multi-site mode: shared roles, host-specific state in inventory
enroll harvest --out /tmp/enroll-harvest
enroll manifest --harvest /tmp/enroll-harvest --out ./ansible --fqdn "$(hostname -f)"
# Run the per-host playbook
ansible-playbook ./ansible/playbooks/"$(hostname -f)".yml</code></pre>
</div>
<div class="smallprint mt-3">Rule of thumb: single-site for "one server, easy-to-read roles"; <code>--fqdn</code> for "many servers, high abstraction, fast adoption".</div>
</div>
<div class="tab-pane fade" id="qs4" role="tabpanel" aria-labelledby="qs4-tab">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#qs4code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="qs4code"># Compare two harvests and get a human-friendly report
enroll diff --old /path/to/harvestA --new /path/to/harvestB --format markdown
# Send a webhook when differences are detected
enroll diff \
--old /path/to/harvestA \
--new /path/to/harvestB \
--webhook https://example.net/webhook \
--webhook-format json \
--webhook-header 'X-Enroll-Secret: ...' \
--exit-code</code></pre>
</div>
</div>
</div>
</div>
</section>
<section class="py-5" id="demos" aria-label="Demos">
<div class="container">
<div class="row g-3 align-items-end mb-3">
<div class="col-lg-8">
<h2 class="section-title display-6 fw-bold mb-2">Demonstrations</h2>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Harvest</div>
<div class="text-muted mb-3">Collect state into a bundle.</div>
<div class="asciicast" data-asciinema-id="765203"><script src="https://asciinema.org/a/765203.js" id="asciicast-765203" async="true"></script>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Manifest</div>
<div class="text-muted mb-3">Render Ansible roles/playbooks.</div>
<div class="asciicast" data-asciinema-id="765204"><script src="https://asciinema.org/a/765204.js" id="asciicast-765204" async="true"></script></div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Single-shot</div>
<div class="text-muted mb-3">One command → workable output.</div>
<div class="asciicast" data-asciinema-id="765127"><script src="https://asciinema.org/a/765127.js" id="asciicast-765127" async="true"></script></div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-6">
<div class="card feature-card h-100">
<div class="card-body p-4">
<div class="fw-semibold mb-1">Diff</div>
<div class="text-muted mb-3">Drift report + notifications.</div>
<div class="asciicast" data-asciinema-id="765128"><script src="https://asciinema.org/a/765128.js" id="asciicast-765128" async="true"></script></div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="py-5 bg-light" id="install" aria-label="Install">
<div class="container">
<div class="row g-3 align-items-end mb-3">
<div class="col-lg-8">
<h2 class="section-title display-6 fw-bold mb-2">Install</h2>
<p class="text-muted mb-0">Use your preferred packaging. An AppImage is also available.</p>
</div>
<div class="col-lg-4 text-lg-end">
<a class="btn btn-outline-dark" href="https://git.mig5.net/mig5/enroll" target="_blank" rel="noreferrer"><i class="bi bi-link-45deg"></i>Other install steps</a>
</div>
</div>
<ul class="nav nav-pills gap-2" id="installTabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#inst1" type="button" role="tab">Debian/Ubuntu</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#inst2" type="button" role="tab">Fedora</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#inst3" type="button" role="tab">Pip</button></li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="inst1" role="tabpanel">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst1code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst1code">sudo mkdir -p /usr/share/keyrings
curl -fsSL https://mig5.net/static/mig5.asc | sudo gpg --dearmor -o /usr/share/keyrings/mig5.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/mig5.gpg] https://apt.mig5.net $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/mig5.list
sudo apt update
sudo apt install enroll</code></pre>
</div>
</div>
<div class="tab-pane fade" id="inst2" role="tabpanel">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst2code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst2code">sudo rpm --import https://mig5.net/static/mig5.asc
sudo tee /etc/yum.repos.d/mig5.repo > /dev/null << 'EOF'
[mig5]
name=mig5 Repository
baseurl=https://rpm.mig5.net/rpm/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mig5.net/static/mig5.asc
EOF
sudo dnf upgrade --refresh
sudo dnf install enroll</code></pre>
</div>
</div>
<div class="tab-pane fade" id="inst3" role="tabpanel">
<div class="codeblock">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#inst3code"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="terminal mb-0"><code id="inst3code">pip install enroll
# or: pipx install enroll</code></pre>
</div>
</div>
</div>
</div>
</section>
<footer class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-5">
<div class="d-flex align-items-center gap-2 mb-2">
<img class="brand-mark" src="assets/img/enroll.svg" alt="Enroll">
<div class="fw-bold">Enroll (a mig5 project)</div>
</div>
<div class="smallprint">Reverse-engineering servers into Ansible.</div>
</div>
<div class="col-lg-7">
<div class="row g-3">
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Site</div>
<div class="d-flex flex-column gap-1">
<a href="docs.html" class="link-secondary text-decoration-none">Docs</a>
<a href="examples.html" class="link-secondary text-decoration-none">Examples</a>
<a href="security.html" class="link-secondary text-decoration-none">Security Design</a>
</div>
</div>
<div class="col-6 col-md-3">
<div class="fw-semibold mb-2">Project</div>
<div class="d-flex flex-column gap-1">
<a href="https://git.mig5.net/mig5/enroll" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Repo</a>
<a href="https://pypi.org/project/enroll/" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">PyPI</a>
<a href="https://git.mig5.net/mig5/enroll/releases" target="_blank" rel="noreferrer" class="link-secondary text-decoration-none">Releases</a>
</div>
</div>
<div class="col-12 col-md-6">
<div class="fw-semibold mb-2">Contact</div>
<li><a class="link-secondary text-decoration-none" href="https://nr.mig5.net/forms/mig5/contact" target="_blank" rel="noreferrer">Form</a></li>
<li><span class="text-secondary">Fediverse:</span> <a class="link-secondary text-decoration-none" href="https://goto.mig5.net/@mig5" target="_blank" rel="noreferrer">@mig5</a></li>
</div>
</div>
</div>
</div>
<hr class="my-4">
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 smallprint">
<div class="text-secondary">© <span id="year"></span> <a href="https://mig5.net" target="_blank" rel="noopener noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>
<script>
document.getElementById('year').textContent = new Date().getFullYear();
</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"></script>
</body>
</html>

199
src/security.html Normal file
View file

@ -0,0 +1,199 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Enroll Security</title>
<meta name="description" content="Security posture and safe workflows for Enroll outputs.">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- 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" rel="stylesheet">
</head>
<body>
<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">
<img class="brand-mark" src="assets/img/enroll.svg" alt="Enroll">
<span>Enroll</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav" aria-controls="nav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="docs.html">Docs</a></li>
<li class="nav-item"><a class="nav-link" href="examples.html">Examples</a></li>
<li class="nav-item"><a class="nav-link" href="security.html">Security Design</a></li>
<li class="nav-item 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
</a>
</li>
</ul>
</div>
</div>
</nav>
<header class="py-5 hero">
<div class="container py-3">
<div class="kicker mb-3"><i class="bi bi-shield-lock"></i> Security</div>
<h1 class="display-6 fw-bold mb-2">Safe by default. Powerful when you opt in.</h1>
<p class="lead mb-0">Enroll can touch sensitive files. This page helps you use it confidently.</p>
</div>
</header>
<main class="py-5">
<div class="container">
<div class="row g-4">
<div class="col-lg-8">
<div class="feature-card p-4">
<h2 class="h4 fw-bold mb-2">Default behavior</h2>
<p class="text-secondary mb-0">In normal mode, Enroll attempts to avoid harvesting likely secrets using a combination of path deny-lists, content sniffing, and size caps. This means you may see some files intentionally skipped.</p>
</div>
<div class="feature-card p-4 mt-4 border border-warning">
<div class="d-flex align-items-start gap-3">
<div class="icon-pill" style="background: rgba(255,193,7,0.20); border-color: rgba(255,193,7,0.45);"><i class="bi bi-exclamation-triangle"></i></div>
<div>
<h2 class="h4 fw-bold mb-2">The <code>--dangerous</code> flag</h2>
<p class="mb-2">This disables secret-safety checks. It can copy private keys, API tokens, DB passwords, TLS key material, etc.</p>
<p class="small text-secondary mb-0"><strong>Rule:</strong> if you use <code>--dangerous</code>, treat the output as sensitive data and plan secure storage before you run it. Don't store secrets in plaintext in a public place!</p>
</div>
</div>
</div>
<div class="feature-card p-4 mt-4">
<h2 class="h4 fw-bold mb-2">Encrypt bundles at rest with SOPS</h2>
<p class="text-secondary">You can install <a href="https://github.com/getsops/sops" target="_blank" rel="noopener noreferrer">SOPS</a> on your <code>$PATH</code>, then use <code>--sops</code> to write a single encrypted <code>.tar.gz.sops</code> file for harvests and/or manifests. This is meant for storage-at-rest and backups.</p>
<div class="codeblock terminal">
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="#sec-sops"><i class="bi bi-clipboard"></i> Copy</button>
<pre class="mb-0"><code id="sec-sops"><span class="prompt">$</span> enroll harvest --out /tmp/enroll-harvest --dangerous --sops &lt;FINGERPRINT&gt;
<span class="prompt">$</span> enroll manifest --harvest /tmp/enroll-harvest/harvest.tar.gz.sops \
--out /tmp/enroll-ansible --sops &lt;FINGERPRINT&gt;</code></pre>
</div>
<div class="alert alert-secondary mt-3 mb-0">
<div class="fw-semibold">Important</div>
<div class="small mb-0">In manifest <code>--sops</code> mode, you'll need to decrypt and extract the bundle before running <code>ansible-playbook</code>.</div>
</div>
</div>
</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>Start with default mode (no <code>--dangerous</code>).</li>
<li>Add <code>--include-path</code> for a small set of extra files you genuinely want managed.</li>
<li>If you must capture secrets, use <code>--dangerous</code> <strong>and</strong> <code>--sops</code>.</li>
<li>Keep outputs out of public repos; review before committing.</li>
<li>Rotate credentials if you ever suspect they were captured or exposed.</li>
</ol>
</div>
<div class="callout p-4 mt-3">
<div class="fw-semibold mb-2">Storage ideas</div>
<ul class="small mb-0">
<li>Encrypted SOPS bundle stored in a password manager vault</li>
<li>Private git repo with additional encryption at rest</li>
<li>Offline backup in an encrypted volume</li>
</ul>
</div>
<div class="callout p-4 mt-3">
<div class="fw-semibold mb-2">Scope control</div>
<p class="small text-secondary mb-3">You can explicitly include or exclude paths. Excludes take precedence over includes.</p>
<div class="terminal"><pre class="mb-0"><code><span class="prompt">$</span> enroll harvest \
--out /tmp/enroll-harvest \
--include-path '/home/*/.profile' \
--exclude-path '/home/*/.ssh/**'</code></pre></div>
</div>
</div>
</div>
<hr class="my-5">
<div class="feature-card p-4">
<h2 class="h4 fw-bold mb-2">Threat model</h2>
<div class="row g-3">
<div class="col-md-6">
<div class="fw-semibold">What Enroll tries to prevent</div>
<ul class="small mb-0">
<li>Accidentally copying obvious secrets in default mode</li>
<li>Harvesting huge/unbounded file sets by mistake</li>
<li>One host's difference causing problems for other hosts in terms of Ansible task steps (multi-site mode)</li>
</ul>
</div>
<div class="col-md-6">
<div class="fw-semibold">What you still need to think about</div>
<ul class="small mb-0">
<li>Where outputs are stored and who can access them</li>
<li>Reviewing what was captured before committing/sharing</li>
<li>Choosing encryption and secret-management strategy</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<footer class="py-5">
<div class="container">
<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" 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>
</div>
<p class="smallprint mb-3">Reverse-engineering servers into Ansible.</p>
<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>
</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">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 Design</a></li>
</ul>
</div>
<div class="col-lg-3">
<div class="fw-semibold mb-2">Contact</div>
<ul class="list-unstyled small mb-0">
<li><a class="link-secondary text-decoration-none" href="https://nr.mig5.net/forms/mig5/contact" target="_blank" rel="noreferrer">Form</a></li>
<li><span class="text-secondary">Fediverse:</span> <a class="link-secondary text-decoration-none" href="https://goto.mig5.net/@mig5" target="_blank" rel="noreferrer">@mig5</a></li>
</ul>
</div>
</div>
<hr class="my-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 small">
<div class="text-secondary">© <span id="year"></span> <a href="https://mig5.net" target="_blank" rel="noopener noreferrer">mig5 system administration</a></div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/site.js"></script>
<script>document.getElementById('year').textContent = new Date().getFullYear();</script>
</body>
</html>

10
upload.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/bash
set -eou pipefail
SRC="src"
DEST="/opt/www/enroll.sh"
rsync -aHPvz ${SRC}/ root@lupin.mig5.net:${DEST}/
ssh root@lupin.mig5.net "chown -R web:web ${DEST}"