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:
12
Makefile
12
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
|
||||
|
||||
126
docs/openbao.md
126
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 '<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
|
||||
|
||||
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:
|
||||
|
||||
18
openbao/policies/warden-sign.hcl
Normal file
18
openbao/policies/warden-sign.hcl
Normal 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"]
|
||||
}
|
||||
27
openbao/ssh/roles-spec.yaml
Normal file
27
openbao/ssh/roles-spec.yaml
Normal 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
|
||||
234
scripts/openbao-apply-ssh-engine.sh
Executable file
234
scripts/openbao-apply-ssh-engine.sh
Executable 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
|
||||
111
scripts/openbao-verify-ssh-engine.sh
Executable file
111
scripts/openbao-verify-ssh-engine.sh
Executable 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'
|
||||
Reference in New Issue
Block a user