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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ config/seal.hcl
|
||||
config/tls/
|
||||
data/
|
||||
.env
|
||||
artifacts/
|
||||
|
||||
38
README.md
38
README.md
@@ -167,6 +167,44 @@ sudo systemctl list-timers openbao-cert-renew.timer # next run
|
||||
sudo /home/lutz/Projects/OpenBAO/scripts/renew-openbao-cert.sh --force # renew now
|
||||
```
|
||||
|
||||
## SSH access via OpenBAO (signed certificates)
|
||||
|
||||
The `ssh/` engine is an SSH CA (RSA-4096; pubkey in
|
||||
[ca/openbao-ssh-ca.pub](ca/openbao-ssh-ca.pub)) with two roles:
|
||||
|
||||
| Role | Signs | Used for |
|
||||
|--------|------------------|----------|
|
||||
| `user` | user certs (8h) | logging into hosts as a principal (e.g. `lutz`) — no `authorized_keys` |
|
||||
| `host` | host certs (3y) | clients verify host identity — no "unknown host" prompts |
|
||||
|
||||
**Log in** (signs a fresh 8h cert, then connects):
|
||||
|
||||
```bash
|
||||
scripts/ssh-login.sh 192.168.0.26 # user lutz by default
|
||||
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):
|
||||
|
||||
```
|
||||
TrustedUserCAKeys /etc/ssh/openbao_user_ca.pub # trust user certs
|
||||
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub # present host cert
|
||||
```
|
||||
|
||||
**Client trust** for host verification — one line in `~/.ssh/known_hosts`:
|
||||
|
||||
```
|
||||
@cert-authority <host-or-pattern> <contents of ca/openbao-ssh-ca.pub>
|
||||
```
|
||||
|
||||
Currently wired: **192.168.0.26** (`pifour`) — user login as `lutz` + host
|
||||
verification, both confirmed.
|
||||
|
||||
## Git credential via OpenBAO
|
||||
|
||||
The push credential for the gitea remote is stored in OpenBAO KV
|
||||
|
||||
1
ca/openbao-ssh-ca.pub
Normal file
1
ca/openbao-ssh-ca.pub
Normal file
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTPUjjBap8y5vvfEhTnEbQgYIw7CKXl2dMSS/2IE0+CC3uMuAhfScfkxMOC00GoK0rBwaJTkyyDIAL1XrAQmUB098WfVQP01KKj/n924dAZkNIRy0X6DKd5V0G1eY3M+kPK61IvaH81i3oexdmVMS9ax9E+kLRnxNK0hfSbZrIkTdMp7jCpidoADo4gVKvqucIMSqSKOZduJYQj2WC1cNxIy2DND+ZyXlSlsavOSeZlIswiwIPiPmGbF2QxWwvRMk5NfQRed38eYN+YUJYIUm7gq0UEUq8vhT3+1pKbyyFXnNN5yaI90L8bSdML/H5039lfQcu+MsstUaOLmWcKD1D9EVYzyr2HX/am2oMOVWlefbLwsNXaHpECleGfGFAcOmnIo13RI8gCCDlAVNorGgwEFjgk8RZAW6Om+9d8ae1AhAwzbIx0GR5qi14A0dGua1zyL8HyTRiei5Qz6XZNLX9roHFd3AUfOBdFIXG3TdPi4wAaBCIXuIkg9uBcv63AdAOmUFeMJYlv/9xreZlN/lPJaJbMlBCowMbhqPRFYk02kt7dr+9EGtNhAtEp2PReW6Vca4osUPYmsSovgwZmavWWD3GUg6KY06dxypZIWHtUJIgq42RZ1TGrnvkq9q3gy28k/WcaXD5xJ8vZDAl1oYNVHcSwWu89zf3tMlcgj1BDQ==
|
||||
41
scripts/ssh-login.sh
Executable file
41
scripts/ssh-login.sh
Executable 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}" "$@"
|
||||
Reference in New Issue
Block a user