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.
- Latest version:
1.0.1, published - Source chart:
oci://ghcr.io/goauthentik/helm-charts/authentik:2026.8.5@sha256:9683518d02b5e41f69d045b59a779c40260608b5631d18273407ea2cfe708712 - Proof level:
deploys, compared with the chart by the kadet-hub/recipes CI renderspassed in the release run of with check 0.18.0, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Prerequisites, installed once per cluster before the recipe:
cnpg - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade F 342 findings, one per advisory and package, in the 1 image of version 1.0.1: CRITICAL 2, HIGH 82, MEDIUM 140, LOW 118, UNKNOWN 0. Scanned 2026-11-07 11:47 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:ghcr.io/goauthentik/server.
What it deploys
recipes.authentik(init.yml)recipes.authentik.server(server.yml):authentik-serverrecipes.authentik.worker(worker.yml):authentik-worker
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
1.0.1, :sha256:ec827f1f2a501d0ac1f7f9f4a51e47f1ec6c37a911014ff636f1b6cb378761621.0.0, :sha256:aa4e7e320dbf21088633bbe43017955d01686da9abfe7443e58606d2da9bc4ec
The recipe's files are in kadet-hub/recipes. All recipes