weebo-authentik

Connect an AuthentikInstance

Point the operator at a real Authentik server.

Every other CRD in this operator references an AuthentikInstance by name (spec.instanceRef, or implicitly for cluster-scoped kinds — see the allow-list guide and the CRD reference). AuthentikInstance itself has no corresponding object on the Authentik side — it just describes how to reach one.

1. Create a Secret with the API token

Generate an API token in Authentik (Admin interface → Directory → Tokens, or via a service account), then:

kubectl create secret generic authentik-api-token \
  --namespace weebo-authentik \
  --from-literal=token=<your-api-token>

2. Create the AuthentikInstance CR

apiVersion: authentik.weebo.io/v1alpha1
kind: AuthentikInstance
metadata:
  name: main
spec:
  url: https://login.example.com
  tokenSecretRef:
    name: authentik-api-token
    namespace: weebo-authentik
    key: token

AuthentikInstance is cluster-scoped, so its name (main above) is what every AuthentikApplication's spec.instanceRef points back to.

spec.url is the web base URL, not the /api/v3 REST endpoint: the operator appends /api/v3 itself for API calls, and uses the bare URL as the root of the per-application OIDC issuer it writes into an oauth2 application's AUTHENTIK_URL (<url>/application/o/<slug>/). A URL that already ends in /api/v3 is accepted and trimmed back to the web base rather than taken literally.

TLS

The operator verifies Authentik's certificate against the platform trust store (plus SSL_CERT_FILE/SSL_CERT_DIR, if you'd rather inject the CA into the pod's own trust store). When Authentik is served by a private CA — a cluster-internal PKI, cert-manager's own CA issuer — point spec.tls.caSecretRef at a Kubernetes Secret holding that CA's PEM bundle instead:

apiVersion: authentik.weebo.io/v1alpha1
kind: AuthentikInstance
metadata:
  name: main
spec:
  url: https://login.example.com
  tokenSecretRef:
    name: authentik-api-token
    namespace: weebo-authentik
    key: token
  tls:
    caSecretRef:
      name: authentik-ca
      namespace: weebo-authentik
      key: ca.crt # must hold a PEM bundle

The operator reads the Secret via the API and hands the PEM to the HTTP client it talks to this instance with — no volume mount, which matters because the chart exposes podAnnotations and extraEnv but no extraVolumes. The CA is added on top of the platform trust store rather than replacing it, so one operator can serve a private-CA instance and a publicly-signed one at once. A single certificate and a concatenated chain are both accepted.

The Secret is re-read on every reconcile, so rotating the CA takes effect without restarting the operator. A configured-but-unreadable caSecretRef (missing Secret, missing key, or contents with no PEM certificate in them) is a hard error, never a silent fall-back to the platform roots.

spec.tls.insecureSkipVerify: true turns certificate verification off entirely. It exists for throwaway/self-signed instances; prefer caSecretRef, which keeps verification on.

This block covers the Authentik API connection only. Vault has its own CA setting — they are routinely signed by different CAs — see Trusting a private Vault CA below.

Secret storage backend

AuthentikApplication's oauth2 credentials (AUTHENTIK_CLIENT_ID/ AUTHENTIK_CLIENT_SECRET/AUTHENTIK_URL) are written to a Kubernetes Secret by default — one per AuthentikApplication CR, same name/ namespace. To write them to Vault instead, set spec.secretStore on the AuthentikInstance:

apiVersion: authentik.weebo.io/v1alpha1
kind: AuthentikInstance
metadata:
  name: main
spec:
  url: https://login.example.com
  tokenSecretRef:
    name: authentik-api-token
    namespace: weebo-authentik
    key: token
  secretStore:
    backend: vault
    vault:
      address: https://vault.example.com:8200
      mount: secret
      pathPrefix: weebo-authentik # optional, this is the default
      kubernetesAuthRole: weebo-authentik-operator
      kubernetesAuthMount: kubernetes # optional, this is the default

The operator authenticates to Vault using its own ServiceAccount JWT against Vault's Kubernetes auth backend (kubernetesAuthRole must already be configured Vault-side to trust that ServiceAccount) — no static Vault token to provision or rotate. Credentials land at <mount>/data/<pathPrefix>/<namespace>/<name> (KV v2), one secret per AuthentikApplication CR, same naming convention as the Kubernetes backend. An empty pathPrefix (pathPrefix: "") drops the segment: <mount>/data/<namespace>/<name>, where a Vault policy templated on the namespace already reads.

The write is conditional: the operator reads the path first and only issues a KV v2 set when the stored document differs from what it would write. This matters because set always creates a new version, even for an identical document, while the reconciler rewrites credentials on every pass (a synced object requeues every 300s) — writing unconditionally would churn a new Vault version per application every five minutes and roll the KV version history over continuously. Drift is still corrected: a path edited by hand no longer matches and is rewritten on the next pass. The Kubernetes backend needs no equivalent, since re-applying an identical Secret is already a no-op at the API server.

Trusting a private Vault CA

If Vault (or OpenBao) presents a TLS certificate signed by a private CA, point secretStore.vault.caSecretRef at a Kubernetes Secret holding the CA's PEM bundle. The operator reads it via the API and hands it to its Vault client — no volume mount or VAULT_CACERT env wiring needed:

  secretStore:
    backend: vault
    vault:
      address: https://openbao.example.com:8200
      mount: secret
      kubernetesAuthRole: weebo-authentik-operator
      caSecretRef:
        name: openbao-tls
        namespace: weebo-authentik
        key: ca.crt # must hold a PEM bundle

When caSecretRef is unset, the system trust store is used. A configured-but-unreadable caSecretRef (missing Secret or key) is a hard reconcile error, never a silent fall-back to the system roots. The operator's ClusterRole already grants get on Secrets cluster-wide, so the CA Secret may live in any namespace.

Per-application targets and fan-out

The secretStore above defines the default destination for every application on the instance: a target named after the application, in its namespace. An individual AuthentikApplication can override that by listing explicit spec.secretTargets. When one or more targets are set, the credentials are written to every one of them; Kubernetes and Vault targets may be mixed, so a single application can populate two Vault paths and a Kubernetes Secret at once:

apiVersion: authentik.weebo.io/v1alpha1
kind: AuthentikApplication
metadata:
  name: frontend
  namespace: team-a
spec:
  instanceRef:
    name: main
  name: Frontend
  slug: frontend
  provider:
    kind: oauth2
    oauth2:
      authorizationFlow: default-authorization-flow
      invalidationFlow: default-invalidation-flow
  secretTargets:
    - backend: vault
      name: frontend # default path: <pathPrefix>/team-a/frontend
    - backend: vault
      name: frontend-shared
      path: weebo-authentik/team-a/shared/frontend # explicit KV path
    - name: frontend-oauth # backend defaults to kubernetes

Notes:

  • Every target has a name: the Secret name for Kubernetes, the last segment of the default <pathPrefix>/<namespace>/<name> path for Vault (when path is unset). backend defaults to kubernetes.
  • A target stays in the application's namespace (namespace, when set, must be the application's own), and an explicit Vault path must stay under <pathPrefix>/<namespace>/ — a team can't write over another team's credentials — unless the instance's pathGrants open it (see below). A path out of reach is refused at admission (kubectl apply fails with SecretStoreError) and again at every reconcile.
  • A vault target reuses the connection/auth config from the instance's secretStore.vault (address, mount, Kubernetes-auth role) — it only chooses where under that mount to write. The instance must therefore carry a secretStore.vault block for Vault targets to work, even if the instance's default backend is kubernetes; otherwise the application reconcile errors. Vault is only logged in to when a target needs it.
  • The operator records which application owns each value (label app.kubernetes.io/managed-by: weebo-authentik and annotation authentik.weebo.io/owner on Secrets, owner custom metadata in Vault) and never overwrites nor deletes a value owned by someone else: such a target fails the reconcile with SecretStoreError. Secrets written by earlier releases are adopted.
  • secretTargets is only meaningful for provider.kind: oauth2 — proxy providers carry no client secret, so it is ignored for them.

Writing outside the namespace subtree

When an admin declares every application in one namespace (say auth) and each credential must land where its consumer already reads it (argocd/auth, batlehub/auth…), the AuthentikInstance opens those paths with secretStore.vault.pathGrants:

  secretStore:
    vault:
      address: https://vault.example.com:8200
      mount: secret
      kubernetesAuthRole: weebo-authentik-operator
      pathGrants:
        - namespaces: [auth] # names, `*`, or a prefix glob like `team-*`
          paths:
            - argocd/auth
            - batlehub/auth
            - eclipse-che/auth

An application in a covered namespace may then use those paths as explicit paths. Patterns are compared segment by segment: a literal segment matches itself, + exactly one segment (+/auth covers batlehub/auth, not a/b/auth), and a last * one or more (batlehub/*). Every pattern holds at least one literal segment, and the CRD refuses .., empty segments and a * before the last segment. A grant lists at most 64 paths, an instance at most 32 grants.

Before granting:

  • A grant opens its paths to every subject allowed to create an AuthentikApplication in a covered namespace. A grant on team-* opens them to every team. It adds to RBAC and AuthentikNamespacePolicy, it doesn't replace them.
  • Ownership still applies: a path already owned by another CR is never overwritten. But a value with no owner yet (written by Terraform, by hand) is taken over by its first writer. Prefer exact paths over +/* wherever unmanaged values live.
  • Vault stays the hard bound. Grants only stop the operator from writing what its role could write; restrict the Vault policy of kubernetesAuthRole to the same paths.
  • Removing a grant leaves the values in place, and the applications that wrote there fail with SecretStoreError — deleting them too — until the grant is back or their path changes.

AuthentikLocalInstances have no Vault connection, hence no grants.

A namespace's own instance

An AuthentikLocalInstance is an AuthentikInstance owned by a namespace: its token (and TLS CA) live in a Secret of that namespace, and only the AuthentikApplications of that namespace use it, unless its sharedWith lists other namespaces (namespaces, namespaceSelector) or accepts cluster-scoped objects. Applications bound to it are not governed by the AuthentikNamespacePolicy — the namespace's own token bounds what they can do — and write their credentials as Kubernetes Secrets only (Vault targets need an AuthentikInstance: the operator's Vault identity is shared by every namespace).

apiVersion: v1
kind: Secret
metadata:
  name: authentik-api-token
  namespace: team-a
  labels:
    authentik.weebo.io/instance-token: "true"
stringData:
  token: <API token>
---
apiVersion: authentik.weebo.io/v1alpha1
kind: AuthentikLocalInstance
metadata:
  name: main
  namespace: team-a
spec:
  url: https://login.team-a.example.com
  tokenSecretRef:
    name: authentik-api-token
    key: token

The token Secret must carry the label authentik.weebo.io/instance-token: "true", or the instance and its applications fail with InstanceTokenNotAllowed. The operator reads Secrets with its own rights and sends the token to spec.url: without that consent, anyone allowed to create an AuthentikLocalInstance could send any Secret of the namespace to a server of their choosing. An AuthentikInstance, created by cluster admins, needs no label.

Upgrading from a release before 0.17: label every Secret an AuthentikLocalInstance names before upgrading (kubectl label secret -n team-a authentik-api-token authentik.weebo.io/instance-token=true).

spec.instanceRef finds the instance by name: an operator watching the whole cluster tries the AuthentikInstance first, then the AuthentikLocalInstance of the application's namespace; one limited to a namespace (watch.namespace) only uses its local instances. kind (Instance or LocalInstance) and namespace (another namespace's local instance) pin one. Once resolved, the instance is recorded in status.instance and kept: a cluster AuthentikInstance created later under the same name doesn't take the application over (InstanceBindingChanged until kind says which).

spec:
  instanceRef:
    name: main
    kind: LocalInstance

Upgrading from a release where instanceRef was a bare name: stop the operator, apply the new CRDs, run weebo-authentik-operator migrate-instance-ref (--dry-run first), start the operator — and rewrite instanceRef: main as instanceRef: {name: main} in your manifests.

Multiple instances

Only AuthentikApplication carries an explicit instanceRef. AuthentikGroup/AuthentikUser/AuthentikOutpost/AuthentikBrand have no such field — the operator resolves them against the single AuthentikInstance CR in the cluster, and errors (AmbiguousDefaultInstance) if there's more than one. Running against more than one real Authentik instance at a time isn't supported yet for those kinds.

On this page