Activate ACTIVITY-WP-0025: Authelia SSO ingress for ops and Temporal UI
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 54s
Build and Publish Container Image / build-and-push (push) Successful in 1m47s

Mark workplan active. Add Traefik ForwardAuth middleware and Ingress
manifests for activity.coulomb.social and activity-temporal.coulomb.social.
Prefer Authelia SSO identity for ops mutations; document DNS gate and
fleet pattern (docs/ops-sso-access.md).
This commit is contained in:
tegwick 2026-07-22 00:47:29 +02:00
parent 7e71c0c837
commit f885697e96
13 changed files with 366 additions and 57 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** | 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 ### Drift risks

94
docs/ops-sso-access.md Normal file
View file

@ -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
```

View file

@ -105,31 +105,39 @@ the UI** — change definition files and sync.
### Production access (railiance01) ### Production access (railiance01)
API remains **ClusterIP** (no public Ingress in WP-0024; SSO planned in **Primary (SSO — ACTIVITY-WP-0025):** after DNS A records exist:
ACTIVITY-WP-0025). Use the **hosteurope / railiance01** kubeconfig, not a local
empty cluster. | 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 ```bash
export KUBECONFIG=~/.kube/config-hosteurope export KUBECONFIG=~/.kube/config-hosteurope
# Ops console (activity-core API)
kubectl -n activity-core port-forward svc/actcore-api 8010:8010 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 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 ```bash
# on actcore-api ACTIVITY_CORE_TEMPORAL_UI_URL=https://activity-temporal.coulomb.social
ACTIVITY_CORE_TEMPORAL_UI_URL=http://127.0.0.1:8080 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): Bootstrap token (operator workstation; never commit the value):
```bash ```bash

View file

@ -384,4 +384,5 @@ spec:
- name: TEMPORAL_ADDRESS - name: TEMPORAL_ADDRESS
value: actcore-temporal:7233 value: actcore-temporal:7233
- name: TEMPORAL_CORS_ORIGINS - 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -111,20 +111,33 @@ kubectl -n activity-core get pods
kubectl -n activity-core get svc 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 ```bash
kubectl -n activity-core port-forward svc/actcore-api 8010:8010 # DNS A records → 92.205.62.239 (once):
# UI: http://127.0.0.1:8010/ops/ui # activity.coulomb.social
# JSON: http://127.0.0.1:8010/ops/automations/status?since=sunday # 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` - Ops: https://activity.coulomb.social/ops/ui (Authelia SSO)
(injected via `envFrom` on `actcore-api`). Merge the key into the existing - Temporal: https://activity-temporal.coulomb.social
secret (do not replace DB URL keys). Header: `X-Operator-Token`. See - Design: `docs/ops-sso-access.md`
`docs/runbook.md` § Operator automation console.
Cron/schedule expression changes remain git-owned (definition files + sync); ### Break-glass port-forward
the console supports Run now, enable/disable, and Temporal pause/unpause only.
```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.

View file

@ -15,7 +15,12 @@ from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from temporalio.client import Client 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 ( from activity_core.ops_console import (
is_side_effect_definition, is_side_effect_definition,
ops_definition_detail, ops_definition_detail,
@ -248,6 +253,11 @@ async def auth_status() -> dict[str, Any]:
"mutation_header": HEADER_NAME, "mutation_header": HEADER_NAME,
"mutations_require_token": operator_token_configured() or not allow, "mutations_require_token": operator_token_configured() or not allow,
"temporal_ui_url": temporal_ui_url(), "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: 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 Override with ACTIVITY_CORE_TEMPORAL_UI_URL. Default prefers the public SSO
port-forward of actcore-temporal-ui to 127.0.0.1:8080 (see runbook). hostname when set via env; otherwise local port-forward on :8080.
""" """
raw = ( raw = (
os.environ.get("ACTIVITY_CORE_TEMPORAL_UI_URL") os.environ.get("ACTIVITY_CORE_TEMPORAL_UI_URL")
or os.environ.get("TEMPORAL_UI_URL") or os.environ.get("TEMPORAL_UI_URL")
or "http://127.0.0.1:8080" or "https://activity-temporal.coulomb.social"
).strip() ).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: def _page(title: str, body: str) -> HTMLResponse:

View file

@ -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: Mutations under ``/ops`` are fail-closed. Accepted principals (in order):
- If ``ACTIVITY_CORE_OPERATOR_TOKEN`` is set, requests must send matching
``X-Operator-Token`` (or ``Authorization: Bearer <token>``).
- If the token is **unset**, mutations are refused unless
``ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS`` is truthy (local dev only).
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 from __future__ import annotations
@ -21,6 +24,15 @@ OPERATOR_TOKEN_ENV = "ACTIVITY_CORE_OPERATOR_TOKEN"
ALLOW_UNAUTH_ENV = "ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS" ALLOW_UNAUTH_ENV = "ACTIVITY_CORE_OPS_ALLOW_UNAUTH_MUTATIONS"
HEADER_NAME = "X-Operator-Token" 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: def operator_token_configured() -> bool:
return bool((os.environ.get(OPERATOR_TOKEN_ENV) or "").strip()) return bool((os.environ.get(OPERATOR_TOKEN_ENV) or "").strip())
@ -47,8 +59,30 @@ def extract_operator_token(
return None 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: 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() expected = (os.environ.get(OPERATOR_TOKEN_ENV) or "").strip()
if not expected: if not expected:
if allow_unauth_mutations(): if allow_unauth_mutations():
@ -57,17 +91,18 @@ def verify_operator_token(provided: str | None) -> str:
status_code=403, status_code=403,
detail=( detail=(
"operator auth not configured; set " "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: if not provided:
raise HTTPException( raise HTTPException(
status_code=401, 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): if not hmac.compare_digest(provided, expected):
raise HTTPException(status_code=401, detail="invalid operator token") raise HTTPException(status_code=401, detail="invalid operator token")
return "operator" return "operator-token"
async def require_operator( async def require_operator(
@ -75,8 +110,11 @@ async def require_operator(
x_operator_token: Annotated[str | None, Header(alias=HEADER_NAME)] = None, x_operator_token: Annotated[str | None, Header(alias=HEADER_NAME)] = None,
authorization: Annotated[str | None, Header()] = None, authorization: Annotated[str | None, Header()] = None,
) -> str: ) -> str:
"""FastAPI dependency: require valid operator token for mutations.""" """FastAPI dependency: SSO principal or valid operator token."""
# Prefer dependency headers; fall back to raw request (HTML form headers rare). sso = extract_sso_principal(request)
if sso:
return f"sso:{sso}"
provided = extract_operator_token( provided = extract_operator_token(
x_operator_token=x_operator_token, x_operator_token=x_operator_token,
authorization=authorization, authorization=authorization,

View file

@ -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 __future__ import annotations
from unittest.mock import MagicMock
import pytest import pytest
from fastapi import HTTPException from fastapi import HTTPException
from activity_core.ops_auth import ( from activity_core.ops_auth import (
extract_operator_token, extract_operator_token,
extract_sso_principal,
operator_token_configured, operator_token_configured,
require_operator,
verify_operator_token, 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: def test_verify_token_match(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("ACTIVITY_CORE_OPERATOR_TOKEN", "correct-horse") 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: with pytest.raises(HTTPException) as exc:
verify_operator_token("wrong") verify_operator_token("wrong")
assert exc.value.status_code == 401 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 assert operator_token_configured() is False
monkeypatch.setenv("ACTIVITY_CORE_OPERATOR_TOKEN", "x") monkeypatch.setenv("ACTIVITY_CORE_OPERATOR_TOKEN", "x")
assert operator_token_configured() is True 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"

View file

@ -218,7 +218,7 @@ async def test_ui_index_renders(ops_app: FastAPI, monkeypatch: pytest.MonkeyPatc
assert "Daily Triage" in res.text assert "Daily Triage" in res.text
assert "Operator token" in res.text assert "Operator token" in res.text
assert "Temporal UI" 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 @pytest.mark.asyncio

View file

@ -4,7 +4,7 @@ type: workplan
title: "SSO access control for ops console and Temporal UI" title: "SSO access control for ops console and Temporal UI"
domain: infotech domain: infotech
repo: activity-core repo: activity-core
status: proposed status: active
owner: codex owner: codex
topic_slug: activity-core topic_slug: activity-core
created: "2026-07-22" created: "2026-07-22"
@ -81,7 +81,7 @@ git or State Hub.
```task ```task
id: ACTIVITY-WP-0025-T01 id: ACTIVITY-WP-0025-T01
status: todo status: done
priority: high priority: high
state_hub_task_id: "de28ac08-cae5-476b-b093-82fcf8e3fa5e" state_hub_task_id: "de28ac08-cae5-476b-b093-82fcf8e3fa5e"
``` ```
@ -100,7 +100,7 @@ hostnames and IdP group named.
```task ```task
id: ACTIVITY-WP-0025-T02 id: ACTIVITY-WP-0025-T02
status: todo status: done
priority: high priority: high
state_hub_task_id: "650db102-ad4e-4d8e-86c1-eee50f5bafcc" state_hub_task_id: "650db102-ad4e-4d8e-86c1-eee50f5bafcc"
``` ```
@ -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: todo status: progress
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: todo status: progress
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: todo status: progress
priority: high priority: high
state_hub_task_id: "77fff202-bfb6-49eb-b8e5-3760abacc8bd" state_hub_task_id: "77fff202-bfb6-49eb-b8e5-3760abacc8bd"
``` ```
@ -170,7 +170,7 @@ token not required for normal operator use.
```task ```task
id: ACTIVITY-WP-0025-T06 id: ACTIVITY-WP-0025-T06
status: todo status: wait
priority: medium priority: medium
state_hub_task_id: "b14e1f2c-1320-4218-8bd3-2a309295a69c" state_hub_task_id: "b14e1f2c-1320-4218-8bd3-2a309295a69c"
``` ```
@ -186,7 +186,7 @@ account.
```task ```task
id: ACTIVITY-WP-0025-T07 id: ACTIVITY-WP-0025-T07
status: todo status: progress
priority: medium priority: medium
state_hub_task_id: "c5945e64-c460-4830-a19e-a944bb44a67e" 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` - `src/activity_core/ops_api.py``ACTIVITY_CORE_TEMPORAL_UI_URL`
- `k8s/railiance/``actcore-api`, `actcore-temporal-ui` ClusterIP services - `k8s/railiance/``actcore-api`, `actcore-temporal-ui` ClusterIP services
- ACTIVITY-WP-0024 operator automation console - 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.