kadet-hub logokadet-hub

authentik on Kubernetes without Helm, with Kapitan

authentik 2026.8.5, the identity provider for OAuth2, OpenID Connect, SAML, LDAP and RADIUS, as one server and one worker, rendered by the kadet-hub kubernetes generator 1.14.0 from authentik’s Helm chart 2026.8.5. The database is the included recipe postgresql.

What it deploys

Required parameters

applications.authentik.secret_key is AUTHENTIK_SECRET_KEY. authentik signs its cookies with it, so a new key ends every session. Any random string works, such as one that openssl rand -base64 60 prints.

applications.authentik.bootstrap_password and applications.authentik.bootstrap_token are the password of the admin account akadmin and its API token authentik-bootstrap-token, which does not expire. They are AUTHENTIK_BOOTSTRAP_PASSWORD and AUTHENTIK_BOOTSTRAP_TOKEN. The worker sets each only when it creates the account or the token, so a later change in the target does not reach them. Change the password in authentik. The worker creates a deleted token again from the target’s value when a new authentik release changes its bootstrap blueprint. To retire the token, delete it and set bootstrap_token to a random value that nobody keeps. Without a password, authentik would let anyone who reaches it first set one through its initial setup flow.

Set the three as refs with the backend of your choice. Without one of them the compile stops with Interpolation key '<key>' not found. postgresql requires no parameter.

Include

A target includes the recipe and its options as classes and can override anything the recipe sets:

classes:
  - recipes.authentik
parameters:
  namespace: authentik
  kapitan:
    vars:
      target: authentik
  applications:
    authentik:
      secret_key: <ref>
      bootstrap_password: <ref>
      bootstrap_token: <ref>

Fetching it

The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.

A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.1:

parameters:
  kapitan:
    dependencies:
      - type: oci
        source: ghcr.io/kadet-hub/recipes/authentik:1.0.1@sha256:ec827f1f2a501d0ac1f7f9f4a51e47f1ec6c37a911014ff636f1b6cb37876162
        output_path: inventory/classes/recipes/authentik

Version 1.0.1 includes postgresql 1.1.1. The consumer workflow adds the included recipes at the digests the index lists. Kapitan's --fetch reads no index, so for a local compile pull them by digest first:

tmp=$(mktemp -d)
oras pull -o "$tmp" ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9
mkdir -p inventory/classes/recipes/postgresql
tar -xf "$tmp/postgresql.tar" -C inventory/classes/recipes/postgresql --no-same-owner

The first command fetches the recipe into inventory/classes/recipes/authentik, the second the generators and compiles:

kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch

The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe and of the recipes it includes:

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/authentik@sha256:ec827f1f2a501d0ac1f7f9f4a51e47f1ec6c37a911014ff636f1b6cb37876162 \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9 \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.

Settings

<s> stands for components.authentik-server, <w> for components.authentik-worker, <d> for applications.authentik.component_defaults, which both components take, and <e> for generators.kubernetes.secrets.authentik.string_data.

Chart value Inventory
global.image.tag <d>.image, tag and digest
server.replicas, worker.replicas <s>.replicas, <w>.replicas, 1
authentik.log_level <e>.AUTHENTIK_LOG_LEVEL.value, info
authentik.email.* <e>.AUTHENTIK_EMAIL__<KEY>.value, such as AUTHENTIK_EMAIL__HOST
authentik.error_reporting.enabled <e>.AUTHENTIK_ERROR_REPORTING__ENABLED.value, "false"
authentik.postgresql.name, .user, .host applications.postgresql.database, .owner and .cluster, which postgresql’s README lists
global.env, server.env, worker.env <d>.env, <s>.env, <w>.env
server.service.type <s>.service.type, ClusterIP
server.resources, worker.resources <d>.resources for both, or <s>.resources and <w>.resources
global.nodeSelector, tolerations <d>.node_selector, <d>.tolerations

The Secret authentik holds the chart’s settings as authentik’s variables, AUTHENTIK_<SECTION>__<KEY>, and both components read all of it. Values are strings, so quote "true" and numbers. authentik connects to PostgreSQL with sslmode=require, which encrypts the connection without verifying the server’s certificate, because CloudNativePG signs it with its own CA. This is authentik.postgresql.sslmode: require in tests/values.yaml. The password comes from postgresql’s Secret postgresql-cluster-app.

The pod annotation checksum/secret is the chart’s hash of its Secret for the values of tests/values.yaml. It does not follow the target’s values, so after changing the Secret restart both Deployments with kubectl rollout restart.

The worker runs as the ServiceAccount authentik, which may manage Secrets, Services, ConfigMaps, Deployments, Ingresses, HTTPRoutes, Traefik middlewares and ServiceMonitors in the namespace and list CRDs, as the chart’s serviceAccount.create sets it up. authentik uses it to deploy the outposts it manages into the namespace. The server runs as the namespace’s default ServiceAccount. GeoIP, email, the Ingress, the HTTPRoute, the metrics Services, ServiceMonitors, the PrometheusRule, the HPAs and the PodDisruptionBudgets are off by default and not part of the recipe. The chart’s PostgreSQL subchart stays off.

Any list set in the target is appended to the recipe’s list. To replace one, set the key to ~ in a class included after the recipe and the new list in the target.

Renamed RBAC objects

Chart Recipe
Role/authentik, RoleBinding/authentik Role/authentik-worker, RoleBinding/authentik-worker
ClusterRole/authentik-<namespace>, ClusterRoleBinding/authentik-<namespace> ClusterRole/<namespace>-authentik-worker, ClusterRoleBinding/<namespace>-authentik-worker

Proof

check authentik --level deploys with tests/values.yaml and tests/target.yml, on kind for each Kubernetes minor CI tests, with cnpg installed first. The objects of postgresql are left out of the comparison and applied with the recipe. The Deployments select on name, the chart on app.kubernetes.io/name, instance and component, and both select the same pods. The pods carry the chart’s labels too, so the anti-affinity of each Deployment selects its own pods. The pods and the ServiceAccount set automountServiceAccountToken: true, the API default the chart leaves unset. The Service targets its ports by name. The chart has no hooks or Helm tests. tests/target.yml runs one PostgreSQL instance.

tests/smoke waits for the Cluster and gets /-/health/live/ and /-/health/ready/ of the server through the Service. With the bootstrap token it reads /api/v3/core/users/me/, which must name akadmin, and /api/v3/admin/version/, which must report the version of the image tag. The worker creates the account, the token and the default flows from authentik’s blueprints after its rollout, so these requests need the server, the worker and PostgreSQL, and the test waits up to five minutes for the token and for the flows. A request with another token gets 403. It then creates an OAuth2 provider on the default flows and an application through the API and gets the application’s /.well-known/openid-configuration, whose issuer must name the application.

Versions

The recipe's files are in kadet-hub/recipes. All recipes