250 lines
13 KiB
YAML
250 lines
13 KiB
YAML
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 <auth@brunner.ninja>
|
|
- 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"
|