feat(openbao): add SSH engine automation for ops-warden signing

Declarative roles, warden-sign policy, apply/verify scripts, and Makefile
targets openbao-configure-ssh and openbao-verify-ssh. Document operator flow
in docs/openbao.md for NET-WP-0020 T5 / WP-0008 T2.
This commit is contained in:
tegwick 2026-06-18 01:06:43 +02:00
parent 108944cd3e
commit c24956fb5a
6 changed files with 521 additions and 7 deletions

View file

@ -13,6 +13,7 @@ OPENBAO_CHART_VERSION ?= 0.28.2
OPENBAO_NAMESPACE ?= openbao OPENBAO_NAMESPACE ?= openbao
OPENBAO_RELEASE ?= openbao OPENBAO_RELEASE ?= openbao
OPENBAO_VALUES ?= helm/openbao-values.yaml OPENBAO_VALUES ?= helm/openbao-values.yaml
OPENBAO_MIDDLEWARE ?= helm/openbao-middleware.yaml
OPENBAO_VERIFY_AUTH_ARGS ?= OPENBAO_VERIFY_AUTH_ARGS ?=
OPENBAO_RESTORE_EVIDENCE ?= /tmp/netkingdom-openbao-restore-drill/evidence.json OPENBAO_RESTORE_EVIDENCE ?= /tmp/netkingdom-openbao-restore-drill/evidence.json
OPENBAO_EMERGENCY_EVIDENCE ?= /tmp/netkingdom-openbao-emergency-drill/evidence.json OPENBAO_EMERGENCY_EVIDENCE ?= /tmp/netkingdom-openbao-emergency-drill/evidence.json
@ -104,6 +105,7 @@ openbao-dry-run: openbao-repo ## Render the OpenBao Helm release without applyin
openbao-deploy: openbao-repo ## Deploy / upgrade OpenBao to the openbao namespace openbao-deploy: openbao-repo ## Deploy / upgrade OpenBao to the openbao namespace
$(KUBECTL) create namespace $(OPENBAO_NAMESPACE) --dry-run=client -o yaml | $(KUBECTL) apply -f - $(KUBECTL) create namespace $(OPENBAO_NAMESPACE) --dry-run=client -o yaml | $(KUBECTL) apply -f -
$(KUBECTL) apply -f $(OPENBAO_MIDDLEWARE)
$(HELM) upgrade --install $(OPENBAO_RELEASE) openbao/openbao \ $(HELM) upgrade --install $(OPENBAO_RELEASE) openbao/openbao \
--version $(OPENBAO_CHART_VERSION) \ --version $(OPENBAO_CHART_VERSION) \
--namespace $(OPENBAO_NAMESPACE) \ --namespace $(OPENBAO_NAMESPACE) \
@ -127,6 +129,14 @@ openbao-configure-initial: ## Apply first post-unseal audit, auth, mounts, and p
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-apply-initial-config.sh OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-apply-initial-config.sh
openbao-configure-ssh: ## Enable SSH secrets engine, roles, and warden-sign policy
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-apply-ssh-engine.sh
openbao-verify-ssh: ## Verify SSH engine mount, roles, and warden-sign policy
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-verify-ssh-engine.sh
openbao-verify-authenticated: ## Run authenticated non-mutating OpenBao audit/auth/mount checks openbao-verify-authenticated: ## Run authenticated non-mutating OpenBao audit/auth/mount checks
KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \
OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-verify-authenticated.sh $(OPENBAO_VERIFY_AUTH_ARGS) OPENBAO_RELEASE=$(OPENBAO_RELEASE) scripts/openbao-verify-authenticated.sh $(OPENBAO_VERIFY_AUTH_ARGS)
@ -151,4 +161,4 @@ help: ## Show this help
/^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \ /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
.PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-verify-authenticated openbao-validate-restore-evidence openbao-validate-emergency-evidence backup help .PHONY: db-deploy db-status db-shell db-logs apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-validate-restore-evidence openbao-validate-emergency-evidence backup help

View file

@ -18,15 +18,26 @@ S5 workloads / operators
-> openbao-0 -> openbao-0
-> integrated Raft storage on local-path PVC -> integrated Raft storage on local-path PVC
-> audit storage PVC mounted at /openbao/audit -> audit storage PVC mounted at /openbao/audit
Platform operators with approved admin identity
-> https://bao.coulomb.social
-> Traefik Ingress + TLS
-> openbao-ui service
-> OpenBao UI/API
-> KeyCape OIDC at https://kc.coulomb.social for login
``` ```
- OpenBao is the canonical Railiance S3 secrets service. - OpenBao is the canonical Railiance S3 secrets service.
- SOPS/age remains the Git-at-rest bootstrap mechanism. - SOPS/age remains the Git-at-rest bootstrap mechanism.
- The first Railiance01 deployment is single-replica Raft, not true HA. - The first Railiance01 deployment is single-replica Raft, not true HA.
- Public ingress is disabled. Operators use `kubectl exec` or port-forwarding. - Browser UI/API exposure is declared for `https://bao.coulomb.social`.
Operators authenticate through KeyCape/OIDC with MFA and the
`platform-admin` role. Do not use the root token through the browser UI.
- `kubectl exec` and port-forwarding remain valid break-glass/operator paths
for maintenance and non-browser verification.
- TLS is disabled inside the pod listener for this internal-only bootstrap. Add - TLS is disabled inside the pod listener for this internal-only bootstrap. Add
cert-manager-backed internal TLS before exposing OpenBao beyond cluster-local cert-manager-backed internal TLS before relying on cluster-internal traffic
traffic. from untrusted namespaces.
## Deployment ## Deployment
@ -41,6 +52,10 @@ make openbao-deploy
make openbao-status make openbao-status
``` ```
`make openbao-deploy` also applies `helm/openbao-middleware.yaml`, which
defines the Traefik rate-limit and HSTS middlewares referenced by the OpenBao
Ingress.
On Railiance01 directly: On Railiance01 directly:
```bash ```bash
@ -51,10 +66,13 @@ sudo env KUBECONFIG=/etc/rancher/k3s/k3s.yaml make openbao-status
``` ```
If the repo is not present on Railiance01 yet, copy only the non-secret values If the repo is not present on Railiance01 yet, copy only the non-secret values
file and run Helm directly: and middleware files, then run Helm directly:
```bash ```bash
scp helm/openbao-values.yaml tegwick@92.205.62.239:/tmp/openbao-values.yaml scp helm/openbao-values.yaml tegwick@92.205.62.239:/tmp/openbao-values.yaml
scp helm/openbao-middleware.yaml tegwick@92.205.62.239:/tmp/openbao-middleware.yaml
ssh tegwick@92.205.62.239 \
'sudo env KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl apply -f /tmp/openbao-middleware.yaml'
ssh tegwick@92.205.62.239 \ ssh tegwick@92.205.62.239 \
'sudo env KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm upgrade --install openbao openbao/openbao \ 'sudo env KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm upgrade --install openbao openbao/openbao \
--version 0.28.2 \ --version 0.28.2 \
@ -78,6 +96,8 @@ Expected immediately after install:
- `openbao-0` is Running. - `openbao-0` is Running.
- `openbao`, `openbao-active`, `openbao-internal`, and `openbao-ui` services - `openbao`, `openbao-active`, `openbao-internal`, and `openbao-ui` services
exist as cluster-internal services. exist as cluster-internal services.
- After DNS points at the cluster ingress, `https://bao.coulomb.social` serves
the OpenBao UI over valid TLS.
- data and audit PVCs are Bound. - data and audit PVCs are Bound.
- `bao status` reports `Initialized: false` and `Sealed: true`. - `bao status` reports `Initialized: false` and `Sealed: true`.
@ -213,6 +233,50 @@ Store that token through the approved operator secret path, then revoke or
tightly escrow the initial root token. The root token should not become the tightly escrow the initial root token. The root token should not become the
normal operator credential. normal operator credential.
## SSH Secrets Engine (ops-warden)
After `openbao-configure-initial`, enable the SSH user CA used by `ops-warden`
(`warden sign` via `backend: vault`). This is **NET-WP-0020 T5** / **WP-0008 T2**
prerequisite.
Declarative artifacts:
- `openbao/ssh/roles-spec.yaml``adm-role`, `agt-role`, `atm-role` TTLs
- `openbao/policies/warden-sign.hcl` — least-privilege signing policy
- `scripts/openbao-apply-ssh-engine.sh` — idempotent apply via `kubectl exec`
- `scripts/openbao-verify-ssh-engine.sh` — non-mutating verification
Apply (requires `platform-admin` or equivalent token with `ssh/*` admin):
```bash
mkdir -p ~/.local/openbao
# Store platform-admin token locally (mode 600, never commit):
# echo '<token>' > ~/.local/openbao/platform-admin.token && chmod 600 ~/.local/openbao/platform-admin.token
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token OPENBAO_SSH_CA_PUBKEY_OUT=/tmp/openbao-ssh-ca.pub make openbao-configure-ssh
OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token make openbao-verify-ssh
```
The apply script exports the CA public key to `OPENBAO_SSH_CA_PUBKEY_OUT` and
updates K8s secret `openbao/openbao-ssh-ca-pub` (non-secret pubkey only).
Create a dedicated warden signing token (do not use platform-admin daily):
```bash
kubectl exec -n openbao openbao-0 -- bao token create -policy=warden-sign -period=8h -orphan
```
Host trust and principals are **railiance-infra** scope:
```bash
cd ~/railiance-infra
make bootstrap-ssh-ca SSH_CA_PUBKEY=/tmp/openbao-ssh-ca.pub
```
Then on the workstation: `bao login` (or export `VAULT_TOKEN` from the
`warden-sign` token) and run `warden sign` per `ops-warden/wiki/OpenBaoSshEngineChecklist.md`.
## Auth And Workload Integration ## Auth And Workload Integration
Initial auth model: Initial auth model:
@ -228,6 +292,56 @@ Initial auth model:
| Human identity | NetKingdom IAM Profile/OIDC | target model; OpenBao is not the identity provider | | Human identity | NetKingdom IAM Profile/OIDC | target model; OpenBao is not the identity provider |
| Automation | Kubernetes auth or short-lived operator token | no root tokens in automation | | Automation | Kubernetes auth or short-lived operator token | no root tokens in automation |
### Browser UI Login
The browser operator surface is:
```text
https://bao.coulomb.social
```
Use the KeyCape-backed auth method:
```text
method: OIDC
namespace: leave blank
mount path: netkingdom
role: platform-admin
```
The OpenBao UI redirects the browser to KeyCape at `kc.coulomb.social`, then
returns to:
```text
https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback
```
The legacy `keycape` mount remains a compatibility alias for existing
operator notes and CLI experiments. The preferred browser mount is
`netkingdom`.
The browser callback URI must be present in both:
- KeyCape `openbao-admin` client redirect URIs; and
- OpenBao `auth/netkingdom/role/platform-admin` `allowed_redirect_uris`.
If the compatibility alias is kept enabled, also keep
`https://bao.coulomb.social/ui/vault/auth/keycape/oidc/callback` in the
KeyCape client and `auth/keycape/role/platform-admin`.
Use the browser UI for metadata inspection and attended operator workflows.
Do not use the OpenBao root token through the browser UI. Do not copy secret
values, Inter-Hub keys, unseal shares, root tokens, OIDC client secrets, or
screenshots of secret values into Git, State Hub, chat, or workplans.
For `HF-WP-0001`, prefer metadata-only inspection of candidate paths such as:
```text
platform/
platform/operators/
platform/operators/inter-hub/
```
Workload delivery choice: Workload delivery choice:
- Prefer External Secrets Operator for values that should become Kubernetes - Prefer External Secrets Operator for values that should become Kubernetes
@ -299,8 +413,8 @@ make openbao-verify-authenticated
The target prompts for the token without echoing it, never puts the token on The target prompts for the token without echoing it, never puts the token on
the command line, and only runs non-mutating checks. It verifies that the command line, and only runs non-mutating checks. It verifies that
`bao audit list` shows `file/`, `bao secrets list` shows `platform/`, `bao audit list` shows `file/`, `bao secrets list` shows `platform/`,
`bao auth list` shows both `kubernetes/` and `keycape/`, and that the file `bao auth list` shows `kubernetes/`, `netkingdom/`, and `keycape/`, and that
audit log is non-empty. the file audit log is non-empty.
If a previous attended OIDC login stored a still-valid token in the pod token If a previous attended OIDC login stored a still-valid token in the pod token
helper, use: helper, use:

View file

@ -0,0 +1,18 @@
# Narrow policy for ops-warden SSH signing (Vault/OpenBao SSH secrets engine).
# Bind to dedicated tokens or Kubernetes auth roles not platform-admin.
path "ssh/sign/adm-role" {
capabilities = ["create", "update"]
}
path "ssh/sign/agt-role" {
capabilities = ["create", "update"]
}
path "ssh/sign/atm-role" {
capabilities = ["create", "update"]
}
path "ssh/roles" {
capabilities = ["list", "read"]
}

View file

@ -0,0 +1,27 @@
# Declarative SSH CA roles for ops-warden ActorType policy.
# TTL max: adm 48h, agt 24h, atm 8h — wiki/OpsWardenConfig.md (ops-warden)
mount: ssh
roles:
adm-role:
key_type: ca
allowed_users: "*"
allow_user_certificates: true
default_user: adm
ttl: 48h
max_ttl: 48h
agt-role:
key_type: ca
allowed_users: "*"
allow_user_certificates: true
default_user: agt
ttl: 24h
max_ttl: 24h
atm-role:
key_type: ca
allowed_users: "*"
allow_user_certificates: true
default_user: atm
ttl: 8h
max_ttl: 8h

View file

@ -0,0 +1,234 @@
#!/usr/bin/env bash
set -euo pipefail
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}"
OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}"
KUBECTL="${KUBECTL:-kubectl}"
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}"
SSH_MOUNT="${OPENBAO_SSH_MOUNT:-ssh}"
ROLES_SPEC="${ROLES_SPEC:-}"
POLICY_DIR="${POLICY_DIR:-}"
CA_PUBKEY_OUT="${OPENBAO_SSH_CA_PUBKEY_OUT:-}"
K8S_CA_SECRET="${OPENBAO_SSH_CA_K8S_SECRET:-openbao-ssh-ca-pub}"
DRY_RUN=0
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ROLES_SPEC="${ROLES_SPEC:-$REPO_DIR/openbao/ssh/roles-spec.yaml}"
POLICY_DIR="${POLICY_DIR:-$REPO_DIR/openbao/policies}"
usage() {
cat <<'USAGE'
Usage: scripts/openbao-apply-ssh-engine.sh [--dry-run]
Applies the OpenBao SSH secrets engine for ops-warden signing:
- enable ssh/ mount (idempotent)
- write adm/agt/atm roles from openbao/ssh/roles-spec.yaml
- load warden-sign policy
- export CA public key (optional K8s secret + local file)
Requires initialized, unsealed OpenBao and a token with ssh engine admin
(typically platform-admin). Token from OPENBAO_TOKEN_FILE or prompt.
Env:
OPENBAO_SSH_CA_PUBKEY_OUT Write CA pubkey to this path (non-secret)
OPENBAO_SSH_CA_K8S_SECRET K8s secret name in openbao namespace (default: openbao-ssh-ca-pub)
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "ERROR: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
pod="${OPENBAO_RELEASE}-0"
WARNINGS=0
warn() {
WARNINGS=$((WARNINGS + 1))
printf 'WARN: %s\n' "$*" >&2
}
read_token() {
if [ "$DRY_RUN" -eq 1 ]; then
printf 'dry-run-token\n'
return
fi
if [ -n "$TOKEN_FILE" ]; then
if [ ! -f "$TOKEN_FILE" ]; then
echo "ERROR: OPENBAO_TOKEN_FILE does not exist: $TOKEN_FILE" >&2
exit 1
fi
head -n 1 "$TOKEN_FILE"
return
fi
local token
read -r -s -p "OpenBao token: " token
printf '\n' >&2
printf '%s\n' "$token"
}
remote_bao() {
local token="$1"
shift
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao %s\n' "$*"
return 0
fi
printf '%s\n' "$token" | $KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"' sh "$@"
}
write_policy() {
local token="$1"
local name="$2"
local file="$3"
if [ ! -f "$file" ]; then
echo "ERROR: missing policy file: $file" >&2
exit 1
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao policy write %s %s\n' "$name" "$file"
return 0
fi
{ printf '%s\n' "$token"; cat "$file"; } | $KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao policy write "$1" -' sh "$name"
}
enable_ssh_engine() {
local token="$1"
local output status
if output="$(remote_bao "$token" secrets enable -path="$SSH_MOUNT" ssh 2>&1)"; then
printf '%s\n' "$output"
return 0
fi
status=$?
case "$output" in
*"path is already in use"*)
printf 'OK: %s/ SSH secrets engine is already enabled.\n' "$SSH_MOUNT"
return 0
;;
*)
printf '%s\n' "$output" >&2
echo "ERROR: failed to enable SSH engine (exit $status)" >&2
exit 1
;;
esac
}
apply_roles() {
local token="$1"
if [ ! -f "$ROLES_SPEC" ]; then
echo "ERROR: roles spec not found: $ROLES_SPEC" >&2
exit 1
fi
if [ "$DRY_RUN" -eq 1 ]; then
python3 - "$ROLES_SPEC" "$SSH_MOUNT" <<'PY'
import sys, yaml
spec = yaml.safe_load(open(sys.argv[1]))
mount = sys.argv[2]
for name, params in (spec.get("roles") or {}).items():
args = " ".join(f"{k}={v}" for k, v in params.items())
print(f"DRY-RUN: bao write {mount}/roles/{name} {args}")
PY
return 0
fi
python3 - "$token" "$ROLES_SPEC" "$SSH_MOUNT" "$OPENBAO_NAMESPACE" "$pod" "$KUBECTL" <<'PY'
import subprocess
import sys
token, spec_path, mount, namespace, pod, kubectl = sys.argv[1:7]
import yaml
spec = yaml.safe_load(open(spec_path))
roles = spec.get("roles") or {}
for role_name, params in roles.items():
args = [f"{k}={v}" for k, v in params.items()]
cmd = ["bao", "write", f"{mount}/roles/{role_name}"] + args
proc = subprocess.run(
[kubectl, "exec", "-i", "-n", namespace, pod, "--", "sh", "-c",
"read -r BAO_TOKEN; export BAO_TOKEN; exec bao \"$@\"", "sh"] + cmd,
input=(token + "\n").encode(),
capture_output=True,
)
if proc.returncode != 0:
sys.stderr.write(proc.stderr.decode())
raise SystemExit(f"failed to write role {role_name}")
print(f"OK: {mount}/roles/{role_name}")
PY
}
export_ca_pubkey() {
local token="$1"
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao read -field=public_key %s/public_key\n' "$SSH_MOUNT"
return 0
fi
local pubkey
pubkey="$(remote_bao "$token" read -field=public_key "${SSH_MOUNT}/public_key")"
if [ -z "$pubkey" ]; then
warn "Could not read SSH CA public key from ${SSH_MOUNT}/public_key"
return 0
fi
local fingerprint
fingerprint="$(printf '%s' "$pubkey" | sha256sum | awk '{print $1}')"
printf 'OK: SSH CA public key fingerprint sha256:%s\n' "$fingerprint"
if [ -n "$CA_PUBKEY_OUT" ]; then
mkdir -p "$(dirname "$CA_PUBKEY_OUT")"
printf '%s\n' "$pubkey" >"$CA_PUBKEY_OUT"
chmod 644 "$CA_PUBKEY_OUT"
printf 'OK: wrote CA pubkey to %s\n' "$CA_PUBKEY_OUT"
fi
if [ -n "$K8S_CA_SECRET" ]; then
local tmp
tmp="$(mktemp)"
printf '%s\n' "$pubkey" >"$tmp"
$KUBECTL create secret generic "$K8S_CA_SECRET" \
--namespace "$OPENBAO_NAMESPACE" \
--from-file=ca_user.pub="$tmp" \
--dry-run=client -o yaml | $KUBECTL apply -f -
rm -f "$tmp"
printf 'OK: K8s secret %s/%s updated\n' "$OPENBAO_NAMESPACE" "$K8S_CA_SECRET"
fi
}
token="$(read_token)"
if [ -z "$token" ]; then
echo "ERROR: empty token" >&2
exit 1
fi
remote_bao "$token" status
enable_ssh_engine "$token"
apply_roles "$token"
write_policy "$token" warden-sign "$POLICY_DIR/warden-sign.hcl"
remote_bao "$token" list "${SSH_MOUNT}/roles"
export_ca_pubkey "$token"
cat <<NEXT
OpenBao SSH engine configuration applied.
Next steps:
1. make openbao-verify-ssh
2. railiance-infra: make bootstrap-ssh-ca SSH_CA_PUBKEY=<path>
3. ops-warden: warden sign smoke (WP-0008 T2)
NEXT
if [ "$WARNINGS" -gt 0 ]; then
printf '\nCompleted with %s warning(s).\n' "$WARNINGS"
fi

View file

@ -0,0 +1,111 @@
#!/usr/bin/env bash
set -euo pipefail
OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}"
OPENBAO_RELEASE="${OPENBAO_RELEASE:-openbao}"
KUBECTL="${KUBECTL:-kubectl}"
TOKEN_FILE="${OPENBAO_TOKEN_FILE:-}"
SSH_MOUNT="${OPENBAO_SSH_MOUNT:-ssh}"
EXPECTED_ROLES="${OPENBAO_SSH_EXPECTED_ROLES:-adm-role agt-role atm-role}"
USE_TOKEN_HELPER=0
DRY_RUN=0
usage() {
cat <<'USAGE'
Usage: scripts/openbao-verify-ssh-engine.sh [--dry-run] [--use-token-helper]
Non-mutating checks: ssh/ mount present, expected roles listed, warden-sign policy.
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--dry-run) DRY_RUN=1; shift ;;
--use-token-helper) USE_TOKEN_HELPER=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
pod="${OPENBAO_RELEASE}-0"
FAILURES=0
fail() { FAILURES=$((FAILURES + 1)); printf '[FAIL] %s\n' "$*" >&2; }
ok() { printf '[OK] %s\n' "$*"; }
read_token() {
if [ "$USE_TOKEN_HELPER" -eq 1 ]; then
printf '__USE_TOKEN_HELPER__\n'
return
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf 'dry-run-token\n'
return
fi
if [ -n "$TOKEN_FILE" ] && [ -f "$TOKEN_FILE" ]; then
head -n 1 "$TOKEN_FILE"
return
fi
local token
read -r -s -p "OpenBao token: " token
printf '\n' >&2
printf '%s\n' "$token"
}
remote_bao() {
local token="$1"
shift
if [ "$token" = "__USE_TOKEN_HELPER__" ]; then
$KUBECTL exec -n "$OPENBAO_NAMESPACE" "$pod" -- bao "$@"
return
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao %s\n' "$*"
return 0
fi
printf '%s\n' "$token" | $KUBECTL exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; exec bao "$@"' sh "$@"
}
token="$(read_token)"
secrets_out="$(remote_bao "$token" secrets list 2>&1)" || {
fail "secrets list failed: $secrets_out"
exit 1
}
if printf '%s\n' "$secrets_out" | grep -Eq "(^|[[:space:]])${SSH_MOUNT}/"; then
ok "SSH mount ${SSH_MOUNT}/ is enabled"
else
fail "SSH mount ${SSH_MOUNT}/ not found in secrets list"
fi
roles_out="$(remote_bao "$token" list "${SSH_MOUNT}/roles" 2>&1)" || {
fail "list ${SSH_MOUNT}/roles failed: $roles_out"
exit 1
}
for role in $EXPECTED_ROLES; do
if printf '%s\n' "$roles_out" | grep -q "$role"; then
ok "role ${role} exists"
else
fail "role ${role} missing"
fi
done
policy_out="$(remote_bao "$token" policy list 2>&1)" || {
fail "policy list failed: $policy_out"
exit 1
}
if printf '%s\n' "$policy_out" | grep -q 'warden-sign'; then
ok "policy warden-sign present"
else
fail "policy warden-sign missing"
fi
if [ "$FAILURES" -gt 0 ]; then
printf '\nSSH engine verification failed (%s failure(s)).\n' "$FAILURES" >&2
exit 1
fi
printf '\nSSH engine verification passed.\n'