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
This commit was merged in pull request #1.
This commit is contained in:
2026-07-16 22:26:13 +10:00
15 changed files with 351 additions and 1 deletions
+6
View File
@@ -0,0 +1,6 @@
.terraform/
*.tfstate
*.tfstate.backup
*.tfplan
backend.tf
.terragrunt-cache/
+24
View File
@@ -0,0 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
types: [yaml]
- id: trailing-whitespace
types: [yaml]
- repo: https://github.com/gruntwork-io/pre-commit
rev: v0.1.30
hooks:
- id: tofu-fmt
- id: tofu-validate
- id: tflint
- id: terragrunt-hcl-fmt
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1
hooks:
- id: yamllint
args:
[
"-d {extends: relaxed, rules: {line-length: disable}, ignore: chart}",
"-s",
]
+23
View File
@@ -0,0 +1,23 @@
when:
- event: push
branch: main
steps:
- name: apply
image: git.unkin.net/unkin/almalinux9-opentofu:20260606
environment:
VAULT_AUTH_METHOD: kubernetes
commands:
- dnf install vault -y
- make plan
- make apply
backend_options:
kubernetes:
serviceAccountName: terraform-rancher
resources:
requests:
memory: 512Mi
cpu: 1
limits:
memory: 2Gi
cpu: 2
+21
View File
@@ -0,0 +1,21 @@
when:
- event: pull_request
steps:
- name: plan
image: git.unkin.net/unkin/almalinux9-opentofu:20260606
environment:
VAULT_AUTH_METHOD: kubernetes
commands:
- dnf install vault -y
- make plan
backend_options:
kubernetes:
serviceAccountName: terraform-rancher
resources:
requests:
memory: 512Mi
cpu: 1
limits:
memory: 2Gi
cpu: 2
+18
View File
@@ -0,0 +1,18 @@
when:
- event: pull_request
steps:
- name: pre-commit
image: git.unkin.net/unkin/almalinux9-opentofu:20260606
commands:
- uvx pre-commit run --all-files
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 512Mi
cpu: 1
limits:
memory: 2Gi
cpu: 2
+42
View File
@@ -0,0 +1,42 @@
.PHONY: init plan apply format
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
define vault_env
@export VAULT_ADDR="https://vault.service.consul:8200" && \
if [ "$(VAULT_AUTH_METHOD)" = "kubernetes" ]; then \
export VAULT_TOKEN=$$(vault write -field=token $(VAULT_K8S_MOUNT)/login role=$(VAULT_K8S_ROLE) jwt=$$(cat $(VAULT_K8S_JWT_PATH))); \
else \
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))
endef
init:
@$(call vault_env) && \
terragrunt run --all --non-interactive init -- -upgrade
plan: init
@$(call vault_env) && \
terragrunt run --all --parallelism 4 --non-interactive plan
apply: init
@$(call vault_env) && \
terragrunt run --all --parallelism 2 --non-interactive apply
format:
@echo "Formatting OpenTofu files..."
@tofu fmt -recursive .
@echo "Formatting Terragrunt files..."
@terragrunt hcl fmt
+38 -1
View File
@@ -1,3 +1,40 @@
# terraform-rancher
Terraform configuration for managing Rancher (auth, roles) via the rancher2 provider
Terraform configuration for managing Rancher (rancher.k8s.syd1.au.unkin.net)
authentication via the [rancher2](https://registry.terraform.io/providers/rancher/rancher2)
provider. Mirrors the `terraform-authentik` pattern.
## Managed Resources
- **Keycloak(OIDC) auth config** — Authentik OIDC login for Rancher.
## Configuration
`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.
`access_mode: unrestricted` lets any authenticated Authentik user log in; Rancher
roles are granted to users/groups separately. This avoids locking the admin out
when the provider is first enabled.
## Usage
```sh
make plan # init + plan
make apply # init + plan + apply
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.
Set `VAULT_ROLEID` for local AppRole auth, or `VAULT_AUTH_METHOD=kubernetes`
for CI (Woodpecker).
+12
View File
@@ -0,0 +1,12 @@
locals {
config_files = fileset(".", "**/*.yaml")
all_configs = {
for file_path in local.config_files :
file_path => yamldecode(file(file_path))
}
config = {
keycloakoidc = try(local.all_configs["keycloakoidc.yaml"], null)
}
}
+14
View File
@@ -0,0 +1,14 @@
# Rancher Keycloak(OIDC) auth provider backed by Authentik.
# client_secret is read from Vault (the same secret Authentik sets on its
# provider), not committed. access_mode "unrestricted" avoids admin lockout:
# any Authentik user can authenticate; Rancher roles are assigned separately.
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
access_mode: unrestricted
enabled: true
client_secret_vault:
mount: kv
path: kubernetes/namespace/cattle-system/default/oauth-credentials
@@ -0,0 +1,21 @@
include "root" {
path = find_in_parent_folders("root.hcl")
expose = true
}
include "config" {
path = "${get_repo_root()}/config/config.hcl"
expose = true
}
locals {
config = include.config.locals.config
}
terraform {
source = "../../modules/rancher"
}
inputs = {
keycloakoidc = local.config.keycloakoidc
}
+35
View File
@@ -0,0 +1,35 @@
generate "backend" {
path = "backend.tf"
if_exists = "overwrite"
contents = <<EOF
provider "rancher2" {
api_url = "https://${path_relative_to_include()}"
token_key = var.rancher_token
}
# Reads the OAuth client secret seeded in Vault (kv-v2). Auth via VAULT_ADDR +
# VAULT_TOKEN from the environment (set by the Makefile vault_env helper).
# skip_child_token is required because the short-lived CI token cannot create
# child tokens.
provider "vault" {
skip_child_token = true
}
variable "rancher_token" {
type = string
sensitive = true
}
terraform {
backend "consul" {
address = "https://consul.service.consul"
path = "infra/terraform/rancher/${path_relative_to_include()}/state"
scheme = "https"
lock = true
ca_file = "/etc/pki/tls/certs/ca-bundle.crt"
}
required_version = ">= 1.10"
# required_providers are declared in the module's versions.tf.
}
EOF
}
+43
View File
@@ -0,0 +1,43 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/vault" {
version = "5.10.1"
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",
]
}
provider "registry.opentofu.org/rancher/rancher2" {
version = "14.1.1"
constraints = ">= 5.0.0"
hashes = [
"h1:XguC/YWN5PuuX+oQ9U7HZdrpLZ/z32XFTmWcTZTO/AY=",
"zh:11c4c34ba996b633d116e83f48b0baecaa1e0cbf50562abdfd9424cd6f6141a4",
"zh:205a3a4b5f078b2eeea06f4a13ab15bcf34864460f789568d384a03e47f0a009",
"zh:2dc7df1fc9c6bb23ca9b0be326aac125212d525cde029a3c44c943f82e07c59f",
"zh:50752ce808ff054c36e7a594bec9807299f3dbe2febf5ffa501239aa5a4b3125",
"zh:515d787138ba9a750506ccc45f95a707ccf6bd2601e6bcc732e39f18e1436837",
"zh:8b8ae8881596dc865e3998701ff0048ace2670a73c6ea990dcee0ea9ea8006bc",
"zh:a54086f41c125b7460b8cf1b588f778b8e971a131a8713cd12806e3724e198a8",
"zh:b346815aac1ba419e12210fc03da57cc8606366a5817c6fbb7ce84bc26a65ef0",
"zh:c8b5fae14df8195c3ab21781913f929d3db2f4576d9b6ed5353b663d9f6b7940",
"zh:d990f1b187d1c6779d38a2620c685b2c3efc79be4d603ed6b041ecbdfcc1e79b",
]
}
+22
View File
@@ -0,0 +1,22 @@
# Read the OAuth client secret from Vault so nothing sensitive is committed.
data "vault_kv_secret_v2" "keycloakoidc" {
count = var.keycloakoidc != null && var.keycloakoidc.client_secret_vault != null ? 1 : 0
mount = var.keycloakoidc.client_secret_vault.mount
name = var.keycloakoidc.client_secret_vault.path
}
# 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).
resource "rancher2_auth_config_keycloak_oidc" "this" {
count = var.keycloakoidc != null ? 1 : 0
rancher_url = var.keycloakoidc.rancher_url
client_id = var.keycloakoidc.client_id
client_secret = data.vault_kv_secret_v2.keycloakoidc[0].data["client_secret"]
issuer = var.keycloakoidc.issuer
auth_endpoint = var.keycloakoidc.auth_endpoint
scopes = var.keycloakoidc.scopes
access_mode = var.keycloakoidc.access_mode
enabled = var.keycloakoidc.enabled
}
+19
View File
@@ -0,0 +1,19 @@
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")
access_mode = optional(string, "unrestricted")
enabled = optional(bool, true)
# 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
}
+13
View File
@@ -0,0 +1,13 @@
terraform {
required_version = ">= 1.10"
required_providers {
rancher2 = {
source = "rancher/rancher2"
version = ">= 5.0.0"
}
vault = {
source = "hashicorp/vault"
version = ">= 4.0.0"
}
}
}