Skip to content

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>.service is 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.sh and maid-doctor.sh ship under scripts/ in the MAID source tree; the installer does not stage scripts/ 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/sudo operator, so running it straight from the checkout is fine. The doctor is also referenced by the maid-ops NOPASSWD sudoers rule in §6, and a NOPASSWD Cmnd MUST point at a root-owned, immutable path — never an operator-writable checkout and never the blue-green /opt/maid/current symlink — so it is staged once to /opt/maid/libexec/maid-doctor.sh (see §6).

  1. Provision the host (cloud-init / ansible / fresh install). Ensure openssh-server, python3, sudo, and the MAID package are installed.
  2. Create the three users if they don't exist:
    sudo useradd --system --create-home --shell /usr/sbin/nologin maid-engine
    sudo useradd --system --create-home --shell /bin/bash       maid-ops
    sudo useradd --create-home --shell /bin/bash --groups sudo  maid-admin
    
  3. Stage the key manifest at /etc/maid/ssh-keys.yml. The file MUST be owned root:root, mode 0640. Keep the source-of-truth copy in your secret-store / git-ops repo.
  4. Run the bootstrap installer (idempotent; safe to run on every provisioning pass):
    sudo bash "${MAID_SRC}/scripts/bootstrap-ssh.sh" \
        --manifest /etc/maid/ssh-keys.yml
    
    The installer writes each ~/.ssh/authorized_keys, chmods to 0600, chowns to the target user, and validates the resulting file with ssh-keygen -lf.
  5. Apply the sshd hardening drop-in (one-shot, but idempotent):
    # /etc/ssh/sshd_config.d/10-maid.conf
    PermitRootLogin no
    PasswordAuthentication no
    ChallengeResponseAuthentication no
    KbdInteractiveAuthentication no
    UsePAM yes
    AuthenticationMethods publickey
    PermitEmptyPasswords no
    
    Then:
    sudo sshd -t && sudo systemctl reload sshd
    
  6. Stage the doctor, then grant maid-ops passwordless sudo for the narrow allowlist. The doctor Cmnd below 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):
    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.sh
    
    Then write the allowlist:
    # /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.sh
    
    Validate with sudo visudo -cf /etc/sudoers.d/40-maid-ops.
  7. Bootstrap the backup destination's host key (only required if you use the ssh backup transport):
    sudo -u maid-engine -H ssh -o StrictHostKeyChecking=accept-new \
        maid@backup.example.com true
    
    This populates /var/lib/maid-engine/.ssh/known_hosts with the backup destination's host key. The accept-new policy 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.

sudo bash "${MAID_SRC}/scripts/bootstrap-ssh.sh" \
    --manifest /etc/maid/ssh-keys.yml --check

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:

sudo loginctl terminate-user maid-ops    # or maid-admin

Caveats

  • The bootstrap script is the only supported way to manage authorized_keys on a MAID host. Manual edits are clobbered on the next run.
  • maid-engine is created with shell /usr/sbin/nologin to prevent interactive login; the rsync transport uses ssh directly 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.com before re-bootstrapping the destination.

References