diff --git a/terraform/authentik/.claude/settings.local.json b/terraform/authentik/.claude/settings.local.json index 9f30b05..ec2d7f4 100644 --- a/terraform/authentik/.claude/settings.local.json +++ b/terraform/authentik/.claude/settings.local.json @@ -7,7 +7,8 @@ "Bash(\"C:\\Users\\ab\\AppData\\Local\\Microsoft\\WinGet\\Packages\\Hashicorp.Terraform_Microsoft.Winget.Source_8wekyb3d8bbwe\\terraform.exe\" apply -auto-approve)", "Bash(\"C:\\Users\\ab\\AppData\\Local\\Microsoft\\WinGet\\Packages\\Hashicorp.Terraform_Microsoft.Winget.Source_8wekyb3d8bbwe\\terraform.exe\" apply -auto-approve -lock=false)", "Bash(\"C:\\Users\\ab\\AppData\\Local\\Microsoft\\WinGet\\Packages\\Hashicorp.Terraform_Microsoft.Winget.Source_8wekyb3d8bbwe\\terraform.exe\" plan -lock=false)", - "Bash(\"C:\\Users\\ab\\AppData\\Local\\Microsoft\\WinGet\\Packages\\Hashicorp.Terraform_Microsoft.Winget.Source_8wekyb3d8bbwe\\terraform.exe\" apply -replace=\"authentik_outpost.outposts[\"\"kubernetes-outpost\"\"]\" -auto-approve -lock=false)" + "Bash(\"C:\\Users\\ab\\AppData\\Local\\Microsoft\\WinGet\\Packages\\Hashicorp.Terraform_Microsoft.Winget.Source_8wekyb3d8bbwe\\terraform.exe\" apply -replace=\"authentik_outpost.outposts[\"\"kubernetes-outpost\"\"]\" -auto-approve -lock=false)", + "Bash(terraform plan:*)" ], "deny": [], "ask": [] diff --git a/terraform/authentik/.terraform.lock.hcl b/terraform/authentik/.terraform.lock.hcl index ddd5e4b..397c2e1 100644 --- a/terraform/authentik/.terraform.lock.hcl +++ b/terraform/authentik/.terraform.lock.hcl @@ -5,7 +5,6 @@ provider "registry.terraform.io/goauthentik/authentik" { version = "2025.8.1" constraints = ">= 2023.10.0, 2025.8.1" hashes = [ - "h1:L3Fh0LyQ066laexCAeqLd+AVuSPDemwCmYgq1Bges6c=", "h1:R3h8ADB0Kkv/aoY0AaHkBiX2/P4+GnW8sSgkN30kJfQ=", "zh:0c3f1083fd48f20ed06959401ff1459fbb5d454d81c8175b5b6d321b308c0be3", "zh:21c6d93f8d26e688da38a660d121b5624e3597c426c671289f31a17a9771abbf", @@ -28,7 +27,6 @@ provider "registry.terraform.io/hashicorp/random" { version = "3.7.2" constraints = ">= 3.5.0" hashes = [ - "h1:0hcNr59VEJbhZYwuDE/ysmyTS0evkfcLarlni+zATPM=", "h1:356j/3XnXEKr9nyicLUufzoF4Yr6hRy481KIxRVpK0c=", "zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f", "zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc", diff --git a/terraform/authentik/main.tf b/terraform/authentik/main.tf index 0792886..d91e736 100644 --- a/terraform/authentik/main.tf +++ b/terraform/authentik/main.tf @@ -11,15 +11,63 @@ data "authentik_flow" "default_invalidation_flow" { slug = var.default_invalidation_flow } -resource "authentik_group" "groups" { - for_each = var.groups +# Root groups (without parent) +resource "authentik_group" "root_groups" { + for_each = { + for k, v in var.groups : k => v + if v.parent == null + } name = each.value.name is_superuser = each.value.is_superuser - parent = each.value.parent attributes = jsonencode(each.value.attributes) } +# Child groups (with parent) +resource "authentik_group" "child_groups" { + for_each = { + for k, v in var.groups : k => v + if v.parent != null + } + + name = each.value.name + is_superuser = each.value.is_superuser + parent = authentik_group.root_groups[each.value.parent].id + attributes = jsonencode(each.value.attributes) + + depends_on = [authentik_group.root_groups] +} + +# Auto-created groups for proxy applications +resource "authentik_group" "proxy_app_groups" { + for_each = { + for k, v in var.proxy_applications : k => v + if v.create_group == true + } + + name = "TF-${each.value.name} Users" + is_superuser = false + attributes = jsonencode({ + notes = "Auto-created for ${each.value.name} application" + app_slug = each.value.slug + }) +} + +# Auto-created groups for OAuth applications +resource "authentik_group" "oauth_app_groups" { + for_each = { + for k, v in var.oauth_applications : k => v + if v.create_group == true + } + + name = "TF-${each.value.name} Users" + is_superuser = false + attributes = jsonencode({ + notes = "Auto-created for ${each.value.name} application" + app_slug = each.value.slug + }) +} + resource "authentik_certificate_key_pair" "certificates" { for_each = var.certificates @@ -92,6 +140,16 @@ module "oauth_applications" { meta_description = each.value.meta_description meta_launch_url = each.value.meta_launch_url meta_icon = each.value.meta_icon + scope_mappings = each.value.scope_mappings + + # Access control - only pass explicitly defined groups + access_groups = [ + for group_key in each.value.access_groups : + try( + authentik_group.root_groups[group_key].id, + authentik_group.child_groups[group_key].id + ) + ] } module "proxy_applications" { @@ -119,6 +177,76 @@ module "proxy_applications" { meta_description = each.value.meta_description meta_launch_url = each.value.meta_launch_url meta_icon = each.value.meta_icon + + # Access control - only pass explicitly defined groups + access_groups = [ + for group_key in each.value.access_groups : + try( + authentik_group.root_groups[group_key].id, + authentik_group.child_groups[group_key].id + ) + ] +} + +# Binding auto-created groups to their applications +resource "authentik_policy_binding" "auto_group_bindings" { + for_each = { + for k, v in var.proxy_applications : k => v + if v.create_group == true + } + + target = module.proxy_applications[each.key].application_uuid + group = authentik_group.proxy_app_groups[each.key].id + order = 100 + + depends_on = [ + module.proxy_applications, + authentik_group.proxy_app_groups + ] +} + +# Binding auto-created groups to their OAuth applications +resource "authentik_policy_binding" "oauth_auto_group_bindings" { + for_each = { + for k, v in var.oauth_applications : k => v + if v.create_group == true + } + + target = module.oauth_applications[each.key].application_uuid + group = authentik_group.oauth_app_groups[each.key].id + order = 100 + + depends_on = [ + module.oauth_applications, + authentik_group.oauth_app_groups + ] +} + +module "saml_applications" { + source = "./modules/saml-provider" + + for_each = var.saml_applications + + name = each.value.name + app_name = each.value.name + app_slug = each.value.slug + app_group = each.value.group + authorization_flow = try(authentik_flow.flows[each.value.authorization_flow].id, data.authentik_flow.default_authorization_flow.id) + invalidation_flow = data.authentik_flow.default_invalidation_flow.id + acs_url = each.value.acs_url + issuer = each.value.issuer + audience = each.value.audience + sp_binding = each.value.sp_binding + signing_key = each.value.signing_key + property_mappings = [for pm in each.value.property_mappings : authentik_property_mapping_provider_saml.saml_mappings[pm].id] + name_id_mapping = each.value.name_id_mapping != null ? authentik_property_mapping_provider_saml.saml_mappings[each.value.name_id_mapping].id : null + assertion_valid_not_before = each.value.assertion_valid_not_before + assertion_valid_not_on_or_after = each.value.assertion_valid_not_on_or_after + session_valid_not_on_or_after = each.value.session_valid_not_on_or_after + policy_engine_mode = each.value.policy_engine_mode + meta_description = each.value.meta_description + meta_launch_url = each.value.meta_launch_url + meta_icon = each.value.meta_icon } locals { diff --git a/terraform/authentik/modules/oauth-provider/main.tf b/terraform/authentik/modules/oauth-provider/main.tf index b136b3e..f87b9a0 100644 --- a/terraform/authentik/modules/oauth-provider/main.tf +++ b/terraform/authentik/modules/oauth-provider/main.tf @@ -11,6 +11,30 @@ terraform { } } +# Get all available scope mappings +data "authentik_property_mapping_provider_scope" "all_scopes" { + managed_list = [ + "goauthentik.io/providers/oauth2/scope-email", + "goauthentik.io/providers/oauth2/scope-openid", + "goauthentik.io/providers/oauth2/scope-profile" + ] +} + +# Filter scope mappings based on requested scopes +locals { + scope_name_mapping = { + "openid" = "goauthentik.io/providers/oauth2/scope-openid" + "profile" = "goauthentik.io/providers/oauth2/scope-profile" + "email" = "goauthentik.io/providers/oauth2/scope-email" + } + + selected_scope_ids = [ + for scope in var.scope_mappings : + data.authentik_property_mapping_provider_scope.all_scopes.ids[index(data.authentik_property_mapping_provider_scope.all_scopes.managed_list, local.scope_name_mapping[scope])] + if contains(keys(local.scope_name_mapping), scope) + ] +} + resource "random_password" "client_secret" { count = var.client_secret == null ? 1 : 0 length = 40 @@ -25,8 +49,19 @@ resource "authentik_provider_oauth2" "provider" { authorization_flow = var.authorization_flow invalidation_flow = var.invalidation_flow include_claims_in_id_token = var.include_claims_in_id_token + access_code_validity = var.access_code_validity + access_token_validity = var.access_token_validity + refresh_token_validity = var.refresh_token_validity + signing_key = var.signing_key - property_mappings = var.property_mappings + allowed_redirect_uris = [ + for uri in var.redirect_uris : { + matching_mode = "strict" + url = uri + } + ] + + property_mappings = length(var.property_mappings) > 0 ? var.property_mappings : local.selected_scope_ids } resource "random_id" "client_id" { @@ -56,4 +91,13 @@ resource "authentik_policy_binding" "app_access" { timeout = lookup(each.value, "timeout", 30) negate = lookup(each.value, "negate", false) failure_result = lookup(each.value, "failure_result", true) +} + +# Binding groups to the application +resource "authentik_policy_binding" "group_bindings" { + for_each = { for idx, group_id in var.access_groups : idx => group_id } + + target = authentik_application.app.uuid + group = each.value + order = 10 + each.key } \ No newline at end of file diff --git a/terraform/authentik/modules/oauth-provider/outputs.tf b/terraform/authentik/modules/oauth-provider/outputs.tf index f57ff3b..41b1235 100644 --- a/terraform/authentik/modules/oauth-provider/outputs.tf +++ b/terraform/authentik/modules/oauth-provider/outputs.tf @@ -10,7 +10,7 @@ output "application_id" { output "application_uuid" { description = "UUID of the application" - value = authentik_application.app.id + value = authentik_application.app.uuid } output "client_id" { diff --git a/terraform/authentik/modules/oauth-provider/variables.tf b/terraform/authentik/modules/oauth-provider/variables.tf index afa2a3e..cdab7c1 100644 --- a/terraform/authentik/modules/oauth-provider/variables.tf +++ b/terraform/authentik/modules/oauth-provider/variables.tf @@ -135,4 +135,16 @@ variable "access_policies" { failure_result = optional(bool, true) })) default = {} +} + +variable "access_groups" { + description = "List of group IDs that have access to the application" + type = list(string) + default = [] +} + +variable "scope_mappings" { + description = "List of scope mappings for the OAuth provider" + type = list(string) + default = ["openid", "profile", "email"] } \ No newline at end of file diff --git a/terraform/authentik/modules/proxy-provider/main.tf b/terraform/authentik/modules/proxy-provider/main.tf index 7ad2724..44b2fad 100644 --- a/terraform/authentik/modules/proxy-provider/main.tf +++ b/terraform/authentik/modules/proxy-provider/main.tf @@ -46,4 +46,13 @@ resource "authentik_policy_binding" "app_access" { timeout = lookup(each.value, "timeout", 30) negate = lookup(each.value, "negate", false) failure_result = lookup(each.value, "failure_result", true) +} + +# Binding groups to the application +resource "authentik_policy_binding" "group_bindings" { + for_each = { for idx, group_id in var.access_groups : idx => group_id } + + target = authentik_application.app.uuid + group = each.value + order = 10 + each.key } \ No newline at end of file diff --git a/terraform/authentik/modules/proxy-provider/outputs.tf b/terraform/authentik/modules/proxy-provider/outputs.tf index 17e4c8b..58f8d71 100644 --- a/terraform/authentik/modules/proxy-provider/outputs.tf +++ b/terraform/authentik/modules/proxy-provider/outputs.tf @@ -10,7 +10,7 @@ output "application_id" { output "application_uuid" { description = "UUID of the application" - value = authentik_application.app.id + value = authentik_application.app.uuid } output "application_slug" { diff --git a/terraform/authentik/modules/proxy-provider/variables.tf b/terraform/authentik/modules/proxy-provider/variables.tf index ba83526..2829a28 100644 --- a/terraform/authentik/modules/proxy-provider/variables.tf +++ b/terraform/authentik/modules/proxy-provider/variables.tf @@ -142,4 +142,10 @@ variable "access_policies" { failure_result = optional(bool, true) })) default = {} +} + +variable "access_groups" { + description = "List of group IDs that have access to the application" + type = list(string) + default = [] } \ No newline at end of file diff --git a/terraform/authentik/modules/saml-provider/main.tf b/terraform/authentik/modules/saml-provider/main.tf new file mode 100644 index 0000000..33ebd39 --- /dev/null +++ b/terraform/authentik/modules/saml-provider/main.tf @@ -0,0 +1,53 @@ +terraform { + required_providers { + authentik = { + source = "goauthentik/authentik" + version = ">= 2023.10.0" + } + } +} + +data "authentik_certificate_key_pair" "default" { + name = "authentik Self-signed Certificate" +} + +resource "authentik_provider_saml" "provider" { + name = var.name + authorization_flow = var.authorization_flow + invalidation_flow = var.invalidation_flow + acs_url = var.acs_url + issuer = var.issuer + audience = var.audience + sp_binding = var.sp_binding + signing_kp = var.signing_key != null ? var.signing_key : data.authentik_certificate_key_pair.default.id + property_mappings = var.property_mappings + name_id_mapping = var.name_id_mapping + + assertion_valid_not_before = var.assertion_valid_not_before + assertion_valid_not_on_or_after = var.assertion_valid_not_on_or_after + session_valid_not_on_or_after = var.session_valid_not_on_or_after +} + +resource "authentik_application" "app" { + name = var.app_name + slug = var.app_slug + protocol_provider = authentik_provider_saml.provider.id + group = var.app_group + policy_engine_mode = var.policy_engine_mode + meta_description = var.meta_description + meta_launch_url = var.meta_launch_url + meta_icon = var.meta_icon +} + +resource "authentik_policy_binding" "app_access" { + for_each = var.access_policies + + target = authentik_application.app.id + policy = each.value.policy_id + order = each.value.order + + enabled = lookup(each.value, "enabled", true) + timeout = lookup(each.value, "timeout", 30) + negate = lookup(each.value, "negate", false) + failure_result = lookup(each.value, "failure_result", true) +} \ No newline at end of file diff --git a/terraform/authentik/modules/saml-provider/outputs.tf b/terraform/authentik/modules/saml-provider/outputs.tf new file mode 100644 index 0000000..f950740 --- /dev/null +++ b/terraform/authentik/modules/saml-provider/outputs.tf @@ -0,0 +1,24 @@ +output "provider_id" { + description = "ID of the SAML provider" + value = authentik_provider_saml.provider.id +} + +output "application_id" { + description = "ID of the application" + value = authentik_application.app.id +} + +output "provider_name" { + description = "Name of the SAML provider" + value = authentik_provider_saml.provider.name +} + +output "acs_url" { + description = "Assertion Consumer Service URL" + value = authentik_provider_saml.provider.acs_url +} + +output "issuer" { + description = "SAML Issuer" + value = authentik_provider_saml.provider.issuer +} \ No newline at end of file diff --git a/terraform/authentik/modules/saml-provider/variables.tf b/terraform/authentik/modules/saml-provider/variables.tf new file mode 100644 index 0000000..d0dd9a7 --- /dev/null +++ b/terraform/authentik/modules/saml-provider/variables.tf @@ -0,0 +1,124 @@ +variable "name" { + description = "Name of the SAML provider" + type = string +} + +variable "app_name" { + description = "Name of the application" + type = string +} + +variable "app_slug" { + description = "Slug of the application" + type = string +} + +variable "app_group" { + description = "Group of the application" + type = string + default = "" +} + +variable "authorization_flow" { + description = "Authorization flow ID" + type = string +} + +variable "invalidation_flow" { + description = "Invalidation flow ID" + type = string +} + +variable "acs_url" { + description = "Assertion Consumer Service URL" + type = string +} + +variable "issuer" { + description = "SAML Issuer" + type = string +} + +variable "audience" { + description = "SAML Audience" + type = string +} + +variable "sp_binding" { + description = "Service Provider binding (post or redirect)" + type = string + default = "post" +} + +variable "signing_key" { + description = "Certificate key pair ID for signing" + type = string + default = null +} + +variable "property_mappings" { + description = "List of property mapping IDs" + type = list(string) + default = [] +} + +variable "name_id_mapping" { + description = "Property mapping ID for NameID" + type = string + default = null +} + +variable "assertion_valid_not_before" { + description = "Assertion valid not before" + type = string + default = "minutes=-5" +} + +variable "assertion_valid_not_on_or_after" { + description = "Assertion valid not on or after" + type = string + default = "minutes=5" +} + +variable "session_valid_not_on_or_after" { + description = "Session valid not on or after" + type = string + default = "minutes=86400" +} + +variable "policy_engine_mode" { + description = "Policy engine mode" + type = string + default = "all" +} + +variable "meta_description" { + description = "Application description" + type = string + default = "" +} + +variable "meta_launch_url" { + description = "Application launch URL" + type = string + default = "" +} + +variable "meta_icon" { + description = "Application icon URL" + type = string + default = "" +} + +variable "access_policies" { + description = "Access policies for the application" + type = map(object({ + policy_id = string + order = number + enabled = optional(bool, true) + timeout = optional(number, 30) + negate = optional(bool, false) + failure_result = optional(bool, true) + })) + default = {} +} \ No newline at end of file diff --git a/terraform/authentik/outputs.tf b/terraform/authentik/outputs.tf index 4c03b75..f55c287 100644 --- a/terraform/authentik/outputs.tf +++ b/terraform/authentik/outputs.tf @@ -38,12 +38,36 @@ output "outposts" { output "groups" { description = "Groups details" - value = { - for k, v in authentik_group.groups : k => { - id = v.id - name = v.name + value = merge( + { + for k, v in authentik_group.root_groups : k => { + id = v.id + name = v.name + } + }, + { + for k, v in authentik_group.child_groups : k => { + id = v.id + name = v.name + } + }, + { + for k, v in authentik_group.proxy_app_groups : k => { + id = v.id + name = v.name + auto_created = true + type = "proxy" + } + }, + { + for k, v in authentik_group.oauth_app_groups : k => { + id = v.id + name = v.name + auto_created = true + type = "oauth" + } } - } + ) } output "flows" { diff --git a/terraform/authentik/state.tf b/terraform/authentik/state.tf new file mode 100644 index 0000000..1ca6a5a --- /dev/null +++ b/terraform/authentik/state.tf @@ -0,0 +1,8 @@ +terraform { + cloud { + organization = "ultradesu" + workspaces { + name = "Authentik" + } + } +} diff --git a/terraform/authentik/variables.tf b/terraform/authentik/variables.tf index 5485a4c..ce312f1 100644 --- a/terraform/authentik/variables.tf +++ b/terraform/authentik/variables.tf @@ -19,6 +19,9 @@ variable "oauth_applications" { authorization_flow = optional(string, null) signing_key = optional(string, null) outpost = optional(string, null) + create_group = optional(bool, false) + access_groups = optional(list(string), []) + scope_mappings = optional(list(string), ["openid", "profile", "email"]) })) default = {} } @@ -45,6 +48,33 @@ variable "proxy_applications" { authorization_flow = optional(string, null) skip_path_regex = optional(string, "") outpost = optional(string, null) + create_group = optional(bool, false) + access_groups = optional(list(string), []) + })) + default = {} +} + +variable "saml_applications" { + description = "Map of SAML applications" + type = map(object({ + name = string + slug = string + group = optional(string, "") + policy_engine_mode = optional(string, "all") + meta_description = optional(string, "") + meta_launch_url = optional(string, "") + meta_icon = optional(string, "") + acs_url = string + issuer = string + audience = string + sp_binding = optional(string, "post") + signing_key = optional(string, null) + property_mappings = optional(list(string), []) + name_id_mapping = optional(string, null) + assertion_valid_not_before = optional(string, "minutes=-5") + assertion_valid_not_on_or_after = optional(string, "minutes=5") + session_valid_not_on_or_after = optional(string, "minutes=86400") + authorization_flow = optional(string, null) })) default = {} }