# authentik-tenant-mapping-sync — GATED manifest staging **Status:** GATED (suspended). **ADR:** ADR-198 §2.A P1 (Au-1 / Au-3 substrate). **Pairs:** Codex **Cx2-7**. This directory is a **Notes staging area**, NOT a deploy target. The orchestrator relocates `cronjob.yaml` into a `gated/` path **outside** `bluejay-infra/apps/` so ArgoCD's `apps/*` directory generator never picks it up. Nothing here runs until the activation steps below. ## What this is A nightly Kubernetes `CronJob` that runs [`scripts/authentik/authentik-tenant-mapping-sync.py`](../../../scripts/authentik/authentik-tenant-mapping-sync.py) (Notes repo). The script: - reads the 1Password Document **`flowercore-tenant-mapping`** (vault `IAmWorkin`, field `mapping`) via **1Password Connect REST** — never the 1Password CLI/desktop (operator hard rule); - parses + light-validates the mapping JSON (schema: [`authentik-oidc-tenant-mapping-schema.md`](../../standards/authentik-oidc-tenant-mapping-schema.md) — `version==1`, `mappings[]` with `authentikGroup` / `fcTenantId` / `fcRole`); - reconciles each distinct `authentikGroup` into Authentik `/api/v3/core/groups/`: create-if-missing, PATCH-managed-markers-on-drift, **never delete or disable unmanaged groups**; - emits structured (Serilog-shaped JSON) logs and exits 0 on success. It is the **slow nightly fix-up path**. The **<1s hot path** stays the MCP tool `authentik_sync_tenant_mapping` (schema doc §6.2 force-broadcast). This CronJob does NOT broadcast SignalR — group reconcile is its only side effect; services pick up mapping changes on their own 5-minute 1P refresh. ## Why it is GATED (two locks) 1. **`spec.suspend: true`** in `cronjob.yaml` — belt-and-suspenders so even if applied it never fires. 2. **Lives outside `apps/`** — staged here in Notes; ArgoCD does not manage it. Both must be cleared to go live. This pairs Codex **Cx2-7**: do not activate ahead of the Au-3 public-go for tenant self-registration. ## Files | File | Purpose | |------|---------| | `cronjob.yaml` | The suspended `CronJob` + the script-delivery `ConfigMap` (placeholder body). | | `README.md` | This file. | | `scripts/authentik/authentik-tenant-mapping-sync.py` | The reconcile script (canonical source; NOT in this dir). | ## Secrets (referenced, not invented) No secret **values** appear in `cronjob.yaml` — only `secretKeyRef`s: - **`AUTHENTIK_TOKEN`** ← `Secret authentik/authentik-credentials` key `BOOTSTRAP_ADMIN_TOKEN` (already exists; the same token `provision-oidc-client.py` reads). **Au-9 caveat:** this is the never-rotated bootstrap token — when `/rotate-password rotate authentik` (Au-9) lands, this CronJob is one of its fan-out consumers. - **`OP_TOKEN`** ← `Secret authentik/tenant-mapping-sync-op-token` key `token`. ### OP_TOKEN cross-namespace The canonical 1P Connect token Secret is `onepassword-system/onepassword-token`, but this CronJob runs in the `authentik` namespace and K8s Secrets are namespace-scoped. Pick one at activation: - **Option A (copy, simplest).** Mint a same-namespace copy right before un-suspending: ```sh kubectl get secret onepassword-token -n onepassword-system -o jsonpath='{.data.token}' \ | base64 -d \ | kubectl create secret generic tenant-mapping-sync-op-token -n authentik \ --from-file=token=/dev/stdin --dry-run=client -o yaml | kubectl apply -f - ``` (Re-run whenever the Connect token rotates — add this CronJob to the **Au-10** Connect-token fan-out checklist so the copy can't go stale.) - **Option B (CRD, preferred long-term).** Use an `OnePasswordItem` CRD (`feedback_1password_operator_pattern`) so the 1P operator mints/refreshes `authentik/tenant-mapping-sync-op-token` automatically — no manual copy, rotation-safe. > If neither secret exists yet, that's fine **while suspended** — the job never schedules. ## How to ACTIVATE (at Au-3 public-go) 1. **Pre-flight (workstation dry-run, writes nothing):** ```sh export AUTHENTIK_TOKEN=... # or let it read authentik/authentik-credentials via kubectl export OP_TOKEN=... # or rely on credential-helper.sh get_op_token (fcadmin@noc1) python scripts/authentik/authentik-tenant-mapping-sync.py --dry-run --verbose ``` Confirm the planned create/update set matches the 1P mapping document. 2. **Provide `OP_TOKEN` in-cluster** — Option A or B above. 3. **Materialize the script ConfigMap from the canonical file** (do NOT hand-edit a copy into `cronjob.yaml` — the embedded body is a deliberate placeholder): ```sh kubectl create configmap authentik-tenant-mapping-sync-script -n authentik \ --from-file=authentik-tenant-mapping-sync.py=scripts/authentik/authentik-tenant-mapping-sync.py \ --dry-run=client -o yaml | kubectl apply -f - ``` (Or, in the imaged future per ADR-198 §2.B P3, bake the script into `fc-runtime-base` and drop the ConfigMap volume.) 4. **Relocate into bluejay-infra** — move `cronjob.yaml` into a `gated/` (or `apps/`) path in `bluejay-infra` per the orchestrator's placement decision. If under `apps/`, ArgoCD will sync it. 5. **Un-suspend** — set `spec.suspend: false` (commit in `bluejay-infra` so ArgoCD selfHeal doesn't revert), or one-off: ```sh kubectl patch cronjob authentik-tenant-mapping-sync -n authentik \ -p '{"spec":{"suspend":false}}' ``` 6. **Smoke (VG-A1):** trigger an immediate run and check the structured logs: ```sh kubectl create job --from=cronjob/authentik-tenant-mapping-sync tms-smoke -n authentik kubectl logs -n authentik job/tms-smoke ``` Then edit a mapping entry in 1P and confirm the next run reconciles the group; the <1s propagation still comes from the MCP `authentik_sync_tenant_mapping` force-broadcast. ## Rollback Re-suspend (`spec.suspend: true`) or delete the CronJob. The script never deletes Authentik groups, so a bad run can only over-create groups present in the mapping — remove any unwanted group by hand in the Authentik admin UI. No data loss path. ## Idempotency / safety summary - Re-running is a no-op when groups already match (mirrors `provision-oidc-client.py`). - Only the managed attribute block (`fc:managed-by` / `fc:tenant` / `fc:role` / optional `fc:label` / `fc:regulated` / `fc:strict-mode`) is asserted; group parent/users/roles are never touched. - Wildcard SuperAdmin entries (`fcTenantId: "*"`) do not create a per-tenant group. - `--dry-run` prints the plan and writes nothing — always run it first. ## Cross-links - [`docs/standards/auth-acl-unattended-lifecycle-plan.md`](../../standards/auth-acl-unattended-lifecycle-plan.md) — ADR-198; Au-1/Au-3 lanes, VG-A1/A2. - [`docs/standards/authentik-oidc-tenant-mapping-schema.md`](../../standards/authentik-oidc-tenant-mapping-schema.md) — the mapping JSON shape + 1P item layout (§2/§3). - [`scripts/authentik/provision-oidc-client.py`](../../../scripts/authentik/provision-oidc-client.py) — sibling idempotent provisioner (same API + posture). - [`scripts/credential-helper.sh`](../../../scripts/credential-helper.sh) — `get_op_token` 1P Connect bootstrap (fcadmin@noc1).