Add prepare-host.sh: one-command SSH host onboarding
Generalize the .26 rollout into a reusable template: - authorize the host principal on ssh/roles/host (allowlist, merged) - sign the host certificate (ssh/sign/host) - emit a self-contained installer (user-CA TrustedUserCAKeys + HostCertificate) to artifacts/, additive and lockout-safe - add @cert-authority for the host to the client's known_hosts Needs no SSH access to the target (ssh-keyscan). README updated. Also gitignore .claude/ (local harness settings). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,3 +9,4 @@ config/tls/
|
||||
data/
|
||||
.env
|
||||
artifacts/
|
||||
.claude/
|
||||
|
||||
22
README.md
22
README.md
@@ -187,9 +187,25 @@ scripts/ssh-login.sh --sign-only HOST # just refresh the cert
|
||||
Auth to OpenBAO uses a scoped `ssh-sign-user` periodic token at
|
||||
`~/.config/openbao/ssh-sign.token` (can only call `ssh/sign/user`).
|
||||
|
||||
**Onboard a new host** (run on that host, as root): install the user CA +
|
||||
its signed host cert and point sshd at them. `scripts/` generates a ready
|
||||
self-contained installer per host — the pattern (additive, no lockout):
|
||||
**Onboard a new host** — one command generates everything:
|
||||
|
||||
```bash
|
||||
scripts/prepare-host.sh 192.168.0.30 # IP you'll SSH to
|
||||
scripts/prepare-host.sh 192.168.0.30 --hostname pi.famfi.home # extra principal
|
||||
```
|
||||
|
||||
It authorizes the host's principal on `ssh/roles/host`, signs its host cert,
|
||||
writes a self-contained installer to `artifacts/openbao-ssh-setup-on-<host>.sh`,
|
||||
and adds `@cert-authority` for it to your `~/.ssh/known_hosts`. Needs no SSH
|
||||
access to the target (uses `ssh-keyscan`). Then finish on the target (as root):
|
||||
|
||||
```bash
|
||||
scp artifacts/openbao-ssh-setup-on-<host>.sh <user>@<host>:/tmp/
|
||||
ssh <user>@<host> 'sudo bash /tmp/openbao-ssh-setup-on-<host>.sh' # additive, no lockout
|
||||
scripts/ssh-login.sh <host> # sign a user cert + connect
|
||||
```
|
||||
|
||||
The installer just adds these sshd directives (additive):
|
||||
|
||||
```
|
||||
TrustedUserCAKeys /etc/ssh/openbao_user_ca.pub # trust user certs
|
||||
|
||||
107
scripts/prepare-host.sh
Executable file
107
scripts/prepare-host.sh
Executable file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# Onboard a host to OpenBAO SSH access in one command:
|
||||
# * authorize the host's principal on the ssh/roles/host role (allowlist)
|
||||
# * sign its SSH host certificate (ssh/sign/host)
|
||||
# * generate a self-contained installer to run on the target (user-CA trust
|
||||
# via TrustedUserCAKeys + the HostCertificate) — additive, no lockout risk
|
||||
# * trust the host on THIS client (@cert-authority in ~/.ssh/known_hosts)
|
||||
#
|
||||
# Needs NO SSH access to the target (uses ssh-keyscan). Auth: $BAO_TOKEN or the
|
||||
# root token in init-output.json.
|
||||
#
|
||||
# prepare-host.sh <host> [--user lutz] [--hostname NAME] [--no-client-trust]
|
||||
#
|
||||
# <host> is the address you will SSH to (IP or name); it becomes the host
|
||||
# cert's principal and the client @cert-authority match. --hostname adds an
|
||||
# extra principal (e.g. sign both the IP and a DNS name).
|
||||
set -euo pipefail
|
||||
SELF="$(cd "$(dirname "$0")" && pwd)"; ROOT_DIR="$(cd "$SELF/.." && pwd)"
|
||||
INIT="$ROOT_DIR/init-output.json"; ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
|
||||
CAFILE="$ROOT_DIR/ca/openbao-ssh-ca.pub"; ARTDIR="$ROOT_DIR/artifacts"
|
||||
|
||||
HOST=""; LOGIN_USER="lutz"; HOSTNAME_EXTRA=""; CLIENT_TRUST=1
|
||||
while [ $# -gt 0 ]; do case "$1" in
|
||||
--user) LOGIN_USER="$2"; shift 2;;
|
||||
--hostname) HOSTNAME_EXTRA="$2"; shift 2;;
|
||||
--no-client-trust) CLIENT_TRUST=0; shift;;
|
||||
-*) echo "unknown option: $1" >&2; exit 1;;
|
||||
*) HOST="$1"; shift;;
|
||||
esac; done
|
||||
[ -n "$HOST" ] || { echo "usage: prepare-host.sh <host> [--user U] [--hostname NAME] [--no-client-trust]" >&2; exit 1; }
|
||||
|
||||
TOKEN="${BAO_TOKEN:-}"; [ -z "$TOKEN" ] && [ -r "$INIT" ] && TOKEN="$(python3 -c "import json;print(json.load(open('$INIT'))['root_token'])")"
|
||||
[ -n "$TOKEN" ] || { echo "no token (set BAO_TOKEN)" >&2; exit 1; }
|
||||
api(){ curl -sS -H "X-Vault-Token: $TOKEN" "$@"; }
|
||||
|
||||
PRINCIPALS="$HOST"; [ -n "$HOSTNAME_EXTRA" ] && PRINCIPALS="$HOST,$HOSTNAME_EXTRA"
|
||||
|
||||
echo "[1/5] authorize principal(s) '$PRINCIPALS' on ssh/roles/host (allowlist)"
|
||||
cur="$(api "$ADDR/v1/ssh/roles/host" | python3 -c "import sys,json;print(json.load(sys.stdin)['data'].get('allowed_domains',''))" 2>/dev/null || true)"
|
||||
merged="$(CUR="$cur" ADD="$PRINCIPALS" python3 -c '
|
||||
import os
|
||||
seen=[]
|
||||
for x in (os.environ["CUR"].split(",")+os.environ["ADD"].split(",")):
|
||||
x=x.strip()
|
||||
if x and x not in seen: seen.append(x)
|
||||
print(",".join(seen))')"
|
||||
body="$(M="$merged" python3 -c 'import json,os;print(json.dumps({"key_type":"ca","allow_host_certificates":True,"allow_user_certificates":False,"allowed_domains":os.environ["M"],"allow_bare_domains":True,"allow_subdomains":True,"ttl":"26280h"}))')"
|
||||
api --data "$body" "$ADDR/v1/ssh/roles/host" -o /dev/null -w " role updated: %{http_code} (allowed: $merged)\n"
|
||||
|
||||
echo "[2/5] scan ed25519 host key of $HOST (no SSH login needed)"
|
||||
HK="$(ssh-keyscan -t ed25519 "$HOST" 2>/dev/null | grep -v '^#' | awk '{print $2" "$3}' | head -1)"
|
||||
[ -n "$HK" ] || { echo " FAILED to scan host key from $HOST (reachable? sshd up?)" >&2; exit 1; }
|
||||
|
||||
echo "[3/5] sign host certificate (principals: $PRINCIPALS)"
|
||||
req="$(HK="$HK" PR="$PRINCIPALS" python3 -c 'import json,os;print(json.dumps({"public_key":os.environ["HK"],"cert_type":"host","valid_principals":os.environ["PR"],"ttl":"26280h"}))')"
|
||||
HOSTCERT="$(printf '%s' "$req" | api --data @- "$ADDR/v1/ssh/sign/host" | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['signed_key'])")"
|
||||
[ -n "$HOSTCERT" ] || { echo " signing failed" >&2; exit 1; }
|
||||
|
||||
echo "[4/5] generate installer"
|
||||
CAPUB="$(cat "$CAFILE" 2>/dev/null || api "$ADDR/v1/ssh/config/ca" | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['public_key'])")"
|
||||
mkdir -p "$ARTDIR"; INSTALLER="$ARTDIR/openbao-ssh-setup-on-$HOST.sh"
|
||||
cat > "$INSTALLER" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
# Run on $HOST as root (sudo). Additive — keeps existing auth, won't lock you out.
|
||||
set -euo pipefail
|
||||
[ "\$(id -u)" = 0 ] || { echo "run with sudo"; exit 1; }
|
||||
install -m 0644 /dev/stdin /etc/ssh/openbao_user_ca.pub <<'CA'
|
||||
$CAPUB
|
||||
CA
|
||||
install -m 0644 /dev/stdin /etc/ssh/ssh_host_ed25519_key-cert.pub <<'HC'
|
||||
$HOSTCERT
|
||||
HC
|
||||
read -r -d '' D <<'DIR' || true
|
||||
# OpenBAO SSH CA integration
|
||||
TrustedUserCAKeys /etc/ssh/openbao_user_ca.pub
|
||||
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
|
||||
DIR
|
||||
if grep -qE '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d' /etc/ssh/sshd_config; then
|
||||
printf '%s\n' "\$D" > /etc/ssh/sshd_config.d/10-openbao-ca.conf
|
||||
else
|
||||
grep -q 'OpenBAO SSH CA integration' /etc/ssh/sshd_config || printf '\n%s\n' "\$D" >> /etc/ssh/sshd_config
|
||||
fi
|
||||
sshd -t
|
||||
systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null || service ssh reload
|
||||
echo "OK: OpenBAO SSH CA + host cert installed on \$(hostname)"
|
||||
EOF
|
||||
chmod +x "$INSTALLER"
|
||||
echo " -> $INSTALLER"
|
||||
|
||||
echo "[5/5] client trust (@cert-authority for $HOST)"
|
||||
if [ "$CLIENT_TRUST" = 1 ]; then
|
||||
ssh-keygen -R "$HOST" >/dev/null 2>&1 || true
|
||||
KH="$HOME/.ssh/known_hosts"; touch "$KH"
|
||||
grep -qF "@cert-authority $HOST " "$KH" || printf '@cert-authority %s %s\n' "$HOST" "$CAPUB" >> "$KH"
|
||||
echo " ~/.ssh/known_hosts updated"
|
||||
else
|
||||
echo " skipped (--no-client-trust)"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done. To finish on the target host:
|
||||
scp $INSTALLER ${LOGIN_USER}@${HOST}:/tmp/
|
||||
ssh ${LOGIN_USER}@${HOST} 'sudo bash /tmp/$(basename "$INSTALLER")'
|
||||
Then log in with a signed cert:
|
||||
scripts/ssh-login.sh ${HOST}
|
||||
EOF
|
||||
Reference in New Issue
Block a user