Scaffold terraform-rancher: Authentik OIDC auth config
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.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
*.tfplan
|
||||
backend.tf
|
||||
.terragrunt-cache/
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Generated
+43
@@ -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",
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user