Add Authentik OIDC SSO as the default human login for OpenBao
Human access to OpenBao is LDAP-only today, so operators carry a second set of credentials outside Authentik and group membership is maintained twice. This makes Authentik SSO the offered default on the UI login page and gives `bao login -method=oidc` a working CLI path, while approle and kubernetes (CI and agents) plus the break-glass root path are untouched. Add three modules mirroring the auth_ldap_* structure: auth_oidc_backend mounts a vault_jwt_auth_backend of type oidc, auth_oidc_role creates the default login role, and auth_oidc_group creates an external vault_identity_group plus its group alias so IdP groups map onto policies. Mount the backend at the literal path "oidc". The Authentik provider registers strict redirect URIs containing /ui/vault/auth/oidc/oidc/callback, so the path is load-bearing and must not be renamed. Read client_id and client_secret from kv/service/authentik/oidc-vault, which terraform-authentik generates and writes; nothing is seeded by hand. Match groups on the ak_groups claim rather than groups, because Authentik's default profile mapping only emits direct memberships and the estate nests akP-* permission groups under akR-* roles. Bind akP-vault-admin to global-root, the same policy the LDAP vault_admin group already carries. Only akP-* permission groups are named in config or policy; akR-* roles stay grouping-only. Grant the deployer auth/oidc/* and identity group management, both of which it currently lacks. AppRole capabilities are fixed at login, so these land in an apply before the resources that need them.
This commit is contained in:
@@ -64,6 +64,55 @@ module "auth_ldap_group" {
|
||||
depends_on = [module.auth_ldap_backend]
|
||||
}
|
||||
|
||||
module "auth_oidc_backend" {
|
||||
source = "./modules/auth_oidc_backend"
|
||||
|
||||
for_each = var.auth_oidc_backend
|
||||
|
||||
path = each.key
|
||||
description = each.value.description
|
||||
oidc_discovery_url = each.value.oidc_discovery_url
|
||||
client_secret_mount = each.value.client_secret_mount
|
||||
client_secret_path = each.value.client_secret_path
|
||||
default_role = each.value.default_role
|
||||
listing_visibility = each.value.listing_visibility
|
||||
default_lease_ttl = each.value.default_lease_ttl
|
||||
max_lease_ttl = each.value.max_lease_ttl
|
||||
}
|
||||
|
||||
module "auth_oidc_role" {
|
||||
source = "./modules/auth_oidc_role"
|
||||
|
||||
for_each = var.auth_oidc_role
|
||||
|
||||
backend = each.value.backend
|
||||
role_name = each.value.role_name
|
||||
user_claim = each.value.user_claim
|
||||
groups_claim = each.value.groups_claim
|
||||
oidc_scopes = each.value.oidc_scopes
|
||||
bound_audiences = each.value.bound_audiences
|
||||
allowed_redirect_uris = each.value.allowed_redirect_uris
|
||||
token_ttl = each.value.token_ttl
|
||||
token_max_ttl = each.value.token_max_ttl
|
||||
# Human authorization comes from the external identity groups below, so a
|
||||
# login role carrying no policies is the normal case.
|
||||
token_policies = try(var.policy_auth_map[each.value.backend][each.value.role_name], [])
|
||||
|
||||
depends_on = [module.auth_oidc_backend]
|
||||
}
|
||||
|
||||
module "auth_oidc_group" {
|
||||
source = "./modules/auth_oidc_group"
|
||||
|
||||
for_each = var.auth_oidc_group
|
||||
|
||||
groupname = each.value.groupname
|
||||
policies = var.policy_auth_map[each.value.backend][each.value.groupname]
|
||||
mount_accessor = module.auth_oidc_backend[each.value.backend].accessor
|
||||
|
||||
depends_on = [module.auth_oidc_backend]
|
||||
}
|
||||
|
||||
module "auth_kubernetes_backend" {
|
||||
source = "./modules/auth_kubernetes_backend"
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Expected keys in KV secret: client_id, client_secret (generated and written by
|
||||
# terraform-authentik's oauth2 provider module, never seeded by hand).
|
||||
data "vault_kv_secret_v2" "oidc_client" {
|
||||
mount = var.client_secret_mount
|
||||
name = var.client_secret_path
|
||||
}
|
||||
|
||||
resource "vault_jwt_auth_backend" "oidc" {
|
||||
path = var.path
|
||||
type = "oidc"
|
||||
description = var.description
|
||||
oidc_discovery_url = var.oidc_discovery_url
|
||||
oidc_client_id = data.vault_kv_secret_v2.oidc_client.data["client_id"]
|
||||
oidc_client_secret = data.vault_kv_secret_v2.oidc_client.data["client_secret"]
|
||||
default_role = var.default_role
|
||||
|
||||
tune {
|
||||
default_lease_ttl = var.default_lease_ttl
|
||||
max_lease_ttl = var.max_lease_ttl
|
||||
listing_visibility = var.listing_visibility
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "accessor" {
|
||||
description = "Accessor of the OIDC auth mount, used to bind identity group aliases"
|
||||
value = vault_jwt_auth_backend.oidc.accessor
|
||||
}
|
||||
|
||||
output "path" {
|
||||
description = "Mount path of the OIDC auth backend"
|
||||
value = vault_jwt_auth_backend.oidc.path
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
terraform {
|
||||
required_version = ">= 1.10"
|
||||
required_providers {
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "5.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
variable "path" {
|
||||
description = "Mount path of the OIDC auth backend"
|
||||
type = string
|
||||
default = "oidc"
|
||||
}
|
||||
|
||||
variable "description" {
|
||||
description = "Human-readable description of the auth mount"
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "oidc_discovery_url" {
|
||||
description = "OIDC issuer discovery URL of the identity provider"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "client_secret_mount" {
|
||||
description = "KV-v2 mount holding the OIDC client credentials"
|
||||
type = string
|
||||
default = "kv"
|
||||
}
|
||||
|
||||
variable "client_secret_path" {
|
||||
description = "Path within the KV-v2 mount holding client_id and client_secret"
|
||||
type = string
|
||||
default = "service/authentik/oidc-vault"
|
||||
}
|
||||
|
||||
variable "default_role" {
|
||||
description = "Role used when none is supplied at login (the UI/CLI default)"
|
||||
type = string
|
||||
default = "default"
|
||||
}
|
||||
|
||||
variable "listing_visibility" {
|
||||
description = "Specifies whether to show this mount in the UI-specific listing endpoint. Valid values are 'unauth' or 'hidden'"
|
||||
type = string
|
||||
default = null
|
||||
validation {
|
||||
condition = var.listing_visibility == null || contains(["unauth", "hidden"], var.listing_visibility)
|
||||
error_message = "listing_visibility must be either 'unauth' or 'hidden'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "default_lease_ttl" {
|
||||
description = "Specifies the default time-to-live. If set, this overrides the global default. Must be a valid duration string"
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "max_lease_ttl" {
|
||||
description = "Specifies the maximum time-to-live. If set, this overrides the global default. Must be a valid duration string"
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# External identity group: membership is asserted by the IdP through the role's
|
||||
# groups_claim, matched by the alias name below.
|
||||
resource "vault_identity_group" "group" {
|
||||
name = var.groupname
|
||||
type = "external"
|
||||
policies = var.policies
|
||||
}
|
||||
|
||||
resource "vault_identity_group_alias" "alias" {
|
||||
name = var.groupname
|
||||
mount_accessor = var.mount_accessor
|
||||
canonical_id = vault_identity_group.group.id
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
terraform {
|
||||
required_version = ">= 1.10"
|
||||
required_providers {
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "5.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
variable "groupname" {
|
||||
description = "Name of the IdP group, as it appears in the groups claim"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "policies" {
|
||||
description = "List of policies to assign to the identity group"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "mount_accessor" {
|
||||
description = "Accessor of the OIDC auth mount the alias is bound to"
|
||||
type = string
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
resource "vault_jwt_auth_backend_role" "role" {
|
||||
backend = var.backend
|
||||
role_name = var.role_name
|
||||
role_type = "oidc"
|
||||
user_claim = var.user_claim
|
||||
groups_claim = var.groups_claim
|
||||
oidc_scopes = var.oidc_scopes
|
||||
bound_audiences = var.bound_audiences
|
||||
allowed_redirect_uris = var.allowed_redirect_uris
|
||||
token_ttl = var.token_ttl
|
||||
token_max_ttl = var.token_max_ttl
|
||||
token_policies = var.token_policies
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
terraform {
|
||||
required_version = ">= 1.10"
|
||||
required_providers {
|
||||
vault = {
|
||||
source = "hashicorp/vault"
|
||||
version = "5.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
variable "backend" {
|
||||
description = "The unique path of the OIDC auth backend to configure"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "role_name" {
|
||||
description = "The name of the role"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "user_claim" {
|
||||
description = "Claim used as the entity alias name (the Vault identity of the human)"
|
||||
type = string
|
||||
default = "email"
|
||||
}
|
||||
|
||||
variable "groups_claim" {
|
||||
description = "Claim holding the caller's group memberships, matched against identity group aliases"
|
||||
type = string
|
||||
default = "ak_groups"
|
||||
}
|
||||
|
||||
variable "oidc_scopes" {
|
||||
description = "Scopes requested from the identity provider during the authorization request"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "bound_audiences" {
|
||||
description = "List of audiences (aud claim) accepted in the ID token"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "allowed_redirect_uris" {
|
||||
description = "Redirect URIs accepted for this role. Must match the provider's registered URIs exactly"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "token_ttl" {
|
||||
description = "The TTL period of tokens issued using this role, in seconds"
|
||||
type = number
|
||||
default = 3600
|
||||
}
|
||||
|
||||
variable "token_max_ttl" {
|
||||
description = "The maximum lifetime for generated tokens in number of seconds. Its current value will be referenced at renewal time."
|
||||
type = number
|
||||
default = 28800
|
||||
}
|
||||
|
||||
variable "token_policies" {
|
||||
description = "List of policies to assign to the role (passed from policy_auth_map). Human authorization normally comes from external identity groups instead"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
@@ -62,6 +62,46 @@ variable "auth_ldap_group" {
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "auth_oidc_backend" {
|
||||
description = "Map of OIDC (JWT) auth backends to create"
|
||||
type = map(object({
|
||||
oidc_discovery_url = string
|
||||
description = optional(string)
|
||||
client_secret_mount = optional(string, "kv")
|
||||
client_secret_path = optional(string, "service/authentik/oidc-vault")
|
||||
default_role = optional(string, "default")
|
||||
listing_visibility = optional(string)
|
||||
default_lease_ttl = optional(string)
|
||||
max_lease_ttl = optional(string)
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "auth_oidc_role" {
|
||||
description = "Map of OIDC auth roles to create"
|
||||
type = map(object({
|
||||
role_name = string
|
||||
backend = string
|
||||
allowed_redirect_uris = list(string)
|
||||
user_claim = optional(string, "email")
|
||||
groups_claim = optional(string, "ak_groups")
|
||||
oidc_scopes = optional(list(string), [])
|
||||
bound_audiences = optional(list(string), [])
|
||||
token_ttl = optional(number, 3600)
|
||||
token_max_ttl = optional(number, 28800)
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "auth_oidc_group" {
|
||||
description = "Map of external identity groups bound to an OIDC auth mount"
|
||||
type = map(object({
|
||||
groupname = string
|
||||
backend = string
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "auth_kubernetes_backend" {
|
||||
description = "Map of Kubernetes auth backends to create"
|
||||
type = map(object({
|
||||
|
||||
Reference in New Issue
Block a user