diff --git a/Makefile b/Makefile index c9b5987..c8a0747 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ OPENBAO_VALUES ?= helm/openbao-values.yaml OPENBAO_MIDDLEWARE ?= helm/openbao-middleware.yaml OPENBAO_UI_OVERLAY_DIR ?= helm/openbao-ui-overlay OPENBAO_UI_OVERLAY_K8S ?= helm/openbao-ui-overlay-k8s.yaml +OPENBAO_PUBLIC_INGRESS_ROLLBACK ?= helm/openbao-public-ingress.rollback.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 @@ -215,7 +216,6 @@ openbao-verify-login-overlay: ## Verify public KeyCape login overlay is active 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) \ @@ -223,6 +223,29 @@ openbao-deploy: openbao-repo ## Deploy / upgrade OpenBao to the openbao namespac --wait --timeout 5m $(MAKE) openbao-overlay-apply +.PHONY: openbao-public-listener-dry-run openbao-public-listener-status openbao-public-listener-retract openbao-public-listener-rollback + +openbao-public-listener-dry-run: ## Validate the guarded public-listener retraction without mutation + KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ + OPENBAO_UI_OVERLAY_K8S=$(OPENBAO_UI_OVERLAY_K8S) \ + OPENBAO_PUBLIC_INGRESS_ROLLBACK=$(OPENBAO_PUBLIC_INGRESS_ROLLBACK) \ + scripts/openbao-public-listener-transition.sh dry-run + +openbao-public-listener-status: ## Report public Ingress and private tunnel readiness + KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ + scripts/openbao-public-listener-transition.sh status + +openbao-public-listener-retract: ## Attended deletion after exact tunnel/login confirmation + KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ + OPENBAO_PUBLIC_INGRESS_ROLLBACK=$(OPENBAO_PUBLIC_INGRESS_ROLLBACK) \ + scripts/openbao-public-listener-transition.sh apply + +openbao-public-listener-rollback: ## Attended restoration of the prior public Ingress + KUBECTL='$(KUBECTL)' OPENBAO_NAMESPACE=$(OPENBAO_NAMESPACE) \ + OPENBAO_MIDDLEWARE=$(OPENBAO_MIDDLEWARE) \ + OPENBAO_PUBLIC_INGRESS_ROLLBACK=$(OPENBAO_PUBLIC_INGRESS_ROLLBACK) \ + scripts/openbao-public-listener-transition.sh rollback + openbao-status: ## Show OpenBao pods, services, PVCs, and seal/init status $(KUBECTL) get pods,svc,pvc -n $(OPENBAO_NAMESPACE) \ -l app.kubernetes.io/instance=$(OPENBAO_RELEASE) -o wide diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 22e6308..6dfb3ae 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -24,11 +24,12 @@ | workplan | RAILIANCE-WP-0023 | finished | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md | | workplan | RAILIANCE-WP-0024 | active | — | workplans/RAILIANCE-WP-0024-audit-core-custody-and-recovery-coordination.md | | workplan | RAILIANCE-WP-0025 | finished | — | workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md | -| workplan | RAILIANCE-WP-0026 | ready | — | workplans/RAILIANCE-WP-0026-attended-login-output-containment.md | +| workplan | RAILIANCE-WP-0026 | finished | — | workplans/RAILIANCE-WP-0026-attended-login-output-containment.md | | workplan | RPF-WP-0018 | finished | — | workplans/RPF-WP-0018-policy-surface-alignment.md | | workplan | RPF-WP-0019 | finished | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md | | workplan | RPF-WP-0020 | finished | — | workplans/RPF-WP-0020-ccr-schema-drift.md | | workplan | RPF-WP-0021 | finished | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | +| workplan | RAILIANCE-WP-0027 | blocked | — | workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md | | task | RAILIANCE-WP-0005-T01 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md | | task | RAILIANCE-WP-0005-T02 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md | | task | RAILIANCE-WP-0005-T03 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md | @@ -109,7 +110,7 @@ | task | RAILIANCE-WP-0025-T04 | done | — | workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md | | task | RAILIANCE-WP-0025-T05 | done | — | workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md | | task | RAILIANCE-WP-0025-T06 | done | — | workplans/RAILIANCE-WP-0025-versioned-ephemeral-custody-lifecycle.md | -| task | RAILIANCE-WP-0026-T01 | todo | — | workplans/RAILIANCE-WP-0026-attended-login-output-containment.md | +| task | RAILIANCE-WP-0026-T01 | done | — | workplans/RAILIANCE-WP-0026-attended-login-output-containment.md | | task | RPF-WP-0018-T01 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md | | task | RPF-WP-0018-T02 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md | | task | RPF-WP-0018-T03 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md | @@ -130,3 +131,6 @@ | task | RPF-WP-0021-T03 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | | task | RPF-WP-0021-T04 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | | task | RPF-WP-0021-T05 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md | +| task | RAILIANCE-WP-0027-T01 | done | — | workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md | +| task | RAILIANCE-WP-0027-T02 | done | — | workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md | +| task | RAILIANCE-WP-0027-T03 | wait | — | workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md | diff --git a/docs/openbao-public-listener-transition.md b/docs/openbao-public-listener-transition.md new file mode 100644 index 0000000..c9e6068 --- /dev/null +++ b/docs/openbao-public-listener-transition.md @@ -0,0 +1,68 @@ +# OpenBao public-listener transition + +Status: source implementation complete; live retraction held. + +This is the S3-owner procedure for `RMASTER-WP-0020-T09`. It preserves the +OpenBao Deployment, the `openbao-ui-gateway` Deployment, and its ClusterIP +Service while removing the public `bao.coulomb.social` Ingress. + +## Target access + +- Exposure posture: `operator` +- Named tunnel: `openbao-ui-railiance01` +- Local UI: `http://127.0.0.1:18200` +- Cluster target: `openbao/openbao-ui-gateway:8080` +- Public Ingress: absent after the transition + +The tunnel is an ops-bridge local forward to the current ClusterIP and does not +open a node port. If the Service is recreated with a new ClusterIP, update and +revalidate the tunnel before use. + +Browser OIDC derives its callback from the local origin. Before retraction, +both the KeyCape `openbao-admin` client and OpenBao +`auth/netkingdom/role/platform-admin` must allow exactly: + +```text +http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback +``` + +Record only a value-safe attended-login result. Do not record the authorization +code, token, accessor, callback query, browser storage, or screenshots. + +## Guarded sequence + +```bash +make openbao-public-listener-dry-run +make openbao-public-listener-status +``` + +The status command deliberately fails if ops-bridge does not report the named +tunnel as lifecycle-healthy, even when an orphan forward still answers. Repair +that state before proceeding. + +After KeyCape/OpenBao callback configuration and one attended MFA login through +the local URL, use the exact hold point: + +```bash +OPENBAO_OPERATOR_LOGIN_VERIFIED=true \ +OPENBAO_PUBLIC_LISTENER_CONFIRM='RETRACT RMASTER-WP-0020-T09 PUBLIC OPENBAO LISTENER' \ +make openbao-public-listener-retract +``` + +The command pins the railiance01 cluster UID, rechecks the ClusterIP gateway and +tunnel, deletes only `openbao/openbao-ui-gateway` Ingress, then proves the +private path remains healthy and the public name no longer returns a successful +HTTP status. + +## Rollback + +During the bounded observation window: + +```bash +OPENBAO_PUBLIC_LISTENER_CONFIRM='ROLLBACK RMASTER-WP-0020-T09 PUBLIC OPENBAO LISTENER' \ +make openbao-public-listener-rollback +``` + +Rollback reapplies only the retained Traefik middleware and the explicitly +named rollback Ingress. It does not mutate OpenBao policy, Raft, PVCs, Secrets, +or the CoulombCore retention state. diff --git a/docs/openbao.md b/docs/openbao.md index 449b073..a684a2e 100644 --- a/docs/openbao.md +++ b/docs/openbao.md @@ -29,8 +29,9 @@ S5 workloads / operators -> audit storage PVC mounted at /openbao/audit Platform operators with approved admin identity - -> https://bao.coulomb.social - -> Traefik Ingress + TLS + -> bridge up openbao-ui-railiance01 + -> http://127.0.0.1:18200 + -> SSH local forward to the ClusterIP -> openbao-ui service -> OpenBao UI/API -> KeyCape OIDC at https://kc.coulomb.social for login @@ -39,7 +40,8 @@ Platform operators with approved admin identity - 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. -- Browser UI/API exposure is declared for `https://bao.coulomb.social`. +- Browser UI exposure is operator-only through the named + `openbao-ui-railiance01` tunnel. 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 @@ -61,11 +63,11 @@ make openbao-deploy make openbao-status ``` -`make openbao-deploy` applies `helm/openbao-middleware.yaml` (Traefik -rate-limit and HSTS), upgrades the OpenBao Helm release, then applies the -KeyCape login overlay gateway (`helm/openbao-ui-overlay-k8s.yaml`). Public -ingress for `bao.coulomb.social` targets `openbao-ui-gateway`, not the chart -ingress (which stays disabled in `helm/openbao-values.yaml`). +`make openbao-deploy` upgrades the OpenBao Helm release, then applies the +KeyCape login overlay gateway (`helm/openbao-ui-overlay-k8s.yaml`) as a +ClusterIP-only surface. The chart ingress stays disabled. Public-listener +retraction and rollback are separate guarded commands documented in +`docs/openbao-public-listener-transition.md`. On Railiance01 directly: @@ -107,8 +109,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. +- With `openbao-ui-railiance01` healthy, + `http://127.0.0.1:18200` serves the operator UI. - data and audit PVCs are Bound. - `bao status` reports `Initialized: false` and `Sealed: true`. @@ -327,17 +329,17 @@ Initial auth model: ### Browser UI Login -The browser operator surface is: +The browser operator surface is the named local tunnel: ```text -https://bao.coulomb.social +http://127.0.0.1:18200 ``` -Operators see a streamlined **Sign in with KeyCape** mask. The raw OpenBao -fields (namespace, method, mount path, role) are hidden presets applied by the -UI overlay in `helm/openbao-ui-overlay/`. Public ingress targets the -`openbao-ui-gateway` nginx proxy, which injects overlay assets and forwards to -the OpenBao service. +Run `bridge up openbao-ui-railiance01` first. Operators then see a streamlined +**Sign in with KeyCape** mask. The raw OpenBao fields (namespace, method, mount +path, role) are hidden presets applied by the UI overlay in +`helm/openbao-ui-overlay/`. The ClusterIP-only `openbao-ui-gateway` nginx proxy +injects overlay assets and forwards to the OpenBao service. Hidden defaults (also in `helm/openbao-ui-overlay/presets.json`): @@ -378,10 +380,10 @@ standalone page that exchanges the authorization code, stores the UI session token, and redirects into the Ember app (no popup/`window.opener` flow). The OpenBao UI redirects the browser to KeyCape at `kc.coulomb.social`, then -returns to: +returns through the same local tunnel to: ```text -https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback +http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback ``` The legacy `keycape` mount remains a compatibility alias for existing @@ -394,7 +396,7 @@ The browser callback URI must be present in both: - 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 +`http://127.0.0.1:18200/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. diff --git a/helm/openbao-public-ingress.rollback.yaml b/helm/openbao-public-ingress.rollback.yaml new file mode 100644 index 0000000..a3e9074 --- /dev/null +++ b/helm/openbao-public-ingress.rollback.yaml @@ -0,0 +1,33 @@ +# Rollback-only public listener for an attended RMASTER-WP-0020-T09 reversal. +# This file is never applied by the ordinary platform deployment path. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: openbao-ui-gateway + namespace: openbao + labels: + app.kubernetes.io/name: openbao-ui-gateway + app.kubernetes.io/part-of: railiance-platform + railiance-platform/component: secrets + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.middlewares: >- + openbao-openbao-rate-limit@kubernetescrd, + openbao-openbao-hsts@kubernetescrd +spec: + ingressClassName: traefik + tls: + - secretName: bao-tls + hosts: + - bao.coulomb.social + rules: + - host: bao.coulomb.social + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: openbao-ui-gateway + port: + number: 8080 diff --git a/helm/openbao-ui-overlay-k8s.yaml b/helm/openbao-ui-overlay-k8s.yaml index 4d9b203..a80606a 100644 --- a/helm/openbao-ui-overlay-k8s.yaml +++ b/helm/openbao-ui-overlay-k8s.yaml @@ -1,6 +1,6 @@ # OpenBao browser UI gateway — injects the KeyCape login overlay and proxies -# to the OpenBao service. Public ingress for bao.coulomb.social targets this -# gateway instead of the chart-managed OpenBao ingress. +# to the OpenBao service. The gateway is ClusterIP-only and is reached through +# the named openbao-ui-railiance01 operator tunnel. # # ConfigMap data is applied by scripts/openbao-ui-overlay-apply.sh from # helm/openbao-ui-overlay/*. @@ -87,35 +87,3 @@ spec: port: 8080 targetPort: http protocol: TCP ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: openbao-ui-gateway - namespace: openbao - labels: - app.kubernetes.io/name: openbao-ui-gateway - app.kubernetes.io/part-of: railiance-platform - railiance-platform/component: secrets - annotations: - cert-manager.io/cluster-issuer: letsencrypt-prod - traefik.ingress.kubernetes.io/router.middlewares: >- - openbao-openbao-rate-limit@kubernetescrd, - openbao-openbao-hsts@kubernetescrd -spec: - ingressClassName: traefik - tls: - - secretName: bao-tls - hosts: - - bao.coulomb.social - rules: - - host: bao.coulomb.social - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: openbao-ui-gateway - port: - number: 8080 \ No newline at end of file diff --git a/helm/openbao-ui-overlay/README.md b/helm/openbao-ui-overlay/README.md index b8c4dd9..5d8cd68 100644 --- a/helm/openbao-ui-overlay/README.md +++ b/helm/openbao-ui-overlay/README.md @@ -1,8 +1,9 @@ # OpenBao KeyCape login overlay -Streamlines the browser login mask at `https://bao.coulomb.social` to a single -**Sign in with KeyCape** action. Namespace, auth method, mount path, and role -are preset in `presets.json` and hidden by `overlay.css` / `overlay.js`. +Streamlines the browser login mask reached through the named +`openbao-ui-railiance01` operator tunnel at `http://127.0.0.1:18200` to a +single **Sign in with KeyCape** action. Namespace, auth method, mount path, and +role are preset in `presets.json` and hidden by `overlay.css` / `overlay.js`. ## Mechanism (T01 decision) @@ -54,9 +55,9 @@ make openbao-verify-login-overlay 3. Fetch live UI assets and compare hashes: ```bash - curl -sS https://bao.coulomb.social/ui/ -o /tmp/index.html + curl -sS http://127.0.0.1:18200/ui/ -o /tmp/index.html # locate vault-*.js path in /tmp/index.html, then: - curl -sS "https://bao.coulomb.social/ui/assets/vault-....js" -o /tmp/vault.js + curl -sS "http://127.0.0.1:18200/ui/assets/vault-....js" -o /tmp/vault.js sha256sum /tmp/index.html /tmp/vault.js ``` @@ -64,6 +65,9 @@ make openbao-verify-login-overlay `overlay.css` / `overlay.js` selectors against the new Ember templates. 5. Write `patches//manifest.sha256`, update `VERSION`. 6. Run `make openbao-verify-login-overlay CHECK_UPSTREAM_DRIFT=1`. -7. Attended browser login through KeyCape MFA. +7. Attended browser login through KeyCape MFA. The OpenBao role and KeyCape + client must both allow the exact loopback callback + `http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback` before the + former public listener is retracted. -Workplan: `helix-forge/workplans/HF-WP-0003-openbao-keycape-login-overlay.md` \ No newline at end of file +Workplan: `helix-forge/workplans/HF-WP-0003-openbao-keycape-login-overlay.md` diff --git a/helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256 b/helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256 index e0f761c..a646982 100644 --- a/helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256 +++ b/helm/openbao-ui-overlay/patches/2.5.4/manifest.sha256 @@ -1,8 +1,8 @@ # OpenBao UI asset fingerprints for image tag 2.5.4. # Regenerate after an OpenBao image bump when login markup drifts. # Compare vault.js only — index.html is intentionally modified by the gateway. -# curl -sS https://bao.coulomb.social/ui/ -o /tmp/index.html +# curl -sS http://127.0.0.1:18200/ui/ -o /tmp/index.html # vault_path=$(rg -o '/ui/assets/vault-[a-f0-9]+\\.js' /tmp/index.html | head -1) -# curl -sS "https://bao.coulomb.social${vault_path}" -o /tmp/vault.js +# curl -sS "http://127.0.0.1:18200${vault_path}" -o /tmp/vault.js # sha256sum /tmp/vault.js -f0214b5be89377395f8d6521c34139877529bd95ba703901c78b527ab0f1c231 ui/assets/vault-bae6b876038fbf475728f993b5a62002.js \ No newline at end of file +f0214b5be89377395f8d6521c34139877529bd95ba703901c78b527ab0f1c231 ui/assets/vault-bae6b876038fbf475728f993b5a62002.js diff --git a/scripts/openbao-public-listener-transition.sh b/scripts/openbao-public-listener-transition.sh new file mode 100755 index 0000000..f519de2 --- /dev/null +++ b/scripts/openbao-public-listener-transition.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail + +ACTION="${1:-status}" +OPENBAO_NAMESPACE="${OPENBAO_NAMESPACE:-openbao}" +KUBECTL="${KUBECTL:-kubectl}" +INGRESS_NAME="${OPENBAO_PUBLIC_INGRESS_NAME:-openbao-ui-gateway}" +SERVICE_NAME="${OPENBAO_UI_SERVICE_NAME:-openbao-ui-gateway}" +DEPLOYMENT_NAME="${OPENBAO_UI_DEPLOYMENT_NAME:-openbao-ui-gateway}" +TUNNEL_NAME="${OPENBAO_UI_TUNNEL_NAME:-openbao-ui-railiance01}" +TUNNEL_URL="${OPENBAO_UI_BASE_URL:-http://127.0.0.1:18200}" +PUBLIC_URL="${OPENBAO_PUBLIC_URL:-https://bao.coulomb.social/}" +EXPECTED_CLUSTER_UID="${RAILIANCE01_CLUSTER_UID:-a553c742-0115-43d4-99a4-a5ca56fe0786}" +PRIVATE_MANIFEST="${OPENBAO_UI_OVERLAY_K8S:-helm/openbao-ui-overlay-k8s.yaml}" +ROLLBACK_MANIFEST="${OPENBAO_PUBLIC_INGRESS_ROLLBACK:-helm/openbao-public-ingress.rollback.yaml}" +MIDDLEWARE_MANIFEST="${OPENBAO_MIDDLEWARE:-helm/openbao-middleware.yaml}" +CONFIRM_RETRACT="RETRACT RMASTER-WP-0020-T09 PUBLIC OPENBAO LISTENER" +CONFIRM_ROLLBACK="ROLLBACK RMASTER-WP-0020-T09 PUBLIC OPENBAO LISTENER" + +usage() { + cat <<'USAGE' +Usage: scripts/openbao-public-listener-transition.sh status|dry-run|preflight|apply|rollback + +`apply` requires both: + OPENBAO_OPERATOR_LOGIN_VERIFIED=true + OPENBAO_PUBLIC_LISTENER_CONFIRM='RETRACT RMASTER-WP-0020-T09 PUBLIC OPENBAO LISTENER' + +`rollback` requires: + OPENBAO_PUBLIC_LISTENER_CONFIRM='ROLLBACK RMASTER-WP-0020-T09 PUBLIC OPENBAO LISTENER' + +The script never reads OpenBao credentials or secret values. +USAGE +} + +fail() { printf '[ERR] %s\n' "$*" >&2; exit 1; } +ok() { printf '[OK] %s\n' "$*"; } + +kube() { + # KUBECTL may include an explicit --kubeconfig argument from Make. + # shellcheck disable=SC2086 + $KUBECTL "$@" +} + +require_cluster() { + local observed + observed="$(kube get namespace kube-system -o jsonpath='{.metadata.uid}')" + [ "$observed" = "$EXPECTED_CLUSTER_UID" ] || + fail "cluster identity mismatch: expected $EXPECTED_CLUSTER_UID, observed $observed" + ok "railiance01 cluster identity pinned" +} + +require_private_source() { + [ -f "$PRIVATE_MANIFEST" ] || fail "missing private gateway manifest: $PRIVATE_MANIFEST" + if grep -Eq '^kind:[[:space:]]*Ingress[[:space:]]*$' "$PRIVATE_MANIFEST"; then + fail "ordinary gateway manifest still contains an Ingress" + fi + ok "ordinary gateway manifest is Ingress-free" +} + +require_private_runtime() { + local service_type service_port + service_type="$(kube -n "$OPENBAO_NAMESPACE" get service "$SERVICE_NAME" -o jsonpath='{.spec.type}')" + service_port="$(kube -n "$OPENBAO_NAMESPACE" get service "$SERVICE_NAME" -o jsonpath='{.spec.ports[?(@.name=="http")].port}')" + [ "$service_type" = "ClusterIP" ] || fail "gateway Service type is $service_type, not ClusterIP" + [ "$service_port" = "8080" ] || fail "gateway Service http port is $service_port, not 8080" + kube -n "$OPENBAO_NAMESPACE" rollout status "deployment/$DEPLOYMENT_NAME" --timeout=30s >/dev/null + ok "gateway Deployment Ready behind ClusterIP" +} + +require_tunnel() { + command -v bridge >/dev/null || fail "bridge CLI not found" + command -v jq >/dev/null || fail "jq not found" + local report + if ! report="$(bridge check "$TUNNEL_NAME" --json 2>/dev/null)"; then + fail "named tunnel $TUNNEL_NAME is not lifecycle-healthy" + fi + jq -e --arg name "$TUNNEL_NAME" '.[] | select(.tunnel == $name and .ok == true)' \ + <<<"$report" >/dev/null || fail "named tunnel $TUNNEL_NAME did not report ok=true" + curl -fsS --max-time 10 "$TUNNEL_URL/ui/platform-overlay/presets.json" >/dev/null || + fail "operator UI is not reachable through $TUNNEL_NAME" + ok "named operator tunnel is healthy and reaches the UI" +} + +status() { + require_cluster + require_private_source + require_private_runtime + if kube -n "$OPENBAO_NAMESPACE" get ingress "$INGRESS_NAME" >/dev/null 2>&1; then + printf '[INFO] public Ingress is present\n' + else + printf '[INFO] public Ingress is absent\n' + fi + require_tunnel +} + +dry_run() { + require_cluster + require_private_source + [ -f "$ROLLBACK_MANIFEST" ] || fail "missing rollback manifest: $ROLLBACK_MANIFEST" + kube apply --server-side --dry-run=server -f "$PRIVATE_MANIFEST" >/dev/null + kube apply --server-side --dry-run=server -f "$ROLLBACK_MANIFEST" >/dev/null + kube -n "$OPENBAO_NAMESPACE" delete ingress "$INGRESS_NAME" --dry-run=server >/dev/null + ok "private apply, rollback apply, and Ingress deletion pass server dry-run" +} + +preflight() { + status + kube -n "$OPENBAO_NAMESPACE" get ingress "$INGRESS_NAME" >/dev/null 2>&1 || + fail "public Ingress is already absent; use status" + ok "public Ingress is present for attended transition" +} + +apply_retraction() { + [ "${OPENBAO_OPERATOR_LOGIN_VERIFIED:-false}" = "true" ] || + fail "attended operator login has not been explicitly verified" + [ "${OPENBAO_PUBLIC_LISTENER_CONFIRM:-}" = "$CONFIRM_RETRACT" ] || + fail "exact retraction confirmation is missing" + preflight + kube -n "$OPENBAO_NAMESPACE" delete ingress "$INGRESS_NAME" --wait=true + require_private_runtime + require_tunnel + if kube -n "$OPENBAO_NAMESPACE" get ingress "$INGRESS_NAME" >/dev/null 2>&1; then + fail "public Ingress still exists after deletion" + fi + local code + code="$(curl -kLsS --max-time 10 -o /dev/null -w '%{http_code}' "$PUBLIC_URL" || true)" + case "$code" in + 2??|3??) fail "public hostname still returns successful HTTP status $code" ;; + esac + ok "public Ingress absent; private gateway and named tunnel remain healthy" +} + +rollback() { + [ "${OPENBAO_PUBLIC_LISTENER_CONFIRM:-}" = "$CONFIRM_ROLLBACK" ] || + fail "exact rollback confirmation is missing" + require_cluster + [ -f "$MIDDLEWARE_MANIFEST" ] || fail "missing middleware manifest: $MIDDLEWARE_MANIFEST" + [ -f "$ROLLBACK_MANIFEST" ] || fail "missing rollback manifest: $ROLLBACK_MANIFEST" + kube apply -f "$MIDDLEWARE_MANIFEST" + kube apply -f "$ROLLBACK_MANIFEST" + kube -n "$OPENBAO_NAMESPACE" get ingress "$INGRESS_NAME" >/dev/null + ok "prior public Ingress restored from rollback-only manifest" +} + +case "$ACTION" in + status) status ;; + dry-run) dry_run ;; + preflight) preflight ;; + apply) apply_retraction ;; + rollback) rollback ;; + -h|--help|help) usage ;; + *) usage >&2; exit 2 ;; +esac diff --git a/scripts/openbao-ui-overlay-apply.sh b/scripts/openbao-ui-overlay-apply.sh index d71584d..929bff0 100755 --- a/scripts/openbao-ui-overlay-apply.sh +++ b/scripts/openbao-ui-overlay-apply.sh @@ -11,8 +11,9 @@ usage() { cat <<'USAGE' Usage: scripts/openbao-ui-overlay-apply.sh -Builds and applies the OpenBao KeyCape login overlay ConfigMaps and gateway -Deployment/Service/Ingress. Idempotent — safe to run on every openbao-deploy. +Builds and applies the OpenBao KeyCape login overlay ConfigMaps and private +gateway Deployment/ClusterIP Service. Idempotent — safe on every deploy. It +does not create or delete a public Ingress. Environment: OPENBAO_NAMESPACE Kubernetes namespace. Default: openbao @@ -71,4 +72,4 @@ $KUBECTL rollout restart deployment/openbao-ui-gateway -n "$OPENBAO_NAMESPACE" # shellcheck disable=SC2086 $KUBECTL rollout status deployment/openbao-ui-gateway -n "$OPENBAO_NAMESPACE" --timeout=120s -printf '[OK] OpenBao UI overlay applied from %s\n' "$OVERLAY_DIR" \ No newline at end of file +printf '[OK] OpenBao UI overlay applied from %s\n' "$OVERLAY_DIR" diff --git a/scripts/openbao-verify-login-overlay.sh b/scripts/openbao-verify-login-overlay.sh index 94817e8..4b23e6f 100755 --- a/scripts/openbao-verify-login-overlay.sh +++ b/scripts/openbao-verify-login-overlay.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -BASE_URL="${OPENBAO_UI_BASE_URL:-https://bao.coulomb.social}" +BASE_URL="${OPENBAO_UI_BASE_URL:-http://127.0.0.1:18200}" OVERLAY_DIR="${OPENBAO_UI_OVERLAY_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/helm/openbao-ui-overlay}" CHECK_DRIFT="${CHECK_UPSTREAM_DRIFT:-0}" @@ -13,11 +13,11 @@ usage() { cat <<'USAGE' Usage: scripts/openbao-verify-login-overlay.sh [--check-upstream-drift] -Verifies the public OpenBao UI serves the KeyCape login overlay assets and -that index.html injection is present. +Verifies the operator-tunneled OpenBao UI serves the KeyCape login overlay +assets and that index.html injection is present. Environment: - OPENBAO_UI_BASE_URL Public UI base URL. Default: https://bao.coulomb.social + OPENBAO_UI_BASE_URL Operator UI base URL. Default: http://127.0.0.1:18200 OPENBAO_UI_OVERLAY_DIR Local overlay directory for drift fingerprints CHECK_UPSTREAM_DRIFT Set to 1 to compare live UI hashes with patches/ USAGE @@ -170,4 +170,4 @@ if [ "$CHECK_DRIFT" = "1" ]; then ok "vault bundle hash matches patches/$version/manifest.sha256 (${expected_vault_path:-$vault_asset})" fi -printf '\nOpenBao login overlay verification passed for %s\n' "$BASE_URL" \ No newline at end of file +printf '\nOpenBao login overlay verification passed for %s\n' "$BASE_URL" diff --git a/workplans/RAILIANCE-WP-0026-attended-login-output-containment.md b/workplans/RAILIANCE-WP-0026-attended-login-output-containment.md index cf34a86..fa190c4 100644 --- a/workplans/RAILIANCE-WP-0026-attended-login-output-containment.md +++ b/workplans/RAILIANCE-WP-0026-attended-login-output-containment.md @@ -4,7 +4,7 @@ type: workplan title: "Contain attended OpenBao login output before authentication" domain: financials repo: railiance-platform -status: ready +status: finished owner: codex topic_slug: railiance created: "2026-08-23" @@ -50,7 +50,7 @@ seal/unseal ceremony, or reuse of the terminal drill scenario. ```task id: RAILIANCE-WP-0026-T01 -status: todo +status: done priority: high ``` @@ -90,10 +90,19 @@ both reviewers, and `railiance-platform` accepts the exact revision. Completion of this task still does not authorize a live drill; that requires fresh owner receipts, current preflight, and a new human decision under a new scenario id. +Completed 2026-08-23. Railiance Platform accepts ops-warden revision +`0fae0904ce8d8694338dd53a8a79abec5fec788d`. The committed owner receipt at +ops-warden revision `bc1966da825fbeaa4d83947a4f942b5d5f085408` has SHA-256 +`d2ba444ed16989590325697e69d25283dc75a9432c29a72e627e80bf9fd987e4`, +reports 42 focused tests and 390 repository tests passing, and records all +seven containment outcomes without sensitive material. Independent +railiance-infra review at revision `186b030` approves the same implementation. +This is source acceptance only and authorizes no live OIDC or drill. + ## Acceptance -- [ ] The adapter refuses unsafe storage before OIDC. -- [ ] No underlying login output can reach the agent transcript. -- [ ] Any issued credential is contained and revoked on failure. -- [ ] Focused success and failure-path tests pass without credential material. -- [ ] Both direct reviewers receive and accept the value-safe fix receipt. +- [x] The adapter refuses unsafe storage before OIDC. +- [x] No underlying login output can reach the agent transcript. +- [x] Any issued credential is contained and revoked on failure. +- [x] Focused success and failure-path tests pass without credential material. +- [x] Both direct reviewers receive and accept the value-safe fix receipt. diff --git a/workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md b/workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md new file mode 100644 index 0000000..2002f61 --- /dev/null +++ b/workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md @@ -0,0 +1,66 @@ +--- +id: RAILIANCE-WP-0027 +type: workplan +title: "Retract public OpenBao listener behind operator-only access" +domain: financials +repo: railiance-platform +status: blocked +owner: codex +topic_slug: railiance +created: "2026-08-23" +updated: "2026-08-23" +related: + - RMASTER-WP-0020-T09 + - RAPP-OPENBAO-WP-0002 +--- + +# RAILIANCE-WP-0027 — OpenBao operator-only access + +## Goal + +Implement the S3-owner half of RMASTER-WP-0020-T09 without coupling it to +destructive CoulombCore cleanup. + +## T01 — Align the retained compatibility source + +```task +id: RAILIANCE-WP-0027-T01 +status: done +priority: high +``` + +The retained platform manifest now matches the canonical package posture: +Deployment plus ClusterIP Service only. Ordinary deploy no longer applies the +public-only middleware. The old Ingress remains solely in an explicitly named +rollback artifact. + +## T02 — Add guarded retraction and rollback + +```task +id: RAILIANCE-WP-0027-T02 +status: done +priority: high +``` + +`scripts/openbao-public-listener-transition.sh` pins the cluster UID, verifies +source and runtime packet posture, requires a lifecycle-healthy named tunnel, +and gates live deletion on exact confirmation plus attended-login verification. +It deletes only the Ingress and provides an exact rollback path. + +## T03 — Complete the attended operator cutover + +```task +id: RAILIANCE-WP-0027-T03 +status: wait +priority: high +``` + +Blocked on two facts: KeyCape and the OpenBao role must accept the exact +loopback callback, and an attended MFA login must pass. The host-namespace +preflight already proves `openbao-ui-railiance01` lifecycle-healthy and reaches +the expected overlay. Then execute the guarded retraction, coordinate public +DNS withdrawal with railiance-infra, and return non-secret acceptance evidence +to Railiance Master. + +This workplan authorizes no OpenBao seal/unseal, policy broadening, PVC or +Secret mutation, reboot, restore, or RMASTER-WP-0020-T08 cleanup.