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>
108 lines
5.2 KiB
Bash
Executable File
108 lines
5.2 KiB
Bash
Executable File
#!/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
|