Compare commits

...

3 Commits

Author SHA1 Message Date
423eccc8e9 feat(openbao): enable bao.coulomb.social ingress and Traefik middlewares
Expose OpenBao UI via TLS ingress with rate-limit and HSTS middlewares.
Track netkingdom OIDC mount in authenticated verify checks.
2026-06-18 01:23:02 +02:00
7838df6069 fix(openbao): complete SSH apply script for OpenBao 2.5.x issuers
Generate default CA via ssh/config/ca, split composite KUBECTL for role writes,
read pubkey from config/ca, allow warden key_id in roles, prefer production kubeconfig.
2026-06-18 01:18:56 +02:00
c24956fb5a 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.
2026-06-18 01:06:43 +02:00
10 changed files with 621 additions and 10 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ helm/*.yaml
!helm/*.sops.yaml !helm/*.sops.yaml
!helm/*.yaml.template !helm/*.yaml.template
!helm/openbao-values.yaml !helm/openbao-values.yaml
!helm/openbao-middleware.yaml
# Kubernetes manifests (no secrets) are safe to commit # Kubernetes manifests (no secrets) are safe to commit
!helm/*-cluster.yaml !helm/*-cluster.yaml
!helm/*-networkpolicies.yaml !helm/*-networkpolicies.yaml

View File

@@ -1,7 +1,7 @@
SHELL := /usr/bin/env bash SHELL := /usr/bin/env bash
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
KUBECONFIG ?= $(firstword $(wildcard $(HOME)/.kube/config-hosteurope) $(HOME)/.kube/config) KUBECONFIG ?= $(firstword $(wildcard $(HOME)/.kube/config) $(HOME)/.kube/config-hosteurope)
KUBECTL_BIN ?= $(firstword $(shell command -v kubectl 2>/dev/null) $(wildcard $(HOME)/.local/bin/kubectl) kubectl) KUBECTL_BIN ?= $(firstword $(shell command -v kubectl 2>/dev/null) $(wildcard $(HOME)/.local/bin/kubectl) kubectl)
KUBECTL := $(KUBECTL_BIN) --kubeconfig=$(KUBECONFIG) KUBECTL := $(KUBECTL_BIN) --kubeconfig=$(KUBECONFIG)
HELM := helm --kubeconfig=$(KUBECONFIG) HELM := helm --kubeconfig=$(KUBECONFIG)
@@ -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,38 @@
# Traefik middlewares for OpenBao browser UI/API exposure.
#
# These names are referenced by helm/openbao-values.yaml as:
# openbao-openbao-rate-limit@kubernetescrd
# openbao-openbao-hsts@kubernetescrd
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: openbao-rate-limit
namespace: openbao
labels:
app.kubernetes.io/name: openbao
app.kubernetes.io/part-of: railiance-platform
railiance-platform/component: secrets
spec:
rateLimit:
# The OpenBao browser UI performs a burst of API calls on load, including
# repeated /v1/sys/health checks. Keep this high enough for normal admin
# use while still bounding runaway clients.
average: 600
period: 1m
burst: 180
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: openbao-hsts
namespace: openbao
labels:
app.kubernetes.io/name: openbao
app.kubernetes.io/part-of: railiance-platform
railiance-platform/component: secrets
spec:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true

View File

@@ -31,7 +31,23 @@ server:
memory: 512Mi memory: 512Mi
ingress: ingress:
enabled: false enabled: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.middlewares: >-
openbao-openbao-rate-limit@kubernetescrd,
openbao-openbao-hsts@kubernetescrd
ingressClassName: traefik
pathType: Prefix
activeService: true
hosts:
- host: bao.coulomb.social
paths:
- /
tls:
- secretName: bao-tls
hosts:
- bao.coulomb.social
authDelegator: authDelegator:
enabled: true enabled: true

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,30 @@
# 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
allow_user_key_ids: true
default_user: adm
ttl: 48h
max_ttl: 48h
agt-role:
key_type: ca
allowed_users: "*"
allow_user_certificates: true
allow_user_key_ids: true
default_user: agt
ttl: 24h
max_ttl: 24h
atm-role:
key_type: ca
allowed_users: "*"
allow_user_certificates: true
allow_user_key_ids: true
default_user: atm
ttl: 8h
max_ttl: 8h

View File

@@ -0,0 +1,271 @@
#!/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"
}
kubectl_exec() {
# shellcheck disable=SC2086
$KUBECTL "$@"
}
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 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 exec -i -n "$OPENBAO_NAMESPACE" "$pod" -- \
sh -c 'read -r BAO_TOKEN; export BAO_TOKEN; bao policy write "$1" -' sh "$name"
}
ensure_default_issuer() {
local token="$1"
local issuers_out
if [ "$DRY_RUN" -eq 1 ]; then
printf 'DRY-RUN: bao read %s/config/issuers\n' "$SSH_MOUNT"
printf 'DRY-RUN: bao write %s/config/ca generate_signing_key=true key_type=ed25519\n' "$SSH_MOUNT"
return 0
fi
if issuers_out="$(remote_bao "$token" read "${SSH_MOUNT}/config/issuers" 2>&1)"; then
printf 'OK: default SSH issuer already configured.\n'
return 0
fi
case "$issuers_out" in
*"no default issuer"*)
remote_bao "$token" write "${SSH_MOUNT}/config/ca" \
generate_signing_key=true key_type=ed25519
printf 'OK: generated default SSH CA issuer.\n'
;;
*)
printf '%s\n' "$issuers_out" >&2
echo "ERROR: failed to read SSH issuer configuration" >&2
exit 1
;;
esac
}
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 shlex
import subprocess
import sys
token, spec_path, mount, namespace, pod, kubectl = sys.argv[1:7]
import yaml
kubectl_parts = shlex.split(kubectl) if kubectl else ["kubectl"]
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_parts + ["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}/config/ca" 2>/dev/null || true)"
if [ -z "$pubkey" ]; then
pubkey="$(remote_bao "$token" read -field=public_key "${SSH_MOUNT}/public_key" 2>/dev/null || true)"
fi
if [ -z "$pubkey" ]; then
warn "Could not read SSH CA public key from ${SSH_MOUNT}/config/ca or ${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_exec create secret generic "$K8S_CA_SECRET" \
--namespace "$OPENBAO_NAMESPACE" \
--from-file=ca_user.pub="$tmp" \
--dry-run=client -o yaml | kubectl_exec 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"
ensure_default_issuer "$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

@@ -15,7 +15,7 @@ Usage: scripts/openbao-verify-authenticated.sh [--dry-run] [--use-token-helper]
Runs authenticated, non-mutating OpenBao readiness checks: Runs authenticated, non-mutating OpenBao readiness checks:
- audit list includes file/ - audit list includes file/
- secrets list includes platform/ - secrets list includes platform/
- auth list includes kubernetes/ and keycape/ - auth list includes kubernetes/, netkingdom/, and keycape/
- audit log exists and is non-empty - audit log exists and is non-empty
The token is read from OPENBAO_TOKEN_FILE or an interactive hidden prompt. The The token is read from OPENBAO_TOKEN_FILE or an interactive hidden prompt. The
@@ -130,6 +130,7 @@ Path Type
---- ---- ---- ----
keycape/ oidc keycape/ oidc
kubernetes/ kubernetes kubernetes/ kubernetes
netkingdom/ oidc
token/ token token/ token
AUTH AUTH
;; ;;
@@ -210,6 +211,7 @@ step "Auth methods"
if auth_output="$(remote_bao "$token" auth list 2>&1)"; then if auth_output="$(remote_bao "$token" auth list 2>&1)"; then
printf '%s\n' "$auth_output" printf '%s\n' "$auth_output"
require_pattern "kubernetes/ auth method is visible" "$auth_output" '(^|[[:space:]])kubernetes/' require_pattern "kubernetes/ auth method is visible" "$auth_output" '(^|[[:space:]])kubernetes/'
require_pattern "netkingdom/ auth method is visible" "$auth_output" '(^|[[:space:]])netkingdom/'
require_pattern "keycape/ auth method is visible" "$auth_output" '(^|[[:space:]])keycape/' require_pattern "keycape/ auth method is visible" "$auth_output" '(^|[[:space:]])keycape/'
else else
printf '%s\n' "$auth_output" >&2 printf '%s\n' "$auth_output" >&2

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'