Finish WP-0025 SSO cutover except group allowlist residual.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 29s

Mark T03–T05 and T07–T08 done after live Authelia/TLS verification,
prefer SSO principal in ops UI copy and audits, and document break-glass
port-forward. Leave T06 waiting on net-kingdom LLDAP/Authelia group rules.
This commit is contained in:
tegwick 2026-07-22 10:23:36 +02:00
parent 27c087bcb4
commit 91353df7d0
8 changed files with 151 additions and 67 deletions

View file

@ -213,7 +213,7 @@ Open product/policy workplan: **ACTIVITY-WP-0022** (IssueSink no-default-Forgejo
| **G7. Credential delivery** | Low (residual) | **FORGEJO_TOKEN** via ESO `actcore-forgejo-admin` (WP-0023-T05, Ready). issue-core `GITEA_BACKEND_TOKEN` still 503 forgejo-inbox for path A rest — **issue-core owner** (WP-0023-T06). | | **G7. Credential delivery** | Low (residual) | **FORGEJO_TOKEN** via ESO `actcore-forgejo-admin` (WP-0023-T05, Ready). issue-core `GITEA_BACKEND_TOKEN` still 503 forgejo-inbox for path A rest — **issue-core owner** (WP-0023-T06). |
| **G8. Live-images hygiene** | Medium (ops) | Multi-cluster `live-images-all.txt` must be refreshed after deploys or prune can delete live tags (incident 2026-07-21, restored). `scripts/refresh_live_images.sh` (T04). | | **G8. Live-images hygiene** | Medium (ops) | Multi-cluster `live-images-all.txt` must be refreshed after deploys or prune can delete live tags (incident 2026-07-21, restored). `scripts/refresh_live_images.sh` (T04). |
| **G9. Evidence federation** | Low | Progress often lands on railiance01 edge/hub; workstation primary hub may not show the same feed without tunnel/outbox health. | | **G9. Evidence federation** | Low | Progress often lands on railiance01 edge/hub; workstation primary hub may not show the same feed without tunnel/outbox health. |
| **G10. API external access** | Medium (in progress) | WP-0025: Traefik + Authelia SSO for `activity.coulomb.social` + `temporal.coulomb.social`; port-forward remains break-glass until DNS/certs Ready. | | **G10. API external access** | Medium (mostly done) | WP-0025: Traefik + Authelia SSO live for `activity.coulomb.social` + `temporal.coulomb.social`; port-forward break-glass; residual T06 group allowlist. |
### Drift risks ### Drift risks

View file

@ -145,12 +145,12 @@
| task | ACTIVITY-WP-0024-T09 | done | — | workplans/ACTIVITY-WP-0024-operator-automation-console.md | | task | ACTIVITY-WP-0024-T09 | done | — | workplans/ACTIVITY-WP-0024-operator-automation-console.md |
| task | ACTIVITY-WP-0025-T01 | done | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md | | task | ACTIVITY-WP-0025-T01 | done | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md |
| task | ACTIVITY-WP-0025-T02 | done | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md | | task | ACTIVITY-WP-0025-T02 | done | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md |
| task | ACTIVITY-WP-0025-T03 | progress | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md | | task | ACTIVITY-WP-0025-T03 | done | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md |
| task | ACTIVITY-WP-0025-T04 | progress | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md | | task | ACTIVITY-WP-0025-T04 | done | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md |
| task | ACTIVITY-WP-0025-T05 | progress | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md | | task | ACTIVITY-WP-0025-T05 | done | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md |
| task | ACTIVITY-WP-0025-T06 | wait | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md | | task | ACTIVITY-WP-0025-T06 | wait | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md |
| task | ACTIVITY-WP-0025-T07 | progress | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md | | task | ACTIVITY-WP-0025-T07 | done | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md |
| task | ACTIVITY-WP-0025-T08 | todo | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md | | task | ACTIVITY-WP-0025-T08 | done | — | workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md |
| task | ADHOC-2026-06-01-T01 | done | — | workplans/ADHOC-2026-06-01.md | | task | ADHOC-2026-06-01-T01 | done | — | workplans/ADHOC-2026-06-01.md |
| task | ADHOC-2026-06-01-T02 | done | — | workplans/ADHOC-2026-06-01.md | | task | ADHOC-2026-06-01-T02 | done | — | workplans/ADHOC-2026-06-01.md |
| task | ADHOC-2026-06-01-T03 | done | — | workplans/ADHOC-2026-06-01.md | | task | ADHOC-2026-06-01-T03 | done | — | workplans/ADHOC-2026-06-01.md |

View file

@ -12,8 +12,10 @@ Founders / platform operators who already have Authelia accounts under
| `activity.coulomb.social` | `actcore-api:8010` | Ops console `/ops/ui`, JSON `/ops/*`, existing API | | `activity.coulomb.social` | `actcore-api:8010` | Ops console `/ops/ui`, JSON `/ops/*`, existing API |
| `temporal.coulomb.social` | `actcore-temporal-ui:8080` | Temporal Web UI (clean short name) | | `temporal.coulomb.social` | `actcore-temporal-ui:8080` | Temporal Web UI (clean short name) |
Both resolve to the railiance01 Traefik LB (`92.205.62.239`) once DNS A records Both resolve to the railiance01 Traefik LB (`92.205.62.239`) — same pattern as
exist (same pattern as `forgejo.coulomb.social`). `forgejo.coulomb.social`. **DNS A records + Let's Encrypt certs are live**
(verified 2026-07-22); Certificate objects `actcore-ops-tls` /
`actcore-temporal-ui-tls` are Ready.
## Auth edge (fleet pattern) ## Auth edge (fleet pattern)
@ -59,20 +61,17 @@ Priority for ops mutations (`POST /ops/...`):
## DNS (operator) ## DNS (operator)
Create A records (or CNAME to the forgejo host pattern): Expected records (live as of 2026-07-22):
```text ```text
activity.coulomb.social A 92.205.62.239 activity.coulomb.social A 92.205.62.239
temporal.coulomb.social A 92.205.62.239 temporal.coulomb.social A 92.205.62.239
``` ```
**Important:** do **not** leave a stale **AAAA** (IPv6) record for **Important:** do **not** leave a stale **AAAA** (IPv6) record for either host
`activity.coulomb.social` pointing at parking (e.g. IONOS pointing at parking (e.g. IONOS `2001:8d8:100f:f000::200` / `217.160.0.253`).
`2001:8d8:100f:f000::200` / `217.160.0.253`). Cluster resolvers prefer AAAA; Cluster resolvers prefer AAAA; cert-manager HTTP-01 self-check then hits the
cert-manager HTTP-01 self-check then hits the wrong host and returns 204. wrong host. Either remove AAAA or set it to the railiance01 public IPv6.
Either remove AAAA or set it to the railiance01 public IPv6.
Until DNS is correct, cert-manager Certificate may stay Pending.
## Break-glass ## Break-glass

View file

@ -57,18 +57,23 @@ Prefer the **ops console** over ad-hoc SSH/SQL for “did automations run?” an
### Auth ### Auth
| Mode | When | How |
| --- | --- | --- |
| **SSO (primary)** | Browser via `activity.coulomb.social` | Authelia session; app trusts `Remote-User` / `Remote-Email` from Traefik ForwardAuth |
| **Break-glass token** | Port-forward / emergency / scripts | `X-Operator-Token` or `Authorization: Bearer` |
| **Local dev** | No token configured | `ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS=1` only |
| Env | Purpose | | Env | Purpose |
| --- | --- | | --- | --- |
| `ACTIVITY_CORE_OPERATOR_TOKEN` | Shared operator token; required for **mutations** | | `ACTIVITY_CORE_OPERATOR_TOKEN` | Shared operator token (break-glass); custody in `actcore-runtime-secret` |
| `ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS` | `1` only for local dev without a token | | `ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS` | `1` only for local dev without a token |
Mutations: `POST /ops/automations/{id}/trigger|enable|disable|pause|unpause` Mutations: `POST /ops/automations/{id}/trigger|enable|disable|pause|unpause`.
Header: `X-Operator-Token: <token>` (or `Authorization: Bearer <token>`).
Fail-closed: if the token is unset and unauth is not allowed, mutations return Fail-closed: without SSO headers and without a valid token (and unauth not
**403**. Reads (`GET /ops/...`) do not require the token (ClusterIP / port-forward allowed), mutations return **401/403**. Reads (`GET /ops/...`) do not require
posture). **Do not** put the token in git, chat, or workplans. Store in auth at the app layer (ingress still gates browser access via Authelia).
`actcore-runtime-secret` (or local `.env`) via operator custody. **Do not** put the token in git, chat, or workplans.
### Daily checklist ### Daily checklist
@ -99,13 +104,13 @@ curl -sS -X POST "http://localhost:8010/ops/automations/<id>/disable" \
-H "X-Operator-Token: $ACTIVITY_CORE_OPERATOR_TOKEN" -H "X-Operator-Token: $ACTIVITY_CORE_OPERATOR_TOKEN"
``` ```
Thin UI: open `/ops/ui`, paste the operator token into the browser field Thin UI: open https://activity.coulomb.social/ops/ui (SSO). Break-glass UI still
(localStorage only), then use Run now / pause actions. **Cron edits are not in accepts a pasted operator token (localStorage only). **Cron edits are not in
the UI** — change definition files and sync. the UI** — change definition files and sync.
### Production access (railiance01) ### Production access (railiance01)
**Primary (SSO — ACTIVITY-WP-0025):** after DNS A records exist: **Primary (SSO — ACTIVITY-WP-0025, live):**
| UI | URL | | UI | URL |
| --- | --- | | --- | --- |
@ -113,12 +118,12 @@ the UI** — change definition files and sync.
| Temporal Web UI | https://temporal.coulomb.social | | Temporal Web UI | https://temporal.coulomb.social |
Login via Authelia (`auth.coulomb.social`). Design: `docs/ops-sso-access.md`. Login via Authelia (`auth.coulomb.social`). Design: `docs/ops-sso-access.md`.
Mutations accept SSO identity headers; shared token is break-glass only. Mutations use SSO identity; shared token is break-glass only.
**DNS (required for TLS):** **DNS (already set for TLS):**
```text ```text
activity.coulomb.social A 92.205.62.239 activity.coulomb.social A 92.205.62.239
temporal.coulomb.social A 92.205.62.239 temporal.coulomb.social A 92.205.62.239
``` ```

View file

@ -1,9 +1,9 @@
# Railiance01 Kubernetes Deployment # Railiance01 Kubernetes Deployment
This bundle establishes activity-core as an internal production service on the This bundle establishes activity-core as an internal production service on the
railiance01 K3s cluster. It keeps the unauthenticated API as a ClusterIP service; railiance01 K3s cluster. Services remain ClusterIP; browser access to the ops
publish it through an authenticated ingress only after choosing the final host console and Temporal UI is via Traefik + Authelia SSO Ingress
name and access policy. (`activity.coulomb.social`, `temporal.coulomb.social` — ACTIVITY-WP-0025).
## Layout ## Layout
@ -113,23 +113,26 @@ kubectl -n activity-core get svc
## Operator automation console (ACTIVITY-WP-0024 / 0025) ## Operator automation console (ACTIVITY-WP-0024 / 0025)
### SSO (primary, after DNS) ### SSO (primary — live)
Manifests `30-``32-*.yaml` are applied; TLS certs Ready; Authelia ForwardAuth
redirects unauthenticated browsers to `auth.coulomb.social`.
```bash ```bash
# DNS A records → 92.205.62.239 (once): # Re-apply if needed:
# activity.coulomb.social
# temporal.coulomb.social
kubectl apply -f k8s/railiance/30-authelia-middleware.yaml kubectl apply -f k8s/railiance/30-authelia-middleware.yaml
kubectl apply -f k8s/railiance/31-ingress-ops-sso.yaml kubectl apply -f k8s/railiance/31-ingress-ops-sso.yaml
kubectl apply -f k8s/railiance/32-ingress-temporal-sso.yaml kubectl apply -f k8s/railiance/32-ingress-temporal-sso.yaml
kubectl -n activity-core set env deploy/actcore-api \ kubectl -n activity-core set env deploy/actcore-api \
ACTIVITY_CORE_TEMPORAL_UI_URL=https://temporal.coulomb.social ACTIVITY_CORE_TEMPORAL_UI_URL=https://temporal.coulomb.social
kubectl -n activity-core set env deploy/actcore-temporal-ui \
TEMPORAL_CORS_ORIGINS=https://temporal.coulomb.social,http://localhost:8080,http://127.0.0.1:8080
``` ```
- Ops: https://activity.coulomb.social/ops/ui (Authelia SSO) - Ops: https://activity.coulomb.social/ops/ui (Authelia SSO)
- Temporal: https://temporal.coulomb.social - Temporal: https://temporal.coulomb.social
- Design: `docs/ops-sso-access.md` - Design: `docs/ops-sso-access.md`
- Follow-up: LLDAP group `activity-core-operators` + Authelia domain rules (T06)
### Break-glass port-forward ### Break-glass port-forward

View file

@ -248,15 +248,20 @@ async def auth_status() -> dict[str, Any]:
"yes", "yes",
"on", "on",
} }
temporal = temporal_ui_url()
return { return {
"operator_token_configured": operator_token_configured(), "operator_token_configured": operator_token_configured(),
"mutation_header": HEADER_NAME, "mutation_header": HEADER_NAME,
# True when a shared token is required for *break-glass* / non-SSO clients.
# Browser SSO (Authelia Remote-User / Remote-Email) does not need the token.
"mutations_require_token": operator_token_configured() or not allow, "mutations_require_token": operator_token_configured() or not allow,
"temporal_ui_url": temporal_ui_url(), "sso_preferred": True,
"sso_docs": "/docs not required — see docs/ops-sso-access.md", "sso_headers": ["Remote-User", "Remote-Email", "Remote-Groups"],
"temporal_ui_url": temporal,
"sso_docs": "docs/ops-sso-access.md",
"public_hosts": { "public_hosts": {
"ops": "https://activity.coulomb.social", "ops": "https://activity.coulomb.social",
"temporal_ui": "https://temporal.coulomb.social", "temporal_ui": temporal,
}, },
} }
@ -309,7 +314,7 @@ def _page(title: str, body: str) -> HTMLResponse:
<a href="/ops/ui">Inventory</a> <a href="/ops/ui">Inventory</a>
<a href="/ops/ui/status?since=sunday">Status</a> <a href="/ops/ui/status?since=sunday">Status</a>
<a class="external" href="{temporal_href}" target="_blank" rel="noopener noreferrer" <a class="external" href="{temporal_href}" target="_blank" rel="noopener noreferrer"
title="Temporal Web UI — port-forward actcore-temporal-ui :8080 until SSO ingress">Temporal UI</a> title="Temporal Web UI (SSO: temporal.coulomb.social)">Temporal UI</a>
<a href="/ops/auth/status">Auth JSON</a> <a href="/ops/auth/status">Auth JSON</a>
</nav> </nav>
<hr/> <hr/>
@ -351,12 +356,15 @@ def _token_form() -> str:
configured = "yes" if operator_token_configured() else "no" configured = "yes" if operator_token_configured() else "no"
return f""" return f"""
<div class="card"> <div class="card">
<label>Operator token (browser only): <p><strong>Auth:</strong> On
<a href="https://activity.coulomb.social/ops/ui">activity.coulomb.social</a>,
Authelia SSO identity is preferred for mutations (no token paste needed).</p>
<label>Break-glass operator token (port-forward / emergency only):
<input id="op-token" type="password" placeholder="X-Operator-Token value" autocomplete="off"/> <input id="op-token" type="password" placeholder="X-Operator-Token value" autocomplete="off"/>
</label> </label>
<button type="button" onclick="saveToken()">Save token</button> <button type="button" onclick="saveToken()">Save token</button>
<p class="muted">Server token configured: <strong>{configured}</strong>. <p class="muted">Shared token configured on server: <strong>{configured}</strong>.
Mutations require header <code>{html.escape(HEADER_NAME)}</code>.</p> Header <code>{html.escape(HEADER_NAME)}</code> only when not using SSO.</p>
</div> </div>
""" """
@ -405,8 +413,7 @@ async def ui_index() -> HTMLResponse:
<p class="muted">Temporal Web UI (workflow debugger): <p class="muted">Temporal Web UI (workflow debugger):
<a class="external" href="{html.escape(temporal_ui_url(), quote=True)}" <a class="external" href="{html.escape(temporal_ui_url(), quote=True)}"
target="_blank" rel="noopener noreferrer">{html.escape(temporal_ui_url())}</a> target="_blank" rel="noopener noreferrer">{html.escape(temporal_ui_url())}</a>
requires port-forward of <code>svc/actcore-temporal-ui 8080:8080</code> primary SSO URL; port-forward of <code>svc/actcore-temporal-ui</code> is break-glass only.</p>
until SSO ingress (ACTIVITY-WP-0025).</p>
""" """
return _page("Inventory", body) return _page("Inventory", body)

View file

@ -67,6 +67,53 @@ async def test_trigger_requires_token(ops_app: FastAPI, monkeypatch: pytest.Monk
assert res.status_code == 401 assert res.status_code == 401
@pytest.mark.asyncio
async def test_trigger_with_sso_principal(
ops_app: FastAPI, monkeypatch: pytest.MonkeyPatch
) -> None:
"""SSO Remote-User is preferred; audit must record sso:<user>, never the token."""
def_id = uuid.uuid4()
row = MagicMock()
row.name = "Weekly SBOM"
row.context_sources = []
row.task_templates = []
row.trigger_config = {"trigger_type": "cron", "cron_expression": "0 9 * * 1"}
session = AsyncMock()
session.get = AsyncMock(return_value=row)
session.__aenter__ = AsyncMock(return_value=session)
session.__aexit__ = AsyncMock(return_value=None)
ops_app.state.session_factory.return_value = session
handle = MagicMock()
handle.id = f"activity-{def_id}:manual-sso"
ops_app.state.temporal.start_workflow = AsyncMock(return_value=handle)
captured: dict[str, Any] = {}
async def capture_audit(**kwargs: Any) -> dict[str, Any]:
captured.update(kwargs)
return {
"action": kwargs["action"],
"audit_id": "sso-a1",
"principal": kwargs["principal"],
}
monkeypatch.setattr("activity_core.ops_api.record_ops_audit", capture_audit)
transport = ASGITransport(app=ops_app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
res = await client.post(
f"/ops/automations/{def_id}/trigger",
headers={"Remote-User": "alice.operator"},
json={},
)
assert res.status_code == 200
assert res.json()["audit"]["principal"] == "sso:alice.operator"
assert captured["principal"] == "sso:alice.operator"
assert "token" not in str(res.json()).lower() or "test-token" not in str(res.json())
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_trigger_with_token(ops_app: FastAPI, monkeypatch: pytest.MonkeyPatch) -> None: async def test_trigger_with_token(ops_app: FastAPI, monkeypatch: pytest.MonkeyPatch) -> None:
def_id = uuid.uuid4() def_id = uuid.uuid4()
@ -216,7 +263,7 @@ async def test_ui_index_renders(ops_app: FastAPI, monkeypatch: pytest.MonkeyPatc
res = await client.get("/ops/ui/") res = await client.get("/ops/ui/")
assert res.status_code == 200 assert res.status_code == 200
assert "Daily Triage" in res.text assert "Daily Triage" in res.text
assert "Operator token" in res.text assert "SSO" in res.text or "Break-glass" in res.text
assert "Temporal UI" in res.text assert "Temporal UI" in res.text
assert "temporal.coulomb.social" in res.text assert "temporal.coulomb.social" in res.text

View file

@ -116,7 +116,7 @@ state_hub_task_id: "650db102-ad4e-4d8e-86c1-eee50f5bafcc"
```task ```task
id: ACTIVITY-WP-0025-T03 id: ACTIVITY-WP-0025-T03
status: progress status: done
priority: high priority: high
state_hub_task_id: "7084f7d5-4181-4dfa-b239-0dad15efbae9" state_hub_task_id: "7084f7d5-4181-4dfa-b239-0dad15efbae9"
``` ```
@ -134,7 +134,7 @@ port-forward; unauthorized gets login or 403.
```task ```task
id: ACTIVITY-WP-0025-T04 id: ACTIVITY-WP-0025-T04
status: progress status: done
priority: high priority: high
state_hub_task_id: "99b36f31-eb19-4f96-b3f1-9ac1ab02d78f" state_hub_task_id: "99b36f31-eb19-4f96-b3f1-9ac1ab02d78f"
``` ```
@ -151,7 +151,7 @@ port-forward, under SSO.
```task ```task
id: ACTIVITY-WP-0025-T05 id: ACTIVITY-WP-0025-T05
status: progress status: done
priority: high priority: high
state_hub_task_id: "77fff202-bfb6-49eb-b8e5-3760abacc8bd" state_hub_task_id: "77fff202-bfb6-49eb-b8e5-3760abacc8bd"
``` ```
@ -186,7 +186,7 @@ account.
```task ```task
id: ACTIVITY-WP-0025-T07 id: ACTIVITY-WP-0025-T07
status: progress status: done
priority: medium priority: medium
state_hub_task_id: "c5945e64-c460-4830-a19e-a944bb44a67e" state_hub_task_id: "c5945e64-c460-4830-a19e-a944bb44a67e"
``` ```
@ -201,7 +201,7 @@ state_hub_task_id: "c5945e64-c460-4830-a19e-a944bb44a67e"
```task ```task
id: ACTIVITY-WP-0025-T08 id: ACTIVITY-WP-0025-T08
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "f73eafbf-ad11-4d7d-b89e-e910c662ce86" state_hub_task_id: "f73eafbf-ad11-4d7d-b89e-e910c662ce86"
``` ```
@ -215,12 +215,12 @@ state_hub_task_id: "f73eafbf-ad11-4d7d-b89e-e910c662ce86"
## Success criteria ## Success criteria
- [ ] HTTPS SSO URLs for ops console and Temporal UI (no port-forward required) - [x] HTTPS SSO URLs for ops console and Temporal UI (no port-forward required)
- [ ] Only authorized IdP group can access - [ ] Only authorized IdP group can access — **MVP: any Authelia user; T06 residual**
- [ ] Ops mutations attribute to SSO identity; audits show principal - [x] Ops mutations attribute to SSO identity; audits show principal (`sso:<user>`)
- [ ] Ops nav Temporal link points at SSO Temporal URL - [x] Ops nav Temporal link points at SSO Temporal URL
- [ ] Runbook documents SSO primary + break-glass port-forward - [x] Runbook documents SSO primary + break-glass port-forward
- [ ] SCOPE G10 updated to reflect UI SSO posture - [x] SCOPE G10 updated to reflect UI SSO posture
## Dependencies / coordination ## Dependencies / coordination
@ -250,13 +250,36 @@ state_hub_task_id: "f73eafbf-ad11-4d7d-b89e-e910c662ce86"
| --- | --- | --- | | --- | --- | --- |
| T01 | done | Hosts: activity.coulomb.social + temporal.coulomb.social; docs/ops-sso-access.md | | T01 | done | Hosts: activity.coulomb.social + temporal.coulomb.social; docs/ops-sso-access.md |
| T02 | done | Reuse Authelia + Traefik ForwardAuth (sso ns); not oauth2-proxy | | T02 | done | Reuse Authelia + Traefik ForwardAuth (sso ns); not oauth2-proxy |
| T03 | progress | Ingress + middleware manifests landed; **blocked on DNS A records** for cert | | T03 | done | Ingress + cert Ready; unauth → 302 auth.coulomb.social |
| T04 | progress | Temporal Ingress manifest + CORS env; same DNS gate | | T04 | done | Temporal Ingress + cert Ready; `ACTIVITY_CORE_TEMPORAL_UI_URL` + CORS set |
| T05 | progress | SSO headers preferred over shared token in ops_auth | | T05 | done | SSO `Remote-User` preferred; token break-glass; tests + redeploy |
| T06 | wait | LLDAP group activity-core-operators + Authelia rules (net-kingdom) | | T06 | wait | LLDAP group activity-core-operators + Authelia domain rules (net-kingdom) |
| T07 | progress | Runbook + design doc updated | | T07 | done | Runbook + k8s README + design doc: SSO primary, port-forward break-glass |
| T08 | todo | After DNS/cert Ready: smoke HTTPS + SSO login | | T08 | done | See verification checklist below |
**Operator action required:** create DNS A records for both hosts → 92.205.62.239, ### Verification checklist (T08) — 2026-07-22
then apply `k8s/railiance/30-*.yaml``32-*.yaml` and set
`ACTIVITY_CORE_TEMPORAL_UI_URL=https://temporal.coulomb.social` on actcore-api. | Check | Result |
| --- | --- |
| DNS A → 92.205.62.239 for both hosts | ok |
| Certificate Ready (`actcore-ops-tls`, `actcore-temporal-ui-tls`) | ok |
| Unauth HTTPS `/ops/ui` → Authelia login redirect | ok |
| Unauth HTTPS Temporal host → Authelia login redirect | ok |
| Services remain ClusterIP (no public LB) | ok |
| Mutation without SSO/token → 401 fail-closed | ok |
| Mutation with `Remote-User` → auth passes (404 only if unknown def) | ok |
| `ACTIVITY_CORE_TEMPORAL_UI_URL=https://temporal.coulomb.social` | ok |
| `TEMPORAL_CORS_ORIGINS` includes public Temporal host | ok |
| Unit tests `test_ops_auth` + `test_ops_console_api` | 18 passed |
| Image `activity-core:railiance01-prod` rebuilt, imported, api/worker/router rolled | ok |
| NetworkPolicy ingress→services | **skipped** (optional; ClusterIP + Authelia sufficient for MVP) |
| Full browser login + Run now under real MFA session | **operator smoke** (credentials not in agent) |
### Residual (keeps WP active)
**T06** — restrict Authelia access to LLDAP group `activity-core-operators`
(net-kingdom Authelia access_control rules). Until then any authenticated
Authelia user can reach the UIs (org-wide SSO, not least-privilege).
**Operator:** open https://activity.coulomb.social/ops/ui once, confirm inventory
loads and a safe mutation (or dry path) shows `sso:<you>` in `/ops/audits`.