diff --git a/SCOPE.md b/SCOPE.md index 544f49e..abb2324 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -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). | | **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. | -| **G10. API external access** | Low | ClusterIP-only; ops mutations use operator token (WP-0024). Public Ingress / OIDC still deferred. | +| **G10. API external access** | Medium (in progress) | WP-0025: Traefik + Authelia SSO for `activity.coulomb.social` + `activity-temporal.coulomb.social`; port-forward remains break-glass until DNS/certs Ready. | ### Drift risks diff --git a/docs/ops-sso-access.md b/docs/ops-sso-access.md new file mode 100644 index 0000000..fe6bfde --- /dev/null +++ b/docs/ops-sso-access.md @@ -0,0 +1,94 @@ +# Ops console + Temporal UI — SSO access design (ACTIVITY-WP-0025) + +## Audience + +Founders / platform operators who already have Authelia accounts under +`coulomb.social` SSO (LLDAP → Authelia → KeyCape MFA path). + +## Hostnames (locked) + +| Host | Backend | Purpose | +| --- | --- | --- | +| `activity.coulomb.social` | `actcore-api:8010` | Ops console `/ops/ui`, JSON `/ops/*`, existing API | +| `activity-temporal.coulomb.social` | `actcore-temporal-ui:8080` | Temporal Web UI | + +Both resolve to the railiance01 Traefik LB (`92.205.62.239`) once DNS A records +exist (same pattern as `forgejo.coulomb.social`). + +## Auth edge (fleet pattern) + +| Component | Location | +| --- | --- | +| Ingress controller | Traefik (`ingressClassName: traefik`) | +| TLS | cert-manager `ClusterIssuer/letsencrypt-prod` | +| Login portal | Authelia `https://auth.coulomb.social` | +| IAM / MFA | KeyCape `https://kc.coulomb.social` (post-password) | +| Forward auth | Traefik Middleware `forwardAuth` → Authelia `/api/verify` | + +**Not** oauth2-proxy — the live railiance01 stack already runs Authelia in +namespace `sso`. Reuse it. + +### Middleware + +`Middleware/authelia-forwardauth` in `activity-core` calls: + +```text +http://authelia.sso.svc.cluster.local:9091/api/verify?rd=https://auth.coulomb.social/ +``` + +Response headers trusted into the app: + +- `Remote-User` +- `Remote-Groups` +- `Remote-Name` +- `Remote-Email` + +### Access control policy + +Authelia global `default_policy: one_factor` currently applies. MVP accepts any +authenticated Authelia user. Follow-up (T06): LLDAP group +`activity-core-operators` + Authelia domain rules (net-kingdom config change). + +## Mutation identity + +Priority for ops mutations (`POST /ops/...`): + +1. SSO principal from Authelia headers (preferred) +2. Shared `ACTIVITY_CORE_OPERATOR_TOKEN` (break-glass) +3. Else 401/403 fail-closed + +## DNS (operator) + +Create A records (or CNAME to the forgejo host pattern): + +```text +activity.coulomb.social A 92.205.62.239 +activity-temporal.coulomb.social A 92.205.62.239 +``` + +Until DNS exists, cert-manager Certificate will stay Pending and HTTPS will not +serve. Manifests can still be applied. + +## Break-glass + +Port-forward remains documented in `docs/runbook.md` if Ingress/SSO is down. + +## Manifests + +- `k8s/railiance/30-authelia-middleware.yaml` +- `k8s/railiance/31-ingress-ops-sso.yaml` +- `k8s/railiance/32-ingress-temporal-sso.yaml` + +Apply after DNS: + +```bash +export KUBECONFIG=~/.kube/config-hosteurope +kubectl apply -f k8s/railiance/30-authelia-middleware.yaml +kubectl apply -f k8s/railiance/31-ingress-ops-sso.yaml +kubectl apply -f k8s/railiance/32-ingress-temporal-sso.yaml +# set public Temporal URL on API after cert Ready: +kubectl -n activity-core set env deploy/actcore-api \ + ACTIVITY_CORE_TEMPORAL_UI_URL=https://activity-temporal.coulomb.social +kubectl -n activity-core set env deploy/actcore-temporal-ui \ + TEMPORAL_CORS_ORIGINS=https://activity-temporal.coulomb.social +``` diff --git a/docs/runbook.md b/docs/runbook.md index 775f285..0ae0419 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -105,31 +105,39 @@ the UI** — change definition files and sync. ### Production access (railiance01) -API remains **ClusterIP** (no public Ingress in WP-0024; SSO planned in -ACTIVITY-WP-0025). Use the **hosteurope / railiance01** kubeconfig, not a local -empty cluster. +**Primary (SSO — ACTIVITY-WP-0025):** after DNS A records exist: + +| UI | URL | +| --- | --- | +| Ops console | https://activity.coulomb.social/ops/ui | +| Temporal Web UI | https://activity-temporal.coulomb.social | + +Login via Authelia (`auth.coulomb.social`). Design: `docs/ops-sso-access.md`. +Mutations accept SSO identity headers; shared token is break-glass only. + +**DNS (required for TLS):** + +```text +activity.coulomb.social A 92.205.62.239 +activity-temporal.coulomb.social A 92.205.62.239 +``` + +**Break-glass port-forward** (hosteurope kubeconfig): ```bash export KUBECONFIG=~/.kube/config-hosteurope - -# Ops console (activity-core API) kubectl -n activity-core port-forward svc/actcore-api 8010:8010 -# Browser: http://127.0.0.1:8010/ops/ui - -# Temporal Web UI (second terminal — linked from ops nav as "Temporal UI") kubectl -n activity-core port-forward svc/actcore-temporal-ui 8080:8080 -# Browser: http://127.0.0.1:8080 +# http://127.0.0.1:8010/ops/ui and http://127.0.0.1:8080 ``` -Override the Temporal link target if needed: +Env overrides: ```bash -# on actcore-api -ACTIVITY_CORE_TEMPORAL_UI_URL=http://127.0.0.1:8080 +ACTIVITY_CORE_TEMPORAL_UI_URL=https://activity-temporal.coulomb.social +ACTIVITY_CORE_OPERATOR_TOKEN=… # break-glass; in actcore-runtime-secret ``` -Ensure `ACTIVITY_CORE_OPERATOR_TOKEN` is set on actcore-api (runtime secret key). - Bootstrap token (operator workstation; never commit the value): ```bash diff --git a/k8s/railiance/10-infrastructure.yaml b/k8s/railiance/10-infrastructure.yaml index dd229b9..d89a420 100644 --- a/k8s/railiance/10-infrastructure.yaml +++ b/k8s/railiance/10-infrastructure.yaml @@ -384,4 +384,5 @@ spec: - name: TEMPORAL_ADDRESS value: actcore-temporal:7233 - name: TEMPORAL_CORS_ORIGINS - value: http://localhost:8080 + # Include SSO host (ACTIVITY-WP-0025) and local port-forward. + value: https://activity-temporal.coulomb.social,http://localhost:8080,http://127.0.0.1:8080 diff --git a/k8s/railiance/30-authelia-middleware.yaml b/k8s/railiance/30-authelia-middleware.yaml new file mode 100644 index 0000000..e3bdbfe --- /dev/null +++ b/k8s/railiance/30-authelia-middleware.yaml @@ -0,0 +1,22 @@ +# Traefik ForwardAuth → Authelia (fleet SSO). +# Pattern: net-kingdom sso-mfa Authelia at auth.coulomb.social +# ACTIVITY-WP-0025 +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: authelia-forwardauth + namespace: activity-core + labels: + app.kubernetes.io/name: activity-core + app.kubernetes.io/part-of: activity-core + app.kubernetes.io/component: sso +spec: + forwardAuth: + # rd= sends the browser to the Authelia login portal when unauthenticated. + address: http://authelia.sso.svc.cluster.local:9091/api/verify?rd=https://auth.coulomb.social/ + trustForwardHeader: true + authResponseHeaders: + - Remote-User + - Remote-Groups + - Remote-Name + - Remote-Email diff --git a/k8s/railiance/31-ingress-ops-sso.yaml b/k8s/railiance/31-ingress-ops-sso.yaml new file mode 100644 index 0000000..4d4c717 --- /dev/null +++ b/k8s/railiance/31-ingress-ops-sso.yaml @@ -0,0 +1,34 @@ +# HTTPS SSO front door for activity-core ops console + API. +# Requires DNS: activity.coulomb.social A → 92.205.62.239 +# ACTIVITY-WP-0025 +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: actcore-ops + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-api + app.kubernetes.io/part-of: activity-core + app.kubernetes.io/component: sso-ingress + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.middlewares: activity-core-authelia-forwardauth@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - activity.coulomb.social + secretName: actcore-ops-tls + rules: + - host: activity.coulomb.social + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: actcore-api + port: + number: 8010 diff --git a/k8s/railiance/32-ingress-temporal-sso.yaml b/k8s/railiance/32-ingress-temporal-sso.yaml new file mode 100644 index 0000000..0a60a66 --- /dev/null +++ b/k8s/railiance/32-ingress-temporal-sso.yaml @@ -0,0 +1,34 @@ +# HTTPS SSO front door for Temporal Web UI. +# Requires DNS: activity-temporal.coulomb.social A → 92.205.62.239 +# ACTIVITY-WP-0025 +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: actcore-temporal-ui + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-temporal-ui + app.kubernetes.io/part-of: activity-core + app.kubernetes.io/component: sso-ingress + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.tls: "true" + traefik.ingress.kubernetes.io/router.middlewares: activity-core-authelia-forwardauth@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - activity-temporal.coulomb.social + secretName: actcore-temporal-ui-tls + rules: + - host: activity-temporal.coulomb.social + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: actcore-temporal-ui + port: + number: 8080 diff --git a/k8s/railiance/README.md b/k8s/railiance/README.md index a88ae7d..60bc563 100644 --- a/k8s/railiance/README.md +++ b/k8s/railiance/README.md @@ -111,20 +111,33 @@ kubectl -n activity-core get pods kubectl -n activity-core get svc ``` -## Operator automation console (ACTIVITY-WP-0024) +## Operator automation console (ACTIVITY-WP-0024 / 0025) -API Service is ClusterIP-only. From a shell with cluster access: +### SSO (primary, after DNS) ```bash -kubectl -n activity-core port-forward svc/actcore-api 8010:8010 -# UI: http://127.0.0.1:8010/ops/ui -# JSON: http://127.0.0.1:8010/ops/automations/status?since=sunday +# DNS A records → 92.205.62.239 (once): +# activity.coulomb.social +# activity-temporal.coulomb.social + +kubectl apply -f k8s/railiance/30-authelia-middleware.yaml +kubectl apply -f k8s/railiance/31-ingress-ops-sso.yaml +kubectl apply -f k8s/railiance/32-ingress-temporal-sso.yaml +kubectl -n activity-core set env deploy/actcore-api \ + ACTIVITY_CORE_TEMPORAL_UI_URL=https://activity-temporal.coulomb.social ``` -Mutations require `ACTIVITY_CORE_OPERATOR_TOKEN` in `actcore-runtime-secret` -(injected via `envFrom` on `actcore-api`). Merge the key into the existing -secret (do not replace DB URL keys). Header: `X-Operator-Token`. See -`docs/runbook.md` § Operator automation console. +- Ops: https://activity.coulomb.social/ops/ui (Authelia SSO) +- Temporal: https://activity-temporal.coulomb.social +- Design: `docs/ops-sso-access.md` -Cron/schedule expression changes remain git-owned (definition files + sync); -the console supports Run now, enable/disable, and Temporal pause/unpause only. +### Break-glass port-forward + +```bash +export KUBECONFIG=~/.kube/config-hosteurope +kubectl -n activity-core port-forward svc/actcore-api 8010:8010 +# UI: http://127.0.0.1:8010/ops/ui +``` + +Mutations: SSO headers when behind Authelia, else `X-Operator-Token` from +`actcore-runtime-secret`. Cron edits remain git-owned. diff --git a/src/activity_core/ops_api.py b/src/activity_core/ops_api.py index eeaf203..84fc4a6 100644 --- a/src/activity_core/ops_api.py +++ b/src/activity_core/ops_api.py @@ -15,7 +15,12 @@ from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from temporalio.client import Client -from activity_core.ops_auth import HEADER_NAME, operator_token_configured, require_operator +from activity_core.ops_auth import ( + HEADER_NAME, + extract_sso_principal, + operator_token_configured, + require_operator, +) from activity_core.ops_console import ( is_side_effect_definition, ops_definition_detail, @@ -248,6 +253,11 @@ async def auth_status() -> dict[str, Any]: "mutation_header": HEADER_NAME, "mutations_require_token": operator_token_configured() or not allow, "temporal_ui_url": temporal_ui_url(), + "sso_docs": "/docs not required — see docs/ops-sso-access.md", + "public_hosts": { + "ops": "https://activity.coulomb.social", + "temporal_ui": "https://activity-temporal.coulomb.social", + }, } @@ -271,17 +281,17 @@ pre { background: #f8f8f8; padding: 0.75rem; overflow: auto; font-size: 0.8rem; def temporal_ui_url() -> str: - """Browser URL for Temporal Web UI (port-forward or future SSO ingress). + """Browser URL for Temporal Web UI (SSO ingress or local port-forward). - Override with ACTIVITY_CORE_TEMPORAL_UI_URL. Default assumes a local - port-forward of actcore-temporal-ui to 127.0.0.1:8080 (see runbook). + Override with ACTIVITY_CORE_TEMPORAL_UI_URL. Default prefers the public SSO + hostname when set via env; otherwise local port-forward on :8080. """ raw = ( os.environ.get("ACTIVITY_CORE_TEMPORAL_UI_URL") or os.environ.get("TEMPORAL_UI_URL") - or "http://127.0.0.1:8080" + or "https://activity-temporal.coulomb.social" ).strip() - return raw.rstrip("/") or "http://127.0.0.1:8080" + return raw.rstrip("/") or "https://activity-temporal.coulomb.social" def _page(title: str, body: str) -> HTMLResponse: diff --git a/src/activity_core/ops_auth.py b/src/activity_core/ops_auth.py index 14c4ef2..db91e1f 100644 --- a/src/activity_core/ops_auth.py +++ b/src/activity_core/ops_auth.py @@ -1,12 +1,15 @@ -"""Operator token auth for activity-core ops console (ACTIVITY-WP-0024). +"""Operator auth for activity-core ops console (ACTIVITY-WP-0024 / 0025). -Mutations under ``/ops`` are fail-closed: -- If ``ACTIVITY_CORE_OPERATOR_TOKEN`` is set, requests must send matching - ``X-Operator-Token`` (or ``Authorization: Bearer ``). -- If the token is **unset**, mutations are refused unless - ``ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS`` is truthy (local dev only). +Mutations under ``/ops`` are fail-closed. Accepted principals (in order): -Read endpoints do not require the token (ClusterIP / port-forward posture). +1. **SSO** — Authelia ForwardAuth response headers (``Remote-User``, + ``Remote-Email``, etc.) when the request came through Traefik SSO. +2. **Break-glass token** — ``ACTIVITY_CORE_OPERATOR_TOKEN`` via + ``X-Operator-Token`` or ``Authorization: Bearer``. +3. **Local dev only** — ``ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS`` truthy + when no token is configured. + +Token values are never logged or returned. """ from __future__ import annotations @@ -21,6 +24,15 @@ OPERATOR_TOKEN_ENV = "ACTIVITY_CORE_OPERATOR_TOKEN" ALLOW_UNAUTH_ENV = "ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS" HEADER_NAME = "X-Operator-Token" +# Authelia forwardAuth + common proxies (case-insensitive lookup via Starlette) +SSO_USER_HEADERS = ( + "Remote-User", + "Remote-Email", + "X-Forwarded-User", + "X-Auth-Request-User", + "X-Auth-Request-Email", +) + def operator_token_configured() -> bool: return bool((os.environ.get(OPERATOR_TOKEN_ENV) or "").strip()) @@ -47,8 +59,30 @@ def extract_operator_token( return None +def extract_sso_principal(request: Request) -> str | None: + """Return authenticated SSO subject from Authelia/proxy headers, if any.""" + for name in SSO_USER_HEADERS: + value = request.headers.get(name) + if value and value.strip(): + return value.strip() + # Starlette lowercases; also try explicit lower keys + headers = request.headers + for name in SSO_USER_HEADERS: + value = headers.get(name.lower()) + if value and value.strip(): + return value.strip() + return None + + +def extract_sso_groups(request: Request) -> list[str]: + raw = request.headers.get("Remote-Groups") or request.headers.get("remote-groups") or "" + if not raw.strip(): + return [] + return [part.strip() for part in raw.replace(";", ",").split(",") if part.strip()] + + def verify_operator_token(provided: str | None) -> str: - """Return operator principal label or raise HTTPException.""" + """Return operator principal label from shared token, or raise.""" expected = (os.environ.get(OPERATOR_TOKEN_ENV) or "").strip() if not expected: if allow_unauth_mutations(): @@ -57,17 +91,18 @@ def verify_operator_token(provided: str | None) -> str: status_code=403, detail=( "operator auth not configured; set " - f"{OPERATOR_TOKEN_ENV} or enable {ALLOW_UNAUTH_ENV} for local dev" + f"{OPERATOR_TOKEN_ENV}, use SSO (Authelia), " + f"or enable {ALLOW_UNAUTH_ENV} for local dev" ), ) if not provided: raise HTTPException( status_code=401, - detail=f"missing operator token ({HEADER_NAME} or Authorization Bearer)", + detail=f"missing operator token ({HEADER_NAME} or Authorization Bearer) or SSO session", ) if not hmac.compare_digest(provided, expected): raise HTTPException(status_code=401, detail="invalid operator token") - return "operator" + return "operator-token" async def require_operator( @@ -75,8 +110,11 @@ async def require_operator( x_operator_token: Annotated[str | None, Header(alias=HEADER_NAME)] = None, authorization: Annotated[str | None, Header()] = None, ) -> str: - """FastAPI dependency: require valid operator token for mutations.""" - # Prefer dependency headers; fall back to raw request (HTML form headers rare). + """FastAPI dependency: SSO principal or valid operator token.""" + sso = extract_sso_principal(request) + if sso: + return f"sso:{sso}" + provided = extract_operator_token( x_operator_token=x_operator_token, authorization=authorization, diff --git a/tests/test_ops_auth.py b/tests/test_ops_auth.py index 62668e6..1e9d4ae 100644 --- a/tests/test_ops_auth.py +++ b/tests/test_ops_auth.py @@ -1,13 +1,17 @@ -"""Unit tests for operator token auth (ACTIVITY-WP-0024-T02/T06).""" +"""Unit tests for operator token + SSO auth (ACTIVITY-WP-0024/0025).""" from __future__ import annotations +from unittest.mock import MagicMock + import pytest from fastapi import HTTPException from activity_core.ops_auth import ( extract_operator_token, + extract_sso_principal, operator_token_configured, + require_operator, verify_operator_token, ) @@ -42,7 +46,7 @@ def test_verify_allows_anonymous_dev(monkeypatch: pytest.MonkeyPatch) -> None: def test_verify_token_match(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("ACTIVITY_CORE_OPERATOR_TOKEN", "correct-horse") - assert verify_operator_token("correct-horse") == "operator" + assert verify_operator_token("correct-horse") == "operator-token" with pytest.raises(HTTPException) as exc: verify_operator_token("wrong") assert exc.value.status_code == 401 @@ -56,3 +60,37 @@ def test_operator_token_configured(monkeypatch: pytest.MonkeyPatch) -> None: assert operator_token_configured() is False monkeypatch.setenv("ACTIVITY_CORE_OPERATOR_TOKEN", "x") assert operator_token_configured() is True + + +def test_extract_sso_principal_remote_user() -> None: + req = MagicMock() + req.headers = {"Remote-User": "bernd", "Remote-Email": "bernd@example.com"} + # MagicMock headers.get needs side_effect + headers = {"Remote-User": "bernd", "Remote-Email": "bernd@example.com"} + + class H(dict): + def get(self, key, default=None): # type: ignore[no-untyped-def] + for k, v in self.items(): + if k.lower() == str(key).lower(): + return v + return default + + req.headers = H(headers) + assert extract_sso_principal(req) == "bernd" + + +@pytest.mark.asyncio +async def test_require_operator_prefers_sso(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("ACTIVITY_CORE_OPERATOR_TOKEN", "tok") + + class H(dict): + def get(self, key, default=None): # type: ignore[no-untyped-def] + for k, v in self.items(): + if k.lower() == str(key).lower(): + return v + return default + + req = MagicMock() + req.headers = H({"Remote-User": "alice"}) + principal = await require_operator(req, x_operator_token=None, authorization=None) + assert principal == "sso:alice" diff --git a/tests/test_ops_console_api.py b/tests/test_ops_console_api.py index 30e7e48..0b9c245 100644 --- a/tests/test_ops_console_api.py +++ b/tests/test_ops_console_api.py @@ -218,7 +218,7 @@ async def test_ui_index_renders(ops_app: FastAPI, monkeypatch: pytest.MonkeyPatc assert "Daily Triage" in res.text assert "Operator token" in res.text assert "Temporal UI" in res.text - assert "127.0.0.1:8080" in res.text + assert "activity-temporal.coulomb.social" in res.text @pytest.mark.asyncio diff --git a/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md b/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md index dbfc6b0..aa75ef3 100644 --- a/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md +++ b/workplans/ACTIVITY-WP-0025-ops-ui-sso-access.md @@ -4,7 +4,7 @@ type: workplan title: "SSO access control for ops console and Temporal UI" domain: infotech repo: activity-core -status: proposed +status: active owner: codex topic_slug: activity-core created: "2026-07-22" @@ -81,7 +81,7 @@ git or State Hub. ```task id: ACTIVITY-WP-0025-T01 -status: todo +status: done priority: high state_hub_task_id: "de28ac08-cae5-476b-b093-82fcf8e3fa5e" ``` @@ -100,7 +100,7 @@ hostnames and IdP group named. ```task id: ACTIVITY-WP-0025-T02 -status: todo +status: done priority: high state_hub_task_id: "650db102-ad4e-4d8e-86c1-eee50f5bafcc" ``` @@ -116,7 +116,7 @@ state_hub_task_id: "650db102-ad4e-4d8e-86c1-eee50f5bafcc" ```task id: ACTIVITY-WP-0025-T03 -status: todo +status: progress priority: high state_hub_task_id: "7084f7d5-4181-4dfa-b239-0dad15efbae9" ``` @@ -134,7 +134,7 @@ port-forward; unauthorized gets login or 403. ```task id: ACTIVITY-WP-0025-T04 -status: todo +status: progress priority: high state_hub_task_id: "99b36f31-eb19-4f96-b3f1-9ac1ab02d78f" ``` @@ -151,7 +151,7 @@ port-forward, under SSO. ```task id: ACTIVITY-WP-0025-T05 -status: todo +status: progress priority: high state_hub_task_id: "77fff202-bfb6-49eb-b8e5-3760abacc8bd" ``` @@ -170,7 +170,7 @@ token not required for normal operator use. ```task id: ACTIVITY-WP-0025-T06 -status: todo +status: wait priority: medium state_hub_task_id: "b14e1f2c-1320-4218-8bd3-2a309295a69c" ``` @@ -186,7 +186,7 @@ account. ```task id: ACTIVITY-WP-0025-T07 -status: todo +status: progress priority: medium state_hub_task_id: "c5945e64-c460-4830-a19e-a944bb44a67e" ``` @@ -243,3 +243,20 @@ state_hub_task_id: "f73eafbf-ad11-4d7d-b89e-e910c662ce86" - `src/activity_core/ops_api.py` — `ACTIVITY_CORE_TEMPORAL_UI_URL` - `k8s/railiance/` — `actcore-api`, `actcore-temporal-ui` ClusterIP services - ACTIVITY-WP-0024 operator automation console + +## Progress 2026-07-22 + +| Task | Status | Notes | +| --- | --- | --- | +| T01 | done | Hosts: activity.coulomb.social + activity-temporal.coulomb.social; docs/ops-sso-access.md | +| T02 | done | Reuse Authelia + Traefik ForwardAuth (sso ns); not oauth2-proxy | +| T03 | progress | Ingress + middleware manifests landed; **blocked on DNS A records** for cert | +| T04 | progress | Temporal Ingress manifest + CORS env; same DNS gate | +| T05 | progress | SSO headers preferred over shared token in ops_auth | +| T06 | wait | LLDAP group activity-core-operators + Authelia rules (net-kingdom) | +| T07 | progress | Runbook + design doc updated | +| T08 | todo | After DNS/cert Ready: smoke HTTPS + SSO login | + +**Operator action required:** create DNS A records for both hosts → 92.205.62.239, +then apply `k8s/railiance/30-*.yaml`–`32-*.yaml` and set +`ACTIVITY_CORE_TEMPORAL_UI_URL=https://activity-temporal.coulomb.social` on actcore-api.