Add SSH CA integration: signed user + host certs, ssh-login helper

Turn the ssh/ engine into an SSH CA for cert-based access:

- ssh/ roles: "user" (8h user certs, principal-restricted) and "host"
  (long-lived host certs); mount max-lease-ttl raised for host certs
- scripts/ssh-login.sh: sign a fresh user cert via a scoped ssh/sign/user
  token (API, no bao binary) and connect — no authorized_keys on targets
- ca/openbao-ssh-ca.pub: the SSH CA public key (for TrustedUserCAKeys and
  client @cert-authority trust)
- README: usage, host onboarding, client trust
- gitignore generated per-host artifacts/

First host wired + verified end-to-end: 192.168.0.26 (pifour) — lutz cert
login and host-cert verification both confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 14:55:01 +02:00
parent 37a97cb0f4
commit 5a111b7a20
4 changed files with 81 additions and 0 deletions

41
scripts/ssh-login.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Sign the local SSH key with OpenBAO's SSH CA (role "user"), then connect.
# The cert is short-lived (8h) and refreshed on every call — no long-lived
# authorized_keys on the target.
#
# ssh-login [user@]host [extra ssh args...] # sign + connect
# ssh-login --sign-only [user@]host # just refresh the cert
#
# Auth to OpenBAO uses a scoped, periodic token (ssh/sign/user only).
set -euo pipefail
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
TOKF="${OPENBAO_SSH_TOKEN_FILE:-$HOME/.config/openbao/ssh-sign.token}"
ROLE="${OPENBAO_SSH_ROLE:-user}"
KEY="${OPENBAO_SSH_KEY:-$HOME/.ssh/id_ed25519}"
CERT="${KEY}-cert.pub"
TTL="${OPENBAO_SSH_TTL:-8h}"
sign_only=0
[ "${1:-}" = "--sign-only" ] && { sign_only=1; shift; }
target="${1:?usage: ssh-login [--sign-only] [user@]host [ssh args]}"; shift || true
if [[ "$target" == *@* ]]; then principal="${target%@*}"; host="${target#*@}"
else principal="${OPENBAO_SSH_USER:-lutz}"; host="$target"; fi
[ -r "$TOKF" ] || { echo "ssh-login: no OpenBAO token at $TOKF" >&2; exit 1; }
[ -r "${KEY}.pub" ] || { echo "ssh-login: no public key at ${KEY}.pub" >&2; exit 1; }
req="$(PUB="$(cat "${KEY}.pub")" PRIN="$principal" TTL="$TTL" python3 -c \
'import json,os;print(json.dumps({"public_key":os.environ["PUB"],"valid_principals":os.environ["PRIN"],"ttl":os.environ["TTL"]}))')"
resp="$(printf '%s' "$req" | curl -sS --max-time 10 -H "X-Vault-Token: $(cat "$TOKF")" \
--data @- "${ADDR}/v1/ssh/sign/${ROLE}")"
signed="$(printf '%s' "$resp" | jq -r '.data.signed_key // empty')"
[ -n "$signed" ] || { echo "ssh-login: signing failed: $(printf '%s' "$resp" | jq -c '.errors // .')" >&2; exit 1; }
printf '%s' "$signed" > "$CERT"; chmod 644 "$CERT"
exp="$(ssh-keygen -L -f "$CERT" 2>/dev/null | awk '/Valid:/{ $1=""; print }')"
echo "ssh-login: signed cert for '${principal}' (valid:${exp})" >&2
[ "$sign_only" = 1 ] && exit 0
exec ssh -i "$KEY" -o CertificateFile="$CERT" "${principal}@${host}" "$@"