Files
unkin-agent 187e29d068
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Validate user and role names instead of munging binding names
Rancher requires global role binding names to be RFC 1123 labels. The
per-user binding name lowercased the key and replaced only "/", so
usernames containing ".", "_" or "@" still produced an invalid name and
failed at apply, and hyphenated names could collide (foo-bar + baz vs
foo + bar-baz) into one object.

Add validation blocks on var.users requiring the username and every
referenced role name to be RFC 1123 labels, so non-compliant input fails
the plan with an actionable message. The binding name is then built from
the two parts directly. A precondition rejects the remaining hyphen
ambiguity at plan time rather than as a mid-apply conflict. Document the
constraint in the users schema section of the README. The group binding
path is unchanged.
2026-08-30 00:58:59 +10:00

116 lines
4.6 KiB
Terraform

variable "keycloakoidc" {
description = "Rancher Keycloak(OIDC) auth provider config, or null to leave unmanaged."
type = object({
rancher_url = string # Rancher redirect URL, must end with /verify-auth
client_id = string
issuer = string # OIDC issuer (Authentik application URL)
auth_endpoint = string # OIDC authorization endpoint
scopes = optional(string, "openid profile email")
# OIDC claim to read group names from (default Rancher uses "groups").
groups_field = optional(string, "groups")
access_mode = optional(string, "unrestricted")
enabled = optional(bool, true)
# PEM CA cert Rancher trusts for the IdP's TLS on OIDC discovery. Leave null
# to default to the internal PKI ca_chain read from Vault.
certificate = optional(string, null)
# client_secret is never committed. Point at a Vault kv-v2 secret whose
# `client_secret` key holds the value (seeded out of band); TF reads it.
client_secret_vault = optional(object({
mount = string
path = string
}), null)
})
default = null
}
# Global role bindings mapping an Authentik group (permission group) to a Rancher
# global role. The group name is the map key; the OIDC group principal id is
# derived as keycloakoidc_group://<name>.
variable "global_role_bindings" {
type = map(object({
global_role_id = string # e.g. "admin", "user"
}))
default = {}
}
# Rancher-local users (config/users/<username>.yaml). Map key is the username.
# rancher2_user requires a password, so every entry must point at a Vault kv-v2
# secret holding one; the value is read at plan time, so the secret must exist
# before a user yaml is added.
variable "users" {
type = map(object({
name = optional(string, null) # display name, defaults to the username
enabled = optional(bool, true)
must_change_password = optional(bool, false)
password_vault = object({
mount = string
path = string
key = optional(string, "password")
})
# Global roles granted to this user, e.g. ["user", "admin"].
global_role_bindings = optional(list(string), [])
}))
default = {}
# Rancher names each global role binding as an RFC 1123 label and rejects
# anything else at apply time. The binding name is built from the username and
# the role name verbatim, so both must already be compliant — fail the plan
# with a clear message instead of munging the input and hoping.
validation {
condition = alltrue([
for username in keys(var.users) :
can(regex("^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", username))
])
error_message = "Usernames must be RFC 1123 labels: lowercase alphanumerics and '-', starting and ending alphanumeric (no '.', '_', '@' or uppercase). Rename config/users/<username>.yaml to a compliant username."
}
validation {
condition = alltrue(flatten([
for user in values(var.users) : [
for role in user.global_role_bindings :
can(regex("^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", role))
]
]))
error_message = "Values in global_role_bindings must be RFC 1123 labels: lowercase alphanumerics and '-', starting and ending alphanumeric."
}
}
# Custom Rancher global roles (config/roles/<name>.yaml). Map key is the role
# name, which is also the id other configs bind to via global_role_id.
variable "global_roles" {
type = map(object({
description = optional(string, null)
new_user_default = optional(bool, false)
inherited_cluster_roles = optional(list(string), [])
rules = optional(list(object({
api_groups = optional(list(string), [])
resources = optional(list(string), [])
verbs = optional(list(string), [])
non_resource_urls = optional(list(string), [])
resource_names = optional(list(string), [])
})), [])
}))
default = {}
}
# API tokens (config/tokens/<name>.yaml). Map key names the Terraform resource
# only. rancher2_token has no user selector (user_id is computed at 14.1.1), so
# every token here is minted for the identity the provider authenticates as.
variable "tokens" {
type = map(object({
description = optional(string, null) # defaults to the map key
ttl = optional(number, null) # seconds; 0/null = provider default
renew = optional(bool, null)
cluster_id = optional(string, null) # scope to one cluster, null = global
}))
default = {}
}
# Rancher settings (config/settings/<name>.yaml). Map key is the setting name.
variable "settings" {
type = map(object({
value = string
}))
default = {}
}