Compare commits

..

19 Commits

Author SHA1 Message Date
benvin 2aa5cea027 Merge pull request 'ci: mint rancher token from the vault rancher engine' (#9) from benvin/ephemeral-runner-creds into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #9
2026-08-30 14:27:33 +10:00
benvin 2d5fff80dd Merge pull request 'Manage Rancher users, global roles, tokens and settings' (#8) from benvin/users-roles-tokens-settings into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #8
2026-08-30 14:25:47 +10:00
benvin bb64931de7 Merge pull request 'refactor: parameterise rancher api_url and pin rancher2 to ~> 14.0' (#7) from benvin/provider-plumbing into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #7
2026-08-30 14:22:45 +10:00
unkin-agent 6db8100aba ci: mint rancher token from the vault rancher engine
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
Retire the static 90-day admin token in kv/service/terraform/rancher; the
vault rancher engine and the read grant on rancher/creds/ci are live.

- Makefile: vault_env reads TF_VAR_rancher_token from rancher/creds/ci
- README: document the ephemeral 1h credential model
2026-08-30 14:12:23 +10:00
unkin-agent 187e29d068 Validate user and role names instead of munging binding names
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
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
unkin-agent c7ef079c88 Manage Rancher users, global roles, tokens and settings
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Extend the yaml-discovery config to four more rancher2 kinds so Rancher
objects beyond the OIDC auth provider stop being unmanaged clickops.

- Discover config/users, config/roles, config/tokens and config/settings
  in config/config.hcl and pass them through terragrunt.hcl
- Add rancher2_user (password from Vault kv-v2) plus per-user
  rancher2_global_role_binding on "<username>/<role>" keys
- Add rancher2_global_role with dynamic rules blocks
- Add rancher2_token, documenting that the provider has no user selector
- Add rancher2_setting and seed config/settings/server-url.yaml
- Document every yaml schema in the README
2026-08-30 00:48:29 +10:00
unkin-agent b315698634 refactor: parameterise rancher api_url and pin rancher2 to ~> 14.0
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
The provider api_url was hardcoded to the derived environment URL, leaving CI
no way to point a plan at a different endpoint. The rancher2 constraint of
>= 5.0.0 also permitted nine majors of drift against the 14.1.1 the lockfile
resolves.

- Add a generated variable "rancher_url" defaulting to the same
  https://${path_relative_to_include()} expression; the provider now reads
  var.rancher_url, so TF_VAR_rancher_url overrides without changing defaults.
- Pin rancher/rancher2 to ~> 14.0 in modules/rancher/versions.tf.
2026-08-30 00:45:43 +10:00
benvin 8322c5480e Merge pull request 'ci: fetch vault from artifactapi instead of dnf install' (#6) from benvin/vault-via-artifactapi into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #6
2026-08-23 22:42:40 +10:00
unkin-agent 11260a81a4 ci: fetch vault from artifactapi instead of dnf install
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
dnf install reads metadata for every enabled repo and downloads the
vendored vault RPM on every pipeline run. Fetch the pinned upstream zip
from the artifactapi hashicorp-releases remote instead, matching
terraform-vault and terraform-artifactapi.

- Replace dnf install vault with a pinned curl of the vault zip from the
  artifactapi hashicorp-releases remote, extracted to /usr/local/bin.
2026-08-23 22:38:04 +10:00
benvin 420ccd4cfd Merge pull request 'Run plan without acquiring the Consul state lock' (#5) from benvin/lockfree-plan into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #5
2026-08-23 22:30:44 +10:00
unkin-agent 0ff9f6bf7d Run plan without acquiring the Consul state lock
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
PR plan pipelines were failing with "Error acquiring the state lock"
when they collided with a concurrent apply (or another plan) holding
the lock on the same Consul-backed state.

- plan: pass -lock=false to terragrunt plan; apply keeps locking
2026-08-23 22:21:18 +10:00
benvin 7be6a3e6e4 Merge pull request 'Trust internal CA for Rancher OIDC discovery' (#4) from benvin/rancher-oidc-ca into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #4
2026-07-31 21:20:25 +10:00
unkinben dbd1f6db8a Trust internal CA for Rancher OIDC discovery
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
Rancher's server-side OIDC discovery call to the Authentik issuer
(https://identity.unkin.net) fails with x509 "certificate signed by
unknown authority" because Rancher does not trust the internal unkin.net
PKI. The keycloak_oidc auth config never set a CA certificate.

- Read the internal PKI ca_chain (intermediate + root) from Vault via a
  vault_generic_secret data source (pki_int/cert/ca_chain).
- Set certificate on rancher2_auth_config_keycloak_oidc, defaulting to the
  Vault-sourced chain so trust cannot go stale on rotation; add an optional
  keycloakoidc.certificate override for an explicit value.

Issuer, client, scopes and role bindings are unchanged.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-31 20:30:55 +10:00
benvin 08fbc9b8ab Merge pull request 'Fix global role binding name to be RFC 1123 compliant' (#3) from benvin/rancher-grb-name into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #3
2026-07-18 22:59:02 +10:00
unkinben f2f41fc1c0 Fix global role binding name to be RFC 1123 compliant
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
rancher2_global_role_binding.name must be a lowercase RFC 1123 label, but the
akP-* group keys are mixed-case, so apply failed with InvalidFormat 422.
Lowercase the name; keep the group principal id in original case to match the
Authentik group.
2026-07-18 22:55:38 +10:00
benvin 1dc43580f6 Merge pull request 'Wire Rancher to Authentik ak_groups + akP-rancher global roles' (#2) from benvin/rancher-akgroups into main
ci/woodpecker/push/apply Pipeline failed
Reviewed-on: #2
2026-07-18 20:53:18 +10:00
unkinben 80fa1b2844 Wire Rancher to Authentik ak_groups + akP-rancher global roles
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
Consume the two-tier Authentik RBAC (terraform-authentik): read the hierarchical
`ak_groups` claim and grant Rancher global roles to the akP-rancher permission
groups. Members of akR-global-admin/akR-standard-user inherit these.

- keycloakoidc: scopes += ak_groups; groups_field = ak_groups
- global_role_bindings: akP-rancher-admin -> admin, akP-rancher-user -> user
  (group principal keycloakoidc_group://<name>)
2026-07-18 16:25:18 +10:00
benvin 5b6a0527ee Merge pull request 'Scaffold terraform-rancher: Authentik OIDC auth config' (#1) from benvin/scaffold into main
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #1
2026-07-16 22:26:13 +10:00
unkinben 90a01563dc Scaffold terraform-rancher: Authentik OIDC auth config
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
Manages Rancher's Keycloak(OIDC) auth provider via the rancher2 provider,
pointed at Authentik. Mirrors the terraform-authentik layout (terragrunt +
Vault-sourced secrets + Woodpecker plan/apply/pre-commit pipelines).

- modules/rancher: rancher2_auth_config_keycloak_oidc, client_secret read from
  Vault (kv/kubernetes/namespace/cattle-system/default/oauth-credentials);
  access_mode unrestricted to avoid admin lockout on enable.
- config/keycloakoidc.yaml: issuer/auth_endpoint at identity.unkin.net,
  client_id rancher, /verify-auth redirect, openid/profile/email scopes.
- environments/rancher.k8s.syd1.au.unkin.net: consul state at
  infra/terraform/rancher/, rancher2 provider api_url from the env name.
- rancher2 admin token read from kv/service/terraform/rancher (Makefile);
  to migrate to a dedicated Vault Rancher secrets engine (90-day token cap).

Validated with `tofu validate` (config valid against the rancher2 provider).
A live `plan` needs the Rancher admin API token seeded in Vault first.
2026-07-16 22:21:00 +10:00
17 changed files with 378 additions and 24 deletions
+2 -1
View File
@@ -7,8 +7,9 @@ steps:
image: git.unkin.net/unkin/almalinux9-opentofu:20260606
environment:
VAULT_AUTH_METHOD: kubernetes
VAULT_VERSION: "1.20.0"
commands:
- dnf install vault -y
- curl -fsSL -o /tmp/vault.zip "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/hashicorp-releases/vault/$${VAULT_VERSION}/vault_$${VAULT_VERSION}_linux_amd64.zip" && python3 -m zipfile -e /tmp/vault.zip /tmp/ && install -m0755 /tmp/vault /usr/local/bin/vault && rm -f /tmp/vault.zip /tmp/vault /tmp/LICENSE.txt
- make plan
- make apply
backend_options:
+2 -1
View File
@@ -6,8 +6,9 @@ steps:
image: git.unkin.net/unkin/almalinux9-opentofu:20260606
environment:
VAULT_AUTH_METHOD: kubernetes
VAULT_VERSION: "1.20.0"
commands:
- dnf install vault -y
- curl -fsSL -o /tmp/vault.zip "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/hashicorp-releases/vault/$${VAULT_VERSION}/vault_$${VAULT_VERSION}_linux_amd64.zip" && python3 -m zipfile -e /tmp/vault.zip /tmp/ && install -m0755 /tmp/vault /usr/local/bin/vault && rm -f /tmp/vault.zip /tmp/vault /tmp/LICENSE.txt
- make plan
backend_options:
kubernetes:
+5 -9
View File
@@ -4,13 +4,9 @@ VAULT_AUTH_METHOD ?= approle
VAULT_K8S_ROLE ?= woodpecker_terraform_rancher
VAULT_K8S_MOUNT ?= auth/k8s/au/syd1
VAULT_K8S_JWT_PATH ?= /var/run/secrets/kubernetes.io/serviceaccount/token
# kv-v2 location of the Rancher admin API token used by the rancher2 provider.
# TODO: migrate to the dedicated Vault Rancher secrets engine once it exists
# (swap the `vault kv get` below for `vault read -field=token rancher/creds/<role>`).
# Until then note Rancher API tokens have a 90-day max lifetime and must be rotated.
RANCHER_TOKEN_KV_MOUNT ?= kv
RANCHER_TOKEN_KV_PATH ?= service/terraform/rancher
RANCHER_TOKEN_KV_FIELD ?= token
# Vault Rancher engine role minting the token the rancher2 provider uses; the
# token is ephemeral (1h lease) and Vault revokes it in Rancher on expiry.
RANCHER_CREDS_PATH ?= rancher/creds/ci
define vault_env
@export VAULT_ADDR="https://vault.service.consul:8200" && \
@@ -20,7 +16,7 @@ define vault_env
export VAULT_TOKEN=$$(vault write -field=token auth/approle/login role_id=$$VAULT_ROLEID); \
fi && \
export CONSUL_HTTP_TOKEN=$$(vault read -field=token consul_root/au/syd1/creds/terraform-rancher) && \
export TF_VAR_rancher_token=$$(vault kv get -mount=$(RANCHER_TOKEN_KV_MOUNT) -field=$(RANCHER_TOKEN_KV_FIELD) $(RANCHER_TOKEN_KV_PATH))
export TF_VAR_rancher_token=$$(vault read -field=token $(RANCHER_CREDS_PATH))
endef
init:
@@ -29,7 +25,7 @@ init:
plan: init
@$(call vault_env) && \
terragrunt run --all --parallelism 4 --non-interactive plan
terragrunt run --all --parallelism 4 --non-interactive plan -- -lock=false
apply: init
@$(call vault_env) && \
+83 -7
View File
@@ -7,9 +7,18 @@ provider. Mirrors the `terraform-authentik` pattern.
## Managed Resources
- **Keycloak(OIDC) auth config** — Authentik OIDC login for Rancher.
- **Global role bindings** — Authentik group → Rancher global role.
- **Users** — Rancher-local users and their global roles.
- **Global roles** — custom Rancher global roles.
- **Tokens** — API tokens for the identity Terraform authenticates as.
- **Settings** — Rancher settings such as `server-url`.
## Configuration
Every kind is discovered from YAML by `config/config.hcl`: the file name is the
object's key/name and the file body holds its attributes. Adding a file is the
only step needed to manage a new object; an empty directory means an empty map.
`config/keycloakoidc.yaml` defines the auth provider. The OAuth client secret is
read from Vault (kv-v2) — the same secret Authentik sets on its `rancher`
provider — and is never committed.
@@ -18,6 +27,76 @@ provider — and is never committed.
roles are granted to users/groups separately. This avoids locking the admin out
when the provider is first enabled.
### `config/global_role_bindings/<group>.yaml`
```yaml
global_role_id: admin
```
### `config/users/<username>.yaml`
```yaml
name: Some Person # optional display name, defaults to the file name
enabled: true # optional, default true
must_change_password: false # optional, default false
password_vault: # required — rancher2_user requires a password
mount: kv
path: service/rancher/users/someperson
key: password # optional, default "password"
global_role_bindings: # optional
- user
```
The password is read from Vault **at plan time**, so seed the kv-v2 secret
before adding the file, or the plan fails.
The file name (the username) and every entry in `global_role_bindings` must be
an RFC 1123 label — `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`, i.e. lowercase
alphanumerics and `-`, starting and ending alphanumeric. No `.`, `_`, `@` or
uppercase. Rancher names each binding `akuser-<username>-<role>` and rejects
anything else, so non-compliant values fail the plan with an explicit error
instead of the apply. Names that are individually valid but collapse to the same
binding name (e.g. `foo-bar` + `baz` and `foo` + `bar-baz`) are rejected at plan
time too.
### `config/roles/<name>.yaml`
```yaml
description: Read-only across all clusters
new_user_default: false # optional, default false
inherited_cluster_roles: # optional
- view
rules: # optional
- api_groups: ["management.cattle.io"]
resources: ["clusters"]
verbs: ["get", "list", "watch"]
# non_resource_urls and resource_names are also supported
```
### `config/tokens/<name>.yaml`
```yaml
description: CI token # optional, defaults to the file name
ttl: 7776000 # optional, seconds
renew: true # optional
cluster_id: c-m-abcdefgh # optional, scopes the token to one cluster
```
> **Caveat:** `rancher2_token` has no user selector — `user_id` is computed by
> the provider (14.1.1), not settable. Tokens declared here are minted for the
> identity the rancher2 provider authenticates as (the admin token Terraform
> runs with), **not** for users in `config/users/`. A token for another user has
> to be created by that user. Token values land in Terraform state.
### `config/settings/<name>.yaml`
```yaml
value: https://rancher.k8s.syd1.au.unkin.net
```
Rancher ships defaults for its settings, so an entry takes over an existing
setting rather than creating a new one.
## Usage
```sh
@@ -28,13 +107,10 @@ make format # fmt tofu + terragrunt hcl
### Authentication
The rancher2 provider needs a Rancher admin API token, read from Vault at
`kv/service/terraform/rancher` (field `token`).
> **Note:** Rancher API tokens have a **90-day maximum** lifetime, so the static
> token must be rotated. This is intended to move to a dedicated Vault Rancher
> secrets engine that mints short-lived tokens on demand; when that lands, update
> the Makefile `vault_env` helper to `vault read` from that engine.
The rancher2 provider needs a Rancher API token, minted on demand by the Vault
Rancher secrets engine at `rancher/creds/ci`. The token is ephemeral (1h lease)
and Vault revokes it in Rancher when the lease expires, so nothing needs
rotating.
Set `VAULT_ROLEID` for local AppRole auth, or `VAULT_AUTH_METHOD=kubernetes`
for CI (Woodpecker).
+25
View File
@@ -8,5 +8,30 @@ locals {
config = {
keycloakoidc = try(local.all_configs["keycloakoidc.yaml"], null)
global_role_bindings = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "global_role_bindings/")
}
users = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "users/")
}
roles = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "roles/")
}
tokens = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "tokens/")
}
settings = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "settings/")
}
}
}
@@ -0,0 +1,3 @@
# akP-rancher-admin -> Rancher global admin. Granted to akR-global-admin members
# (inherited) and direct members.
global_role_id: admin
@@ -0,0 +1,3 @@
# akP-rancher-user -> standard Rancher user global role. Granted to
# akR-standard-user members (inherited) and direct members.
global_role_id: user
+4 -1
View File
@@ -6,7 +6,10 @@ rancher_url: https://rancher.k8s.syd1.au.unkin.net/verify-auth
client_id: rancher
issuer: https://identity.unkin.net/application/o/rancher/
auth_endpoint: https://identity.unkin.net/application/o/authorize/
scopes: openid profile email
# ak_groups = hierarchical group claim from terraform-authentik (permission
# groups inherited via role groups). groups_field points Rancher at that claim.
scopes: openid profile email ak_groups
groups_field: ak_groups
access_mode: unrestricted
enabled: true
client_secret_vault:
View File
+3
View File
@@ -0,0 +1,3 @@
# Rancher's server-url setting. Must match the URL Rancher is reached on,
# otherwise newly registered cluster agents dial the wrong endpoint.
value: https://rancher.k8s.syd1.au.unkin.net
View File
View File
@@ -17,5 +17,10 @@ terraform {
}
inputs = {
keycloakoidc = local.config.keycloakoidc
keycloakoidc = local.config.keycloakoidc
global_role_bindings = local.config.global_role_bindings
users = local.config.users
global_roles = local.config.roles
tokens = local.config.tokens
settings = local.config.settings
}
+8 -1
View File
@@ -3,7 +3,7 @@ generate "backend" {
if_exists = "overwrite"
contents = <<EOF
provider "rancher2" {
api_url = "https://${path_relative_to_include()}"
api_url = var.rancher_url
token_key = var.rancher_token
}
@@ -20,6 +20,13 @@ variable "rancher_token" {
sensitive = true
}
# Defaults to the environment's public URL; an exported TF_VAR_rancher_url
# takes precedence over terragrunt inputs.
variable "rancher_url" {
type = string
default = "https://${path_relative_to_include()}"
}
terraform {
backend "consul" {
address = "https://consul.service.consul"
+135
View File
@@ -5,6 +5,16 @@ data "vault_kv_secret_v2" "keycloakoidc" {
name = var.keycloakoidc.client_secret_vault.path
}
# Read the internal PKI CA chain (intermediate + root) from Vault. Rancher makes
# its OIDC discovery call to https://identity.unkin.net server-side; without the
# internal CA it fails with x509 "certificate signed by unknown authority". The
# ca_chain field is public/non-sensitive. Sourced from Vault (not hardcoded) so
# the trust never goes stale on rotation.
data "vault_generic_secret" "internal_ca" {
count = var.keycloakoidc != null ? 1 : 0
path = "pki_int/cert/ca_chain"
}
# Rancher's Keycloak(OIDC) auth provider, pointed at Authentik. access_mode
# "unrestricted" means any authenticated Authentik user can log in; Rancher
# roles are granted to users/groups separately (avoids admin lockout on enable).
@@ -17,6 +27,131 @@ resource "rancher2_auth_config_keycloak_oidc" "this" {
issuer = var.keycloakoidc.issuer
auth_endpoint = var.keycloakoidc.auth_endpoint
scopes = var.keycloakoidc.scopes
groups_field = var.keycloakoidc.groups_field
access_mode = var.keycloakoidc.access_mode
enabled = var.keycloakoidc.enabled
# CA cert Rancher uses to trust the IdP's TLS during OIDC discovery. Defaults
# to the internal PKI chain from Vault; an explicit config value overrides it.
certificate = coalesce(var.keycloakoidc.certificate, data.vault_generic_secret.internal_ca[0].data["ca_chain"])
}
# Grant Rancher global roles to Authentik permission groups. The keycloak_oidc
# group principal id is keycloakoidc_group://<group name>. Members of a role
# group (e.g. akR-global-admin) inherit the permission group, so they receive
# the bound global role.
resource "rancher2_global_role_binding" "group" {
for_each = var.global_role_bindings
# Binding name must be an RFC 1123 label (lowercase); the group name may be
# mixed-case (akP-*), so lowercase it here. The principal id keeps the original
# case to match the group.
name = "akgroup-${lower(each.key)}"
global_role_id = each.value.global_role_id
group_principal_id = "keycloakoidc_group://${each.key}"
depends_on = [rancher2_auth_config_keycloak_oidc.this]
}
# Rancher-local users. rancher2_user makes `password` required, so each user's
# password comes from a Vault kv-v2 secret rather than the repo. Vault data
# sources resolve at plan time: seed the secret before adding the yaml.
data "vault_kv_secret_v2" "user_password" {
for_each = var.users
mount = each.value.password_vault.mount
name = each.value.password_vault.path
}
resource "rancher2_user" "this" {
for_each = var.users
username = each.key
name = coalesce(each.value.name, each.key)
enabled = each.value.enabled
must_change_password = each.value.must_change_password
password = data.vault_kv_secret_v2.user_password[each.key].data[each.value.password_vault.key]
}
# Flatten users -> their global roles into "<username>/<role>" keys so adding or
# removing one role never re-indexes the others.
locals {
user_global_role_bindings = merge([
for username, user in var.users : {
for role in user.global_role_bindings :
"${username}/${role}" => {
username = username
global_role_id = role
}
}
]...)
}
resource "rancher2_global_role_binding" "user" {
for_each = local.user_global_role_bindings
# Both parts are RFC 1123 labels already (var.users validates them), so the
# binding name needs no transformation.
name = "akuser-${each.value.username}-${each.value.global_role_id}"
global_role_id = each.value.global_role_id
user_id = rancher2_user.this[each.value.username].id
lifecycle {
# Hyphens are legal inside both parts, so distinct bindings can still flatten
# to the same name (foo-bar + baz vs foo + bar-baz). Catch that at plan time
# rather than as a duplicate-object conflict mid-apply.
precondition {
condition = length(distinct([
for binding in local.user_global_role_bindings :
"akuser-${binding.username}-${binding.global_role_id}"
])) == length(local.user_global_role_bindings)
error_message = "Two user global role bindings collapse to the same Rancher binding name (akuser-<username>-<role>). Rename one of the users or roles involved."
}
}
}
# Custom global roles. Rules are optional: a role with none grants nothing on its
# own and is useful purely as a container for inherited_cluster_roles.
resource "rancher2_global_role" "this" {
for_each = var.global_roles
name = each.key
description = each.value.description
new_user_default = each.value.new_user_default
inherited_cluster_roles = each.value.inherited_cluster_roles
dynamic "rules" {
for_each = each.value.rules
content {
api_groups = rules.value.api_groups
resources = rules.value.resources
verbs = rules.value.verbs
non_resource_urls = rules.value.non_resource_urls
resource_names = rules.value.resource_names
}
}
}
# CAVEAT: rancher2_token has no user selector — `user_id` is computed by the
# provider at 14.1.1, not settable. Every token declared here is minted for the
# identity the rancher2 provider authenticates as (the CI admin service account),
# NOT for any user in config/users/. There is no way to mint a token on another
# user's behalf through this provider; that has to be done by that user.
# The token/secret_key values land in Terraform state, so treat state as secret.
resource "rancher2_token" "this" {
for_each = var.tokens
description = coalesce(each.value.description, each.key)
ttl = each.value.ttl
renew = each.value.renew
cluster_id = each.value.cluster_id
}
# Rancher settings, e.g. server-url. Rancher ships defaults for these, so an
# entry here takes over an existing setting rather than creating a new one.
resource "rancher2_setting" "this" {
for_each = var.settings
name = each.key
value = each.value.value
}
+98 -2
View File
@@ -6,8 +6,13 @@ variable "keycloakoidc" {
issuer = string # OIDC issuer (Authentik application URL)
auth_endpoint = string # OIDC authorization endpoint
scopes = optional(string, "openid profile email")
access_mode = optional(string, "unrestricted")
enabled = optional(bool, true)
# 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({
@@ -17,3 +22,94 @@ variable "keycloakoidc" {
})
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 = {}
}
+1 -1
View File
@@ -3,7 +3,7 @@ terraform {
required_providers {
rancher2 = {
source = "rancher/rancher2"
version = ">= 5.0.0"
version = "~> 14.0"
}
vault = {
source = "hashicorp/vault"