#!/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 [--user lutz] [--hostname NAME] [--no-client-trust] # # 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 [--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" < /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 <