diff --git a/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml b/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml index 1e4988c..41c1faf 100644 --- a/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml +++ b/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml @@ -97,6 +97,14 @@ spec: readOnly: true {{- end }} env: + {{- if .Values.renamePreflight.enabled }} + - name: REPOSITORY_RENAME_PREFLIGHT_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.renamePreflight.secretName | quote }} + key: REPOSITORY_RENAME_PREFLIGHT_SECRET + optional: false + {{- end }} - name: HOME value: /tmp {{- if .Values.sweep.enabled }} diff --git a/deploy/railiance/apps/charts/state-hub/values.yaml b/deploy/railiance/apps/charts/state-hub/values.yaml index bc7bf9f..97bb087 100644 --- a/deploy/railiance/apps/charts/state-hub/values.yaml +++ b/deploy/railiance/apps/charts/state-hub/values.yaml @@ -190,3 +190,8 @@ sweep: hostname: "" hostPath: /home/tegwick sshHostPath: /home/tegwick/.ssh + +# RPF-WP-0035-T04: API-only, separately owned ESO Secret; no secret values here. +renamePreflight: + enabled: false + secretName: state-hub-rename-preflight diff --git a/tests/test_preflight_signing_rotation.py b/tests/test_preflight_signing_rotation.py new file mode 100644 index 0000000..ae0e3d6 --- /dev/null +++ b/tests/test_preflight_signing_rotation.py @@ -0,0 +1,38 @@ +"""Single-key invalidation guarantees needed by the platform rotation fence.""" +from datetime import datetime, timedelta, timezone + +import pytest + +from api.config import settings +from api.services.repository_rename import ( + RenamePreconditionFailed, RenameServiceUnavailable, + _sign_preflight, _verify_preflight_token, +) + + +def test_rotation_invalidates_predecessor_and_accepts_successor(monkeypatch): + payload = {'expires_at': (datetime.now(timezone.utc) + timedelta(minutes=15)).isoformat()} + monkeypatch.setattr(settings, 'repository_rename_preflight_secret', 'fixture-old-key') + predecessor = _sign_preflight(payload) + assert _verify_preflight_token(predecessor) == payload + monkeypatch.setattr(settings, 'repository_rename_preflight_secret', 'fixture-new-key') + with pytest.raises(RenamePreconditionFailed): + _verify_preflight_token(predecessor) + assert _verify_preflight_token(_sign_preflight(payload)) == payload + + +def test_tamper_expiry_and_no_key_fail_closed(monkeypatch): + monkeypatch.setattr(settings, 'repository_rename_preflight_secret', 'fixture-key') + future = {'expires_at': (datetime.now(timezone.utc) + timedelta(minutes=15)).isoformat()} + token = _sign_preflight(future) + encoded, signature = token.split('.') + with pytest.raises(RenamePreconditionFailed): + _verify_preflight_token(encoded + '.' + ('A' if signature[0] != 'A' else 'B') + signature[1:]) + past = {'expires_at': (datetime.now(timezone.utc) - timedelta(seconds=1)).isoformat()} + with pytest.raises(RenamePreconditionFailed, match='expired'): + _verify_preflight_token(_sign_preflight(past)) + monkeypatch.setattr(settings, 'repository_rename_preflight_secret', None) + with pytest.raises(RenameServiceUnavailable): + _sign_preflight(future) + with pytest.raises(RenameServiceUnavailable): + _verify_preflight_token(token) diff --git a/workplans/STATE-WP-0088-preflight-signing-runtime-acceptance.md b/workplans/STATE-WP-0088-preflight-signing-runtime-acceptance.md new file mode 100644 index 0000000..b9f5eb6 --- /dev/null +++ b/workplans/STATE-WP-0088-preflight-signing-runtime-acceptance.md @@ -0,0 +1,50 @@ +--- +id: STATE-WP-0088 +type: workplan +title: "Accept the platform preflight signing lane in the State Hub API" +domain: infotech +repo: state-hub +status: active +owner: codex +topic_slug: infotech +created: "2026-09-05" +updated: "2026-09-05" +related: + - RPF-WP-0035 + - STATE-WP-0085 + - FLEX-WP-0020 +quality_dor: DoR-Ok +quality_dor_note: "Exact platform design and user-requested task reviewed against live primary; bounded API-only delivery and controlled-outage rotation fence." +--- + +## Wire and validate API-only delivery + +```task +id: STATE-WP-0088-T01 +status: done +priority: high +``` + +Chart opt-in `renamePreflight.enabled` adds a required explicit Secret ref only +to the API container. Default disabled; no plaintext chart values or shared env +Secret ownership. Helm rendering proves MCP/migration exclusion. Existing +repository-rename API tests pass (13 tests). Platform owns CCR-2026-0015 and ESO. + +## Accept live signing and fenced rotation + +```task +id: STATE-WP-0088-T02 +status: progress +priority: high +``` + +Fresh live flex-auth -> access-engine preflight returns exactly the +`preflight_signing_unavailable` blocker. Target is primary/railiance01, +namespace/release/deployment state-hub, current API SA state-hub and one replica. +After platform custody verification, enable the chart, prove API-only delivery, +all-replica key equality, health and non-mutating signed preflight. Then stop all +API replicas (including terminating pods), rotate with CAS through platform, +wait ESO, restart and prove predecessor invalidation and forward recovery. +Runbook: railiance-platform/docs/credential-lane-designs/state-hub-preflight-activation.md. +No repository rename is in scope. Live completion is pending attended OpenBao +OIDC/MFA; ambient session returned 403.