SSH Bootstrap Procedure¶
Audience: operators provisioning a new MAID host (bare-metal or VM) or rotating SSH access for an existing one.
Status: required before
maid-engine@<instance>.serviceis enabled. The doctor (scripts/maid-doctor.sh --phase install) verifies the outcome of this procedure as part of its preflight gates.
Goals¶
A MAID host has three logical operator surfaces, each backed by a dedicated OS user. Production hosts MUST authenticate all three over SSH with public keys only; password and root login MUST be disabled.
| User | Purpose |
|---|---|
maid-engine |
runs the engine + reads /var/lib/maid-engine/.ssh/id_ed25519 |
for the rsync/ssh:// backup transport. Owned by the package. |
|
maid-ops |
runs scheduled / one-shot maintenance (maid ops *, scripts/*). |
| Passwordless sudo for a narrow allowlist (see §6). | |
maid-admin |
human admin login. Member of wheel/sudo; needs sudo password. |
Each user has its own ~/.ssh/authorized_keys. The bootstrap script
below installs them from a single declarative YAML manifest so the
state is reproducible across hosts and recoverable from version
control.
Inputs¶
A YAML manifest (default path: /etc/maid/ssh-keys.yml) of the form:
# See deploy/ssh-keys.yml.example for the full annotated template.
users:
maid-engine:
- "ssh-ed25519 AAAA... backup@runner-2025"
maid-ops:
- "ssh-ed25519 AAAA... ops-bot@ci"
- "ssh-ed25519 AAAA... alice@laptop"
maid-admin:
- "ssh-ed25519 AAAA... alice@laptop"
- "ssh-ed25519 AAAA... bob@yubikey"
Only users listed under users: are touched. Empty lists are
permitted (they clear the file). Keys are written in the order
listed, deduplicated, and the trailing newline is normalised.
Procedure (operator)¶
Source checkout (
MAID_SRC).bootstrap-ssh.shandmaid-doctor.shship underscripts/in the MAID source tree; the installer does not stagescripts/under/opt/maid/current(staging is deferred to M1.4). Run them from a checkout:export MAID_SRC=/path/to/maid-checkout. Bootstrap runs as an interactive root/sudooperator, so running it straight from the checkout is fine. The doctor is also referenced by themaid-opsNOPASSWD sudoers rule in §6, and a NOPASSWDCmndMUST point at a root-owned, immutable path — never an operator-writable checkout and never the blue-green/opt/maid/currentsymlink — so it is staged once to/opt/maid/libexec/maid-doctor.sh(see §6).
- Provision the host (cloud-init / ansible / fresh install).
Ensure
openssh-server,python3,sudo, and the MAID package are installed. - Create the three users if they don't exist:
- Stage the key manifest at
/etc/maid/ssh-keys.yml. The file MUST be ownedroot:root, mode0640. Keep the source-of-truth copy in your secret-store / git-ops repo. - Run the bootstrap installer (idempotent; safe to run on every
provisioning pass):
The installer writes each
~/.ssh/authorized_keys, chmods to0600, chowns to the target user, and validates the resulting file withssh-keygen -lf. - Apply the sshd hardening drop-in (one-shot, but idempotent): Then:
- Stage the doctor, then grant
maid-opspasswordless sudo for the narrow allowlist. The doctorCmndbelow must reference a root-owned, immutable path, so install it once to/opt/maid/libexec/(root:root, mode 0755) — re-run after any release that changes the script (M1.4 will fold this into the installer):Then write the allowlist:sudo install -d -o root -g root -m 0755 /opt/maid/libexec sudo install -o root -g root -m 0755 \ "${MAID_SRC}/scripts/maid-doctor.sh" /opt/maid/libexec/maid-doctor.shValidate with# /etc/sudoers.d/40-maid-ops maid-ops ALL=(root) NOPASSWD: /usr/bin/systemctl restart maid-engine@*.service # NOTE: `systemctl reload` sends SIGHUP, which the engine does NOT handle — # it terminates and systemd restarts it (Restart=on-failure). Reload is # therefore equivalent to a restart (drops sessions); there is no in-place # config reload. Prefer `restart` above. This grant is kept only for # ExecReload compatibility. maid-ops ALL=(root) NOPASSWD: /usr/bin/systemctl reload maid-engine@*.service maid-ops ALL=(root) NOPASSWD: /usr/bin/systemctl start maid-backup@*.service maid-ops ALL=(root) NOPASSWD: /usr/bin/systemctl start maid-restore-drill@*.service maid-ops ALL=(root) NOPASSWD: /opt/maid/libexec/maid-doctor.shsudo visudo -cf /etc/sudoers.d/40-maid-ops. - Bootstrap the backup destination's host key (only required if
you use the
sshbackup transport): This populates/var/lib/maid-engine/.ssh/known_hostswith the backup destination's host key. Theaccept-newpolicy means a subsequent host-key change is rejected (MitM-safe TOFU).
Verification¶
Re-run bootstrap-ssh.sh with --check. It exits 0 if the live
state matches the manifest, 1 if a drift would be applied, and 2 on
configuration error.
The integration test tests/scripts/test_bootstrap_ssh.sh runs the
script twice against a fixture tree under $TMPDIR and asserts the
second run produces no diff (idempotency contract).
Rotation / Revocation¶
To revoke a key, remove it from the manifest and re-run the
installer. The script truncates each authorized_keys and rewrites
it from the manifest — there is no "additive merge" mode by design,
so deletions propagate cleanly. After revocation, terminate any
existing sessions:
Caveats¶
- The bootstrap script is the only supported way to manage
authorized_keyson a MAID host. Manual edits are clobbered on the next run. maid-engineis created with shell/usr/sbin/nologinto prevent interactive login; the rsync transport usessshdirectly via the user's key and does NOT need a login shell.- Host-key changes for the backup destination require
sudo -u maid-engine ssh-keygen -R backup.example.combefore re-bootstrapping the destination.
References¶
docs/runbooks/backup_failed.md— what to do when thessh://backup transport fails.deploy/scripts/lib/transport-ssh.sh— the bash-side ssh transport that consumes/var/lib/maid-engine/.ssh/id_ed25519.packages/maid-engine/src/maid_engine/backup/transports/ssh.py— the Python ssh transport that consumes the same key.