From c24956fb5add1252cca226d406ebffc7d6d549d4 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 18 Jun 2026 01:06:43 +0200 Subject: [PATCH] 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. --- Makefile | 12 +- docs/openbao.md | 126 ++++++++++++++- openbao/policies/warden-sign.hcl | 18 +++ openbao/ssh/roles-spec.yaml | 27 ++++ scripts/openbao-apply-ssh-engine.sh | 234 +++++++++++++++++++++++++++ scripts/openbao-verify-ssh-engine.sh | 111 +++++++++++++ 6 files changed, 521 insertions(+), 7 deletions(-) create mode 100644 openbao/policies/warden-sign.hcl create mode 100644 openbao/ssh/roles-spec.yaml create mode 100755 scripts/openbao-apply-ssh-engine.sh create mode 100755 scripts/openbao-verify-ssh-engine.sh diff --git a/Makefile b/Makefile index e13aff2..625dce3 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ OPENBAO_CHART_VERSION ?= 0.28.2 OPENBAO_NAMESPACE ?= openbao OPENBAO_RELEASE ?= openbao OPENBAO_VALUES ?= helm/openbao-values.yaml +OPENBAO_MIDDLEWARE ?= helm/openbao-middleware.yaml OPENBAO_VERIFY_AUTH_ARGS ?= OPENBAO_RESTORE_EVIDENCE ?= /tmp/netkingdom-openbao-restore-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 $(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 \ --version $(OPENBAO_CHART_VERSION) \ --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) \ 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 KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ 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 } \ /^##@/ { 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 diff --git a/docs/openbao.md b/docs/openbao.md index 680c344..e06723c 100644 --- a/docs/openbao.md +++ b/docs/openbao.md @@ -18,15 +18,26 @@ S5 workloads / operators -> openbao-0 -> integrated Raft storage on local-path PVC -> 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. - SOPS/age remains the Git-at-rest bootstrap mechanism. - 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 - cert-manager-backed internal TLS before exposing OpenBao beyond cluster-local - traffic. + cert-manager-backed internal TLS before relying on cluster-internal traffic + from untrusted namespaces. ## Deployment @@ -41,6 +52,10 @@ make openbao-deploy 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: ```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 -file and run Helm directly: +and middleware files, then run Helm directly: ```bash 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 \ 'sudo env KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm upgrade --install openbao openbao/openbao \ --version 0.28.2 \ @@ -78,6 +96,8 @@ Expected immediately after install: - `openbao-0` is Running. - `openbao`, `openbao-active`, `openbao-internal`, and `openbao-ui` 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. - `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 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 '' > ~/.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 Initial auth model: @@ -228,6 +292,56 @@ Initial auth model: | 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 | +### 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: - 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 command line, and only runs non-mutating checks. It verifies that `bao audit list` shows `file/`, `bao secrets list` shows `platform/`, -`bao auth list` shows both `kubernetes/` and `keycape/`, and that the file -audit log is non-empty. +`bao auth list` shows `kubernetes/`, `netkingdom/`, and `keycape/`, and that +the file audit log is non-empty. If a previous attended OIDC login stored a still-valid token in the pod token helper, use: diff --git a/openbao/policies/warden-sign.hcl b/openbao/policies/warden-sign.hcl new file mode 100644 index 0000000..3de3b00 --- /dev/null +++ b/openbao/policies/warden-sign.hcl @@ -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"] +} \ No newline at end of file diff --git a/openbao/ssh/roles-spec.yaml b/openbao/ssh/roles-spec.yaml new file mode 100644 index 0000000..b8fdf78 --- /dev/null +++ b/openbao/ssh/roles-spec.yaml @@ -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 \ No newline at end of file diff --git a/scripts/openbao-apply-ssh-engine.sh b/scripts/openbao-apply-ssh-engine.sh new file mode 100755 index 0000000..92f1b11 --- /dev/null +++ b/scripts/openbao-apply-ssh-engine.sh @@ -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 < + 3. ops-warden: warden sign smoke (WP-0008 T2) +NEXT + +if [ "$WARNINGS" -gt 0 ]; then + printf '\nCompleted with %s warning(s).\n' "$WARNINGS" +fi \ No newline at end of file diff --git a/scripts/openbao-verify-ssh-engine.sh b/scripts/openbao-verify-ssh-engine.sh new file mode 100755 index 0000000..39fe693 --- /dev/null +++ b/scripts/openbao-verify-ssh-engine.sh @@ -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' \ No newline at end of file