From 09943a6439edcc61855d97571f4511737c27daa6 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Fri, 2 Jan 2026 08:23:28 +1100 Subject: [PATCH] Initial commit --- .gitignore | 1 + src/assets/css/site.css | 149 +++++++++++ src/assets/img/enroll.svg | 107 ++++++++ src/assets/js/site.js | 60 +++++ src/docs.html | 506 ++++++++++++++++++++++++++++++++++++++ src/examples.html | 200 +++++++++++++++ src/favicon.ico | Bin 0 -> 4286 bytes src/index.html | 427 ++++++++++++++++++++++++++++++++ src/security.html | 199 +++++++++++++++ upload.sh | 10 + 10 files changed, 1659 insertions(+) create mode 100644 .gitignore create mode 100644 src/assets/css/site.css create mode 100644 src/assets/img/enroll.svg create mode 100644 src/assets/js/site.js create mode 100644 src/docs.html create mode 100644 src/examples.html create mode 100644 src/favicon.ico create mode 100644 src/index.html create mode 100644 src/security.html create mode 100755 upload.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/src/assets/css/site.css b/src/assets/css/site.css new file mode 100644 index 0000000..f4cdb50 --- /dev/null +++ b/src/assets/css/site.css @@ -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); +} diff --git a/src/assets/img/enroll.svg b/src/assets/img/enroll.svg new file mode 100644 index 0000000..2a03d41 --- /dev/null +++ b/src/assets/img/enroll.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/js/site.js b/src/assets/js/site.js new file mode 100644 index 0000000..6866eb2 --- /dev/null +++ b/src/assets/js/site.js @@ -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
+ // 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 = '
Add your asciinema id here: data-asciinema-id.
'; + 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(); + }); +})(); diff --git a/src/docs.html b/src/docs.html new file mode 100644 index 0000000..5d21c77 --- /dev/null +++ b/src/docs.html @@ -0,0 +1,506 @@ + + + + + + Enroll Docs + + + + + + + + + + + + + + + + + + + +
+
+
Documentation
+

How Enroll works

+

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

+
+
+ +
+
+
+ + +
+
+

Mental model

+

Enroll is intentionally simple: it collects facts first, then renders Ansible from those facts.

+ +
+
+
+
+
+
+
1) Harvest
+
Snapshot state into a bundle
+
+
+
    +
  • Detect installed packages and services
  • +
  • Collect config that deviates from packaged defaults (where possible)
  • +
  • Grab relevant custom/unowned files in service dirs
  • +
  • Capture non-system users & SSH public keys
  • +
+
+
+
+
+
+
+
+
2) Manifest
+
Generate an Ansible repo structure
+
+
+
    +
  • Roles with files/templates and defaults
  • +
  • Playbooks to apply the captured state
  • +
  • Optional inventory structure for multi-host runs: each host gets its own playbook
  • +
+
+
+
+ +
+
Typical flow
+
$ enroll harvest --out /tmp/enroll-harvest
+$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible
+$ ansible-playbook -i "localhost," -c local /tmp/enroll-ansible/playbook.yml
+
+
+ +
+

Single-site vs multi-site

+

Manifest output has two styles. Choose based on how you'll use the result.

+ +
+
+
+
Single-site (default)
+

Best when you are enrolling one host, or you're producing a reusable "golden" role set that could be applied anywhere.

+
    +
  • Roles are self-contained
  • +
  • Raw files live in each role's files/
  • +
  • Template variables live in defaults/main.yml
  • +
+
+
+
+
+
Multi-site (--fqdn)
+

Best when you want to enroll several existing servers quickly, especially if they differ.

+
    +
  • Roles are shared; raw files live in host-specific inventory
  • +
  • Inventory decides what gets managed on each host (files/packages/services)
  • +
  • Non-templated files go under inventory/host_vars/<fqdn>/<role>/.files
  • +
+
+
+
+ +
+
Multi-site example
+
$ enroll manifest --harvest /tmp/enroll-harvest --out /tmp/enroll-ansible --fqdn "$(hostname -f)"
+$ ansible-playbook /tmp/enroll-ansible/playbooks/"$(hostname -f)".yml
+
+
+ +
+

Remote harvesting over SSH

+

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

+
+
$ enroll harvest --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-harvest
+$ enroll single-shot --remote-host myhost.example.com --remote-user myuser --out /tmp/enroll-ansible --fqdn myhost.example.com
+
+
+
Tip
+
If you don't want/need sudo on the remote side, add --no-sudo. However, be aware that you may get a more limited harvest depending on permissions.
+
+
+ +
+

JinjaTurtle integration

+

If JinjaTurtle (one of my other projects) is installed, Enroll can also produce Jinja2 templates for ini/json/xml/toml-style config and extract variables cleanly into Ansible, instead of just storing the 'raw' files.

+
+
+
+
Modes
+
    +
  • --jinjaturtle to force on
  • +
  • --no-jinjaturtle to force off
  • +
  • Default is auto
  • +
+
+
+
+
+
Where variables land
+
    +
  • Single-site: roles/<role>/defaults/main.yml
  • +
  • Multi-site: inventory/host_vars/<fqdn>/<role>.yml
  • +
+
+
+
+
+ +
+

INI config file

+

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

+ +
+
Discovery order
+
You can pass -c/--config, set ENROLL_CONFIG, or let Enroll auto-discover ./enroll.ini, ./.enroll.ini, or ~/.config/enroll/enroll.ini.
+
+ +
+ +
[enroll]
+# (future global flags may live here)
+
+[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
+
+ +
+
Note
+
In INI sections, option names use underscores (e.g. include_path) even when the CLI flag uses hyphens (e.g. --include-path).
+
+
+ + +
+

Drift detection with enroll diff

+

One of the things I miss from my Puppet days, was the way the Puppet 'agent' would check in with the server and realign itself to the declared desired state. With Ansible, it's easy for systems to fall 'out of date', especially if someone is doing the wrong thing and changing things on-the-fly instead of via config management!

+

The purpose of enroll diff is to compare two 'harvests' and detect what has changed - be it adding/removing of programs, change to systemd unit state, modifications, addition or removal of files, and so on.

+ +
+
Notifications for diff
+
The enroll diff feature supports sending the difference to a webhook of your choosing, or by e-mail. The payload can be sent in json, plain text, or markdown.
+
+ +

A great way to use enroll diff is to run it periodically (e.g via cron or a systemd timer). Below is an example.

+ +

Store the below file at /usr/local/bin/enroll-harvest-diff.sh and make it executable.

+
+ +
#!/usr/bin/env bash
+set -euo pipefail
+
+# Required env
+: "${WEBHOOK_URL:?Set WEBHOOK_URL in /etc/enroll/enroll-harvest-diff}"
+: "${ENROLL_SECRET:?Set ENROLL_SECRET in /etc/enroll/enroll-harvest-diff}"
+
+# Optional env
+STATE_DIR="${ENROLL_STATE_DIR:-/var/lib/enroll}"
+GOLDEN_DIR="${STATE_DIR}/golden"
+PROMOTE_NEW="${PROMOTE_NEW:-1}"          # 1=promote new->golden; 0=keep golden fixed
+KEEP_BACKUPS="${KEEP_BACKUPS:-7}"        # only used if PROMOTE_NEW=1
+LOCKFILE="${STATE_DIR}/.enroll-harvest-diff.lock"
+
+mkdir -p "${STATE_DIR}"
+chmod 700 "${STATE_DIR}" || true
+
+# single-instance lock (avoid overlapping timer runs)
+exec 9>"${LOCKFILE}"
+flock -n 9 || exit 0
+
+tmp_new=""
+cleanup() {
+  if [[ -n "${tmp_new}" && -d "${tmp_new}" ]]; then
+    rm -rf "${tmp_new}"
+  fi
+}
+trap cleanup EXIT
+
+make_tmp_dir() {
+  mktemp -d "${STATE_DIR}/.harvest.XXXXXX"
+}
+
+run_harvest() {
+  local out_dir="$1"
+  rm -rf "${out_dir}"
+  mkdir -p "${out_dir}"
+  chmod 700 "${out_dir}" || true
+  enroll harvest --out "${out_dir}" >/dev/null
+}
+
+# A) create golden if missing
+if [[ ! -f "${GOLDEN_DIR}/state.json" ]]; then
+  tmp="$(make_tmp_dir)"
+  run_harvest "${tmp}"
+  rm -rf "${GOLDEN_DIR}"
+  mv "${tmp}" "${GOLDEN_DIR}"
+  echo "Golden harvest created at ${GOLDEN_DIR}"
+  exit 0
+fi
+
+# B) create new harvest
+tmp_new="$(make_tmp_dir)"
+run_harvest "${tmp_new}"
+
+# C) diff + webhook notify
+enroll diff \
+  --old "${GOLDEN_DIR}" \
+  --new "${tmp_new}" \
+  --webhook "${WEBHOOK_URL}" \
+  --webhook-format json \
+  --webhook-header "X-Enroll-Secret: ${ENROLL_SECRET}" # You can send multiple --webhook-header params as you need
+
+# Promote or discard new harvest
+if [[ "${PROMOTE_NEW}" == "1" || "${PROMOTE_NEW,,}" == "true" || "${PROMOTE_NEW}" == "yes" ]]; then
+  ts="$(date -u +%Y%m%d-%H%M%S)"
+  backup="${STATE_DIR}/golden.prev.${ts}"
+  mv "${GOLDEN_DIR}" "${backup}"
+  mv "${tmp_new}" "${GOLDEN_DIR}"
+  tmp_new=""  # don't delete it in trap
+
+  # Keep only latest N backups
+  if [[ "${KEEP_BACKUPS}" =~ ^[0-9]+$ ]] && (( KEEP_BACKUPS > 0 )); then
+    ls -1dt "${STATE_DIR}"/golden.prev.* 2>/dev/null | tail -n +"$((KEEP_BACKUPS+1))" | xargs -r rm -rf
+  fi
+
+  echo "Diff complete; baseline updated."
+else
+  # tmp_new will be deleted by trap
+  echo "Diff complete; baseline unchanged (PROMOTE_NEW=${PROMOTE_NEW})."
+fi
+
+
+
+

Save these environment variables in /etc/enroll/enroll-harvest-diff

+
+ +

+# Where to store golden + temp harvests
+ENROLL_STATE_DIR=/var/lib/enroll
+
+# 1 = each run becomes new baseline ("since last harvest")
+# 0 = compare against a fixed baseline ("since golden")
+PROMOTE_NEW=1
+
+# If PROMOTE_NEW=1, keep this many old baselines
+KEEP_BACKUPS=7
+
+WEBHOOK_URL=https://example.com/webhook/xxxxxxxx
+ENROLL_SECRET=xxxxxxxxxxxxxxxxxxxx
+
+
+ +
+
+
Webhook headers
+
The --webhook-header parameter can be used multiple times. You can, for example, send X-Enroll-Secret and a secret value of your choice, to help secure your webhook endpoint.
+
+ +

Save this systemd unit file to /etc/systemd/system/enroll-harvest-diff.service

+
+ +

+[Unit]
+Description=Enroll harvest + diff + webhook notify
+Wants=network-online.target
+After=network-online.target
+ConditionPathExists=/etc/enroll/enroll-harvest-diff
+
+[Service]
+Type=oneshot
+EnvironmentFile=/etc/enroll/enroll-harvest-diff
+Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+UMask=0077
+
+# Create /var/lib/enroll automatically
+StateDirectory=enroll
+
+ExecStart=/usr/local/bin/enroll-harvest-diff.sh
+
+
+ +
+

Save this systemd timer to /etc/systemd/system/enroll-harvest-diff.timer

+
+ +

+[Unit]
+Description=Run Enroll harvest diff hourly
+
+[Timer]
+OnCalendar=hourly
+RandomizedDelaySec=10m
+Persistent=true
+
+[Install]
+WantedBy=timers.target
+
+
+ +
+

Now you can enable and test it!

+
+ +

+sudo systemctl daemon-reload
+sudo systemctl enable --now enroll-harvest-diff.timer
+
+# run once now
+sudo systemctl start enroll-harvest-diff.service
+# watch it in the logs
+sudo journalctl -u enroll-harvest-diff.service -n 200 --no-pager
+
+
+ +
+
+
Need help with writing webhooks?
+
I use Node-RED. Here's a sample Node-RED flow that might help run your webhook, pre-configured to parse the enroll diff JSON payload!
+
+ +
+ + +
+

Tips

+
+
+
+
Start safe
+

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

+
+
+
+
+
Encrypt at rest
+

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

+
+
+
+
+
Multi-host safety
+

For fleets, prefer multi-site output so roles stay generic and host inventory controls what is applied per host - reducing "shared role breaks other host" surprises.

+
+
+
+
+
Keep it reproducible
+

Commit the manifest output, run it in CI, and use enroll diff as a drift alarm (webhook/email).

+
+
+
+
+ +
+
+
+
+ + + + + + + + + + diff --git a/src/examples.html b/src/examples.html new file mode 100644 index 0000000..6c273ee --- /dev/null +++ b/src/examples.html @@ -0,0 +1,200 @@ + + + + + + Enroll Examples + + + + + + + + + + + + + + + + + + + +
+
+
Examples
+

Copy/paste recipes

+

Practical flows you can adapt to your environment.

+
+
+ +
+
+ +
+
+
+
Enroll a single host (local)
+
+ +
$ enroll harvest --out /tmp/enroll-harvest
+$ enroll manifest --harvest /tmp/enroll-harvest \
+  --out /tmp/enroll-ansible
+$ ansible-playbook -i "localhost," -c local \
+  /tmp/enroll-ansible/playbook.yml --diff --check
+
+

Great for "make this box reproducible" or building a golden role set.

+
+
+ +
+
+
Enroll a remote host (over SSH)
+
+ +
$ enroll harvest \
+  --remote-host myhost.example.com \
+  --remote-user myuser \
+  --out /tmp/enroll-harvest
+$ enroll manifest \
+  --harvest /tmp/enroll-harvest \
+  --out /tmp/enroll-ansible
+
+

No need to manually run commands on the server - your bundle lands locally.

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

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

+
+
+ +
+
+
Drift detection with enroll diff
+
+ +
$ enroll diff \
+  --old /path/to/harvestA \
+  --new /path/to/harvestB \
+  --format markdown
+$ 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
+
+

Use it in cron or CI to alert on change.

+
+
+
+ +
+ +
+
+
+
Safe harvesting (default)
+

Enroll tries to avoid harvesting files that might contain secrets. If you need to capture "everything", pass --dangerous and treat the output as sensitive.

+

You can still control what gets collected and what doesn't by using --include and --exclude flags.

+
$ enroll harvest --dangerous --out /tmp/enroll-harvest
+
+
+
+
+
Encrypt bundles at rest (SOPS)
+

Produce a single encrypted file for harvest and/or manifest output (requires SOPS to be installed).

+

This is especially a good idea if you are using --dangerous, which might sweep up secrets (see above).

+
$ enroll harvest --dangerous --out /tmp/harvest \
+  --sops <FINGERPRINT>
+$ enroll manifest --harvest /tmp/harvest/harvest.tar.gz.sops \
+  --out /tmp/enroll-ansible --sops <FINGERPRINT>
+
+
+
+ +
+
+ + + + + + + + + + diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..77181af45e9705c27c79096b98a28169fe933e77 GIT binary patch literal 4286 zcmeI0?N5_e7{&=>g%mg>zd*6_)vPu zh{}d8+a4$hl}mX{{3`moGjz!H&U^apaU6P;c$R?@cb`scKm6|19L4Es8%hjL7+>T5{FsqQ1mZD}!S8%=X%fx3zK8|U!~0CdSr@ZBDR!_$w3RyTC!c2pZ3QaZCs*4o_t7!W<|4TZ!1dT0Oi46Gf! zIH_qss#1@QS#?OtHsP~htyr65$J$&+R3c}MYwTe!J@k_CvcbPMPv0=o3k*m;VMJ1) z8%bw-kbKUE^{PQ^s8~j#zP@}wYF$aM@OY#*{ft4f$V@aGULU^9)?rhH4_oz*kep}3 z$7v<_Xm`Pi`s9alBxlxPOI{;3E9;STvPpW$AdAdIV*JqrcIFtc_0lL(uTNr|?mjl3 zwqQ?bGmcd`ada7FTx`R)<#z1S58_943yxGdu3`^$eFg`N<5J9*>yh3i4w+=Hwtp}b!f~}3 zN39dca?VI)E{(ByY)n1pz@}fWNHLGqUmRcbl1VoASS?EQHe{H_klQtjoX%%b%BAu7 zynPx6YX@+mZAyx{Qq*GW9AEU1MJCztqRs8YfvOuQ><-|JXAT8Is@^#idgr2Z?>tW0 z9%0+52IRW@Qp{N^5Nq_XmmYfMT(HO_J6=rB%!>Qei2M#e%6bDR?U_eKUjSudJl`M9 z&vg2cuI)sTds>P)YwTkWdzsTq23cg1ok%#n12|OK26cY`8o?>~gmy3p-Eau1 zo>`ptg`nvVN-<}RYwTgK^aw8*WRaO@@PDnLxf91~I-vE<<5cr73R>2A nUQaL>lFu^F + + + + + Enroll - Reverse-engineering servers into Ansible + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
Reverse-engineering servers into Ansible
+

Get an existing Linux host into Ansible in seconds.

+

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

+ +
+ Super fast + Optional SOPS encryption + Remote over SSH +
+
+ +
+
+
+
Config in the blink of an eye
+
single-shot → ansible-playbook
+
+
+
$ enroll single-shot --out ./ansible
+
$ cd ./ansible && tree -L 2
+
.
+├── ansible.cfg
+├── playbook.yml
+├── roles/
+│   ├── cron/
+│   ├── firewall/
+│   ├── nginx/
+│   ├── openssh-server/
+│   ├── users/
+│   ├── etc_custom/
+└── README.md
+
+
Tip: for multiple hosts, use --fqdn to generate inventory-driven, data-driven roles.
+
+
+
+
+
+ +
+
+
+
+

A simple mental model

+

Enroll is built around two phases, plus an optional drift report:

+
+
+
+
+
Harvest
+
Collect host facts + relevant files into a bundle.
+
+
+
+
+
+
Manifest
+
Render Ansible roles & playbooks from the harvest.
+
+
+
+
+
+
Diff
+
Compare two harvests and notify via webhook/email.
+
+
+
+
+
+
+
+
+
+
Safe-by-default harvesting
+
Enroll avoids likely secrets with a path denylist, content sniffing, and size caps - then lets you opt in to more aggressive collection when you're ready.
+
+
+
+
+
+
+
Multi-site without "shared role broke host2"
+
In --fqdn mode, roles are data-driven and host inventory decides what gets managed per host.
+
+
+
+
+
+
+
Remote over SSH
+
Harvest a remote host from your workstation, then manifest Ansible output locally.
+
+
+
+
+
+
+
Encrypt bundles at rest
+
Use --sops to store harvests/manifests as a single encrypted .tar.gz.sops file (GPG) for safer long-term storage as a DR strategy.
+
+
+
+
+ +
+
Why sysadmins like it
+
+
• Rapid enrolling of existing infra into config management
• Tweak include/exclude paths as needed
+
• Capture what changed from package defaults
diff mode detects and alerts about drift
+
+
+
+
+
+
+ +
+
+
+
+

Quickstart

+

Copy, paste, iterate.

+
+ +
+ + + +
+
+
+
+
+ +
# Harvest → Manifest in one go
+enroll single-shot --out ./ansible
+
+# Then run Ansible locally
+ansible-playbook -i "localhost," -c local ./ansible/playbook.yml
+
+
+
+
+
Good for
+
Disaster recovery snapshots, "make this one host reproducible", and carving a golden role set you'll refine over time.
+
+
Want templates for structured configs? Install JinjaTurtle and use --jinjaturtle (or let it auto-detect).
+
+
+
+
+ +
+
+ +
# Remote harvest over SSH, then manifest locally
+enroll single-shot \
+  --remote-host myhost.example.com \
+  --remote-user myuser \
+  --harvest /tmp/enroll-harvest \
+  --out ./ansible \
+  --fqdn myhost.example.com
+
+
If you don't want/need sudo on the remote host, add --no-sudo (expect a less complete harvest).
+
+ +
+
+ +
# Multi-site mode: shared roles, host-specific state in inventory
+enroll harvest --out /tmp/enroll-harvest
+enroll manifest --harvest /tmp/enroll-harvest --out ./ansible --fqdn "$(hostname -f)"
+
+# Run the per-host playbook
+ansible-playbook ./ansible/playbooks/"$(hostname -f)".yml
+
+
Rule of thumb: single-site for "one server, easy-to-read roles"; --fqdn for "many servers, high abstraction, fast adoption".
+
+ +
+
+ +
# 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
+
+
+
+
+
+ +
+
+
+
+

Demonstrations

+
+
+ +
+
+
+
+
Harvest
+
Collect state into a bundle.
+
+
+
+
+
+
+
+
+
Manifest
+
Render Ansible roles/playbooks.
+
+
+
+
+
+
+
+
+
+
Single-shot
+
One command → workable output.
+
+
+
+
+
+
+
+
Diff
+
Drift report + notifications.
+
+
+
+
+
+ +
+
+ +
+
+
+
+

Install

+

Use your preferred packaging. An AppImage is also available.

+
+ +
+ + + +
+
+
+ +
sudo mkdir -p /usr/share/keyrings
+curl -fsSL https://mig5.net/static/mig5.asc | sudo gpg --dearmor -o /usr/share/keyrings/mig5.gpg
+echo "deb [arch=amd64 signed-by=/usr/share/keyrings/mig5.gpg] https://apt.mig5.net $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/mig5.list
+sudo apt update
+sudo apt install enroll
+
+
+
+
+ +
sudo rpm --import https://mig5.net/static/mig5.asc
+
+sudo tee /etc/yum.repos.d/mig5.repo > /dev/null << 'EOF'
+[mig5]
+name=mig5 Repository
+baseurl=https://rpm.mig5.net/rpm/$basearch
+enabled=1
+gpgcheck=1
+repo_gpgcheck=1
+gpgkey=https://mig5.net/static/mig5.asc
+EOF
+
+sudo dnf upgrade --refresh
+sudo dnf install enroll
+
+
+
+
+ +
pip install enroll
+# or: pipx install enroll
+
+
+
+
+
+ + + + + + + + diff --git a/src/security.html b/src/security.html new file mode 100644 index 0000000..d850a5c --- /dev/null +++ b/src/security.html @@ -0,0 +1,199 @@ + + + + + + Enroll Security + + + + + + + + + + + + + + + + + + + +
+
+
Security
+

Safe by default. Powerful when you opt in.

+

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

+
+
+ +
+
+ +
+
+
+

Default behavior

+

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

+
+ +
+
+
+
+

The --dangerous flag

+

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

+

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

+
+
+
+ +
+

Encrypt bundles at rest with SOPS

+

You can install SOPS on your $PATH, then use --sops to write a single encrypted .tar.gz.sops file for harvests and/or manifests. This is meant for storage-at-rest and backups.

+ +
+ +
$ enroll harvest --out /tmp/enroll-harvest --dangerous --sops <FINGERPRINT>
+$ enroll manifest --harvest /tmp/enroll-harvest/harvest.tar.gz.sops \
+  --out /tmp/enroll-ansible --sops <FINGERPRINT>
+
+ +
+
Important
+
In manifest --sops mode, you'll need to decrypt and extract the bundle before running ansible-playbook.
+
+
+
+ +
+
+
Recommended workflow
+
    +
  1. Start with default mode (no --dangerous).
  2. +
  3. Add --include-path for a small set of extra files you genuinely want managed.
  4. +
  5. If you must capture secrets, use --dangerous and --sops.
  6. +
  7. Keep outputs out of public repos; review before committing.
  8. +
  9. Rotate credentials if you ever suspect they were captured or exposed.
  10. +
+
+ +
+
Storage ideas
+
    +
  • Encrypted SOPS bundle stored in a password manager vault
  • +
  • Private git repo with additional encryption at rest
  • +
  • Offline backup in an encrypted volume
  • +
+
+ +
+
Scope control
+

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

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

Threat model

+
+
+
What Enroll tries to prevent
+
    +
  • Accidentally copying obvious secrets in default mode
  • +
  • Harvesting huge/unbounded file sets by mistake
  • +
  • One host's difference causing problems for other hosts in terms of Ansible task steps (multi-site mode)
  • +
+
+
+
What you still need to think about
+
    +
  • Where outputs are stored and who can access them
  • +
  • Reviewing what was captured before committing/sharing
  • +
  • Choosing encryption and secret-management strategy
  • +
+
+
+
+ +
+
+ + + + + + + + + + diff --git a/upload.sh b/upload.sh new file mode 100755 index 0000000..34fc890 --- /dev/null +++ b/upload.sh @@ -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}"