From 9a7ae8b59a67d7127c8a313b6bc4ecd422d64b31 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 8 Jul 2026 00:04:38 +0200 Subject: [PATCH] Add ExternalSecret for ISSUE_CORE_API_KEY on Railiance Sync the shared issue-core ingestion key from OpenBao into actcore-runtime-secret via External Secrets, with an interim coulombcore ClusterSecretStore bootstrap script and deploy docs. Removes manual key injection from bootstrap-secrets.sh. --- Makefile | 3 + SCOPE.md | 12 ++-- docs/issue-core-emission-boundary.md | 7 ++- .../15-externalsecret-issue-core.yaml | 35 +++++++++++ k8s/railiance/README.md | 22 +++++-- k8s/railiance/bootstrap-secrets.sh | 9 ++- scripts/openbao-eso-token-apply.sh | 60 +++++++++++++++++++ 7 files changed, 128 insertions(+), 20 deletions(-) create mode 100644 k8s/railiance/15-externalsecret-issue-core.yaml create mode 100755 scripts/openbao-eso-token-apply.sh diff --git a/Makefile b/Makefile index b8ca422..9a0abd5 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,9 @@ railiance-up: ## Build image and start full railiance stack (no Elasticsearch) railiance-down: ## Stop and remove railiance stack containers docker compose -f docker-compose.railiance.yml down +openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token for railiance01 activity-core + OPENBAO_TOKEN_FILE="$(HOME)/.local/openbao/platform-admin.token" bash scripts/openbao-eso-token-apply.sh + # ── Local dev processes ─────────────────────────────────────────────────────── start-worker: ## Start Temporal worker (reads env from .env if present) diff --git a/SCOPE.md b/SCOPE.md index c36ade7..873fe31 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -181,12 +181,12 @@ The two evaluation modes: runtime projection failure. The ops inventory probe path has live fallback evidence in State Hub; Inter-Hub per-entity submission remains deferred. - **Task emission posture**: the issue-core REST sink is implemented and - Railiance runtime ConfigMap sets `ISSUE_SINK_TYPE=rest`. The OpenBao ingestion - lane (`RAILIANCE-WP-0009`) is finished; live emission still requires the - operator to populate `ISSUE_CORE_API_KEY` in `actcore-runtime-secret` with the - same value as issue-core. `weekly-sbom-staleness` is the canonical promotion - candidate; use `scripts/smoke_issue_core_emission.py` before trusting production - task creation. + Railiance runtime ConfigMap sets `ISSUE_SINK_TYPE=rest`. `ISSUE_CORE_API_KEY` + is synced into `actcore-runtime-secret` by ExternalSecret + `actcore-issue-core-runtime` (OpenBao path shared with issue-core). Bootstrap + the ESO token with `make openbao-eso-token-apply`. `weekly-sbom-staleness` is + the canonical promotion candidate; use `scripts/smoke_issue_core_emission.py` + before trusting production task creation. - **Stability**: construction risk has shifted to operational hardening and adoption risk. The last recorded full-suite pass in the workplans was 2026-06-04 (`128 passed, 1 skipped`), with later targeted coverage added for diff --git a/docs/issue-core-emission-boundary.md b/docs/issue-core-emission-boundary.md index d42a31e..087172e 100644 --- a/docs/issue-core-emission-boundary.md +++ b/docs/issue-core-emission-boundary.md @@ -73,9 +73,10 @@ review and one live `IssueCoreRestSink` smoke against the target endpoint. ISSUE_CORE_URL=http://127.0.0.1:8765 ISSUE_CORE_API_KEY=... \ uv run python scripts/smoke_issue_core_emission.py --live ``` -5. Set `ISSUE_SINK_TYPE=rest` in `actcore-runtime-config`, ensure - `actcore-runtime-secret` includes `ISSUE_CORE_API_KEY`, and restart - `actcore-worker` / `actcore-event-router`. +5. Set `ISSUE_SINK_TYPE=rest` in `actcore-runtime-config`, apply + `k8s/railiance/15-externalsecret-issue-core.yaml` so External Secrets merges + `ISSUE_CORE_API_KEY` into `actcore-runtime-secret`, and restart + `actcore-worker` / `actcore-event-router` after the ExternalSecret is Ready. 6. Trigger one known-safe run (weekly SBOM staleness on a stale fixture or manual `/activity-definitions//trigger`) and confirm `task_spawn_log` stores the real `issue_id` returned by issue-core. diff --git a/k8s/railiance/15-externalsecret-issue-core.yaml b/k8s/railiance/15-externalsecret-issue-core.yaml new file mode 100644 index 0000000..dcd8732 --- /dev/null +++ b/k8s/railiance/15-externalsecret-issue-core.yaml @@ -0,0 +1,35 @@ +# Sync ISSUE_CORE_API_KEY from OpenBao into actcore-runtime-secret. +# +# Prereqs on railiance01: +# - External Secrets Operator (namespace external-secrets) +# - ClusterSecretStore openbao-activity-core (railiance-platform addon) +# - Secret external-secrets/openbao-activity-core-eso-token (scripts/openbao-eso-token-apply.sh) +# +# OpenBao path: platform/workloads/issue-core/issue-core/issue-core-runtime +# property: ISSUE_CORE_API_KEY (shared with issue-core ingestion) +# +# Merge keeps bootstrap-owned keys (ACTCORE_DB_URL, webhook secrets) intact. +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: actcore-issue-core-runtime + namespace: activity-core + labels: + app.kubernetes.io/name: activity-core + app.kubernetes.io/part-of: activity-core + annotations: + argocd.argoproj.io/sync-wave: "0" +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: openbao-activity-core + target: + name: actcore-runtime-secret + creationPolicy: Merge + deletionPolicy: Retain + data: + - secretKey: ISSUE_CORE_API_KEY + remoteRef: + key: platform/workloads/issue-core/issue-core/issue-core-runtime + property: ISSUE_CORE_API_KEY \ No newline at end of file diff --git a/k8s/railiance/README.md b/k8s/railiance/README.md index 6e99216..235f182 100644 --- a/k8s/railiance/README.md +++ b/k8s/railiance/README.md @@ -10,6 +10,8 @@ name and access policy. - `00-namespace.yaml`: namespace and shared labels - `10-infrastructure.yaml`: PostgreSQL for app data, PostgreSQL for Temporal, NATS JetStream, Temporal, and Temporal UI +- `15-externalsecret-issue-core.yaml`: OpenBao → `ISSUE_CORE_API_KEY` merge into + `actcore-runtime-secret` via External Secrets - `20-runtime.yaml`: migrate/sync jobs plus API, worker, and event-router - `bootstrap-secrets.sh`: idempotently creates generated Kubernetes secrets @@ -24,12 +26,15 @@ the ConfigMap projection from that file before enabling the probe schedule. `OPS_HUB_KEY` is created only as an empty Secret placeholder until the operator provisions the Inter-Hub ops-hub key. -`ISSUE_SINK_TYPE` is `rest` in `actcore-runtime-config`. Populate -`ISSUE_CORE_API_KEY` in `actcore-runtime-secret` with the same value as the -issue-core runtime secret (`issue-core/issue-core-runtime` via OpenBao lane -`issue-core-ingestion-api-key`) before trusting live task emission. Roll back -to audit mode by setting `ISSUE_SINK_TYPE=null` and restarting worker and -event-router deployments. See `docs/issue-core-emission-boundary.md`. +`ISSUE_SINK_TYPE` is `rest` in `actcore-runtime-config`. `ISSUE_CORE_API_KEY` +is synced from OpenBao into `actcore-runtime-secret` by ExternalSecret +`actcore-issue-core-runtime` (same path as issue-core: +`platform/workloads/issue-core/issue-core/issue-core-runtime`). Prereqs: +`ClusterSecretStore/openbao-activity-core` and ESO token bootstrap +(`OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token ./scripts/openbao-eso-token-apply.sh`). +Roll back to audit mode by setting +`ISSUE_SINK_TYPE=null` and restarting worker and event-router deployments. +See `docs/issue-core-emission-boundary.md`. The same runtime projection now includes the active `daily-statehub-wsjf-triage.md` ActivityDefinition plus its JSON output schema @@ -61,6 +66,11 @@ ssh railiance01 cd ~/activity-core bash k8s/railiance/bootstrap-secrets.sh kubectl apply -f k8s/railiance/10-infrastructure.yaml +# Bootstrap OpenBao ESO token + apply ExternalSecret (once per cluster): +OPENBAO_TOKEN_FILE=~/.local/openbao/platform-admin.token ./scripts/openbao-eso-token-apply.sh +kubectl apply -f ~/railiance-platform/argocd/platform-addons/openbao-secretstore/openbao-activity-core.clustersecretstore.yaml +kubectl apply -f k8s/railiance/15-externalsecret-issue-core.yaml +kubectl -n activity-core wait --for=condition=Ready externalsecret/actcore-issue-core-runtime --timeout=120s kubectl -n activity-core wait --for=condition=ready pod -l app.kubernetes.io/name=actcore-app-db --timeout=180s kubectl -n activity-core wait --for=condition=ready pod -l app.kubernetes.io/name=actcore-temporal-db --timeout=180s kubectl -n activity-core wait --for=condition=ready pod -l app.kubernetes.io/name=actcore-nats --timeout=180s diff --git a/k8s/railiance/bootstrap-secrets.sh b/k8s/railiance/bootstrap-secrets.sh index 67dfb64..dea48b5 100644 --- a/k8s/railiance/bootstrap-secrets.sh +++ b/k8s/railiance/bootstrap-secrets.sh @@ -37,9 +37,8 @@ if ! secret_exists actcore-runtime-secret; then --from-literal=ACTCORE_DB_URL="$ACTCORE_DB_URL" \ --from-literal=WEBHOOK_SECRET_GITEA="" \ --from-literal=WEBHOOK_SECRET_GITHUB="" \ - --from-literal=OPS_HUB_KEY="" \ - --from-literal=ISSUE_CORE_API_KEY="" + --from-literal=OPS_HUB_KEY="" fi -# When promoting live task emission, patch ISSUE_CORE_API_KEY on -# actcore-runtime-secret to match issue-core's runtime secret. Never commit the -# value; use the OpenBao lane documented in issue-core-ingestion-api-key. +# ISSUE_CORE_API_KEY is merged into actcore-runtime-secret by ExternalSecret +# actcore-issue-core-runtime (k8s/railiance/15-externalsecret-issue-core.yaml). +# Apply that manifest after ClusterSecretStore openbao-activity-core is Ready. diff --git a/scripts/openbao-eso-token-apply.sh b/scripts/openbao-eso-token-apply.sh new file mode 100755 index 0000000..f9609fd --- /dev/null +++ b/scripts/openbao-eso-token-apply.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Mint a read-limited OpenBao token on coulombcore (bao.coulomb.social) and store +# it on railiance01 for ClusterSecretStore openbao-activity-core / ExternalSecret +# actcore-issue-core-runtime. +set -euo pipefail + +POLICY_NAME="${OPENBAO_ACTIVITY_CORE_POLICY:-workload-kv-read-issue-core-runtime}" +POLICY_FILE="${OPENBAO_ACTIVITY_CORE_POLICY_FILE:-$HOME/railiance-platform/openbao/policies/workload-kv-read-issue-core-runtime.hcl}" +BAO_ADDR="${BAO_ADDR:-https://bao.coulomb.social}" +RAILIANCE01_KUBECONFIG="${RAILIANCE01_KUBECONFIG:-$HOME/.kube/config-hosteurope}" +SECRET_NAME="${OPENBAO_ACTIVITY_CORE_ESO_SECRET:-openbao-activity-core-eso-token}" +SECRET_NS="${OPENBAO_ACTIVITY_CORE_ESO_NAMESPACE:-external-secrets}" + +if ! command -v bao >/dev/null 2>&1; then + echo "ERROR: bao CLI not found" >&2 + exit 1 +fi + +if [[ ! -f "$POLICY_FILE" ]]; then + echo "ERROR: policy file missing: $POLICY_FILE" >&2 + exit 1 +fi + +echo "OpenBao addr: $BAO_ADDR" +echo "Policy: $POLICY_NAME" + +if [[ -n "${BAO_TOKEN:-}" ]]; then + : +elif [[ -n "${OPENBAO_TOKEN_FILE:-}" && -f "${OPENBAO_TOKEN_FILE}" ]]; then + BAO_TOKEN="$(head -n 1 "${OPENBAO_TOKEN_FILE}")" +else + read -r -s -p "OpenBao operator token (coulombcore / bao.coulomb.social): " BAO_TOKEN + echo >&2 +fi + +if [[ -z "${BAO_TOKEN:-}" ]]; then + echo "ERROR: empty OpenBao token" >&2 + exit 1 +fi + +export BAO_ADDR BAO_TOKEN + +health="$(curl -fsS "$BAO_ADDR/v1/sys/health")" +if echo "$health" | grep -q '"sealed":true'; then + echo "ERROR: OpenBao at $BAO_ADDR reports sealed" >&2 + exit 1 +fi + +bao policy write "$POLICY_NAME" "$POLICY_FILE" +token_json="$(bao token create -policy="$POLICY_NAME" -display-name="eso-activity-core-issue-sink" -period=720h -format=json)" +token="$(python3 -c "import json,sys; print(json.load(sys.stdin)['auth']['client_token'])" <<<"$token_json")" + +KUBECONFIG="$RAILIANCE01_KUBECONFIG" kubectl create namespace "$SECRET_NS" --dry-run=client -o yaml | KUBECONFIG="$RAILIANCE01_KUBECONFIG" kubectl apply -f - +KUBECONFIG="$RAILIANCE01_KUBECONFIG" kubectl create secret generic "$SECRET_NAME" \ + --namespace "$SECRET_NS" \ + --from-literal=token="$token" \ + --dry-run=client -o yaml | KUBECONFIG="$RAILIANCE01_KUBECONFIG" kubectl apply -f - + +unset BAO_TOKEN token token_json +echo "ok: applied $SECRET_NS/$SECRET_NAME on railiance01 (token not printed)" \ No newline at end of file