From 11ce791efaedead896c26e2cdedda053cd41394f Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Sun, 18 Oct 2026 23:10:16 +0200 Subject: [PATCH] Add Authentik Argo CD application --- apps/authentik/README.md | 39 +++ .../resources/authentik-media-redirect.yaml | 29 ++ .../resources/authentik-outpost-path.yaml | 19 ++ .../authentik/resources/ceph-object-user.yaml | 7 + apps/authentik/resources/outpost-alerts.yaml | 32 ++ apps/authentik/values.yaml | 311 ++++++++++++++++++ apps/authentik/verify-outposts.sh | 35 ++ bootstrap/authentik-project.yaml | 53 +++ bootstrap/authentik.yaml | 31 ++ bootstrap/kustomization.yaml | 2 + 10 files changed, 558 insertions(+) create mode 100644 apps/authentik/README.md create mode 100644 apps/authentik/resources/authentik-media-redirect.yaml create mode 100644 apps/authentik/resources/authentik-outpost-path.yaml create mode 100644 apps/authentik/resources/ceph-object-user.yaml create mode 100644 apps/authentik/resources/outpost-alerts.yaml create mode 100644 apps/authentik/values.yaml create mode 100755 apps/authentik/verify-outposts.sh create mode 100644 bootstrap/authentik-project.yaml create mode 100644 bootstrap/authentik.yaml diff --git a/apps/authentik/README.md b/apps/authentik/README.md new file mode 100644 index 0000000..8d3cecb --- /dev/null +++ b/apps/authentik/README.md @@ -0,0 +1,39 @@ +# Authentik homelab deployment + +This directory contains only the homelab values and cluster integrations for +the maintained upstream Authentik chart. The chart owns the Authentik server, +worker, PostgreSQL, Services, Ingress, RBAC, probes, and persistent volume +claim. The local resources retain the existing media route, embedded-outpost +path, Ceph object user, and an alert for the three Authentik-managed outposts. + +## Secrets and recovery boundary + +The public repository never contains credentials. The deployment references +the existing `authentik/authentik-runtime` and +`authentik/authentik-postgresql` Secrets. The existing `argocd/argocd-oidc` +Secret is also retained outside Git and must contain the same OIDC client +secret as `ARGOCD_OIDC_CLIENT_SECRET` in `authentik-runtime`. + +Restore those Secrets before synchronizing Authentik into an empty cluster. +PostgreSQL password rotation must update the database role and both Secret +references together during a maintenance window. + +## Managed outposts + +Authentik, not Argo CD, creates the `ak-outpost-ldap`, +`ak-outpost-radius`, and `ak-outpost-rac` Deployments. Their database +`container_image` overrides are intentionally null so Authentik uses its +upstream `ghcr.io/goauthentik/%(type)s:%(version)s` template. That keeps each +outpost on the same version as the server after a chart upgrade without a +second version list in Git. + +After every Authentik upgrade, verify that all three Deployments use the chart +application version and report one updated, ready, and available replica. The +`AuthentikManagedOutpostsUnavailable` alert detects missing or unavailable +generated outposts, which are outside Argo CD's direct resource inventory. +Run `apps/authentik/verify-outposts.sh` for the version, database-pin, image, +and replica checks; it derives the expected version from the Argo Application. + +The Kubernetes Dashboard provider is attached to the embedded outpost in the +Authentik database. This avoids a blueprint replacing the embedded outpost's +complete provider list, because other applications also own providers there. diff --git a/apps/authentik/resources/authentik-media-redirect.yaml b/apps/authentik/resources/authentik-media-redirect.yaml new file mode 100644 index 0000000..2788cb2 --- /dev/null +++ b/apps/authentik/resources/authentik-media-redirect.yaml @@ -0,0 +1,29 @@ +apiVersion: traefik.io/v1alpha1 +#apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: authentik-media-redirect + namespace: authentik +spec: + entryPoints: + - websecure + routes: + - match: Host(`auth.brunner.ninja`) && PathPrefix(`/media/`) + kind: Rule + services: + - name: rook-ceph-rgw-my-store + namespace: rook-ceph + port: 80 + middlewares: + - name: media-strip-prefix +--- +apiVersion: traefik.io/v1alpha1 +#apiVersion: traefik.containo.us/v1alpha1 +kind: Middleware +metadata: + name: media-strip-prefix + namespace: authentik +spec: + stripPrefix: + prefixes: + - /media diff --git a/apps/authentik/resources/authentik-outpost-path.yaml b/apps/authentik/resources/authentik-outpost-path.yaml new file mode 100644 index 0000000..61a9ebb --- /dev/null +++ b/apps/authentik/resources/authentik-outpost-path.yaml @@ -0,0 +1,19 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: authentik-outpost-path + namespace: authentik + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.priority: "10000" +spec: + rules: + - http: + paths: + - path: /outpost.goauthentik.io/ + pathType: Prefix + backend: + service: + name: authentik-server + port: + number: 80 diff --git a/apps/authentik/resources/ceph-object-user.yaml b/apps/authentik/resources/ceph-object-user.yaml new file mode 100644 index 0000000..2631328 --- /dev/null +++ b/apps/authentik/resources/ceph-object-user.yaml @@ -0,0 +1,7 @@ +apiVersion: ceph.rook.io/v1 +kind: CephObjectStoreUser +metadata: + name: authentik + namespace: rook-ceph +spec: + store: my-store diff --git a/apps/authentik/resources/outpost-alerts.yaml b/apps/authentik/resources/outpost-alerts.yaml new file mode 100644 index 0000000..57d5ad7 --- /dev/null +++ b/apps/authentik/resources/outpost-alerts.yaml @@ -0,0 +1,32 @@ +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: authentik-outposts + namespace: authentik + labels: + release: prometheus +spec: + groups: + - name: authentik-outposts + rules: + - alert: AuthentikManagedOutpostsUnavailable + expr: |- + sum( + kube_deployment_status_replicas_available{ + namespace="authentik", + deployment=~"ak-outpost-(ldap|radius|rac)" + } + ) != 3 + or + absent( + kube_deployment_status_replicas_available{ + namespace="authentik", + deployment=~"ak-outpost-(ldap|radius|rac)" + } + ) + for: 10m + labels: + severity: critical + annotations: + summary: Authentik managed outposts are unavailable + description: Expected one available LDAP, RADIUS, and RAC outpost replica. diff --git a/apps/authentik/values.yaml b/apps/authentik/values.yaml new file mode 100644 index 0000000..e2f46d8 --- /dev/null +++ b/apps/authentik/values.yaml @@ -0,0 +1,311 @@ +global: + 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: + # 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 + - 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: "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 + livenessProbe: + timeoutSeconds: 10 + readinessProbe: + timeoutSeconds: 10 + startupProbe: + timeoutSeconds: 10 +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 + 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: + 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" diff --git a/apps/authentik/verify-outposts.sh b/apps/authentik/verify-outposts.sh new file mode 100755 index 0000000..a99bfee --- /dev/null +++ b/apps/authentik/verify-outposts.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -eu + +project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +version=$(awk ' + $1 == "chart:" && $2 == "authentik" { chart = 1; next } + chart && $1 == "targetRevision:" { print $2; exit } +' "$project_dir/bootstrap/authentik.yaml") + +if [ -z "$version" ]; then + echo "Could not determine the Authentik chart version." >&2 + exit 1 +fi + +pins=$(kubectl --namespace authentik exec deployment/authentik-worker -- \ + ak shell --command \ + 'from authentik.outposts.models import Outpost; [print(f"{o.name}={o.config.container_image}") for o in Outpost.objects.filter(name__in=["ldap", "radius", "rac"]).order_by("name")]') +printf '%s\n' "$pins" +if printf '%s\n' "$pins" | grep -Ev '^(ldap|rac|radius)=None$' >/dev/null; then + echo "A managed outpost still has an explicit database image override." >&2 + exit 1 +fi + +for name in ldap radius rac; do + expected="ghcr.io/goauthentik/$name:$version" + actual=$(kubectl --namespace authentik get "deployment/ak-outpost-$name" \ + --output='jsonpath={.spec.template.spec.containers[0].image}') + replicas=$(kubectl --namespace authentik get "deployment/ak-outpost-$name" \ + --output='jsonpath={.status.updatedReplicas}/{.status.readyReplicas}/{.status.availableReplicas}') + if [ "$actual" != "$expected" ] || [ "$replicas" != "1/1/1" ]; then + echo "ak-outpost-$name: expected $expected at 1/1/1, found $actual at $replicas" >&2 + exit 1 + fi + echo "ak-outpost-$name: $actual, $replicas" +done diff --git a/bootstrap/authentik-project.yaml b/bootstrap/authentik-project.yaml new file mode 100644 index 0000000..71fa35d --- /dev/null +++ b/bootstrap/authentik-project.yaml @@ -0,0 +1,53 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: authentik + namespace: argocd +spec: + description: Authentik identity provider and homelab integrations + sourceRepos: + - https://charts.goauthentik.io + - https://code.brunner.ninja/feedc0de/homelab-gitops.git + destinations: + - namespace: authentik + server: https://kubernetes.default.svc + - namespace: rook-ceph + server: https://kubernetes.default.svc + clusterResourceWhitelist: + - group: "" + kind: Namespace + - group: rbac.authorization.k8s.io + kind: ClusterRole + - group: rbac.authorization.k8s.io + kind: ClusterRoleBinding + namespaceResourceWhitelist: + - group: "" + kind: ConfigMap + - group: "" + kind: PersistentVolumeClaim + - group: "" + kind: Service + - group: "" + kind: ServiceAccount + - group: apps + kind: Deployment + - group: apps + kind: StatefulSet + - group: ceph.rook.io + kind: CephObjectStoreUser + - group: monitoring.coreos.com + kind: PrometheusRule + - group: networking.k8s.io + kind: Ingress + - group: networking.k8s.io + kind: NetworkPolicy + - group: policy + kind: PodDisruptionBudget + - group: rbac.authorization.k8s.io + kind: Role + - group: rbac.authorization.k8s.io + kind: RoleBinding + - group: traefik.io + kind: IngressRoute + - group: traefik.io + kind: Middleware diff --git a/bootstrap/authentik.yaml b/bootstrap/authentik.yaml new file mode 100644 index 0000000..927611b --- /dev/null +++ b/bootstrap/authentik.yaml @@ -0,0 +1,31 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: authentik + namespace: argocd +spec: + project: authentik + sources: + - repoURL: https://charts.goauthentik.io + chart: authentik + targetRevision: 2026.8.4 + helm: + releaseName: authentik + valueFiles: + - $values/apps/authentik/values.yaml + - repoURL: https://code.brunner.ninja/feedc0de/homelab-gitops.git + targetRevision: main + ref: values + - repoURL: https://code.brunner.ninja/feedc0de/homelab-gitops.git + targetRevision: main + path: apps/authentik/resources + destination: + server: https://kubernetes.default.svc + namespace: authentik + syncPolicy: + automated: + enabled: true + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/bootstrap/kustomization.yaml b/bootstrap/kustomization.yaml index 5b07d3c..096615a 100644 --- a/bootstrap/kustomization.yaml +++ b/bootstrap/kustomization.yaml @@ -9,6 +9,8 @@ resources: - immich-sync.yaml - immichframe.yaml - anubis.yaml +- authentik-project.yaml +- authentik.yaml - metallb-project.yaml - metallb.yaml - brgkepler-com.yaml