global: image: digest: sha256:21000cebe8e51eca0620034096586d675cedec8925ac750f7f8966d86eeb0da0 env: - name: AUTHENTIK_STORAGE__MEDIA__BACKEND value: s3 - name: AUTHENTIK_STORAGE__MEDIA__S3__BUCKET_NAME value: authentik - name: AUTHENTIK_STORAGE__MEDIA__S3__ENDPOINT value: https://s3.brunner.ninja/ - name: AUTHENTIK_STORAGE__MEDIA__S3__CUSTOM_DOMAIN value: s3.brunner.ninja/authentik - name: AUTHENTIK_MEDIA_S3_REGION_NAME value: my-store - name: AUTHENTIK_EMAIL__HOST value: smtp-relay.gmail.com - name: AUTHENTIK_EMAIL__PORT value: '587' - name: AUTHENTIK_EMAIL__USE_TLS value: 'true' - name: AUTHENTIK_EMAIL__FROM value: brunner.ninja Auth - name: AUTHENTIK_ERROR_REPORTING__ENABLED value: 'true' - name: AUTHENTIK_POSTGRESQL__HOST value: authentik-postgresql - name: AUTHENTIK_POSTGRESQL__NAME value: authentik - name: AUTHENTIK_POSTGRESQL__USER value: authentik - name: AUTHENTIK_POSTGRESQL__PORT value: '5432' envFrom: - secretRef: name: authentik-runtime authentik: enabled: false blueprints: configMaps: - authentik-blueprints serviceAccount: create: true name: authentik server: 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 - name: AUTHENTIK_WEB__BASE_URL value: https://auth.brunner.ninja - name: AUTHENTIK_WEB__MAX_REQUESTS value: '0' podAnnotations: authentik.brunner.ninja/client-credentials-event-filter: 2026.8.0-1 volumeMounts: - name: client-credentials-event-filter mountPath: /authentik/providers/oauth2/token/client_credentials.py subPath: client_credentials.py readOnly: true - name: dev-shm mountPath: /dev/shm volumes: - name: client-credentials-event-filter configMap: name: authentik-client-credentials-event-filter - name: dev-shm emptyDir: medium: Memory sizeLimit: 512Mi ingress: enabled: true hosts: - auth.brunner.ninja annotations: traefik.ingress.kubernetes.io/router.entrypoints: websecure livenessProbe: timeoutSeconds: 10 readinessProbe: timeoutSeconds: 10 startupProbe: timeoutSeconds: 10 worker: enabled: true geoip: enabled: false postgresql: enabled: true auth: existingSecret: authentik-postgresql secretKeys: adminPasswordKey: postgres-password userPasswordKey: password primary: persistence: enabled: true storageClass: rook-ceph-block size: 20Gi additionalObjects: - apiVersion: v1 kind: ConfigMap metadata: name: authentik-blueprints namespace: authentik data: argocd.yaml: | version: 1 metadata: name: Argo CD OIDC labels: blueprints.goauthentik.io/description: Argo CD login through Authentik entries: - model: authentik_providers_oauth2.scopemapping id: argocd-groups state: present identifiers: name: Argo CD groups attrs: scope_name: groups description: Authentik group names for Argo CD RBAC expression: | return {"groups": [group.name for group in request.user.ak_groups.all()]} - model: authentik_providers_oauth2.oauth2provider id: argocd-provider state: present identifiers: name: Argo CD attrs: authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] client_type: confidential grant_types: - authorization_code - refresh_token client_id: argocd client_secret: !Env ARGOCD_OIDC_CLIENT_SECRET issuer_mode: per_provider include_claims_in_id_token: true signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] redirect_uris: - matching_mode: strict url: https://cd.brunner.ninja/auth/callback property_mappings: - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] - !KeyOf argocd-groups - model: authentik_core.application state: present identifiers: slug: argocd attrs: name: Argo CD provider: !KeyOf argocd-provider launch_url: https://cd.brunner.ninja - apiVersion: v1 kind: ConfigMap metadata: name: authentik-client-credentials-event-filter namespace: authentik data: client_credentials.py: "from base64 import b64decode\nfrom datetime import datetime\n\ \nfrom django.http import HttpRequest\nfrom django.utils import timezone\n\ \nfrom authentik.common.oauth.constants import (\n CLIENT_ASSERTION,\n\ \ CLIENT_ASSERTION_TYPE,\n CLIENT_ASSERTION_TYPE_JWT,\n)\nfrom authentik.core.models\ \ import (\n USER_ATTRIBUTE_GENERATED,\n USER_PATH_SYSTEM_PREFIX,\n\ \ USERNAME_MAX_LENGTH,\n Application,\n Token,\n TokenIntents,\n\ \ User,\n UserTypes,\n)\nfrom authentik.events.middleware import audit_ignore\n\ from authentik.events.models import Event, EventAction\nfrom authentik.flows.planner\ \ import PLAN_CONTEXT_APPLICATION\nfrom authentik.providers.oauth2.errors\ \ import TokenError\nfrom authentik.providers.oauth2.token.base_fed import\ \ FederatedTokenRequest\nfrom authentik.root.middleware import ClientIPMiddleware\n\ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS\n\ \n\ndef save_login_event_unless_loopback(request: HttpRequest, user: User,\ \ **context):\n \"\"\"Do not persist internal loopback client-credentials\ \ audit noise.\"\"\"\n if ClientIPMiddleware.get_client_ip(request) ==\ \ \"::1\":\n return\n Event.new(action=EventAction.LOGIN, **context).from_http(request,\ \ user=user)\n\n\nclass ClientCredentialsTokenRequest(FederatedTokenRequest):\n\ \n def parse(self, request: HttpRequest) -> None:\n super().parse(request)\n\ \ # client_credentials flow with client assertion\n if request.POST.get(CLIENT_ASSERTION_TYPE,\ \ \"\") != \"\":\n return self.post_init_client_credentials_jwt(request)\n\ \ # authentik-custom-ish client credentials flow\n if request.POST.get(\"\ username\", \"\") != \"\":\n return self.post_init_client_credentials_creds(\n\ \ request, request.POST.get(\"username\"), request.POST.get(\"\ password\")\n )\n # Standard method which creates an automatic\ \ user\n if self.client_secret == self.provider.client_secret:\n \ \ return self.post_init_client_credentials_generated(request)\n \ \ # Standard workaround method which stores username:password\n \ \ # as client_secret\n try:\n user, _, password = b64decode(self.client_secret).decode(\"\ utf-8\").partition(\":\")\n return self.post_init_client_credentials_creds(request,\ \ user, password)\n except ValueError:\n raise TokenError(\"\ invalid_grant\") from None\n\n def post_init_client_credentials_creds(\n\ \ self, request: HttpRequest, username: str, password: str\n ):\n\ \ # Authenticate user based on credentials\n user = User.objects.filter(username=username,\ \ is_active=True).first()\n if not user:\n raise TokenError(\"\ invalid_grant\")\n token: Token = Token.objects.filter(\n \ \ key=password, intent=TokenIntents.INTENT_APP_PASSWORD, user=user\n \ \ ).first()\n if not token or token.user.uid != user.uid:\n \ \ raise TokenError(\"invalid_grant\")\n self.user = user\n\ \ # Authorize user access\n app = Application.objects.filter(provider=self.provider).first()\n\ \ if not app or not app.provider:\n raise TokenError(\"\ invalid_grant\")\n self.check_policy_access(app, request)\n\n \ \ save_login_event_unless_loopback(\n request,\n user,\n\ \ **{\n PLAN_CONTEXT_METHOD: \"token\",\n \ \ PLAN_CONTEXT_METHOD_ARGS: {\n \"identifier\"\ : token.identifier,\n },\n PLAN_CONTEXT_APPLICATION:\ \ app,\n },\n )\n\n def post_init_client_credentials_jwt(self,\ \ request: HttpRequest):\n assertion_type = request.POST.get(CLIENT_ASSERTION_TYPE,\ \ \"\")\n if assertion_type != CLIENT_ASSERTION_TYPE_JWT:\n \ \ self.logger.warning(\"Invalid assertion type\", assertion_type=assertion_type)\n\ \ raise TokenError(\"invalid_grant\")\n\n client_secret\ \ = request.POST.get(\"client_secret\", None)\n assertion = request.POST.get(CLIENT_ASSERTION,\ \ client_secret)\n if not assertion:\n self.logger.warning(\"\ Missing client assertion\")\n raise TokenError(\"invalid_grant\"\ )\n\n federated_party = self.validate_jwt(assertion)\n\n if\ \ not federated_party:\n self.logger.warning(\"No token could be\ \ verified\")\n raise TokenError(\"invalid_grant\")\n\n \ \ if \"exp\" in federated_party.parsed_token:\n exp = datetime.fromtimestamp(federated_party.parsed_token[\"\ exp\"])\n # Non-timezone aware check since we assume `exp` is in\ \ UTC\n if datetime.now() >= exp:\n self.logger.info(\"\ JWT token expired\")\n raise TokenError(\"invalid_grant\")\n\ \n app = Application.objects.filter(provider=self.provider).first()\n\ \ if not app or not app.provider:\n self.logger.info(\"\ client_credentials grant for provider without application\")\n \ \ raise TokenError(\"invalid_grant\")\n\n if federated_party.user:\n\ \ self.user = federated_party.user\n self.check_policy_access(app,\ \ request, oauth_jwt=federated_party.parsed_token)\n if not federated_party.user:\n\ \ self.user = self.create_user_from_jwt(federated_party, app, request)\n\ \n method_args = {\n \"jwt\": federated_party.parsed_token,\n\ \ federated_party.type: federated_party.party,\n }\n \ \ save_login_event_unless_loopback(\n request,\n \ \ self.user,\n **{\n PLAN_CONTEXT_METHOD: \"jwt\"\ ,\n PLAN_CONTEXT_METHOD_ARGS: method_args,\n \ \ PLAN_CONTEXT_APPLICATION: app,\n },\n )\n\n def post_init_client_credentials_generated(self,\ \ request: HttpRequest):\n # Authorize user access\n app = Application.objects.filter(provider=self.provider).first()\n\ \ if not app or not app.provider:\n raise TokenError(\"\ invalid_grant\")\n with audit_ignore():\n self.user, _ =\ \ User.objects.update_or_create(\n # trim username to ensure\ \ the entire username is max 150 chars\n # (22 chars being\ \ the length of the \"template\")\n username=f\"ak-{self.provider.name[:\ \ USERNAME_MAX_LENGTH - 22]}-client_credentials\",\n defaults={\n\ \ \"last_login\": timezone.now(),\n \ \ \"name\": f\"Autogenerated user from application {app.name} (client credentials)\"\ ,\n \"path\": f\"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}\"\ ,\n \"type\": UserTypes.SERVICE_ACCOUNT,\n \ \ },\n )\n self.user.attributes[USER_ATTRIBUTE_GENERATED]\ \ = True\n self.user.save()\n self.check_policy_access(app,\ \ request)\n\n save_login_event_unless_loopback(\n request,\n\ \ self.user,\n **{\n PLAN_CONTEXT_METHOD:\ \ \"oauth_client_secret\",\n PLAN_CONTEXT_APPLICATION: app,\n\ \ },\n )\n"