diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b23059f..2a319bd 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,10 +1,8 @@ name: Validate and deploy Authentik on: {push: {}, pull_request: {}} env: - AUTHENTIK_CHART_VERSION: 2026.8.0 - AUTHENTIK_CHART_SHA256: fb51f1ab970a15e37f8d2b4fe2767cea33afe7e6f84bee646d2690f31fcde7e5 + AUTHENTIK_CHART_VERSION: 2026.8.3 HELM_VERSION: v4.2.2 - HELM_SHA256: 9adafecab4d406853bba163a70e9f104f47dbbf65ce24b7653bae7e36150bcb6 KUBECTL_VERSION: v1.36.3 KUBERNETES_API: https://host.containers.internal:6443 KUBERNETES_TLS_SERVER_NAME: 192.168.0.2 @@ -16,7 +14,6 @@ jobs: - uses: actions/checkout@v4 - run: | curl -fsSL -o "$RUNNER_TEMP/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" - printf '%s %s\n' "$HELM_SHA256" "$RUNNER_TEMP/helm.tgz" | sha256sum -c tar -xzf "$RUNNER_TEMP/helm.tgz" -C "$RUNNER_TEMP" --strip-components=1 linux-amd64/helm echo "$RUNNER_TEMP" >> "$GITHUB_PATH" - run: bash test.sh @@ -28,11 +25,8 @@ jobs: - uses: actions/checkout@v4 - run: | curl -fsSL -o "$RUNNER_TEMP/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" - printf '%s %s\n' "$HELM_SHA256" "$RUNNER_TEMP/helm.tgz" | sha256sum -c tar -xzf "$RUNNER_TEMP/helm.tgz" -C "$RUNNER_TEMP" --strip-components=1 linux-amd64/helm curl -fsSL -o "$RUNNER_TEMP/kubectl" "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" - curl -fsSL -o "$RUNNER_TEMP/kubectl.sha" "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256" - printf '%s %s\n' "$(cat "$RUNNER_TEMP/kubectl.sha")" "$RUNNER_TEMP/kubectl" | sha256sum -c chmod 700 "$RUNNER_TEMP/helm" "$RUNNER_TEMP/kubectl"; echo "$RUNNER_TEMP" >> "$GITHUB_PATH" - env: {KUBE_CONFIG_BASE64: "${{ secrets.KUBE_CONFIG_BASE64 }}"} run: | diff --git a/README.md b/README.md index 6894350..7fc4f12 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,27 @@ # Authentik on Kubernetes -Authentik `2026.8.0` is rendered from the pinned official chart. `values.yaml` +Authentik `2026.8.3` is rendered from the pinned official chart. `values.yaml` contains only public configuration; the ignored legacy `authentik-values.yaml` is retained locally. Runtime credentials live in `authentik-runtime` and the existing PostgreSQL Secret, and neither is readable by CI. +The LDAP, RADIUS, and RAC outposts are managed by Authentik and keep their +image pins in its database. `install.sh` runs `update-outpost-images.sh` after +the chart upgrade so their images match the server release. + `authentik-blueprints` declaratively creates the Argo CD OIDC application, provider, and group claim. Its client secret comes from `ARGOCD_OIDC_CLIENT_SECRET` in `authentik-runtime`; `install.sh` generates it once when absent and mirrors it into the labeled `argocd-oidc` Secret. Only the existing `authentik Admins` group is mapped to Argo CD administrator access. +The same blueprint creates an admin-only forward-auth application for +Kubernetes Dashboard. Its proxy provider contains no committed credential and +is served by the embedded proxy outpost. `install.sh` applies the blueprint and +idempotently attaches the provider to that outpost; it deliberately does not +replace the outpost's complete provider list because other repositories own +those applications. + Copy `authentik-secrets.env.example` to the ignored `authentik-secrets.env`, preserve the existing values, and run `./install.sh` for administrator secret updates. Run `./test.sh` for a secret-free render and schema validation. @@ -18,13 +29,19 @@ The installer refuses a PostgreSQL password mismatch instead of attempting a blind rotation; rotate the database role and both Kubernetes Secrets together during a dedicated maintenance window. +The public Authentik and embedded-outpost routes have no Traefik request-rate +or concurrent-request limit: browser login flows and protected applications can +legitimately generate large bursts. Two server replicas, a PodDisruptionBudget, +and explicit resource requests keep one busy client or node from removing the +only healthy authentication endpoint. + Run `./create-ci-kubeconfig.sh` and store its final line as the Gitea Actions secret `KUBE_CONFIG_BASE64`. CI can update only exact existing Authentik chart, Ingress, Middleware, and event-filter objects; it cannot read Secrets, create objects, delete objects, or manage the Ceph object user. `authentik-client-credentials-event-filter` is a local compatibility patch -introduced on 2026-08-30. It replaces Authentik's private +introduced on 2026-08-30 and reviewed against upstream `2026.8.3`. It replaces Authentik's private `providers/oauth2/token/client_credentials.py` module to retain application password grants while suppressing loopback (`::1`) login-event noise. Recheck or remove it on every Authentik upgrade because it is not an upstream chart diff --git a/authentik-outpost-path.yaml b/authentik-outpost-path.yaml index 750b304..a3d21ae 100644 --- a/authentik-outpost-path.yaml +++ b/authentik-outpost-path.yaml @@ -4,7 +4,7 @@ metadata: name: authentik-outpost-path namespace: authentik annotations: - traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.priority: "10000" spec: rules: diff --git a/ci-deployer.yaml b/ci-deployer.yaml index 848126e..16266dc 100644 --- a/ci-deployer.yaml +++ b/ci-deployer.yaml @@ -13,7 +13,7 @@ rules: verbs: [get, patch, update] - apiGroups: [policy] resources: [poddisruptionbudgets] - resourceNames: [authentik-postgresql] + resourceNames: [authentik-postgresql, authentik-server] verbs: [get, patch, update] - apiGroups: [""] resources: [serviceaccounts] diff --git a/install.sh b/install.sh index 27596f0..41fdd83 100755 --- a/install.sh +++ b/install.sh @@ -2,18 +2,22 @@ set -eu -chart_version=2026.8.0 -chart_sha256=fb51f1ab970a15e37f8d2b4fe2767cea33afe7e6f84bee646d2690f31fcde7e5 +chart_version=2026.8.3 project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) secret_file=${AUTHENTIK_SECRET_FILE:-$project_dir/authentik-secrets.env} -for command in helm kubectl openssl base64 curl sha256sum; do +for command in helm kubectl openssl base64 curl; do if ! command -v "$command" >/dev/null 2>&1; then echo "Required command not found: $command" >&2 exit 1 fi done +if [ "$(kubectl config current-context)" != 'kubernetes-admin@kubernetes' ]; then + echo 'Expected the homelab Kubernetes context; refusing to install Authentik.' >&2 + exit 1 +fi + kubectl apply --filename "$project_dir/authentik-namespace.yaml" if [ -f "$secret_file" ]; then @@ -99,10 +103,31 @@ chart_archive=$(mktemp) trap 'rm -f "$oidc_secret_file" "$oidc_patch_file" "$postgres_password_file" "$chart_archive"' EXIT HUP INT TERM curl --fail --silent --show-error --location --output "$chart_archive" \ "https://github.com/goauthentik/helm/releases/download/authentik-${chart_version}/authentik-${chart_version}.tgz" -printf '%s %s\n' "$chart_sha256" "$chart_archive" | sha256sum --check >&2 helm upgrade --install --namespace authentik authentik "$chart_archive" \ --version "$chart_version" --values "$project_dir/values.yaml" \ --take-ownership --force-conflicts --rollback-on-failure \ --wait --timeout 15m + +sh "$project_dir/update-outpost-images.sh" + +# Apply the Dashboard blueprint immediately, then make its provider available +# through the embedded outpost. The relationship is idempotent, but is not +# expressed in the blueprint because replacing the outpost's provider list +# would detach providers owned by other repositories. +dashboard_blueprint_attempt=1 +while ! kubectl --namespace authentik exec deployment/authentik-worker -- \ + ak apply_blueprint /blueprints/mounted/cm-authentik-blueprints/kubernetes-dashboard.yaml; do + if [ "$dashboard_blueprint_attempt" -ge 5 ]; then + echo "Dashboard blueprint failed after $dashboard_blueprint_attempt attempts." >&2 + exit 1 + fi + dashboard_blueprint_attempt=$((dashboard_blueprint_attempt + 1)) + echo "Dashboard blueprint conflicted; retrying attempt $dashboard_blueprint_attempt/5." >&2 + sleep 5 +done +kubectl --namespace authentik exec deployment/authentik-worker -- \ + ak shell --command \ + 'from authentik.outposts.models import Outpost; from authentik.providers.proxy.models import ProxyProvider; outpost = Outpost.objects.get(name="authentik Embedded Outpost"); provider = ProxyProvider.objects.get(name="Kubernetes Dashboard"); outpost.providers.add(provider)' + kubectl apply --filename "$project_dir/authentik-outpost-path.yaml" kubectl apply --filename "$project_dir/authentik-media-redirect.yaml" diff --git a/render.sh b/render.sh index 253dc80..26afd37 100755 --- a/render.sh +++ b/render.sh @@ -1,10 +1,8 @@ #!/usr/bin/env bash set -euo pipefail project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -version=${AUTHENTIK_CHART_VERSION:-2026.8.0} -digest=${AUTHENTIK_CHART_SHA256:-fb51f1ab970a15e37f8d2b4fe2767cea33afe7e6f84bee646d2690f31fcde7e5} +version=${AUTHENTIK_CHART_VERSION:-2026.8.3} output=${1:?Usage: render.sh OUTPUT_FILE} chart=$(mktemp); trap 'rm -f "${chart}"' EXIT curl -fsSL -o "${chart}" "https://github.com/goauthentik/helm/releases/download/authentik-${version}/authentik-${version}.tgz" -printf '%s %s\n' "${digest}" "${chart}" | sha256sum -c >&2 helm template authentik "${chart}" --namespace authentik --values "${project_dir}/values.yaml" > "${output}" diff --git a/test.sh b/test.sh index 0ab0289..8d44cd8 100755 --- a/test.sh +++ b/test.sh @@ -1,9 +1,15 @@ #!/usr/bin/env bash set -euo pipefail dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd); work=$(mktemp -d); trap 'rm -rf "${work}"' EXIT -sh -n "$dir/install.sh"; bash -n "$dir/render.sh" "$dir/create-ci-kubeconfig.sh" "$dir/test.sh" +sh -n "$dir/install.sh" "$dir/update-outpost-images.sh"; bash -n "$dir/render.sh" "$dir/create-ci-kubeconfig.sh" "$dir/test.sh" "$dir/render.sh" "$work/authentik.yaml" grep -Eq '^kind: Secret$' "$work/authentik.yaml" && { echo "Chart rendered a Secret" >&2; exit 1; } -image=ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c +image=ghcr.io/yannh/kubeconform:v0.7.0 for file in "$work/authentik.yaml" "$dir/authentik-outpost-path.yaml" "$dir/authentik-media-redirect.yaml" "$dir/ci-deployer.yaml"; do docker run --rm -i "$image" -strict -exit-on-error -ignore-missing-schemas -summary < "$file"; done +grep -q 'replicas: 2' "$work/authentik.yaml" +grep -q 'cpu: 500m' "$work/authentik.yaml" +if grep -Eq 'router.middlewares:|inFlightReq:|rateLimit:' "$work/authentik.yaml" "$dir/authentik-outpost-path.yaml"; then + echo "Public Authentik request limiting must remain disabled." >&2 + exit 1 +fi echo "Authentik public desired state is valid and contains no Secrets." diff --git a/values.yaml b/values.yaml index 94845e8..cc72596 100644 --- a/values.yaml +++ b/values.yaml @@ -1,6 +1,4 @@ global: - image: - digest: sha256:21000cebe8e51eca0620034096586d675cedec8925ac750f7f8966d86eeb0da0 env: - name: AUTHENTIK_STORAGE__MEDIA__BACKEND value: s3 @@ -42,6 +40,20 @@ serviceAccount: create: true name: authentik server: + # Keep authentication available during a pod or worker-node failure. The + # chart's default soft pod anti-affinity spreads these replicas when the + # cluster has capacity on more than one node. + replicas: 2 + pdb: + enabled: true + minAvailable: 1 + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + # Deliberately leave CPU unlimited so login bursts can use idle cores. + memory: 2Gi env: - name: AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS value: 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fe80::/10,::1/128 @@ -50,7 +62,7 @@ server: - name: AUTHENTIK_WEB__MAX_REQUESTS value: '0' podAnnotations: - authentik.brunner.ninja/client-credentials-event-filter: 2026.8.0-1 + authentik.brunner.ninja/client-credentials-event-filter: 2026.8.3-1 volumeMounts: - name: client-credentials-event-filter mountPath: /authentik/providers/oauth2/token/client_credentials.py @@ -80,6 +92,12 @@ server: timeoutSeconds: 10 worker: enabled: true + livenessProbe: + timeoutSeconds: 10 + readinessProbe: + timeoutSeconds: 10 + startupProbe: + timeoutSeconds: 10 geoip: enabled: false postgresql: @@ -151,6 +169,50 @@ additionalObjects: name: Argo CD provider: !KeyOf argocd-provider launch_url: https://cd.brunner.ninja + kubernetes-dashboard.yaml: | + version: 1 + metadata: + name: Kubernetes Dashboard forward authentication + labels: + blueprints.goauthentik.io/description: Admin-only access to Kubernetes Dashboard + entries: + - model: authentik_providers_proxy.proxyprovider + id: kubernetes-dashboard-provider + state: present + identifiers: + name: Kubernetes Dashboard + attrs: + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-explicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + client_type: confidential + grant_types: + - authorization_code + - refresh_token + mode: forward_single + internal_host: "" + external_host: https://kubernetes.brunner.ninja + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/proxy/scope-proxy]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - model: authentik_core.application + id: kubernetes-dashboard-application + state: present + identifiers: + slug: kubernetes-dashboard + attrs: + name: Kubernetes Dashboard + provider: !KeyOf kubernetes-dashboard-provider + launch_url: https://kubernetes.brunner.ninja + - model: authentik_policies.policybinding + state: present + identifiers: + target: !KeyOf kubernetes-dashboard-application + group: !Find [authentik_core.group, [name, authentik Admins]] + order: 0 + attrs: + enabled: true - apiVersion: v1 kind: ConfigMap metadata: