Compare commits

..

18 Commits

Author SHA1 Message Date
benvin 9508c151c4 Merge pull request 'Make the ArgoCD OAuth2 client public and register the mobile callback' (#39) from benvin/argocd-public-client into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #39
2026-09-19 15:40:18 +10:00
unkin-agent 405dede578 Make the ArgoCD OAuth2 client public and register the mobile callback
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
The ArgoCD mobile app and CLI are native clients that cannot hold a
secret, and Authentik derives the iss claim from the application slug,
so they cannot have a client of their own either. Serve all three
clients from the one provider.

- switch client_type to public
- add argocd://auth/callback as a strict redirect URI
2026-09-19 15:26:18 +10:00
benvin 4fceaeeee6 Merge pull request 'Manage human role membership from config/users/' (#37) from benvin/media-adult-membership into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #37
2026-09-19 14:55:02 +10:00
benvin 1e42fdfc2c Merge pull request 'Nest media groups into akR-global-admin' (#36) from benvin/global-admin-media-access into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #36
2026-09-19 14:53:56 +10:00
benvin e2d7de4148 Merge branch 'main' into benvin/media-adult-membership
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
2026-09-19 13:48:16 +10:00
benvin 331330872d Merge branch 'main' into benvin/global-admin-media-access
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
2026-09-19 13:48:05 +10:00
benvin 3f6a046dff Merge pull request 'Pin the authentik provider to 2026.5.1' (#38) from benvin/pin-authentik-provider into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #38
2026-09-19 12:59:13 +10:00
unkin-agent 3662ff15ca Pin the authentik provider to 2026.5.1
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
Provider 2026.8.0 requires pbm_uuid on the applications API, which the
deployed authentik 2026.5.3 server does not return, so every
authentik_application read fails and no plan completes.

- Pin goauthentik/authentik to 2026.5.1, the newest release built on the
  2026.5 API client
- Record 2026.5.1 in the lock file
- Drop -upgrade from make init so the lock is authoritative
2026-09-19 12:51:26 +10:00
unkin-agent d848d9ae86 Manage human role membership from config/users/
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline failed
Add a users/ config kind mapping a human to the akR-* roles they hold.
Look accounts up with data.authentik_user; never declare them. Set
authentik_group.role users only for roles a user file names, leaving
every other role's membership untouched. No assignments yet.
2026-09-19 12:46:58 +10:00
unkin-agent fedbb2fa42 Nest media groups into akR-global-admin
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline failed
Global admins get akP-jellyfin-admin but no path to a jellyfin-bound
group, so Authentik denies them at the application gate. Add
akP-media-fafflix and akP-media-cheeztv to reach it.
2026-09-19 12:44:05 +10:00
benvin e5dbcb56a6 Merge pull request 'Import the stranded artifactapi access binding' (#35) from benvin/fix-artifactapi-policy-binding into main
ci/woodpecker/push/apply Pipeline failed
Reviewed-on: #35
2026-09-08 00:11:37 +10:00
unkin-agent cd31cb6fa1 Import the artifactapi access binding stranded outside state
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
The apply that created authentik_policy_binding.app_access["akP-artifactapi-admin"]
read it back off the Authentik Postgres read replica before the row had
replicated, got a 404, and dropped it, leaving main red.

Import the existing binding c9f22628-d48c-477b-b9ac-a952c7d081ce into state.
2026-09-07 23:49:48 +10:00
benvin 4e164014d7 Merge pull request 'Onboard the artifactapi web UI to Authentik OIDC' (#34) from benvin/artifactapi-ui-oidc into main
ci/woodpecker/push/apply Pipeline failed
Reviewed-on: #34
2026-09-07 19:59:42 +10:00
unkin-agent 0d17dc2cf1 Retrigger plan after credential seed
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
2026-09-07 17:44:02 +10:00
unkin-agent a6d6681bc9 Onboard the artifactapi web UI to Authentik OIDC
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline failed
Add the OAuth2/OIDC provider + application and the akP-artifactapi-admin
permission group gating it, nested under akR-global-admin.
2026-09-07 13:59:45 +10:00
benvin 93c634e4c4 Merge pull request 'Onboard repospawner UI to Authentik OIDC' (#32) from benvin/repospawner-oidc into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #32
2026-08-31 22:21:09 +10:00
unkin-agent 3977f6f86b Merge remote-tracking branch 'origin/main' into benvin/repospawner-oidc
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
# Conflicts:
#	config/roles/akR-global-admin.yaml
2026-08-31 22:07:36 +10:00
unkin-agent 2f80c4a536 Onboard repospawner UI to Authentik OIDC
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline failed
Add an OAuth2/OIDC provider + application for the repospawner operator tool,
fronted by oauth2-proxy, and gate it on a new akP-repospawner-admin permission
group nested under akR-global-admin (mirrors the watchstate precedent).
2026-08-30 15:09:47 +10:00
15 changed files with 205 additions and 56 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ endef
init:
@$(call vault_env) && \
terragrunt run --all --non-interactive init -- -upgrade
terragrunt run --all --non-interactive init
plan: init
@$(call vault_env) && \
+4 -2
View File
@@ -4,7 +4,8 @@ Terraform configuration for managing the Authentik identity provider at identity
## Managed Resources
- **Groups** — roles and group hierarchy (users are invited manually)
- **Groups** — roles and group hierarchy (accounts themselves are created elsewhere)
- **User role membership** — which `akR-*` roles a human holds (see `config/users/`)
- **SAML providers** — SAML application integrations
- **OAuth2/OIDC providers** — OAuth2 and OpenID Connect integrations
- **LDAP providers** — LDAP provider and outpost configuration
@@ -21,7 +22,8 @@ config/
├── providers_saml/ # SAML provider definitions
├── providers_oauth2/ # OAuth2/OIDC provider definitions
├── providers_ldap/ # LDAP provider definitions
└── service_accounts/ # Automation service accounts + API tokens
├── service_accounts/ # Automation service accounts + API tokens
└── users/ # Human role membership (authoritative per named role)
```
## Usage
+5
View File
@@ -42,5 +42,10 @@ locals {
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "service_accounts/")
}
users = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "users/")
}
}
}
@@ -0,0 +1,3 @@
# Permission group akP-artifactapi-admin (name = filename). Grants admin
# access to artifactapi: bound to the artifactapi application, gating the web UI.
application: artifactapi
@@ -0,0 +1,3 @@
# Permission group akP-repospawner-admin (name = filename). Grants admin
# access to repospawner: bound to the repospawner application, gating the UI.
application: repospawner
+14 -2
View File
@@ -1,9 +1,18 @@
# OAuth2/OIDC provider + application for the in-cluster ArgoCD
# (argocd.k8s.syd1.au.unkin.net). client_secret is read from Vault, not committed.
# (argocd.k8s.syd1.au.unkin.net), serving the web UI, the `argocd` CLI and the
# ArgoCD mobile app.
#
# public, not confidential: the native clients cannot hold a secret, and they
# cannot have their own client either -- Authentik derives the `iss` claim from
# the application slug, while ArgoCD validates every token against the single
# issuer in oidc.config, so a second application would issue tokens ArgoCD
# rejects. One client for all three; the strict redirect URIs below are the
# control. Authentik ignores the secret for public clients, but the Vault read
# stays so argocd-cm's `$argocd-oidc:client_secret` keeps resolving.
name: ArgoCD
authorization_flow: default-provider-authorization-implicit-consent
invalidation_flow: default-provider-invalidation-flow
client_type: confidential
client_type: public
client_id: argocd
client_secret_vault:
mount: kv
@@ -19,3 +28,6 @@ redirect_uris:
# `argocd login --sso` CLI callback (local listener).
- matching_mode: strict
url: http://localhost:8085/auth/callback
# Mobile app callback (custom URL scheme, PKCE).
- matching_mode: strict
url: argocd://auth/callback
+23
View File
@@ -0,0 +1,23 @@
# OAuth2/OIDC provider + application for the artifactapi web UI
# (https://artifactapi.k8s.syd1.au.unkin.net/ui in the artifactapi namespace).
# An oauth2-proxy fronts ONLY /ui and /oauth2; access is gated on the user's
# hierarchical ak_groups claim (akP-artifactapi-admin). The package-manager
# surfaces (/api/v1, /api/v2, /v2, /terraform, /.well-known) are NOT behind this
# provider -- yum, containerd, docker, terraform and CI cannot do a browser flow.
# client_secret is read from Vault (seeded out of band), never committed.
name: artifactapi
authorization_flow: default-provider-authorization-implicit-consent
invalidation_flow: default-provider-invalidation-flow
client_type: confidential
client_id: artifactapi
launch_url: https://artifactapi.k8s.syd1.au.unkin.net/ui/
client_secret_vault:
mount: kv
path: kubernetes/namespace/artifactapi/default/oauth-credentials
scope_mappings:
- goauthentik.io/providers/oauth2/scope-openid
- goauthentik.io/providers/oauth2/scope-email
- goauthentik.io/providers/oauth2/scope-profile
redirect_uris:
- matching_mode: strict
url: https://artifactapi.k8s.syd1.au.unkin.net/oauth2/callback
+25
View File
@@ -0,0 +1,25 @@
# OAuth2/OIDC provider + application for repospawner (the internal repository
# provisioning admin tool, served at
# https://repospawner.k8s.syd1.au.unkin.net (cluster hostname) and
# https://repospawner.unkin.net (external hostname) in the repospawner namespace).
# An oauth2-proxy in front of the UI performs the OIDC login; access is gated on
# the user's hierarchical ak_groups claim (akP-repospawner-admin).
# client_secret is read from Vault (seeded out of band), never committed.
name: repospawner
authorization_flow: default-provider-authorization-implicit-consent
invalidation_flow: default-provider-invalidation-flow
client_type: confidential
client_id: repospawner
launch_url: https://repospawner.unkin.net/
client_secret_vault:
mount: kv
path: kubernetes/namespace/repospawner/default/oauth-credentials
scope_mappings:
- goauthentik.io/providers/oauth2/scope-openid
- goauthentik.io/providers/oauth2/scope-email
- goauthentik.io/providers/oauth2/scope-profile
redirect_uris:
- matching_mode: strict
url: https://repospawner.k8s.syd1.au.unkin.net/oauth2/callback
- matching_mode: strict
url: https://repospawner.unkin.net/oauth2/callback
+6
View File
@@ -1,4 +1,6 @@
# Role akR-global-admin (name = filename): full admin across all onboarded apps.
# Nests akP-media-fafflix / akP-media-cheeztv so admins pass the jellyfin
# application gate and can reach the akP-jellyfin-admin rights nested below.
permissions:
- akP-grafana-admin
- akP-argocd-admin
@@ -8,10 +10,14 @@ permissions:
- akP-traefik-admin
- akP-logviewer-admin
- akP-watchstate-admin
- akP-repospawner-admin
- akP-vault-admin
- akP-artifactapi-admin
# arrstack has no admin tier (it is a proxy front door); grant global admins
# the front door plus every per-app entitlement so they reach all media apps.
- akP-arrstack-user
- akP-arrstack-sonarr
- akP-arrstack-radarr
- akP-arrstack-prowlarr
- akP-media-fafflix
- akP-media-cheeztv
+17
View File
@@ -0,0 +1,17 @@
# users
One file per human, `<username>.yaml`, listing the `akR-*` roles they hold:
```yaml
# Human user jane (username = filename). The account itself is not managed here
# (humans come from LDAP sync / invite); only its role membership is.
roles:
- akR-media-adult
```
The account is looked up by username and must already exist — nothing here
creates users. A role that has no `config/roles/<name>.yaml` fails the plan.
**Naming a role here makes Terraform authoritative over that role's entire
member list**: members added by hand in the Authentik UI for that role are
removed on the next apply. Roles no user file names are left untouched.
@@ -24,4 +24,5 @@ inputs = {
providers_oauth2 = local.config.providers_oauth2
providers_ldap = local.config.providers_ldap
service_accounts = local.config.service_accounts
users = local.config.users
}
+50 -50
View File
@@ -2,68 +2,68 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/goauthentik/authentik" {
version = "2026.5.0"
constraints = ">= 2026.5.0"
version = "2026.5.1"
constraints = "2026.5.1"
hashes = [
"h1:SeznjPKBzSrgo8WasRnuxiGMDSeQHEKsv3U/xw8bhQE=",
"zh:0dc1706f6fbff866f4a96de56a4934b9a277954bcdd0713549a29a9b8ec85153",
"zh:218417ec4e864f2d7e585d6c08d39bccb96d8f3bca16c6f762be15365e434234",
"zh:24f9afa7a1174316da3478811848cd76ef348d8a983310b8d75ed6f45abe1a92",
"zh:560092e47cb8a72b890b3eeafe1803202cd25cf27f5f5a6e2c370f645f5d86ae",
"zh:5bc69d8de198007ad1587e146f98cffacf0d1a571800da549b308ff5f4541474",
"zh:65248dce941472ad2a30d0754d2f3c2db6bb6fe5080946316fb097d6ba7cc79f",
"zh:79c9a59a8d3c60280e27a064668889594da44c60f940b046b7c8e63be01067d0",
"zh:87f26cadcd842d6e6d0af94ef0e56860557f5d07f487b10d69d38b63af68bea5",
"zh:8e42c9d0e77d61cc2e5f8c8b761f6e484774d93771927b4cb5fbdae41209dd33",
"zh:94ff632b9b4841527c6b652d51a850a8a47c84c0308a3efc189e0ff7e2558f87",
"zh:b8d32d9f17a905b63c87a23306c02c295b7c8b70f72950071aa3086396932816",
"zh:c91982af99474fc2e4e69be36ed3a68847f261963ed79f6a546fc75703992f99",
"zh:eb9c1fd3020cf61e9b7a6a38d2965f4b521495a9928705e963459a4af857f97d",
"h1:L9q3pjCoeKQdn0/OApv4O1HiC/PDLDqcnM7Ff5XFB+0=",
"zh:05f252734db99792e5d3f52b582a4e0027348ac2614d8c9621bf7e623cff8036",
"zh:0e1cd8041650aed9bb8d4af23301e786223ed5d1a00c2a5150b7ad874ae03917",
"zh:26b1daebd30650df411f903248e061cf963712a063b38e731782fb13a5755ddf",
"zh:362b536ff6bf8866c7c1094c3e2322c22b8c36d8d4caa4db0397cebbcad08561",
"zh:3d94367b853960f5e88efe3ae69d995d02a11dc8d60ec4d7413a37aabf78b379",
"zh:3e840e204677eabe562d17c2ad12afdfa250098725bebac4e91fb8934e233c22",
"zh:4db66139d7a9ec6f7852538573261010d48f0a939e2fe8eeb5c55b01149e3629",
"zh:7b88a830fb26c697f9d240f498f604485454b12e56b7604eda4ca35d10660ecd",
"zh:851bc8bd2d4a16dc71f30da17b6285aa7d85f4bc4175fde7b8319a6a8987ec65",
"zh:8bdfc2c70271d48126eef3b1a30d0d132e868d2118ae7277323ed4c905636e4a",
"zh:e85c03eac43f23f25ad5443a2984cc67977c3abdcb4c4c426d37342c2f25c37e",
"zh:efc296826a700a4e90ee06f8bd3b137617486ce774772aabed3053d4ef8dd906",
"zh:f249033dc242c51600cd3968535efa0780352d5ae7435e8ab1482a7ccc9c2f5e",
]
}
provider "registry.opentofu.org/hashicorp/random" {
version = "3.9.0"
version = "3.9.1"
constraints = ">= 3.6.0"
hashes = [
"h1:8EQU5KSxezcjo/phRSe69rDOI0lk4pSaggj7FsskYp8=",
"zh:03f1114cc20b8913523735ab76e0f0a2b16ce13c92923a53304bf85f07fc0dbc",
"zh:105b678ee72322a3067f105d7e05e940f6143238f377f6e87ff4ec909246ac2a",
"zh:55f3bbf13ea18cbace61a706566a80f25f33fe2b1780b6f3d7b582af2a05b6d2",
"zh:63adf996db48f082f7a6351eb485e219cd88795fc71e6ec60a837263ab0d2cb1",
"zh:7e99550738a4e3cc68b8a467714b0d69371025fe95e3326d5323d026d55653e9",
"zh:8342b54af3a18a37e075eeae61be57f4de2ba71b35d95c5075d402dd2c1f289d",
"zh:83ee18e32ac9dd5fc91298554b7c4cfa4c3a1db50f4c797945637cc93c0844ae",
"zh:993ecc0adbf6bd535a59fbc9b735d8c33950e6f6eb5e621d750da9b71d65d80a",
"zh:ad722bc59d4edbf1415e827fc007c0efe6e0e9462d5568bae20b34be1058a261",
"zh:ae9448e1f87b2f9a6c5197a0e9862162ec6b137cb3a3835e11522995d8939e7c",
"zh:bc9cdd3aac784f759125c6627f6f6416e8726a1c184eb9cf3e55b9edbc94c627",
"zh:c8e35b89572ba1c40a9b20022e033a3395fb8d42e7604d50c900f193ba10382e",
"zh:e2deaa8a9975ef81d9f62baed12c41286918b0a10908e0e031f13f69a3b730a1",
"zh:ee39707557210a0ab1098aa357d2cdfe502e5a312d0dbdffb09d08facc4d3fc5",
"zh:f81afe4eb63e8aa9e0ea71be6c990f0dc69cb360e7191c0742a991f4a5081b64",
"h1:CEQeHfnUDB3uqAkKoEWfWgbj+kpoQHgcuPbAjPzbh+U=",
"zh:09aaf19b0d22726d2378e0e89fbbefc183494d7bd585759d6c4e69ba50951a2f",
"zh:31575ca9bc0db20337096d178ea73bce3ebca343ed071c67f78cf39f800c9ec6",
"zh:624fb6ed552abc34a5aaac41e76a373da65ac08e524b09b672f29c60e6ac896a",
"zh:6a4760d55132b9750ac1a04f6fc32e247034daa999f71452dba9cbca225a529a",
"zh:768a6047cfb8958e7b0b120c580aa3de6624a7fbb2c56ad6df85cd559ed26ec7",
"zh:8983c788ba660bcb587e64ff9c3e4323515caf78facbe0abe6432e7aff8df893",
"zh:8d570eb026a4f00b58a1d36be0ce3c13adf4d973efcd4162b05cb295bbc14257",
"zh:a2259540854d5f699c36b89244fb202ebb2c219b64669a51072687d04fb47152",
"zh:aaa51d905b0e80a28e02f9bee2cf6c91ffade7389d77ab9198aa12809ed04955",
"zh:afb60995e98573facddfb47baedf7e288408680eb00b5d3df570611758947c72",
"zh:b9a46d852ce53fa037f47537a7de53f37b759ccf211600b7ba44c66ba4b616b7",
"zh:bafcfeeefcd0dfefeff120b655b45edb0497c4717534ffe5201b3cb556d1ffe6",
"zh:c3ac24d397eae054aca2290e20943e0c767592cc661c890850c25ac01829308d",
"zh:eafba4127ebadcc5ed0e427935c66fb5e2da7cfdaae39a66d52f4a50d51faf1e",
"zh:f39d4bce213ed9bba3474bad468136af08ff6c4c33adaafcc10c1f78067adfe3",
]
}
provider "registry.opentofu.org/hashicorp/vault" {
version = "5.10.1"
version = "5.12.0"
constraints = ">= 4.0.0"
hashes = [
"h1:wo5cTkl/1nlxMfdn1yEDIHNoRLMczuK6COH2Id4/zeY=",
"zh:0abf976c01f0c0732d0ccc6481e52008be5ee9c8e3d9b5eba0573c640fcf7019",
"zh:2aff4d7ee7ba9eb3de2cd5cda16ba92b4ec7a2b43232aec180984241a323b216",
"zh:2cc186fd0bfc44e100a22b0b40ae8ddcd0ec210a53c1da65d310ee758b1d2b08",
"zh:3f8fb8594736b34af4b26437dd4df4dd4042ad4905223995cfebb8a1f10682ec",
"zh:47fb41b18b74073f557dbcd6aad2183e416293405ccd70c0691a279cfe97f8cd",
"zh:517e2f2764d671c22d22def0384fdfc521b456458189189c0363375495d114dc",
"zh:5a49a2003636f2b8a547d494a6c06d43d62a68299775305408c52eff22b1c11f",
"zh:66d4e716920ada84b0c768f4aca4c8948388995462923349a01bd3818d82b618",
"zh:7599f652e89a3f18fa4b76a59d115cc63255cc36ce6b273850509ba25031abca",
"zh:9c3e38ae7e670de973b6255d7050f526cd2b3ca7c383d7ba7226fc204d97c507",
"zh:d04b046023fa9fd69def678f27e001c298ea34fc99ba51f835cda82e496fdb57",
"zh:d9acd8810f6660cd51bb4c25596632984ae18e93340c82a102d074c6eac95151",
"zh:e161bcb9a22607270b980eeff2ba693335fb62d6978516dff93dd4c91cda99b3",
"zh:ef47502f08cfcb5311b7b16a7905e0052bf28359e07cc00ed080ef454e0946cf",
"zh:f0640ddb52e7e90c5006ff571f6ad0554e593665320c764c57a3d8b7ec31b490",
"h1:HVdhw1ShP/LlYuDOdVKmLO/kLVfngw9VHM2YfDl879Y=",
"zh:070709539eeff2dbf6af13269e6a3c14cea0ee4e5a5fff237d574805df2787e9",
"zh:0e017e993252c37008dcf1848deb32f15a14b303335c3e06885d7efea07fef1d",
"zh:1717ba0a0f8d906ced0eb5a3ed1e2a1808d824868da5095ea1da12d60904777d",
"zh:218f879c5f3f97564f4867bcbb419197c4aa4e41ce5bc3eed50c71926f0519f9",
"zh:2437d8f76d6220883c96a073801af973db957c8b1c79187256c204437f4ed08e",
"zh:9dce3198d2f03ce05c3fb4cb90433de5863f0d1818fc4abc3e3576f562409c85",
"zh:a51d00c192e6cf86d588501d8c4a2a37a258679784651909d55f64d28011b28d",
"zh:c99586f88b0166d5522dedfbec9d246a51efae346bbdfcaeb296ff72b5c80f94",
"zh:cee10cca2a295e4fd9ce2779239f7f79ab97328d9e30833924cceb349e509051",
"zh:d9b5cde1c02db0b211ec3f77b519122f93b442594ce9f20d386ab5d0d05ead93",
"zh:deafe53fa413e71dac120177a58b678e81a64279a692ba77eca09a9bce0bff1a",
"zh:f372b83c70ee5dabd892bd9ec1eb9127fe6331d88178b7b84fa5f52f616b59df",
"zh:f47e4fd82b72e62a5e1eeaaa1b72cc2709545a8431964123b24ed5c03a03e492",
"zh:f629a86c98be3fd2aacb58336d26ce268a76fe754b048f58a3faa0712bb4a837",
"zh:feaef85debc4a4a72d7a23988b18fff0b4dbc47f37e31a0b51022df071ac7b85",
]
}
+36
View File
@@ -16,6 +16,31 @@ resource "authentik_group" "permission" {
attributes = jsonencode(each.value.attributes)
}
# Humans are created outside this module (LDAP sync / invite), so they are
# looked up rather than declared: resolve the username to the numeric pk that
# group membership is keyed on. A missing account fails the plan; nothing here
# can create a user.
data "authentik_user" "human" {
for_each = var.users
username = each.key
lifecycle {
precondition {
condition = length(setsubtract(each.value.roles, keys(var.role_groups))) == 0
error_message = "config/users/${each.key}.yaml names a role with no config/roles/<name>.yaml."
}
}
}
locals {
# Invert user -> roles into role -> member pks. Only roles some user file
# names appear here; every other role falls through to a null `users` below.
role_members = {
for role in distinct(flatten([for u, v in var.users : v.roles])) :
role => [for u, v in var.users : data.authentik_user.human[u].pk if contains(v.roles, role)]
}
}
# Role groups (akR-*): what users are assigned to. Each nests permission groups
# as parents, so a role member is an effective member of every permission it
# grants. Separate resource from permissions so this reference is not a
@@ -27,6 +52,10 @@ resource "authentik_group" "role" {
is_superuser = each.value.is_superuser
parents = [for p in each.value.permissions : authentik_group.permission[p].id]
attributes = jsonencode(each.value.attributes)
# Authoritative: a role claimed by config/users/ has exactly these members, so
# one dropped from a user file is removed. null (every unclaimed role) leaves
# the attribute computed, i.e. membership stays whatever Authentik holds.
users = lookup(local.role_members, each.key, null)
}
# Emit an `ak_groups` claim containing the user's groups AND all inherited
@@ -284,6 +313,13 @@ resource "authentik_policy_binding" "app_access" {
order = 0
}
# Created server-side, but its post-create read-back hit the Authentik Postgres
# read replica before the row replicated and 404'd, so it never reached state.
import {
to = authentik_policy_binding.app_access["akP-artifactapi-admin"]
id = "c9f22628-d48c-477b-b9ac-a952c7d081ce"
}
# Service accounts: non-human identities for automation. Kept out of the group
# hierarchy above (which models human app access) and given capabilities through
# RBAC roles instead.
+15
View File
@@ -156,3 +156,18 @@ variable "service_accounts" {
}))
default = {}
}
# Human role membership. The username is the map key (the config filename). The
# account itself is never managed here — humans are created by LDAP sync/invite
# and only looked up — so this grants and revokes roles, it does not make users.
#
# OWNERSHIP: naming a role in any user file makes Terraform authoritative over
# that role's entire member list, so members added by hand in the UI for that
# role are removed on the next apply. Roles no user file names are left alone.
variable "users" {
type = map(object({
# keys into var.role_groups (akR-*) this user is a member of.
roles = optional(list(string), [])
}))
default = {}
}
+2 -1
View File
@@ -1,9 +1,10 @@
terraform {
required_version = ">= 1.10"
required_providers {
# 2026.8.0 requires pbm_uuid on applications; the deployed 2026.5.3 server does not return it.
authentik = {
source = "goauthentik/authentik"
version = ">= 2026.5.0"
version = "2026.5.1"
}
vault = {
source = "hashicorp/vault"