22 Commits

Author SHA1 Message Date
unkinben 13c1f5c1ba gitea: consume the KV admin seed only at engine-config creation
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Why: after the engine is first configured, rotate-root changes the live admin
password so it diverges from the KV seed. Re-reading the (possibly stale) seed
on a later apply must not push it back and clobber the rotated password. Ben
asked that the seed be used only when initially creating the config.

Change:
- Add lifecycle ignore_changes on admin_username/admin_password to the
  gitea_secret_backend resource, making the module create-only for the seeded
  credential. Noted inline that the rancher/litellm seed modules lack this.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-27 19:31:12 +10:00
unkinben a960757ab2 gitea: add the gitea token secrets engine (mount, config, teabot roles)
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Why: teabot's implementer and reviewer bot users should mint ephemeral,
scoped Gitea tokens on demand instead of holding standing personal access
tokens (Gitea tokens never expire on their own). This registers and mounts
the new vault-plugin-secrets-gitea engine and declares its roles, mirroring
the rancher engine wiring.

Change:
- Register the plugin in the catalog (config/plugins/vault-plugin-secrets-gitea.yaml)
  pinned to the released v0.1.0 binary sha256.
- Add gitea_secret_backend + gitea_secret_backend_role modules and wire them
  through config.hcl, terragrunt.hcl, and vault_cluster variables/main, using
  the giteavaultsecret provider (terraform-unkin registry, v0.1.0).
- Mount the engine at gitea/ against https://git.unkin.net; seeded site-admin
  credentials are read from KV (service/vault/au/syd1/secret_backend/gitea/config).
- Add teabot-implementer (write:repository, write:issue) and teabot-reviewer
  (read:repository, write:issue) roles, ttl 1h / max_ttl 4h.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-27 19:05:08 +10:00
unkinben 31424ea6ff ci: fetch vault from artifactapi instead of dnf install (#99)
ci/woodpecker/push/apply Pipeline was successful
## Why

CI installs vault by shelling out to `dnf install vault -y`. That reads
metadata for every enabled repo (appstream/baseos/crb/epel/ha) and downloads
the 169MB vendored vault RPM from the `unkin` repo on **every** plan/apply run
(~39s per job measured in `almalinux9-opentofu:20260606`).

## Change

- Replace `dnf install vault -y` with a pinned `curl` of the upstream vault zip
  from the artifactapi `hashicorp-releases` remote proxy, extracted with the
  image's `python3` (`python3 -m zipfile`) to `/usr/local/bin/vault`.
- Pin the version via a new `VAULT_VERSION` env var (`1.20.0`); bump the var to
  upgrade.

## Speedup

Measured in `git.unkin.net/unkin/almalinux9-opentofu:20260606`:

| approach | time |
|---|---|
| `dnf install vault -y` (current) | ~39s |
| `dnf --disablerepo='*' --enablerepo=unkin` (still pulls 169MB RPM) | ~9s |
| curl zip from artifactapi + python extract (this PR) | ~6.6s |

~32s saved per plan/apply job. The zip is cached by artifactapi after first
fetch (warm ~3s).

## Caveats

- Assumes the `almalinux9-opentofu` image ships `curl` + `python3` (both
  present in `:20260606`).
- Relies on the existing artifactapi `hashicorp-releases` generic remote whose
  patterns already allow `vault/.*vault_.*_linux_amd64.zip`.

---------

Co-authored-by: benvin <neotheo@gmail.com>
Reviewed-on: #99
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-25 09:47:34 +10:00
unkinben 1fa5900787 Add terraform-enc Vault/Consul plumbing + encapi token grant (#98)
ci/woodpecker/push/apply Pipeline was successful
The new **terragrunt-enc** repo manages all encapi ENC data (statuses, roles, node classifications) via Terraform/Terragrunt and needs its own Vault/Consul plumbing, mirroring terraform-git and terraform-incus. This supersedes the dual-write approach in terraform-incus PR #39; the equivalent terraform-incus grant (PR #97) is being closed, so the encapi-token grant is created fresh here for the new approle.

Changes:
- Add approle role `terraform_enc` and k8s auth role `woodpecker_terraform_enc` (bound to the `terraform-enc` ServiceAccount in the `woodpecker` namespace) for CI auth.
- Add consul secret backend role `terraform-enc` plus its ACL rules granting `write` on `infra/terraform/enc/` (its terragrunt state prefix), and a policy letting both auth roles read `consul_root/au/syd1/creds/terraform-enc`.
- Grant both auth roles read on `kv/data/kubernetes/namespace/encapi/default/environment` (the ENCAPI_WRITE_TOKEN) so `make apply` can write to encapi via the encapi provider.

Reviewed-on: #98
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-24 23:18:56 +10:00
unkinben dcc73131a4 Bump rancher plugin catalog to v0.1.1 (bearerToken fix) (#96)
ci/woodpecker/push/apply Pipeline was successful
## Why

v0.1.0 of the rancher plugin minted non-functional tokens — it returned `status.value` (the secret fragment) instead of `status.bearerToken` (`ext/<name>:<secret>`), so every cred and every root rotation 401'd against Rancher. Fixed in plugin v0.1.1 (PR #2, released).

## Changes

- `config/plugins/vault-plugin-secrets-rancher.yaml` sha256 → v0.1.1 binary `9e597cd9…d5955` (verified against both v0.1.1 RPMs). Re-registers the catalog entry so OpenBao launches the fixed binary.

## Deploy coordination

1. Puppet installs the v0.1.1 RPM on the vault nodes (separate puppet-prod PR).
2. Merge this → apply re-registers the catalog with the new sha.
3. `vault plugin reload -plugin=vault-plugin-secrets-rancher` so the running plugin swaps to v0.1.1 (its sha must match this catalog entry).

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #96
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 22:59:35 +10:00
unkinben 87e3ada14f Mount the rancher secrets engine + seed a service account + roles (#93)
ci/woodpecker/push/apply Pipeline was successful
## Why

Deploy the new Rancher token secrets engine into the cluster (the last of the 4 wiring PRs), mirroring the litellm/gpg pattern. Users can then `vault read rancher/creds/<role>` for short-lived, cluster-scoped Rancher tokens, backed by a seeded admin token the engine auto-rotates before Rancher's 90-day cap.

## Changes

- Add `rancher_secret_backend` module — mount + config via the ranchervaultsecret provider (rancher_url `https://rancher.k8s.syd1.au.unkin.net`).
- Add `rancher_secret_backend_service_account` module — seeds an auto-rotated token (90d TTL / 45d rotation); the seed token is read from KV, not git.
- Add `rancher_secret_backend_role` module + a `ci` role (1h/8h, cluster+TTL scoped).
- Wire `config.hcl` discovery, module variables, `main.tf` blocks, terragrunt inputs, and the `rancher` provider in `root.hcl`.
- Config: `config/rancher_secret_backend/rancher.yaml`, `.../service_account/rancher/admin.yaml`, `.../role/rancher/ci.yaml`.

## Prerequisite

Populate `kv/service/vault/au/syd1/secret_backend/rancher/service_account/admin` with a live Rancher admin token (keys: `token`, optional `token_name`) **before** apply, exactly as litellm's `master_key` is seeded in KV.

## Merge order

Part 4 of 4 (last). Requires: puppet install (#483) → deployer policy (#91) → plugin import (#92) → this. The `plan` needs the KV seed present, so seed KV first.

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #93
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 16:16:54 +10:00
unkinben 193c17d1bc Manage the litellm plugin via config/plugins (import existing registration) (#90)
ci/woodpecker/push/apply Pipeline was successful
Bring the litellm plugin under terraform management like the gpg one, using the new plugin-import module (#89).

## Why
The litellm plugin was registered **manually** before terraform owned the catalog. If we just add it to `config/plugins/`, the first apply tries to *create* a catalog entry that already exists and fails. So its state must be imported first.

## Changes
- Add `config/plugins/vault-plugin-secrets-litellm.yaml` (`sha256` = released **v0.1.1** openbao binary from the RPM Puppet installs).

## Manual pre-step (before apply)
Import the existing registration into state, and verify the sha matches the live catalog:
```sh
cd environments/au/syd1
# confirm the live sha first:
bao read sys/plugins/catalog/secret/vault-plugin-secrets-litellm
# import into state:
terragrunt import \
  'module.plugin["vault-plugin-secrets-litellm"].vault_plugin.this' \
  secret/vault-plugin-secrets-litellm
```
If the live sha differs from the yaml, update the yaml to match (or expect a benign re-register to the on-disk v0.1.1 sha). Needs the deployer's plugin-catalog access (#88, already merged).

Reviewed-on: #90
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 14:55:33 +10:00
unkinben 03e9baf17f Register the rancher plugin in the catalog (import) (#92)
ci/woodpecker/push/apply Pipeline was successful
## Why

Register the released v0.1.0 rancher secrets plugin so the engine can be mounted. Uses the existing config/plugins discovery + plugin module (added for gpg in #89). Mirrors #90 (litellm import).

## Changes

- Add `config/plugins/vault-plugin-secrets-rancher.yaml` (`type: secret`, `command: vault-plugin-secrets-rancher`, `sha256` pinned to the released binary `d2b17f45…ec574a`, verified against both the vault and openbao v0.1.0 RPMs).

## Merge order

Part 2 of 4. Requires the binary on disk (puppet-prod install) and the deployer policy (#91). Merge before the backend PR.

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #92
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 14:47:08 +10:00
benvin b825fdebf7 chore: setup access to enable all plugins, designated to tf-vault user (#94)
ci/woodpecker/push/apply Pipeline was successful
Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #94
2026-07-18 14:42:02 +10:00
unkinben 7da23d47fe Grant vault deployer access to import + manage the rancher engine (#91)
ci/woodpecker/push/apply Pipeline was successful
## Why

Wiring the new Rancher token secrets engine into Vault. The deployer registers the plugin (sudo-protected `sys/plugins/catalog`) and configures the engine via the ranchervaultsecret provider, so it needs catalog + engine-path access. Mirrors #88 (gpg).

## Changes

- Add `policies/rancher/admin.yaml` granting the `tf_vault` approle and `woodpecker_terraform_vault` k8s role: catalog sudo on `vault-plugin-secrets-rancher`, and manage on `rancher/{config,service-accounts,roles}`.

## Merge order

Part 1 of 4. Merge before the plugin-import and backend PRs so apply doesn't 403. (Puppet install + this policy first, then import, then backend.)

---------

Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #91
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-18 14:34:54 +10:00
unkinben 933de177fa Register + mount the GPG secrets engine at gpg/ (#87)
ci/woodpecker/push/apply Pipeline was successful
Complete the deploy of the [vault-plugin-secrets-gpg](https://git.unkin.net/unkin/vault-plugin-secrets-gpg) engine. Puppet ([puppet-prod #480](unkin/puppet-prod#480)) installs the `openbao-plugin-secrets-gpg` RPM onto the OpenBao nodes; this registers that binary in the plugin catalog and enables the secrets engine so `gpg/` is actually usable.

- Add a `gpg_secret_backend` module using the standard `hashicorp/vault` provider (already required at 5.6.0): `vault_plugin` (catalog register with a pinned sha256) + `vault_mount` (enable at the mount path).
- Wire it through `vault_cluster` (new `gpg_secret_backend` variable + module block) and the config discovery (`config.hcl` group + syd1 terragrunt input), mirroring `litellm_secret_backend`.
- Add `config/gpg_secret_backend/gpg.yaml` mounting at `gpg/` and pinning the released v0.1.0 binary sha256 (`0e92d740…a7b20`, extracted from the published RPM). Puppet installs the RPM floating, so this sha must be bumped in lockstep on any plugin upgrade or OpenBao rejects the binary.

Validated locally with `tofu validate` + `tofu fmt`. Granting non-root access to `gpg/*` (auth roles + policies) is a follow-up scoped to whoever consumes the engine (e.g. passv from CI).

Reviewed-on: #87
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-17 23:19:38 +10:00
unkinben ce1185deba Grant vault deployer access to import + manage the gpg engine (#88)
ci/woodpecker/push/apply Pipeline was successful
## Why
Applying the gpg mount (#87) needs two grants the deployer (`tf_vault` approle / `woodpecker_terraform_vault` k8s role) doesn't have. terraform-vault **registers the plugin itself** (`vault_plugin` → `sys/plugins/catalog`, a sudo-protected path) and **manages keys** via the gpgvaultsecret provider (`gpg/keys/*`). The deployer already has `sys/mounts/*` but neither of these, so apply would 403 on the plugin registration and on `gpg/keys` writes — the same failure mode as #84.

## Changes
- Add `policies/gpg/admin.yaml` granting:
  - `create/read/update/delete/sudo` on `sys/plugins/catalog/secret/vault-plugin-secrets-gpg` — to **import** (register/deregister) the plugin.
  - full management of `gpg/keys/*` (+ `gpg/keys` list) — to **manage keys**.
  - assigned to `tf_vault` (approle) + `woodpecker_terraform_vault` (k8s/au/syd1), mirroring `policies/litellm/admin.yaml` (#84).

Should merge/apply **before** #87 so the deployer can register the plugin and create the `pass` key.

Reviewed-on: #88
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-17 23:09:31 +10:00
unkinben 3d59758324 Add a plugin-import module + config/plugins for catalog registration (#89)
ci/woodpecker/push/apply Pipeline was canceled
Split plugin catalog registration out of the per-engine backend modules into its own concern (previously bundled into #87's gpg_secret_backend).

## Changes
- New generic `plugin` module (`vault_plugin`: type/name/command/sha256/plugin_version) that imports a binary into the catalog.
- New `config/plugins/` discovery group (filename = catalog name = mount type), wired through `vault_cluster` (`plugins` variable + module) and the syd1 environment.
- `config/plugins/vault-plugin-secrets-gpg.yaml` pins the released v0.1.0 binary sha256 (`0e92d740…a7b20`, from the published RPM). Puppet installs the RPM floating, so bump this in lockstep on upgrade.

Any engine now registers its plugin by dropping a file in `config/plugins/`; its `*_secret_backend` module just mounts the registered type.

Needs the deployer's plugin-catalog access (#88). Merge order: **#88 → this → #87**.

Reviewed-on: #89
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-17 23:08:04 +10:00
unkinben 8bb071ae46 Add auth and state access for terraform-rancher (#86)
ci/woodpecker/push/apply Pipeline was successful
## Why

The new `terraform-rancher` repo (manages Rancher's Authentik OIDC auth via the rancher2 provider) needs Vault auth + Consul state, mirroring the terraform-authentik runner (#78/#81/#82).

## Change

- `AppRole/terraform_rancher` + k8s auth role `woodpecker_terraform_rancher` (SA terraform-rancher in the woodpecker ns).
- Consul secret-backend role + ACL policy (`resources/secret_backend/consul_root/au/syd1/terraform-rancher.hcl`) granting write to the `infra/terraform/rancher/` state prefix.
- Vault policies: read the Rancher admin API token (`kv/service/terraform/rancher`) and the keycloakoidc client secret (`kv/kubernetes/namespace/cattle-system/default/oauth-credentials`), plus the consul_root state creds.

Scoped the OAuth read to the `cattle-system` path specifically (rather than the `+` wildcard the authentik policy uses) since the Rancher runner only needs its own app's secret.

## Validation

pre-commit (terragrunt-hcl-fmt + yamllint) passed. CI plan will confirm.

Reviewed-on: #86
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-15 21:37:45 +10:00
benvin 0dba5e00a6 chore: update litellm address (#85)
ci/woodpecker/push/apply Pipeline was successful
- update litellm address
- add a test role

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #85
2026-07-09 23:47:32 +10:00
unkinben a400e5dc7e fix: grant vault deployer access to manage the litellm engine (#84)
ci/woodpecker/push/apply Pipeline was successful
## Why
Applying the newly-merged litellm mount (#83) failed at apply time with:

```
Error: failed to write litellm config
URL: PUT https://vault.service.consul:8200/v1/litellm/config
Code: 403. * permission denied
```

The deployer identity (`tf_vault` approle / `woodpecker_terraform_vault` k8s role) can enable the mount via `sys/mounts/admin`, but no policy grants it access to the engine's own data paths, so writing the config and roles is denied.

## Changes
- Add `policies/litellm/admin.yaml` granting `create`/`read`/`update`/`delete` on `litellm/config` and `litellm/roles/*` (plus `read`/`list` on `litellm/roles`), assigned to the same auth roles as the other secret-engine admin policies (`tf_vault`, `woodpecker_terraform_vault`).

## Note
The policy attaches to the deployer's auth roles, so it takes effect on the next token issuance — a re-run of the apply (fresh Vault login) will have the permission and can write `litellm/config` and the roles.

Reviewed-on: #84
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-07 20:10:43 +10:00
unkinben 95e7a81b2e feat: manage litellm secrets engine via terraform-provider-litellmvaultsecret (#83)
ci/woodpecker/push/apply Pipeline failed
## Why
The `vault-plugin-secrets-litellm` engine (mints LiteLLM virtual keys) is registered in Vault, but nothing in this repo declared its mount, config, or roles. This wires in the companion `litellm` provider (`git.unkin.net/unkin/litellmvaultsecret`) so the mount is managed as code alongside the other secret backends.

## Changes
- Add `litellm_secret_backend` module that mounts the engine and writes its config (`base_url`, `request_timeout_seconds`); reads the sensitive `master_key` from KV at `kv/service/vault/<country>/<region>/secret_backend/<path>`, matching the consul/kubernetes backend convention.
- Add `litellm_secret_backend_role` module that manages roles (`models`, `max_budget`, `key_alias_prefix`, `ttl`/`max_ttl` in seconds, `metadata`).
- Register both modules in `vault_cluster` `main.tf` and add typed variables in `variables.tf`.
- Discover `litellm_secret_backend[_role]` YAML in `config.hcl` and pass the maps through the terragrunt inputs.
- Declare the `litellm` provider (pinned `0.1.0`) and a `provider "litellm"` block in the generated root `backend.tf`.
- Add example config for the `litellm` mount and a sample `team-a` role.

## Notes
- Requires the `master_key` KV secret to exist at `kv/service/vault/au/syd1/secret_backend/litellm` before apply (the module reads it, does not create it).
- Assumes provider `git.unkin.net/unkin/litellmvaultsecret` `0.1.0` is published to the artifactapi `terraform-unkin` registry.

Reviewed-on: #83
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-07 00:18:29 +10:00
unkinben bbde79d2a6 policies: let terraform-authentik read its provider API token (#82)
ci/woodpecker/push/apply Pipeline was successful
## Why
terraform-authentik's provider needs an Authentik API token (`TF_VAR_authentik_token`), now sourced from Vault at `kv/service/terraform/authentik` (Makefile wiring in terraform-authentik #2). The CI role needs read access to that path.

## Change
Extend `policies/kv/service/terraform/authentik.yaml` to also grant read on `kv/data/service/terraform/authentik` for the `terraform_authentik` approle + `woodpecker_terraform_authentik` k8s role.

Reviewed-on: #82
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-06 23:41:03 +10:00
unkinben f2c54888de policies: let terraform-authentik read oauth client secrets from kv (#81)
ci/woodpecker/push/apply Pipeline was successful
## Why
terraform-authentik now reads OAuth2 client secrets from Vault (`data.vault_kv_secret_v2`) rather than committing them (terraform-authentik #2). But the `terraform_authentik` approle / `woodpecker_terraform_authentik` k8s role only had the consul-creds policy, so `plan` fails with permission denied on the grafana oauth path.

## Change
Add `policies/kv/service/terraform/authentik.yaml` granting read on `kv/data/kubernetes/namespace/+/default/oauth-credentials` for both the approle and the woodpecker k8s role.

Reviewed-on: #81
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-06 23:05:20 +10:00
unkinben 36d7afbb65 feat: add vault/consul config for media terraform repos (#79)
ci/woodpecker/push/apply Pipeline was successful
Add Kubernetes auth roles, AppRole configs, Consul secret backend roles, Consul ACL policies, and Vault kv read policies for terraform-sonarr, terraform-radarr, and terraform-prowlarr.

Reviewed-on: #79
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-06-28 22:03:25 +10:00
unkinben c33dcdc447 Add auth and state access for terraform-authentik (#78)
ci/woodpecker/push/apply Pipeline was successful
## Summary
- K8s auth role for Woodpecker CI (`terraform-authentik` SA in `woodpecker` namespace)
- AppRole for local terraform runs
- Consul secret backend role (`terraform-authentik`, TTL 120/300)
- Consul ACL policy for `infra/terraform/authentik/` key prefix
- Vault policy granting both auth methods access to Consul creds

Reviewed-on: #78
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-06-28 01:17:51 +10:00
benvin be9bd96cf3 feat: enable consul state store for artifactapi (#77)
ci/woodpecker/push/apply Pipeline was successful
enable the terraform-artifactapi system to manage its state in consul
using dynamic credentials from kubernetes ci jobs in woodpecker

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #77
2026-06-17 21:42:25 +10:00
97 changed files with 1597 additions and 2 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:
@@ -0,0 +1,9 @@
token_ttl: 120
token_max_ttl: 120
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
- "198.18.25.102/32"
- "198.18.26.91/32"
- "198.18.27.40/32"
use_deterministic_role_id: true
@@ -0,0 +1,9 @@
token_ttl: 120
token_max_ttl: 120
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
- "198.18.25.102/32"
- "198.18.26.91/32"
- "198.18.27.40/32"
use_deterministic_role_id: true
@@ -0,0 +1,9 @@
token_ttl: 120
token_max_ttl: 120
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
- "198.18.25.102/32"
- "198.18.26.91/32"
- "198.18.27.40/32"
use_deterministic_role_id: true
@@ -0,0 +1,9 @@
token_ttl: 120
token_max_ttl: 120
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
- "198.18.25.102/32"
- "198.18.26.91/32"
- "198.18.27.40/32"
use_deterministic_role_id: true
@@ -0,0 +1,9 @@
token_ttl: 120
token_max_ttl: 120
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
- "198.18.25.102/32"
- "198.18.26.91/32"
- "198.18.27.40/32"
use_deterministic_role_id: true
@@ -0,0 +1,9 @@
token_ttl: 120
token_max_ttl: 120
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
- "198.18.25.102/32"
- "198.18.26.91/32"
- "198.18.27.40/32"
use_deterministic_role_id: true
@@ -0,0 +1,9 @@
token_ttl: 120
token_max_ttl: 120
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
- "198.18.25.102/32"
- "198.18.26.91/32"
- "198.18.27.40/32"
use_deterministic_role_id: true
@@ -0,0 +1,7 @@
bound_service_account_names:
- terraform-artifactapi
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
@@ -0,0 +1,7 @@
bound_service_account_names:
- terraform-authentik
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
@@ -0,0 +1,7 @@
bound_service_account_names:
- terraform-enc
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
@@ -0,0 +1,7 @@
bound_service_account_names:
- terraform-prowlarr
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
@@ -0,0 +1,7 @@
bound_service_account_names:
- terraform-radarr
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
@@ -0,0 +1,7 @@
bound_service_account_names:
- terraform-rancher
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
@@ -0,0 +1,7 @@
bound_service_account_names:
- terraform-sonarr
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
+67
View File
@@ -185,5 +185,72 @@ locals {
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "pki_mount_only/")
}
litellm_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "litellm_secret_backend/")
}
litellm_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "litellm_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "litellm_secret_backend_role/", ""))
})
if startswith(file_path, "litellm_secret_backend_role/")
}
plugins = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
})
if startswith(file_path, "plugins/")
}
gpg_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "gpg_secret_backend/")
}
gpg_key = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "gpg_key/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "gpg_key/", ""))
})
if startswith(file_path, "gpg_key/")
}
rancher_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "rancher_secret_backend/")
}
rancher_secret_backend_service_account = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "rancher_secret_backend_service_account/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "rancher_secret_backend_service_account/", ""))
})
if startswith(file_path, "rancher_secret_backend_service_account/")
}
rancher_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "rancher_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "rancher_secret_backend_role/", ""))
})
if startswith(file_path, "rancher_secret_backend_role/")
}
gitea_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "gitea_secret_backend/")
}
gitea_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "gitea_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "gitea_secret_backend_role/", ""))
})
if startswith(file_path, "gitea_secret_backend_role/")
}
}
}
@@ -0,0 +1,5 @@
consul_roles:
- terraform-artifactapi
ttl: 120
max_ttl: 300
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-authentik
ttl: 120
max_ttl: 300
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-enc
ttl: 120
max_ttl: 300
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-prowlarr
ttl: 120
max_ttl: 300
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-radarr
ttl: 120
max_ttl: 300
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-rancher
ttl: 120
max_ttl: 300
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-sonarr
ttl: 120
max_ttl: 300
datacenters: []
+11
View File
@@ -0,0 +1,11 @@
# Mounts the gitea token secrets engine at "gitea" and writes its config.
# The seeded site-admin credentials are sensitive and read from KV, not stored
# here:
# kv/service/vault/au/syd1/secret_backend/gitea/config
# -> keys: admin_username (required), admin_password (required)
# Populate that KV path with a purpose-built Gitea site-admin bot (2FA disabled)
# BEFORE applying, then run `vault write -f gitea/config/rotate-root` after the
# first apply so only Vault holds the admin password.
description: "Gitea ephemeral scoped access token engine"
gitea_url: "https://git.unkin.net"
request_timeout_seconds: 30
@@ -0,0 +1,14 @@
# Role minting ephemeral tokens for the teabot-implementer bot user.
# The implementer clones/pushes code and opens pull requests, so it gets write
# on repositories (clone + push + PR create) and write on issues (PR/issue
# comments). Read is implied by write. No admin/org/user-write scopes.
# Reading gitea/creds/teabot-implementer mints a lease-bound token deleted from
# Gitea on revoke/expiry.
---
username: teabot-implementer
scopes:
- write:repository
- write:issue
token_name_prefix: vault-teabot-implementer
ttl: 3600 # 1h
max_ttl: 14400 # 4h
@@ -0,0 +1,14 @@
# Role minting ephemeral tokens for the teabot-reviewer bot user.
# The reviewer reads code and posts pull-request reviews/comments, so it gets
# read on repositories (fetch diffs) and write on issues (PR reviews + issue/PR
# comments). No repository-write, admin, org, or user scopes.
# Reading gitea/creds/teabot-reviewer mints a lease-bound token deleted from
# Gitea on revoke/expiry.
---
username: teabot-reviewer
scopes:
- read:repository
- write:issue
token_name_prefix: vault-teabot-reviewer
ttl: 3600 # 1h
max_ttl: 14400 # 4h
+7
View File
@@ -0,0 +1,7 @@
# config/gpg_key/gpg/pass.yaml
# An OpenPGP key in the gpg engine for password-store (passv). The private key
# stays in Vault; clients import the exported public key to encrypt and delegate
# decryption to gpg/decrypt/pass. Key name = "pass", backend = "gpg".
algorithm: rsa-4096
identity: "pass <pass@unkin.net>"
exportable: false
+4
View File
@@ -0,0 +1,4 @@
# config/gpg_secret_backend/gpg.yaml
# Mounts the gpg secrets engine at "gpg". The plugin itself is registered in the
# catalog separately (see config/plugins/vault-plugin-secrets-gpg.yaml).
description: "GPG/OpenPGP secrets engine (sign/verify/encrypt/decrypt)"
@@ -0,0 +1,6 @@
# Mounts the LiteLLM dynamic secrets engine at "litellm" and writes its config.
# The master key is sensitive and read from KV, not stored here:
# kv/service/vault/au/syd1/secret_backend/litellm -> key "master_key"
description: "LiteLLM dynamic virtual keys"
base_url: "https://litellm.k8s.syd1.au.unkin.net"
request_timeout_seconds: 30
@@ -0,0 +1,9 @@
---
models:
- claude-opus-4-7
max_budget: 5
ttl: 3600 # seconds (1h)
max_ttl: 86400 # seconds (24h)
metadata:
team: testuser
env: prod
@@ -0,0 +1,10 @@
---
models:
- claude-haiku-4-5
- claude-sonnet-4-6
max_budget: 50
ttl: 3600 # seconds (1h)
max_ttl: 86400 # seconds (24h)
metadata:
team: mailfiltering
env: prod
@@ -0,0 +1,11 @@
# config/plugins/vault-plugin-secrets-gitea.yaml
# Imports (registers) the gitea secrets plugin in the catalog. Filename =
# catalog name = mount type. The binary is installed on the OpenBao nodes by
# Puppet (openbao-plugin-secrets-gitea RPM ->
# /opt/openbao-plugins/vault-plugin-secrets-gitea).
#
# sha256 pins the released v0.1.0 binary; bump it in lockstep with any RPM
# upgrade or OpenBao will refuse to launch the plugin.
type: secret
command: vault-plugin-secrets-gitea
sha256: "8f67fbc216effada5fd7399888a710b62fad83be0b31761a439e7dec3d56509b"
@@ -0,0 +1,10 @@
# config/plugins/vault-plugin-secrets-gpg.yaml
# Imports (registers) the gpg secrets plugin in the catalog. Filename = catalog
# name = mount type. The binary is installed on the OpenBao nodes by Puppet
# (openbao-plugin-secrets-gpg RPM -> /opt/openbao-plugins/vault-plugin-secrets-gpg).
#
# sha256 pins the released v0.1.0 binary; bump it in lockstep with any RPM
# upgrade or OpenBao will refuse to launch the plugin.
type: secret
command: vault-plugin-secrets-gpg
sha256: "0e92d7408795688badb55789bc1604e8f1dd4d71998656c7f831991fce9a7b20"
@@ -0,0 +1,13 @@
# config/plugins/vault-plugin-secrets-litellm.yaml
# Imports (registers) the litellm secrets plugin in the catalog. This plugin was
# registered manually before terraform managed the catalog, so its state must be
# imported before the first apply (see the PR description) — otherwise apply
# tries to create an entry that already exists.
#
# sha256 is the released v0.1.1 openbao binary
# (openbao-plugin-secrets-litellm RPM -> /opt/openbao-plugins/vault-plugin-secrets-litellm),
# which Puppet installs floating. Verify against the live catalog during import
# (`bao read sys/plugins/catalog/secret/vault-plugin-secrets-litellm`).
type: secret
command: vault-plugin-secrets-litellm
sha256: "2263ebcb3498877a87ddcf31a9cbc6efca6b81702a5faecf7fe7e40200ca7a1f"
@@ -0,0 +1,11 @@
# config/plugins/vault-plugin-secrets-rancher.yaml
# Imports (registers) the rancher secrets plugin in the catalog. Filename =
# catalog name = mount type. The binary is installed on the OpenBao nodes by
# Puppet (openbao-plugin-secrets-rancher RPM ->
# /opt/openbao-plugins/vault-plugin-secrets-rancher).
#
# sha256 pins the released v0.1.1 binary; bump it in lockstep with any RPM
# upgrade or OpenBao will refuse to launch the plugin.
type: secret
command: vault-plugin-secrets-rancher
sha256: "9e597cd9512a0629f940141dc6611bf95eaf165abcb52470eee027bf467d5955"
@@ -0,0 +1,8 @@
# Mounts the rancher token secrets engine at "rancher" and writes its config.
# The plugin is registered in the catalog separately (see
# config/plugins/vault-plugin-secrets-rancher.yaml). Seeded service-account
# tokens live under config/rancher_secret_backend_service_account/rancher/ and
# roles under config/rancher_secret_backend_role/rancher/.
description: "Rancher API token engine (seeded root rotation + dynamic scoped creds)"
rancher_url: "https://rancher.k8s.syd1.au.unkin.net"
request_timeout_seconds: 30
@@ -0,0 +1,9 @@
# A role that mints short-lived Rancher tokens from the "admin" service account.
# Reading rancher/creds/ci returns a lease-bound token deleted from Rancher on
# revoke. Minted tokens inherit the admin service account's RBAC; only cluster
# and TTL are scoped per-token.
---
service_account: admin
description: "CI/CD ephemeral Rancher token"
ttl: 3600 # seconds (1h)
max_ttl: 28800 # seconds (8h)
@@ -0,0 +1,8 @@
# A seeded, auto-rotated Rancher service-account token on the "rancher" engine.
# The seed token itself is sensitive and read from KV (not stored here):
# kv/service/vault/au/syd1/secret_backend/rancher/service_account/admin
# -> keys: token (required), token_name (optional)
# Populate that KV path with a live Rancher admin token BEFORE applying; the
# engine then rotates it (mints a fresh 90d token every 45d) so it never lapses.
token_ttl: 7776000 # 90d in seconds
rotation_period: 3888000 # 45d in seconds
+12
View File
@@ -68,6 +68,18 @@ inputs = {
kubernetes_secret_backend = local.config.kubernetes_secret_backend
kubernetes_secret_backend_role = local.config.kubernetes_secret_backend_role
pki_mount_only = local.config.pki_mount_only
litellm_secret_backend = local.config.litellm_secret_backend
litellm_secret_backend_role = local.config.litellm_secret_backend_role
plugins = local.config.plugins
gpg_secret_backend = local.config.gpg_secret_backend
gpg_key = local.config.gpg_key
rancher_secret_backend = local.config.rancher_secret_backend
rancher_secret_backend_service_account = local.config.rancher_secret_backend_service_account
rancher_secret_backend_role = local.config.rancher_secret_backend_role
gitea_secret_backend = local.config.gitea_secret_backend
gitea_secret_backend_role = local.config.gitea_secret_backend_role
# Pass policy maps to vault_cluster module
policy_auth_map = local.policies.policy_auth_map
+30
View File
@@ -11,6 +11,24 @@ provider "vault" {
address = local.vault_addr
}
# The LiteLLM secrets engine is managed through its own provider, which talks to
# the same Vault server. Token falls back to the VAULT_TOKEN environment variable.
provider "litellm" {
address = local.vault_addr
}
# The gpg secrets engine's keys are managed through its own provider (same Vault
# server; token falls back to VAULT_TOKEN).
provider "gpg" {
address = local.vault_addr
}
# The rancher token secrets engine is managed through its own provider (same
# Vault server; token falls back to VAULT_TOKEN).
provider "rancher" {
address = local.vault_addr
}
terraform {
backend "consul" {
address = "https://consul.service.consul"
@@ -29,6 +47,18 @@ terraform {
source = "hashicorp/consul"
version = "2.23.0"
}
litellm = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/litellmvaultsecret"
version = "0.1.0"
}
gpg = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/gpgvaultsecret"
version = "0.1.0"
}
rancher = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret"
version = "0.1.0"
}
}
}
EOF
+152
View File
@@ -303,6 +303,158 @@ module "kubernetes_secret_backend_role" {
depends_on = [module.kubernetes_secret_backend]
}
module "litellm_secret_backend" {
source = "./modules/litellm_secret_backend"
for_each = var.litellm_secret_backend
country = var.country
region = var.region
path = each.key
plugin = each.value.plugin
description = each.value.description
base_url = each.value.base_url
request_timeout_seconds = each.value.request_timeout_seconds
}
module "litellm_secret_backend_role" {
source = "./modules/litellm_secret_backend_role"
for_each = var.litellm_secret_backend_role
name = each.value.name
backend = each.value.backend
models = each.value.models
max_budget = each.value.max_budget
key_alias_prefix = each.value.key_alias_prefix
ttl = each.value.ttl
max_ttl = each.value.max_ttl
metadata = each.value.metadata
depends_on = [module.litellm_secret_backend]
}
module "plugin" {
source = "./modules/plugin"
for_each = var.plugins
name = each.value.name
type = each.value.type
command = each.value.command
sha256 = each.value.sha256
plugin_version = each.value.version
}
module "gpg_secret_backend" {
source = "./modules/gpg_secret_backend"
for_each = var.gpg_secret_backend
path = each.key
plugin = each.value.plugin
description = each.value.description
depends_on = [module.plugin]
}
module "gpg_key" {
source = "./modules/gpg_key"
for_each = var.gpg_key
backend = each.value.backend
name = each.value.name
algorithm = each.value.algorithm
identity = each.value.identity
exportable = each.value.exportable
deletion_allowed = each.value.deletion_allowed
min_decryption_version = each.value.min_decryption_version
depends_on = [module.gpg_secret_backend]
}
module "rancher_secret_backend" {
source = "./modules/rancher_secret_backend"
for_each = var.rancher_secret_backend
path = each.key
plugin = each.value.plugin
description = each.value.description
rancher_url = each.value.rancher_url
ca_cert = each.value.ca_cert
tls_skip_verify = each.value.tls_skip_verify
request_timeout_seconds = each.value.request_timeout_seconds
depends_on = [module.plugin]
}
module "rancher_secret_backend_service_account" {
source = "./modules/rancher_secret_backend_service_account"
for_each = var.rancher_secret_backend_service_account
backend = each.value.backend
name = each.value.name
country = var.country
region = var.region
token_ttl = each.value.token_ttl
rotation_period = each.value.rotation_period
depends_on = [module.rancher_secret_backend]
}
module "rancher_secret_backend_role" {
source = "./modules/rancher_secret_backend_role"
for_each = var.rancher_secret_backend_role
backend = each.value.backend
name = each.value.name
service_account = each.value.service_account
cluster_name = each.value.cluster_name
description = each.value.description
ttl = each.value.ttl
max_ttl = each.value.max_ttl
depends_on = [module.rancher_secret_backend_service_account]
}
module "gitea_secret_backend" {
source = "./modules/gitea_secret_backend"
for_each = var.gitea_secret_backend
path = each.key
plugin = each.value.plugin
description = each.value.description
gitea_url = each.value.gitea_url
country = var.country
region = var.region
ca_cert = each.value.ca_cert
tls_skip_verify = each.value.tls_skip_verify
request_timeout_seconds = each.value.request_timeout_seconds
depends_on = [module.plugin]
}
module "gitea_secret_backend_role" {
source = "./modules/gitea_secret_backend_role"
for_each = var.gitea_secret_backend_role
backend = each.value.backend
name = each.value.name
username = each.value.username
scopes = each.value.scopes
token_name_prefix = each.value.token_name_prefix
ttl = each.value.ttl
max_ttl = each.value.max_ttl
depends_on = [module.gitea_secret_backend]
}
module "vault_policy" {
source = "./modules/vault_policy"
@@ -0,0 +1,34 @@
# Mounts the gitea secrets engine and writes its connection config via the
# giteavaultsecret provider. The plugin is registered ("imported") in the
# catalog separately (config/plugins/vault-plugin-secrets-gitea.yaml). The
# seeded site-admin credentials are sensitive and read from KV, not stored in
# git:
# kv/service/vault/<country>/<region>/secret_backend/<path>/config
# Expected keys: admin_username (required), admin_password (required).
data "vault_kv_secret_v2" "config" {
mount = "kv"
name = "service/vault/${var.country}/${var.region}/secret_backend/${var.path}/config"
}
resource "gitea_secret_backend" "this" {
path = var.path
plugin = var.plugin
description = var.description
gitea_url = var.gitea_url
admin_username = data.vault_kv_secret_v2.config.data["admin_username"]
admin_password = data.vault_kv_secret_v2.config.data["admin_password"]
ca_cert = var.ca_cert
tls_skip_verify = var.tls_skip_verify
request_timeout_seconds = var.request_timeout_seconds
lifecycle {
# The KV seed is a bootstrap credential: it is consumed only when the engine
# config is first created. After creation the live admin password is rotated
# in place (vault write -f gitea/config/rotate-root) and diverges from the
# seed, so re-reading the (possibly stale) KV value must never push it back.
# Ignoring the credential attributes makes this module create-only for them.
# (The sibling rancher/litellm seed modules do not yet do this and would
# re-push their seed on a subsequent apply.)
ignore_changes = [admin_username, admin_password]
}
}
@@ -0,0 +1,13 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
gitea = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/giteavaultsecret"
version = "0.1.0"
}
}
}
@@ -0,0 +1,49 @@
variable "path" {
description = "Mount path of the gitea secrets engine (e.g. \"gitea\")"
type = string
}
variable "plugin" {
description = "Registered plugin name to mount (the catalog name = mount type)"
type = string
default = "vault-plugin-secrets-gitea"
}
variable "description" {
description = "Human-friendly description of the mount"
type = string
default = null
}
variable "gitea_url" {
description = "Base URL of the Gitea server (e.g. https://git.unkin.net)"
type = string
}
variable "country" {
description = "Country segment of the KV path holding the seeded admin credentials"
type = string
}
variable "region" {
description = "Region segment of the KV path holding the seeded admin credentials"
type = string
}
variable "ca_cert" {
description = "PEM CA certificate that signed the Gitea server's TLS cert (optional; omit to use the system trust store)"
type = string
default = null
}
variable "tls_skip_verify" {
description = "Skip TLS verification of the Gitea server (not recommended)"
type = bool
default = false
}
variable "request_timeout_seconds" {
description = "HTTP timeout in seconds for calls from the plugin to Gitea"
type = number
default = 30
}
@@ -0,0 +1,12 @@
# A role that mints short-lived, scoped gitea tokens for a target Gitea user.
# Reading gitea/creds/<name> produces a lease-bound token that is deleted from
# Gitea when the lease is revoked or reaches max_ttl.
resource "gitea_secret_backend_role" "this" {
backend = var.backend
name = var.name
username = var.username
scopes = var.scopes
token_name_prefix = var.token_name_prefix
ttl = var.ttl
max_ttl = var.max_ttl
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
gitea = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/giteavaultsecret"
version = "0.1.0"
}
}
}
@@ -0,0 +1,37 @@
variable "backend" {
description = "Mount path of the gitea secrets engine this role belongs to"
type = string
}
variable "name" {
description = "Role name (read gitea/creds/<name> to mint a token)"
type = string
}
variable "username" {
description = "Target Gitea username the minted tokens belong to"
type = string
}
variable "scopes" {
description = "Gitea access-token scopes granted to minted tokens (write: implies read:)"
type = list(string)
}
variable "token_name_prefix" {
description = "Prefix for the generated Gitea token name (optional)"
type = string
default = null
}
variable "ttl" {
description = "Default lease TTL in seconds for minted tokens"
type = number
default = null
}
variable "max_ttl" {
description = "Maximum lease TTL in seconds for minted tokens"
type = number
default = null
}
@@ -0,0 +1,12 @@
# Manages an OpenPGP key inside a gpg secrets engine mount, via the
# gpgvaultsecret provider. The private key never leaves Vault; consumers use the
# exported public_key to encrypt and delegate decryption back to the engine.
resource "gpg_key" "this" {
backend = var.backend
name = var.name
algorithm = var.algorithm
identity = var.identity
exportable = var.exportable
deletion_allowed = var.deletion_allowed
min_decryption_version = var.min_decryption_version
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
gpg = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/gpgvaultsecret"
version = "0.1.0"
}
}
}
@@ -0,0 +1,39 @@
variable "backend" {
description = "Mount path of the gpg secrets engine (e.g. \"gpg\")"
type = string
}
variable "name" {
description = "Name of the key"
type = string
}
variable "algorithm" {
description = "Key algorithm: rsa-2048, rsa-3072, rsa-4096 or ed25519"
type = string
default = "rsa-3072"
}
variable "identity" {
description = "OpenPGP User ID (defaults to the key name)"
type = string
default = null
}
variable "exportable" {
description = "Allow exporting the private key (enable-only)"
type = bool
default = false
}
variable "deletion_allowed" {
description = "Whether the key may be deleted"
type = bool
default = false
}
variable "min_decryption_version" {
description = "Minimum key version usable for decryption/verification"
type = number
default = null
}
@@ -0,0 +1,8 @@
# Mounts the gpg secrets engine. The plugin is registered ("imported") in the
# catalog separately via the config/plugins/ discovery and the plugin module;
# this module just enables a mount of the already-registered plugin type.
resource "vault_mount" "this" {
path = var.path
type = var.plugin
description = var.description
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -0,0 +1,16 @@
variable "path" {
description = "Mount path of the GPG secrets engine (e.g. \"gpg\")"
type = string
}
variable "plugin" {
description = "Registered plugin name to mount (the catalog name = mount type)"
type = string
default = "vault-plugin-secrets-gpg"
}
variable "description" {
description = "Human-friendly description of the mount"
type = string
default = null
}
@@ -0,0 +1,14 @@
# Expected keys in KV secret: master_key
data "vault_kv_secret_v2" "secret_backend_config" {
mount = "kv"
name = "service/vault/${var.country}/${var.region}/secret_backend/${var.path}"
}
resource "litellm_secret_backend" "this" {
path = var.path
plugin = var.plugin
description = var.description
base_url = var.base_url
master_key = data.vault_kv_secret_v2.secret_backend_config.data["master_key"]
request_timeout_seconds = var.request_timeout_seconds
}
@@ -0,0 +1,13 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
litellm = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/litellmvaultsecret"
version = "0.1.0"
}
}
}
@@ -0,0 +1,37 @@
variable "country" {
description = "Country identifier (used to locate the KV secret holding the master key)"
type = string
}
variable "region" {
description = "Region identifier (used to locate the KV secret holding the master key)"
type = string
}
variable "path" {
description = "Mount path of the LiteLLM secrets engine"
type = string
}
variable "plugin" {
description = "Registered plugin name/type to mount"
type = string
default = "vault-plugin-secrets-litellm"
}
variable "description" {
description = "Human-friendly description of the mount"
type = string
default = null
}
variable "base_url" {
description = "Base URL of the LiteLLM proxy (e.g. http://litellm.litellm.svc:4000)"
type = string
}
variable "request_timeout_seconds" {
description = "HTTP timeout in seconds for calls from the plugin to the LiteLLM proxy"
type = number
default = 30
}
@@ -0,0 +1,10 @@
resource "litellm_secret_backend_role" "this" {
backend = var.backend
name = var.name
models = var.models
max_budget = var.max_budget
key_alias_prefix = var.key_alias_prefix
ttl = var.ttl
max_ttl = var.max_ttl
metadata = var.metadata
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
litellm = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/litellmvaultsecret"
version = "0.1.0"
}
}
}
@@ -0,0 +1,45 @@
variable "name" {
description = "Name of the role"
type = string
}
variable "backend" {
description = "Mount path of the LiteLLM secrets engine this role belongs to"
type = string
}
variable "models" {
description = "Models a generated key may access. Empty means unrestricted"
type = list(string)
default = null
}
variable "max_budget" {
description = "Spending limit applied to each generated key. 0 means unlimited"
type = number
default = null
}
variable "key_alias_prefix" {
description = "Prefix for the auto-generated key alias"
type = string
default = null
}
variable "ttl" {
description = "Default lease TTL in seconds for keys generated from this role"
type = number
default = null
}
variable "max_ttl" {
description = "Maximum lease TTL in seconds for keys generated from this role"
type = number
default = null
}
variable "metadata" {
description = "Metadata attached to each generated key"
type = map(string)
default = null
}
@@ -0,0 +1,12 @@
# Registers ("imports") a plugin binary in the Vault/OpenBao plugin catalog so
# it can be mounted. The binary must already exist in the server
# plugin_directory (installed out of band, e.g. by Puppet); `command` is its
# filename there. The sha256 must match the on-disk binary or the server refuses
# to launch the plugin.
resource "vault_plugin" "this" {
type = var.type
name = var.name
command = coalesce(var.command, var.name)
sha256 = var.sha256
version = var.plugin_version
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -0,0 +1,27 @@
variable "name" {
description = "Name to register the plugin under in the catalog (also the mount type)"
type = string
}
variable "type" {
description = "Plugin type: secret, auth or database"
type = string
default = "secret"
}
variable "command" {
description = "Plugin binary filename relative to the server plugin_directory. Defaults to the plugin name."
type = string
default = null
}
variable "sha256" {
description = "SHA-256 of the installed plugin binary; must match the on-disk binary"
type = string
}
variable "plugin_version" {
description = "Optional plugin version to register the catalog entry under"
type = string
default = null
}
@@ -0,0 +1,13 @@
# Mounts the rancher secrets engine and writes its connection config via the
# ranchervaultsecret provider. The plugin is registered ("imported") in the
# catalog separately (config/plugins/vault-plugin-secrets-rancher.yaml). Seeded
# service-account tokens and roles are managed by the sibling modules.
resource "rancher_secret_backend" "this" {
path = var.path
plugin = var.plugin
description = var.description
rancher_url = var.rancher_url
ca_cert = var.ca_cert
tls_skip_verify = var.tls_skip_verify
request_timeout_seconds = var.request_timeout_seconds
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
rancher = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret"
version = "0.1.0"
}
}
}
@@ -0,0 +1,39 @@
variable "path" {
description = "Mount path of the rancher secrets engine (e.g. \"rancher\")"
type = string
}
variable "plugin" {
description = "Registered plugin name to mount (the catalog name = mount type)"
type = string
default = "vault-plugin-secrets-rancher"
}
variable "description" {
description = "Human-friendly description of the mount"
type = string
default = null
}
variable "rancher_url" {
description = "Base URL of the Rancher server (e.g. https://rancher.k8s.syd1.au.unkin.net)"
type = string
}
variable "ca_cert" {
description = "PEM CA certificate that signed the Rancher server's TLS cert (optional; omit to use the system trust store)"
type = string
default = null
}
variable "tls_skip_verify" {
description = "Skip TLS verification of the Rancher server (not recommended)"
type = bool
default = false
}
variable "request_timeout_seconds" {
description = "HTTP timeout in seconds for calls from the plugin to Rancher"
type = number
default = 30
}
@@ -0,0 +1,11 @@
# A role that mints short-lived, optionally cluster-scoped rancher tokens from a
# service account. Reading rancher/creds/<name> produces a lease-bound token.
resource "rancher_secret_backend_role" "this" {
backend = var.backend
name = var.name
service_account = var.service_account
cluster_name = var.cluster_name
description = var.description
ttl = var.ttl
max_ttl = var.max_ttl
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
rancher = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret"
version = "0.1.0"
}
}
}
@@ -0,0 +1,38 @@
variable "backend" {
description = "Mount path of the rancher secrets engine this role lives on"
type = string
}
variable "name" {
description = "Role name"
type = string
}
variable "service_account" {
description = "Service account (seeded token) used to mint credentials; its user's RBAC is inherited by minted tokens"
type = string
}
variable "cluster_name" {
description = "Downstream cluster to scope minted tokens to (empty = full Rancher-server scope)"
type = string
default = null
}
variable "description" {
description = "Description applied to each minted Rancher token"
type = string
default = null
}
variable "ttl" {
description = "Default lease TTL in seconds for tokens minted from this role"
type = number
default = null
}
variable "max_ttl" {
description = "Maximum lease TTL in seconds for tokens minted from this role"
type = number
default = null
}
@@ -0,0 +1,18 @@
# Seeds an auto-rotated rancher service-account token. The seed token is
# sensitive and read from KV, not stored in git:
# kv/service/vault/<country>/<region>/secret_backend/<backend>/service_account/<name>
# Expected keys: token (required), token_name (optional, the ext.cattle.io Token
# metadata.name so the engine can delete the seed after the first rotation).
data "vault_kv_secret_v2" "seed" {
mount = "kv"
name = "service/vault/${var.country}/${var.region}/secret_backend/${var.backend}/service_account/${var.name}"
}
resource "rancher_secret_backend_service_account" "this" {
backend = var.backend
name = var.name
token = data.vault_kv_secret_v2.seed.data["token"]
token_name = lookup(data.vault_kv_secret_v2.seed.data, "token_name", null)
token_ttl = var.token_ttl
rotation_period = var.rotation_period
}
@@ -0,0 +1,13 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
rancher = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret"
version = "0.1.0"
}
}
}
@@ -0,0 +1,31 @@
variable "backend" {
description = "Mount path of the rancher secrets engine this service account lives on"
type = string
}
variable "name" {
description = "Service-account name"
type = string
}
variable "country" {
description = "Country code, used to locate the seed token in KV"
type = string
}
variable "region" {
description = "Region code, used to locate the seed token in KV"
type = string
}
variable "token_ttl" {
description = "Lifetime in seconds requested for each rotated replacement token (default: engine default, 90d)"
type = number
default = null
}
variable "rotation_period" {
description = "Seconds a token is used before rotation (default: engine default, 45d). Must be < token_ttl"
type = number
default = null
}
+126
View File
@@ -289,6 +289,132 @@ variable "kubernetes_secret_backend_role" {
default = {}
}
variable "litellm_secret_backend" {
description = "Map of LiteLLM secret engines to create (mount + config). The master key is read from KV"
type = map(object({
plugin = optional(string, "vault-plugin-secrets-litellm")
description = optional(string)
base_url = string
request_timeout_seconds = optional(number, 30)
}))
default = {}
}
variable "litellm_secret_backend_role" {
description = "Map of LiteLLM roles to create"
type = map(object({
name = string
backend = string
models = optional(list(string))
max_budget = optional(number)
key_alias_prefix = optional(string)
ttl = optional(number)
max_ttl = optional(number)
metadata = optional(map(string))
}))
default = {}
}
variable "plugins" {
description = "Map of plugins to import (register) in the catalog, keyed by catalog name"
type = map(object({
name = string
type = optional(string, "secret")
command = optional(string)
sha256 = string
version = optional(string)
}))
default = {}
}
variable "gpg_secret_backend" {
description = "Map of GPG/OpenPGP secret engines to mount (path => registered plugin + description). The plugin is registered separately via config/plugins."
type = map(object({
plugin = optional(string, "vault-plugin-secrets-gpg")
description = optional(string)
}))
default = {}
}
variable "gpg_key" {
description = "Map of OpenPGP keys to manage in a gpg engine mount"
type = map(object({
name = string
backend = string
algorithm = optional(string, "rsa-3072")
identity = optional(string)
exportable = optional(bool, false)
deletion_allowed = optional(bool, false)
min_decryption_version = optional(number)
}))
default = {}
}
variable "rancher_secret_backend" {
description = "Map of rancher token secret engines to create (mount + config)"
type = map(object({
plugin = optional(string, "vault-plugin-secrets-rancher")
description = optional(string)
rancher_url = string
ca_cert = optional(string)
tls_skip_verify = optional(bool, false)
request_timeout_seconds = optional(number, 30)
}))
default = {}
}
variable "rancher_secret_backend_service_account" {
description = "Map of seeded, auto-rotated rancher service-account tokens (seed token read from KV)"
type = map(object({
name = string
backend = string
token_ttl = optional(number)
rotation_period = optional(number)
}))
default = {}
}
variable "rancher_secret_backend_role" {
description = "Map of rancher token-minting roles to create"
type = map(object({
name = string
backend = string
service_account = string
cluster_name = optional(string)
description = optional(string)
ttl = optional(number)
max_ttl = optional(number)
}))
default = {}
}
variable "gitea_secret_backend" {
description = "Map of gitea token secret engines to create (mount + config; seeded admin creds read from KV)"
type = map(object({
plugin = optional(string, "vault-plugin-secrets-gitea")
description = optional(string)
gitea_url = string
ca_cert = optional(string)
tls_skip_verify = optional(bool, false)
request_timeout_seconds = optional(number, 30)
}))
default = {}
}
variable "gitea_secret_backend_role" {
description = "Map of gitea token-minting roles to create"
type = map(object({
name = string
backend = string
username = string
scopes = list(string)
token_name_prefix = optional(string)
ttl = optional(number)
max_ttl = optional(number)
}))
default = {}
}
variable "policy_auth_map" {
description = "Map of auth mounts -> auth roles -> policy names"
type = map(map(list(string)))
@@ -0,0 +1,11 @@
---
rules:
- path: "consul_root/au/syd1/creds/terraform-artifactapi"
capabilities:
- read
auth:
approle:
- terraform_artifactapi
k8s/au/syd1:
- woodpecker_terraform_artifactapi
@@ -0,0 +1,11 @@
---
rules:
- path: "consul_root/au/syd1/creds/terraform-authentik"
capabilities:
- read
auth:
approle:
- terraform_authentik
k8s/au/syd1:
- woodpecker_terraform_authentik
@@ -0,0 +1,14 @@
# Allow the terragrunt-enc runner to generate credentials for the
# terraform-enc role in consul (used to lock/write its terragrunt state under
# infra/terraform/enc/ on the consul backend).
---
rules:
- path: "consul_root/au/syd1/creds/terraform-enc"
capabilities:
- read
auth:
approle:
- terraform_enc
k8s/au/syd1:
- woodpecker_terraform_enc
@@ -0,0 +1,11 @@
---
rules:
- path: "consul_root/au/syd1/creds/terraform-prowlarr"
capabilities:
- read
auth:
approle:
- terraform_prowlarr
k8s/au/syd1:
- woodpecker_terraform_prowlarr
@@ -0,0 +1,11 @@
---
rules:
- path: "consul_root/au/syd1/creds/terraform-radarr"
capabilities:
- read
auth:
approle:
- terraform_radarr
k8s/au/syd1:
- woodpecker_terraform_radarr
@@ -0,0 +1,11 @@
---
rules:
- path: "consul_root/au/syd1/creds/terraform-rancher"
capabilities:
- read
auth:
approle:
- terraform_rancher
k8s/au/syd1:
- woodpecker_terraform_rancher
@@ -0,0 +1,11 @@
---
rules:
- path: "consul_root/au/syd1/creds/terraform-sonarr"
capabilities:
- read
auth:
approle:
- terraform_sonarr
k8s/au/syd1:
- woodpecker_terraform_sonarr
+35
View File
@@ -0,0 +1,35 @@
# Allow the vault deployer to import the gpg plugin and manage its OpenPGP keys.
#
# terraform-vault registers the plugin itself (vault_plugin -> sys/plugins/catalog,
# a sudo-protected path) and manages keys via the gpgvaultsecret provider, so the
# deployer needs catalog access on top of the mount access it already has
# (sys/mounts/*). Without this, apply 403s on the plugin registration and on
# gpg/keys writes.
---
rules:
# Import / register (and deregister) the gpg plugin in the catalog.
- path: "sys/plugins/catalog/secret/vault-plugin-secrets-gpg"
capabilities:
- create
- read
- update
- delete
- sudo
# Manage keys (create/rotate/config/delete) in the gpg mount.
- path: "gpg/keys/*"
capabilities:
- create
- read
- update
- delete
- list
- path: "gpg/keys"
capabilities:
- read
- list
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
@@ -6,5 +6,8 @@ rules:
- read
auth:
approle:
- terraform_prowlarr
k8s/au/syd1:
- media-apps
- woodpecker_terraform_prowlarr
@@ -6,5 +6,8 @@ rules:
- read
auth:
approle:
- terraform_radarr
k8s/au/syd1:
- media-apps
- woodpecker_terraform_radarr
@@ -6,5 +6,8 @@ rules:
- read
auth:
approle:
- terraform_sonarr
k8s/au/syd1:
- media-apps
- woodpecker_terraform_sonarr
@@ -0,0 +1,18 @@
# Allow the Terraform Authentik runner to read:
# - its own Authentik API token (provider auth), and
# - OAuth2/OIDC client secrets that back authentik providers (per target
# service's kv namespace path, via the module's data.vault_kv_secret_v2).
---
rules:
- path: "kv/data/service/terraform/authentik"
capabilities:
- read
- path: "kv/data/kubernetes/namespace/+/default/oauth-credentials"
capabilities:
- read
auth:
approle:
- terraform_authentik
k8s/au/syd1:
- woodpecker_terraform_authentik
@@ -0,0 +1,14 @@
# Allow the terragrunt-enc runner to read the encapi environment secret
# (ENCAPI_WRITE_TOKEN), so `make apply` can write ENC data (statuses, roles,
# nodes) to encapi via the encapi Terraform provider.
---
rules:
- path: "kv/data/kubernetes/namespace/encapi/default/environment"
capabilities:
- read
auth:
approle:
- terraform_enc
k8s/au/syd1:
- woodpecker_terraform_enc
@@ -0,0 +1,18 @@
# Allow the Terraform Rancher runner to read:
# - its own Rancher admin API token (rancher2 provider auth), and
# - the OAuth2/OIDC client secret backing the Rancher keycloakoidc AuthConfig
# (same secret Authentik sets on the provider).
---
rules:
- path: "kv/data/service/terraform/rancher"
capabilities:
- read
- path: "kv/data/kubernetes/namespace/cattle-system/default/oauth-credentials"
capabilities:
- read
auth:
approle:
- terraform_rancher
k8s/au/syd1:
- woodpecker_terraform_rancher
+26
View File
@@ -0,0 +1,26 @@
# Allow management of the LiteLLM secrets engine (config and roles)
---
rules:
- path: "litellm/config"
capabilities:
- create
- update
- read
- delete
- path: "litellm/roles/*"
capabilities:
- create
- update
- delete
- read
- list
- path: "litellm/roles"
capabilities:
- read
- list
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
+45
View File
@@ -0,0 +1,45 @@
# Allow the vault deployer to manage the rancher secrets engine: its connection
# config, seeded (auto-rotated) service-account tokens, and token-minting roles.
#
# Scoped to rancher/* only. The plugin-catalog grant needed to import the plugin
# lives under policies/sys/plugins/catalog/ so a code owner of this policy path
# cannot grant themselves access outside the rancher mount.
---
rules:
# Engine connection config.
- path: "rancher/config"
capabilities:
- create
- read
- update
- delete
# Seeded, auto-rotated service-account tokens (+ manual rotate).
- path: "rancher/service-accounts/*"
capabilities:
- create
- read
- update
- delete
- list
- path: "rancher/service-accounts"
capabilities:
- read
- list
# Token-minting roles.
- path: "rancher/roles/*"
capabilities:
- create
- read
- update
- delete
- list
- path: "rancher/roles"
capabilities:
- read
- list
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
+16
View File
@@ -0,0 +1,16 @@
# Allow the vault deployer to import (register/deregister) plugin in the catalog
---
rules:
- path: "sys/plugins/catalog/*"
capabilities:
- create
- read
- update
- delete
- sudo
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
@@ -0,0 +1,7 @@
key_prefix "infra/terraform/artifactapi/" {
policy = "write"
}
session_prefix "" {
policy = "write"
}
@@ -0,0 +1,7 @@
key_prefix "infra/terraform/authentik/" {
policy = "write"
}
session_prefix "" {
policy = "write"
}
@@ -0,0 +1,7 @@
key_prefix "infra/terraform/enc/" {
policy = "write"
}
session_prefix "" {
policy = "write"
}
@@ -0,0 +1,7 @@
key_prefix "infra/terraform/prowlarr/" {
policy = "write"
}
session_prefix "" {
policy = "write"
}
@@ -0,0 +1,7 @@
key_prefix "infra/terraform/radarr/" {
policy = "write"
}
session_prefix "" {
policy = "write"
}
@@ -0,0 +1,7 @@
key_prefix "infra/terraform/rancher/" {
policy = "write"
}
session_prefix "" {
policy = "write"
}
@@ -0,0 +1,7 @@
key_prefix "infra/terraform/sonarr/" {
policy = "write"
}
session_prefix "" {
policy = "write"
}