Files
bluejay-infra/gated/authentik-tenant-sync
Andrew Stoltz a65f422147 infra(gated): stage authentik-tenant-mapping-sync CronJob (Au-3, suspended)
Gated substrate (Cl2-4 / Cl-infra-3) — outside apps/ so the ApplicationSet
will not deploy it, and spec.suspend: true. Reconciles the 1Password
tenant-mapping doc into Authentik groups via Connect REST. Activate at Au-3
public-go (un-suspend + materialize the script ConfigMap). Pairs Codex Cx2-7.
Canonical script: FlowerCore.Notes/scripts/authentik/authentik-tenant-mapping-sync.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:34:29 -05:00
..

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 (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.mdversion==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 secretKeyRefs:

  • AUTHENTIK_TOKENSecret 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_TOKENSecret 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:
    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):
    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):
    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:
    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:
    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.