diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13275b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.terraform/ +*.tfstate +*.tfstate.backup +*.tfplan +backend.tf +.terragrunt-cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..646cd65 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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", + ] diff --git a/.woodpecker/apply.yaml b/.woodpecker/apply.yaml new file mode 100644 index 0000000..8bfba46 --- /dev/null +++ b/.woodpecker/apply.yaml @@ -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 diff --git a/.woodpecker/plan.yaml b/.woodpecker/plan.yaml new file mode 100644 index 0000000..ad87ec9 --- /dev/null +++ b/.woodpecker/plan.yaml @@ -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 diff --git a/.woodpecker/pre-commit.yaml b/.woodpecker/pre-commit.yaml new file mode 100644 index 0000000..5c5738f --- /dev/null +++ b/.woodpecker/pre-commit.yaml @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ec5d5fc --- /dev/null +++ b/Makefile @@ -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/`). +# 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 diff --git a/README.md b/README.md index ae16779..f291a54 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,40 @@ # terraform-rancher -Terraform configuration for managing Rancher (auth, roles) via the rancher2 provider \ No newline at end of file +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). diff --git a/config/config.hcl b/config/config.hcl new file mode 100644 index 0000000..016df4a --- /dev/null +++ b/config/config.hcl @@ -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) + } +} diff --git a/config/keycloakoidc.yaml b/config/keycloakoidc.yaml new file mode 100644 index 0000000..71ed52d --- /dev/null +++ b/config/keycloakoidc.yaml @@ -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 diff --git a/environments/rancher.k8s.syd1.au.unkin.net/terragrunt.hcl b/environments/rancher.k8s.syd1.au.unkin.net/terragrunt.hcl new file mode 100644 index 0000000..c5f82f9 --- /dev/null +++ b/environments/rancher.k8s.syd1.au.unkin.net/terragrunt.hcl @@ -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 +} diff --git a/environments/root.hcl b/environments/root.hcl new file mode 100644 index 0000000..321d6a6 --- /dev/null +++ b/environments/root.hcl @@ -0,0 +1,35 @@ +generate "backend" { + path = "backend.tf" + if_exists = "overwrite" + contents = <