159 Commits

Author SHA1 Message Date
unkin-agent afbea53d94 Grant the puppet k8s role PKI issue and SSH host signing
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
2026-09-22 22:32:52 +10:00
unkin-agent 14144584e3 Fix SSH host certificate signing for the sshca role (#153)
ci/woodpecker/push/apply Pipeline was successful
Host signing returns 403 even though login succeeds: the policy grants the literal path sshca/sign/host, but the only role on the sshca mount is signhost, so the grant matches nothing. Hosts named directly under unkin.net also fall outside the role's allowed domains.

- Rename the policy to sshca/sign/signhost and grant that path
- Allow unkin.net alongside main.unkin.net and consul on the signhost role

Reviewed-on: #153
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-19 15:48:44 +10:00
unkin-agent c1e60dad83 Add kubernetes auth roles for the puppet compilers (#152)
ci/woodpecker/push/apply Pipeline was successful
The certmanager and sshsigner approles are CIDR-bound to the six legacy VM puppet-master IPs, so the HPA-autoscaled k8s compilers cannot log in and any catalog compile needing a cert or a signed host key fails.

- Add k8s auth roles `puppet_certmanager` and `puppet_sshsigner` on `k8s/au/syd1`, bound to the `default` service account in namespace `puppet`
- Attach the existing pki/pki_int certmanager and sshca signing policies to them, keeping the approle token TTLs
- Leave the approle roles and their CIDR bindings untouched

Follow-up ships the scripts into the compiler pods.

Reviewed-on: #152
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-13 22:54:44 +10:00
unkin-agent 9e18627567 Grant the agents approle read+write on the woodpecker agent token (#151)
ci/woodpecker/push/apply Pipeline was successful
Agents query the Woodpecker API to inspect pipeline runs and failing steps while reviewing PRs. That token is currently pasted into agent config by hand, so it lives in plaintext on disk instead of in Vault.

- add `policies/kv/service/woodpecker/tokens/agents.yaml`
- grant the `agents` approle create/read/update on `kv/data/service/woodpecker/tokens/agents`
- grant read/list on the matching metadata path; no delete, mirroring the `kv/kubernetes/*` grant

Token seeding follows once this is applied.

Reviewed-on: #151
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-12 13:19:56 +10:00
benvin 94e8ac1b2a chore: extend oidc login to 1 day (#150)
ci/woodpecker/push/apply Pipeline was successful
- 1 day / 7 days for oidc logins

---------

Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Reviewed-on: #150
2026-09-08 22:34:16 +10:00
unkin-agent a19256fdba Grant the vault deployer sudo to enable the oidc auth mount (#149)
ci/woodpecker/push/apply Pipeline was successful
## Why

The deployer identities cannot enable the `oidc` auth mount: `POST /v1/sys/auth/oidc` returns 403 because `sys/auth/<path>` (and its `/tune`) is sudo-protected, and `policies/sys/auth/admin.yaml` granted create/update/delete/read/list without `sudo`.

## How

- Add exact-path rules for `sys/auth/oidc` and `sys/auth/oidc/tune` to `policies/sys/auth/admin.yaml` with the wildcard's capability set plus `sudo` (exact match wins over the glob, so the set is repeated in full); auth block unchanged.

Merge order: apply this, then re-run the master apply so `module.auth_oidc_backend["oidc"]` can create the mount.

Reviewed-on: #149
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-31 22:21:01 +10:00
unkin-agent a1e7029615 Add Authentik OIDC SSO as the default human login for OpenBao (#147)
ci/woodpecker/push/apply Pipeline failed
## Why

Human login to OpenBao is LDAP-only, so operators keep a second credential set outside Authentik and group membership is maintained twice.

## How

- Add `auth_oidc_backend` module: `oidc`-type JWT auth mount, Authentik discovery URL, `listing_visibility: unauth`, credentials from `kv/service/authentik/oidc-vault`.
- Add `auth_oidc_role` module: oidc role with `user_claim` email, `groups_claim` `ak_groups`, scopes `openid profile email ak_groups`, the registered redirect URIs, `bound_audiences` `[vault]`.
- Add `auth_oidc_group` module: external identity group plus group alias on the OIDC mount accessor, policies from `policy_auth_map`.
- Add config under `config/auth_oidc_{backend,role,group}/`, discovery locals in `config/config.hcl`, `vault_cluster` variables and wiring, and terragrunt inputs.
- Bind `akP-vault-admin` on the `oidc` mount to `global-root`, alongside the existing LDAP `vault_admin` binding.
- Pin the mount path to the literal `oidc`: the registered redirect URIs embed `/ui/vault/auth/oidc/oidc/callback`.

Requires #146, terraform-authentik #33 and #148 applied first.

---------

Co-authored-by: BenVincent <benvin@main.unkin.net>
Reviewed-on: #147
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-30 22:38:05 +10:00
unkin-agent a3a8854a16 Grant the vault deployer auth/oidc and identity group capabilities (#148)
ci/woodpecker/push/apply Pipeline was successful
## Why

AppRole capabilities are fixed at login, so the deployer needs `auth/oidc/*` and identity-group grants in an apply that precedes the one creating those resources.

## How

- Add `policies/auth/oidc/admin.yaml`: full `auth/oidc/*` administration (mount config and login roles), mirroring `policies/auth/ldap/admin.yaml`.
- Add `policies/identity/group/admin.yaml`: manage external identity groups, group aliases and `identity/lookup/group`, on both the collection and per-id endpoints.
- Grant both policies to the `tf_vault` approle and the `woodpecker_terraform_vault` k8s role.

Apply before #147.

Reviewed-on: #148
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-30 22:14:41 +10:00
unkin-agent 67e79e72dc Grant the vault deployer read on the Authentik OIDC client secret (#146)
ci/woodpecker/push/apply Pipeline was successful
**Why:** the terraform-vault deployer must read the OpenBao OIDC client credentials that Authentik's provider module generates before it can configure `auth/oidc`, and AppRole capabilities are fixed at login so the grant has to exist in a prior apply.

**How:**
- Add `policies/kv/service/authentik/oidc-vault/read.yaml`: read on `kv/data/service/authentik/oidc-vault` for the deployer identities (approle `tf_vault`, k8s/au/syd1 `woodpecker_terraform_vault`); `terraform_authentik` still owns the write side of `kv/service/authentik/*`.

**Merge order:** this PR must merge and apply *before* the follow-up PR that adds the `auth/oidc` modules. OIDC becomes the default human auth path; approle/k8s (CI and agents) and break-glass are unchanged.

Reviewed-on: #146
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-30 21:50:57 +10:00
unkin-agent 1f03bcdc67 Bump vault-secrets-arrstack provider to 0.2.0 and plumb methods (#145)
ci/woodpecker/push/apply Pipeline was successful
## Why

Engine plugin v0.2.0 (catalog bumped in #144) added a `methods` field to arrstack roles, pinning a minted arrproxy key to a set of HTTP methods so a read-only integration can hold a key that cannot write. Provider v0.2.0 (just published to the `terraform-unkin` registry) exposes it as an optional set attribute, but the module had no input for it, so no role yaml could use it.

## How

- Bumps the `vault-secrets-arrstack` provider pin from 0.1.1 to 0.2.0 in `environments/root.hcl` and both arrstack modules.
- Adds an optional `methods` input to `modules/vault_cluster/modules/arrstack_secret_backend_role` and passes it through to the resource.
- Threads `methods` through the `vault_cluster` `arrstack_secret_backend_role` object type, so a role yaml may now carry a `methods:` list and it flows via the existing config.hcl merge with no discovery change.

`methods` defaults to `null` rather than `[]`: the provider reads an unrestricted role back as null, so a null default keeps a role yaml that omits the field drift-free. An empty-set default would plan `null -> []` on every existing role.

**Expected plan: no resource changes.** No role yaml changes here, so the plan should be a provider-version-only diff (provider upgrade, zero add/change/destroy).

Follow-up PR scopes the mediamark role to GET/HEAD.

Reviewed-on: #145
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-30 18:19:04 +10:00
unkin-agent 6c489c6003 Bump arrstack plugin catalog to v0.2.0 (#144)
ci/woodpecker/push/apply Pipeline was successful
## Why

The v0.2.0 arrstack plugin binary is live on all five OpenBao nodes (RPM applied via post-merge puppet runs), and the catalog still pins the v0.1.0 sha — OpenBao refuses to launch a plugin whose binary hash does not match the catalog entry.

## Changes

- `config/plugins/vault-plugin-secrets-arrstack.yaml`: `version` -> `0.2.0` and `sha256` -> `9ea7f160…12fa1`, computed from the binary extracted from `openbao-plugin-secrets-arrstack-0.2.0-1.x86_64.rpm` (the same RPM puppet pins).

## Post-apply

Run `vault plugin reload -plugin=vault-plugin-secrets-arrstack` after the apply so the running mount swaps to the v0.2.0 binary.

Reviewed-on: #144
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-30 17:07:28 +10:00
unkin-agent bcbf121818 Grant terraform-rancher read on rancher/creds/ci (#143)
ci/woodpecker/push/apply Pipeline was successful
## Why

`terraform-rancher` authenticates the rancher2 provider with a static 90-day admin API token stored in `kv/service/terraform/rancher` — the repo carries its own Makefile TODO to retire it. The Rancher secrets engine is already mounted at `rancher/` with a `ci` role (seeded `admin` service account, ttl 1h / max 8h), but no policy grants read on `rancher/creds/*`, so nothing can use it yet.

## What

Adds `policies/rancher/creds/ci.yaml`: `read` on `rancher/creds/ci`, bound to the same runner identities as the existing kv policy — approle `terraform_rancher` and k8s/au/syd1 role `woodpecker_terraform_rancher`.

Minted tokens are lease-bound and deleted from Rancher on revoke. They inherit the seeded admin service account's RBAC, so this is the same privilege as the static token it replaces, just short-lived.

## Follow-up

A terraform-rancher Makefile PR swapping `vault kv get kv/service/terraform/rancher` for `vault read -field=token rancher/creds/ci` must merge **only after** this one applies.

Reviewed-on: #143
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-30 09:42:00 +10:00
unkin-agent 9fa51f401a Add arrstack creds role and k8s auth for mediamark (#141)
ci/woodpecker/push/apply Pipeline was canceled
## Why

mediamark (kids-content marking UI, namespace `mediamark`) needs Sonarr/Radarr access to list series/movies and read metadata + artwork. It should get ephemeral virtual arrproxy keys from the arrstack secrets engine via arrproxy, not a copy of the static app API keys.

## How

- `config/arrstack_secret_backend_role/arrstack/mediamark.yaml` — role minting keys scoped to `sonarr` + `radarr` (no prowlarr), `ttl: 60` / `max_ttl: 86400`, mirroring the existing per-app roles.
- `config/auth_kubernetes_role/k8s/au/syd1/mediamark.yaml` — k8s auth role `mediamark`, bound to serviceaccount `default` in namespace `mediamark`, `token_ttl`/`token_max_ttl` 600, audience `vault`.
- `policies/arrstack/creds/mediamark.yaml` — `read` on `arrstack/creds/mediamark`, granted to `k8s/au/syd1: [mediamark]` only (deliberately not the shared `default` k8s role, which would expose the creds to every namespace).

Engine mount/config and the existing roles are untouched.

## Note

The companion argocd-apps change consumes `arrstack/creds/mediamark` via a `VaultDynamicSecret` (response fields: `token`, `id`, `apps`, `subject`, `expires_at`).

**GET/HEAD-only is not expressible today.** `arrstack_secret_backend_role` carries `apps`/`ttl`/`max_ttl` only, and arrproxy scopes machine tokens by app — the GET/HEAD restriction on the cheeztv/kids tier is a grant on an OIDC *group*, not on a minted token. The role is scoped as tightly as the engine allows and the limitation is documented in the yaml; per-token method scoping needs a plugin + arrproxy feature.

Reviewed-on: #141
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-30 09:40:26 +10:00
unkin-agent 5879fbd2d8 Add ephemeral Gitea creds for repospawner (#142)
ci/woodpecker/push/apply Pipeline was successful
## Why

`repospawner` creates and seeds Gitea repositories on demand from in-cluster. It needs a Gitea token, and it should not carry a static one -- it gets ephemeral, lease-bound creds like every other service. The `agents` AppRole is CIDR-bound to Ben's workstation, so pods authenticate via Kubernetes auth instead.

## How

- `config/gitea_secret_backend_role/gitea/repospawner.yaml` -- gitea engine role for the `repospawner` user. Scopes `write:repository`, `write:issue`, `read:user` (`read:user` is mandatory: clients validate the login via `GET /api/v1/user`, which 403s without it). ttl 1h / max_ttl 4h. Mirrors `unkin-agent.yaml`.
- `config/auth_kubernetes_role/k8s/au/syd1/repospawner.yaml` -- k8s auth role bound to serviceaccount `repospawner` in namespace `repospawner`, 600s ttl, `audience: vault` (VSO/projected-token flavor, same as `media-apps` / `logging_logarchiver`).
- `policies/gitea/creds/repospawner.yaml` -- `read` on `gitea/creds/repospawner`, bound to `k8s/au/syd1: [repospawner]` only. Deliberately **not** bound to the `agents` AppRole: the service runs in-cluster only.

## Depends on

The terraform-git PR that creates the `repospawner` Gitea user. The engine role cannot mint creds until the user exists -- **merge that one first**.

Reviewed-on: #142
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-30 09:29:26 +10:00
unkin-agent f3a3e4b874 Grant terraform-authentik write on the kv/service/authentik subtree (#140)
ci/woodpecker/push/apply Pipeline was successful
## Why

`kv/service/authentik/agent-api-token` is seeded by hand today. terraform-authentik should own it as IaC (`vault_kv_secret_v2`), along with any future Authentik automation tokens, but its runner identities only hold read on `kv/service/terraform/authentik` and the per-namespace oauth-credentials paths.

This must land and **apply** first: an AppRole/k8s token's capabilities are fixed at login, so the companion terraform-authentik PR would fail its very first plan against a policy that is not yet live.

## How

- Adds `policies/kv/service/authentik/write.yaml`, granting:
  - `kv/data/service/authentik/*` — create, read, update, delete
  - `kv/metadata/service/authentik/*` — read, list, delete
- Assigned to the same identities that already hold the terraform-authentik read policy: approle `terraform_authentik` and `k8s/au/syd1` role `woodpecker_terraform_authentik`.
- `delete` is included (unlike the `kv/kubernetes` agents grant) so `terraform destroy` and resource replacement clean up both the data and the metadata; metadata `read`/`list` is what `vault_kv_secret_v2` hits on every plan.
- Scope note in the file header: the ask was to scope to `*-token` paths, but Vault ACL paths only support a trailing glob, so the whole `kv/service/authentik/` subtree is granted. terraform-authentik is the owner of everything under that prefix.
- Existing `policies/kv/service/authentik/agent-api-token/read.yaml` (agents approle) is untouched.

## Order

Merge + apply this first; the companion terraform-authentik PR merges only afterwards.

Reviewed-on: #140
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 23:01:43 +10:00
unkin-agent 7c7826997e Grant agents approle read on kv/service/authentik/agent-api-token (#139)
ci/woodpecker/push/apply Pipeline was successful
## Summary
- Grants the agents approle read on kv/service/authentik/agent-api-token

## Why
Automation seeds oauth client secrets and LDAP outpost tokens; fetching outpost tokens needs a scoped Authentik API token, seeded at this path by the operator.

Reviewed-on: #139
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-29 12:56:06 +10:00
unkin-agent 36d2b99255 Revert temporary agents read grant on Authentik provider token (#138)
ci/woodpecker/push/apply Pipeline was successful
## Why
The one-off terragrunt import in terraform-authentik that required this grant is complete (jellyfin provider, groups, application, and policy bindings are all reconciled into state; apply pipeline is green). Per the recovery plan the temporary read grant is removed again.

## Changes
- Reverts de9d6e5: removes policies/kv/service/terraform/authentik/read.yaml (agents AppRole read on kv/data/service/terraform/authentik)

Reviewed-on: #138
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-25 21:35:57 +10:00
unkin-agent 9fcd9d7ab5 Grant agents AppRole read on the Authentik provider token (#137)
ci/woodpecker/push/apply Pipeline was successful
A one-off `terragrunt import` in terraform-authentik is needed to reconcile the Authentik resources orphaned by the jellyfin apply failure. The agents AppRole must be able to read the Authentik provider token (`kv/service/terraform/authentik`) to run the import; this grant is read-only on that single path and can be reverted once the import is done.

- Add `kv/service/terraform/authentik/read` policy (read on `kv/data/service/terraform/authentik`) bound to the `agents` AppRole

Reviewed-on: #137
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 22:34:31 +10:00
unkin-agent 57fe279d7a Run plan without acquiring the Consul state lock (#136)
ci/woodpecker/push/apply Pipeline was successful
PR plan pipelines were failing with "Error acquiring the state lock ... OperationTypePlan" when a plan collided with an apply (or another plan) holding the lock on the same Consul-backed state. Plans are read-only and don't need the lock.

- `plan`: pass `-lock=false` to `terragrunt ... plan`; `apply` is unchanged and still locks.

Reviewed-on: #136
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 22:31:58 +10:00
unkin-agent 6c68057e73 operator RO creds: read operator CRD API groups (#135)
ci/woodpecker/push/apply Pipeline was successful
## Why

The `operator` kube context is a Vault-minted, read-only credential (Kubernetes
secret engine role `cluster-operator`, bound to a `get/list/watch`-only
ClusterRole). It is currently RBAC-forbidden from listing operator-owned CRDs —
the immediate breakage is `valkeyclusters.valkey.io` — and likewise every other
operator CRD group deployed via `argocd-apps`. This extends the RO ruleset so the
context can read those CRDs. Still strictly read-only: no create/update/delete.

## Change

- Extend the `cluster-operator` generated_role_rules
  (`resources/secret_backend/kubernetes/au/syd1/roles/cluster-operator.yaml`)
  with `get/list/watch` on the CRD API groups of the operators deployed via
  `argocd-apps` (verbs and `resources: "*"` unchanged; same single rule block).

## API groups added

- `valkey.io` (valkey-operator — immediate need)
- `ceph.unkin.net` (cephrgw-operator)
- `bind.unkin.net` (bind-operator)
- `kea.unkin.net` (kea/dhcp operator)
- `k8up.io` (k8up)
- `grafana.integreatly.org` (grafana-operator)
- `operator.victoriametrics.com` (VictoriaMetrics operator)
- `clickhouse.altinity.com`, `clickhouse-keeper.altinity.com` (altinity clickhouse-operator)
- `acme.cert-manager.io` (cert-manager companion CRD group)
- `deviceplugin.intel.com`, `fpga.intel.com` (intel device plugins operator)
- `autoscaling.k8s.io` (VPA)
- `apm.k8s.elastic.co`, `beat.k8s.elastic.co`, `agent.k8s.elastic.co`,
  `maps.k8s.elastic.co`, `enterprisesearch.k8s.elastic.co`,
  `autoscaling.k8s.elastic.co`, `stackconfigpolicy.k8s.elastic.co` (ECK — the
  `elasticsearch`/`kibana`/`logstash` ECK groups were already granted)
- `snapshot.storage.k8s.io`, `groupsnapshot.storage.k8s.io` (CSI external-snapshotter, deployed via csi-cephfs/csi-cephrbd)

Groups already present (`postgresql.cnpg.io`, `cert-manager.io`,
`externaldns.k8s.io`, `secrets.hashicorp.com`, `purelb.io`, `nfd.k8s-sigs.io`,
`elasticsearch/kibana/logstash.k8s.elastic.co`, `gateway.networking.k8s.io`,
etc.) are unchanged. Rancher/RKE/Calico/cluster-api/fleet management-layer CRD
groups are intentionally excluded — they are not `argocd-apps` operators.

---------

Co-authored-by: unkin-agent <agent@unkin.net>
Reviewed-on: #135
Co-authored-by: Unkin Agent <unkin-agent@unkin.net>
Co-committed-by: Unkin Agent <unkin-agent@unkin.net>
2026-08-23 12:29:02 +10:00
unkin-agent aa8d41a7bb Bump vault-secrets-arrstack provider to 0.1.1 (#134)
ci/woodpecker/push/apply Pipeline was successful
v0.1.1 models the role apps attribute as a Set instead of a List, fixing the ordering-based "inconsistent result after apply" error and superseding the interim alphabetical yaml sort (#133).

- Bumps the vault-secrets-arrstack provider pin from 0.1.0 to 0.1.1 in root.hcl and both arrstack modules

Reviewed-on: #134
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-23 11:30:21 +10:00
unkin-agent 36fbfb86bc Sort arrstack role apps alphabetically (#133)
ci/woodpecker/push/apply Pipeline was successful
The master apply (pipeline 202) fails with "Provider produced inconsistent result after apply": the arrstack Vault engine returns apps alphabetically sorted while the provider models apps as an ordered List, so the declared order [sonarr, radarr, prowlarr] never matches the read-back. This is an interim unblock while the provider moves apps to a Set.

- Reorders apps in config/arrstack_secret_backend_role/arrstack/all.yaml to alphabetical order to match the engine read-back

Reviewed-on: #133
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-22 23:25:44 +10:00
unkin-agent a33fc2b42f Allow CSI auth to read ceph-mediafs-secret KV (#132)
ci/woodpecker/push/apply Pipeline failed
A new VaultStaticSecret `ceph-mediafs-secret` in ns `csi-cephfs` (for the legacy mediafs CephFS static PV) gets 403 on `kv/data/service/kubernetes/au/syd1/csi/ceph-mediafs-secret` — the `ceph-csi` role can already read the sibling `ceph-cephfs-secret` path via the same `ceph-csi-cephfs` VaultAuth, but no policy covers the new path.

- Adds `policies/kv/service/kubernetes/au/syd1/csi/ceph-mediafs-secret/read.yaml` granting read on `kv/data/service/kubernetes/au/syd1/csi/ceph-mediafs-secret`, bound to role `ceph-csi` on mount `k8s/au/syd1` (mirrors the existing ceph-cephfs-secret/ceph-rbd-secret policies)

Reviewed-on: #132
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-22 23:17:18 +10:00
unkin-agent 0e82cda02d vault: mount arrstack engine + config + roles (3/3) (#127)
ci/woodpecker/push/apply Pipeline failed
## Why

Creates the arrstack secrets engine itself: the mount + config and the per-scope roles that mint arrproxy API keys. **PR 3 of 3 (resources)**, stacked on #126 (policy). Final step of the register -> policy -> resources split (was #124).

## Change

- Adds `config/arrstack_secret_backend/arrstack.yaml`: mounts the engine at `arrstack` and writes its config (`base_url`, timeout). The arrproxy admin token stays out of git and is read from KV by the module.
- Adds `config/arrstack_secret_backend_role/arrstack/{all,sonarr,radarr,prowlarr}.yaml`: roles scoped to each arr app (plus one covering all three). Default `ttl` is **60s** (short-lived, renewed on demand); `max_ttl` 86400 mirrors the litellm sibling convention. The engine additionally caps renewal at the arrproxy admin token's fixed mint expiry.
- Adds `modules/vault_cluster/modules/arrstack_secret_backend{,_role}` and wires them in: `config/config.hcl` maps, `modules/vault_cluster/main.tf`, `variables.tf`, the environment inputs, and the root provider block.
- Provider source is `artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-arrstack` (repo `terraform-provider-vault-secrets-arrstack`), local name `arrstack`.

## Apply order

Apply **after PR #126 (policy) AND after `terraform-provider-vault-secrets-arrstack` v0.1.0 is published** to the artifactapi terraform registry. Until the provider is published, `tofu init` cannot resolve it, so **CI/plan on this PR is red by design** — that is expected, not a regression.

Note **plan-green != apply-green**: the KV-sourced `admin_token` is only fetched at apply time, so a green plan does not prove the seeded token is readable.

## Stack

1. register -> #125
2. policy -> #126
3. **resources (this PR)** -> `benvin/arrstack-resources` off `benvin/arrstack-policy`

Supersedes #124.

---------

Co-authored-by: unkin-agent <unkin-agent@git.unkin.net>
Co-authored-by: BenVincent <benvin@main.unkin.net>
Reviewed-on: #127
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-21 00:09:47 +10:00
unkin-agent 08ec281b1d Remove ghp agent role (missing installation_id; unblock apply) (#131)
ci/woodpecker/push/apply Pipeline was successful
## Why

The `ghp` agent role fails at apply with `Code: 400 — installation_id is required for agent tokens`. The role config `config/ghp_secret_backend_role/ghp/agent.yaml` carries only a placeholder `installation_id`, so the role can never be created. This failure blocks the terraform-vault master apply, which in turn blocks the arrstack #127 apply.

Remove the ghp role for now so the master apply goes green. The `ghp` secret backend itself is retained (it now mounts and configures cleanly). The role can be re-added once a real `installation_id` is provided.

Because the role never successfully created (apply failed on it), removing it is non-destructive — it is not in state, so no destroy is introduced.

## Changes

- Delete `config/ghp_secret_backend_role/ghp/agent.yaml`, which empties the `ghp_secret_backend_role` for_each map so no role instance (and no downstream ghp_secret_role) is planned.

Backend `config/ghp_secret_backend/ghp.yaml` and all other config are unchanged. Net diff vs master is exactly this one file deletion.

---------

Co-authored-by: unkin-agent <unkin-agent@users.noreply.git.unkin.net>
Reviewed-on: #131
Co-authored-by: Unkin Agent <unkin-agent@unkin.net>
Co-committed-by: Unkin Agent <unkin-agent@unkin.net>
2026-08-20 00:05:13 +10:00
unkin-agent 392c5d2ac7 Restore ghp secret backend + roles (config now seeded) (#130)
ci/woodpecker/push/apply Pipeline failed
## Why

Reverts the temporary removal in #129. That PR deleted the ghp backend + role
config YAMLs to unblock the `master` apply, which was failing with:

```
Error: no secret found at "kv/data/service/vault/au/syd1/secret_backend/ghp/config"
  from module.ghp_secret_backend["ghp"].data.vault_kv_secret_v2.config
```

The ghp config KV is now seeded: `kv/data/service/vault/au/syd1/secret_backend/ghp/config`
holds key `admin_token`, and the ghp service secret
`kv/kubernetes/namespace/ghp/default/app` carries the matching `service_token`.
With the KV populated, `data.vault_kv_secret_v2.config` resolves, so the ghp
secret backend + role can be created. The ghp module wiring, plugin
registration, and policies were never removed (they stayed on `master`), so
restoring these two YAMLs re-populates the `for_each` maps and instantiates the
backend + role against the seeded config.

## Changes

- Restore `config/ghp_secret_backend/ghp.yaml`.
- Restore `config/ghp_secret_backend_role/ghp/agent.yaml`.

Net diff vs `master` is exactly the re-addition of those two files
(byte-identical to their pre-#129 content, the mirror-inverse of #129).

## Sequence

Final step (4/4) of the remove -> grant write policy -> seed KV -> add-back
sequence: #129 (remove) -> #128 (grant) -> KV seed -> this PR (add back).

## Verification

- `tofu fmt` clean, `yamllint` passes (pre-commit hooks green), `terragrunt validate` succeeds (only unrelated `vault_kv_secret_v2` deprecation warnings).
- `tofu init` installs the `vault-secrets-ghp` provider with no plugin/catalog error.
- ghp config KV path confirmed seeded with `admin_token`, so the previously-failing data source now resolves.
- A full privileged `plan` is not runnable under the agent AppRole (it lacks the policy to mint the consul backend token), so the created/destroyed resource counts are not machine-confirmed here; the git diff is exactly the two file additions, so no config-driven destroys are introduced.
- Note: the ghp backend mount at apply requires the `vault-plugin-secrets-ghp` binary present on the OpenBao nodes (pre-existing Puppet-managed plugin).

Reviewed-on: #130
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-19 23:47:32 +10:00
unkin-agent d1cb790de8 Grant agents approle write on ghp config KV path (to seed) (#128)
ci/woodpecker/push/apply Pipeline was successful
## Why

The `terraform-vault` master apply aborts because the KV path `kv/data/service/vault/au/syd1/secret_backend/ghp/config` (key `admin_token`, a `ghpsvc_` service token) is unseeded. The ghp secrets engine reads that value at `ghp/config` creation time, so the ghp data-source read fails and the apply stops. Granting the `agents` AppRole scoped write to just this one KV path lets an agent seed the value so the apply can proceed.

## Changes

- Add `policies/kv/service/vault/au/syd1/secret_backend/ghp/config_write.yaml`, a `vault_policy` bound to the `agents` AppRole role only.
- Grant `create`, `update`, `read` on the kv-v2 data path `kv/data/service/vault/au/syd1/secret_backend/ghp/config`.
- Grant `read` on the kv-v2 metadata path `kv/metadata/service/vault/au/syd1/secret_backend/ghp/config` (read on plan/apply).
- Scope to this single ghp config path only; no wildcards, no delete, no list, no other `secret_backend` configs (least privilege).

## Caveat

This grant is itself a `vault_policy` applied by the master apply, which currently aborts on the ghp data-source read. So the policy likely needs to be applied first (a targeted apply of just this `vault_policy`) before the agent can seed the KV path. The agent also still needs the actual `ghpsvc_` service token value provided out-of-band to write into `admin_token`.

---------

Co-authored-by: BenVincent <benvin@main.unkin.net>
Co-authored-by: unkin-agent <agent@unkin.net>
Reviewed-on: #128
Co-authored-by: Unkin Agent <unkin-agent@unkin.net>
Co-committed-by: Unkin Agent <unkin-agent@unkin.net>
2026-08-19 23:17:44 +10:00
unkin-agent 31a7abec99 vault: add arrstack policies (deployer + KV read + creds) (2/3) (#126)
ci/woodpecker/push/apply Pipeline was successful
## Why

Grants the Vault access the arrstack engine needs, before any engine resources exist. **PR 2 of 3 (policy)**, stacked on #125 (register). Keeping policy separate from resources honours the never-bundle / sequential-apply rule.

## Change

- Adds `policies/arrstack/admin.yaml`: the terraform-vault deployer (`tf_vault` approle + `woodpecker_terraform_vault` k8s role) may create/read/update/delete `arrstack/config` and manage `arrstack/roles/*`.
- Adds `policies/kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token/read.yaml`: the deployer may read the KV-seeded arrproxy admin token (both `kv/data/...` and `kv/metadata/...`) that the engine config sources. The existing `secret_backends_read` policy does not cover this `kubernetes/namespace` KV path.
- Adds `policies/arrstack/creds/{sonarr,radarr,prowlarr}.yaml`: each `terraform-<app>` run may read its own `arrstack/creds/<app>` to mint a scoped key.
- Policy YAMLs are auto-discovered by `policies/policies.hcl`, so no wiring changes are needed.

## Apply order

Apply **after PR #125 (register)**. Safe to apply before the engine exists — these only grant capabilities on paths.

## Stack

1. register -> #125
2. **policy (this PR)** -> `benvin/arrstack-policy` off `benvin/arrstack-register`
3. resources -> `benvin/arrstack-resources`

Supersedes #124.

Reviewed-on: #126
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-19 22:54:13 +10:00
unkin-agent 2525bae1d7 Temporarily remove ghp secret backend + roles (unblock apply) (#129)
ci/woodpecker/push/apply Pipeline was successful
## Why

The `terraform-vault` master apply aborts with:

```
Error: no secret found at "kv/data/service/vault/au/syd1/secret_backend/ghp/config"
  from module.ghp_secret_backend["ghp"].data.vault_kv_secret_v2.config
```

The ghp secret backend reads its admin token from a KV path that has not been
seeded yet, so the apply fails and blocks every other change — including the
arrstack plugin registration (#125).

This PR **removes only the ghp backend + role config YAMLs (empties the
`for_each` map)**. With no config YAMLs, `var.ghp_secret_backend` /
`var.ghp_secret_backend_role` are empty maps, so zero ghp backend/role
instances are created, the unseeded `ghp/config` KV is never read, and the
apply passes. The ghp module wiring, plugin registration, and policies all stay
in place. This is part 1 of a remove -> grant write policy -> seed KV -> re-add
sequence, and the YAMLs will be restored once the ghp config KV is seeded.

## Changes

- Delete `config/ghp_secret_backend/ghp.yaml`.
- Delete `config/ghp_secret_backend_role/ghp/agent.yaml`.

Net diff vs `master` is exactly those two file deletions. All ghp wiring is
unchanged (identical to master): the `module.ghp_secret_backend` /
`module.ghp_secret_backend_role` instantiations, their variables, the
`config.hcl` parsing blocks, the `terragrunt.hcl` inputs, the
`vault-plugin-secrets-ghp` plugin registration, and the `ghp/admin` +
`ghp/creds/agent` policies all remain.

Reviewed-on: #129
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-19 22:51:55 +10:00
unkin-agent df0510e33b vault: register vault-plugin-secrets-arrstack in the catalog (1/3) (#125)
ci/woodpecker/push/apply Pipeline failed
## Why

Splits the arrstack Vault engine work (was #124) into three independently-appliable PRs so registration, policy, and engine resources are never bundled. This is **PR 1 of 3 (register)**.

## Change

- Registers the `vault-plugin-secrets-arrstack` plugin (v0.1.0) in the OpenBao plugin catalog via `config/plugins/vault-plugin-secrets-arrstack.yaml`.
- `sha256` pins the released v0.1.0 binary.
- No wiring changes needed: the `plugins` glob and `module.plugin` already exist on `master`.

## Apply order

Apply this **after** the Puppet plugin-install PR (unkin/puppet-prod #521, merged) has placed the binary at `/opt/openbao-plugins/vault-plugin-secrets-arrstack` on the OpenBao nodes. Registration fails until the binary is present on-node.

## Stack

1. **register (this PR)** -> `benvin/arrstack-register` off `master`
2. policy -> `benvin/arrstack-policy`
3. resources -> `benvin/arrstack-resources`

Supersedes #124.

Reviewed-on: #125
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-19 21:50:55 +10:00
unkin-agent 41935d5603 vault: wire up ghp secrets engine (backend + role + policies) (#121)
ci/woodpecker/push/apply Pipeline failed
Why: final step of the ordered ghp plugin add. Mounts + configures the ghp OpenBao secrets engine and its token-minting role (catalog registration is #123; policies #122).

How: config/ghp_secret_backend/ghp.yaml (mount at ghp; base_url https://ghp.unkin.net; admin_token read from KV kv/service/vault/au/syd1/secret_backend/ghp/config), config/ghp_secret_backend_role/ghp/agent.yaml (agent role), the two vault_cluster submodules, and the config.hcl ghp_secret_backend/role discovery + terragrunt inputs pulling the ghp provider (vault-secrets-ghp v0.1.0).

ORDER — apply in sequence:
1. puppet-prod#520 — install openbao-plugin-secrets-ghp binary (merged).
2. terraform-vault#122 — config-write policy (merged).
3. terraform-vault#123 — register plugin in catalog.
4. THIS PR — mount + config + role.

Before apply is clean: set the role real ghp App installation_id (currently sentinel 0), and KV dual-seed the ghpsvc_ token (this engine admin_token + the ghp deployment GHP_AUTH_SERVICE_TOKENS).

Reviewed-on: #121
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-16 15:55:01 +10:00
unkin-agent 5084a53015 vault: register ghp plugin in the catalog (config/plugins only) (#123)
ci/woodpecker/push/apply Pipeline was successful
Why: step 3 of the ordered ghp plugin add — register vault-plugin-secrets-ghp in the OpenBao plugin catalog as its own atomic change, before the engine is mounted/configured.

How: add config/plugins/vault-plugin-secrets-ghp.yaml (type: secret; sha256 pins the v0.1.0 binary installed by puppet-prod#520). config/plugins/* is generically discovered by config.hcl, so this is the only file. Catalog import is covered by the shared sudo-protected sys/plugins/catalog grant.

Order: puppet-prod#520 (install) -> terraform-vault#122 (config-write policy, merged) -> **this** (catalog) -> terraform-vault#121 (mount + config + role).
Reviewed-on: #123
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-16 15:43:18 +10:00
unkin-agent 600a7747b0 vault: ghp engine config-write + consumer policies (apply before #121) (#122)
ci/woodpecker/push/apply Pipeline was successful
Why: step 2 of the ordered ghp plugin add (install via puppet -> **this policy** -> resources). The terraform apply identities (tf_vault / woodpecker_terraform_vault) need create/update on ghp/config + ghp/roles/* BEFORE the engine resources are applied — an approle token capability set is fixed at login, so the grant must be effective in a prior apply or writing ghp/config is permission-denied.

How: split out of #121 — policies/ghp/admin.yaml (deployer: ghp/config + ghp/roles/*) and policies/ghp/creds/agent.yaml (agents approle: read ghp/creds/agent).

Order: puppet-prod#520 (install plugin) -> **this** -> #121 (resources).
Reviewed-on: #122
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-08-16 14:49:33 +10:00
unkinben 9e7687fccb Mint the netbox user-management credential dynamically from the single admin token (#119)
ci/woodpecker/push/apply Pipeline was successful
## Why

netbox_user_management authenticates to NetBox to reconcile service users + permissions on every apply. It must not depend on a second static admin token, and it must not break when the engine rotates its admin seed (`netbox/config/rotate` mints a fresh admin token and deletes the old one). The durable shape: keep exactly ONE static admin token, and have the netbox engine mint an ephemeral, user-admin-capable token that the e-breuninger provider uses to manage users.

## How

- `module.netbox_user_mgmt_role` creates `netbox/roles/vault-user-mgmt`, a write-enabled role for a pre-existing NetBox superuser named by `user_mgmt_username`. Minted tokens authenticate AS that superuser (NetBox tokens carry no scope beyond `write_enabled`; the user's permissions apply), so they can create users.
- `netbox_user_management` reads `netbox/creds/vault-user-mgmt` and configures the netbox provider with the minted token. When `user_mgmt_username` is unset it falls back to the single static `admin_token` (a `check` block warns that rotation would then break it) - a bootstrap/degraded path, never a second static token.
- Grant the deployer `read` on `netbox/creds/vault-user-mgmt` (the one deliberate exception to the admin policy's `netbox/creds/*` exclusion).
- Keep the bare-token + `token_version`-match postconditions on the single static admin token.

## Feasibility constraints (worked through, documented in-module)

1. **The engine CAN mint a user-admin token** - roles map to a pre-existing user with only a `write_enabled` gate (`vault-plugin-secrets-netbox` `path_roles.go`, `client.go` `MintToken`); point it at a superuser and minted tokens can manage users.
2. **Token transits state.** The hashicorp/vault provider (5.6.0) exposes ephemeral resources for KV only, not dynamic engine creds, so the mint is read via the `vault_generic_secret` DATA source: the short-lived token is written to state (sensitive, lease-revoked) and re-minted each plan. Migrate to an ephemeral resource once the vault provider ships a dynamic-secret one.
3. **A clean single fresh apply is not possible.** A provider cannot be configured from a role created in the same run (data sources don't defer; OpenTofu 1.11 defers only ephemeral resources, which the vault provider doesn't offer here). So enabling the dynamic path on a backend needs a one-time targeted bootstrap of the mount + role, then normal applies. Documented in `config/netbox_secret_backend/netbox.yaml`.

## Operator follow-up

- Repair the live mount first (unchanged): `vault write netbox/config token=<BARE>` (the mount uses `ignore_changes=[token]`), keep `token_version=2`.
- To enable dynamic minting: set `user_mgmt_username` to the pre-existing superuser, apply the deployer creds policy, then bootstrap once: `tofu apply -target=...netbox_secret_backend -target=...netbox_user_mgmt_role`, then apply normally. Until then user management stays on the static token (non-breaking, with a warning).

Reviewed-on: #119
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-12 00:03:13 +10:00
unkinben 8ccc5f1393 Add the netbox backend and terraform-infra role (#117)
ci/woodpecker/push/apply Pipeline was successful
## Why

- The netbox engine modules stand ready but mount nothing and create no identity until backend and role data exist, so terraform-infra still reads a static NetBox token instead of minting ephemeral scoped tokens.

## How

- Add `config/netbox_secret_backend/netbox.yaml` to mount the engine at `netbox` and point it at the syd1 NetBox URL; the admin token is read from KV, not stored here.
- Add `config/netbox_secret_backend_role/netbox/terraform-infra.yaml` as the single declarative source for the terraform-infra identity: filename-derived role name and NetBox username, write access, short TTLs, and an inline permissions block. Nothing in the file repeats the filename.
- Scope terraform-infra to view/add/change/delete on the IPAM/DCIM objects it manages: prefixes, ip-addresses, ip-ranges, devices, interfaces, mac addresses.
- Add `policies/netbox/creds/terraform-infra.yaml` letting the terraform-infra AppRole and its Woodpecker k8s role read `netbox/creds/terraform-infra`; it attaches to nothing until the separate terraform-infra Vault onboarding lands.

## Dependency order

- Stacked on the modules PR (#115), which stacks on the plugin registration PR. Merge order: plugin -> #115 -> this.

## CI note

- The plan step is red only on the external admin_token KV seed at `kv/data/service/vault/au/syd1/secret_backend/netbox/config` (a NetBox token with add_token + grant_token / superuser). Seeding that path is an environmental prerequisite, not a code defect; everything else validates.

---------

Co-authored-by: BenVincent <benvin@main.unkin.net>
Reviewed-on: #117
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-11 20:43:04 +10:00
unkinben 521ef4f0f3 Add the netbox secrets engine modules and wiring (#115)
ci/woodpecker/push/apply Pipeline was successful
## Why

- Managing NetBox from Vault needs three capabilities the repo does not yet have: mounting the netbox engine, minting scoped tokens through roles, and creating the NetBox service users those roles mint tokens for. Landing the modules and config scaffolding before any backend or role data lets each concrete identity be added as pure data later.

## How

- Add three modules under `modules/vault_cluster/modules`: `netbox_secret_backend` (mount + engine config, admin token read from KV), `netbox_secret_backend_role` (mint ephemeral scoped tokens for a filename-derived NetBox username), and `netbox_user_management` (mirror consul_acl_management: read the seeded admin token, drive one e-breuninger/netbox provider per backend, and synthesize the NetBox user + object permissions from the role map's inline permissions).
- Derive the `netbox_secret_backend` and `netbox_secret_backend_role` maps in `config.hcl`, deriving each role's name and netbox_username from its filename so the engine role and NetBox username match by construction.
- Wire the three module blocks and their variables through `vault_cluster` and the syd1 terragrunt inputs, reusing the sanitized backend-alias pattern the Consul providers use.
- Leave the backend and role maps empty: the modules stand ready and create nothing until backend and role config data are added.

## Dependency order

- Stacked on the plugin registration PR (branch `benvin/netbox-plugin`); merge that first, then this, then the backend + role PR (#117). Plans clean with empty netbox maps.

---------

Co-authored-by: BenVincent <benvin@main.unkin.net>
Reviewed-on: #115
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-09 16:36:18 +10:00
unkinben d080279728 Register the netbox secrets plugin in the catalog (#118)
ci/woodpecker/push/apply Pipeline was successful
## Why

- The netbox secrets engine cannot be mounted until its plugin binary is registered in the OpenBao catalog, so the catalog entry must land before any engine mount or role config references it.

## How

- Add `config/plugins/vault-plugin-secrets-netbox.yaml` registering the plugin as a secret plugin, pinned to the released v0.1.0 binary sha256 that Puppet installs on the OpenBao nodes. Bump the sha in lockstep with any RPM upgrade.

## Dependency order

- First of three stacked PRs: this plugin registration, then the netbox modules + wiring (#115), then the netbox backend + terraform-infra role (#117). Merges to master independently.

Reviewed-on: #118
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-09 16:30:06 +10:00
unkinben 03dc436a89 Add netbox engine admin policy (#116)
ci/woodpecker/push/apply Pipeline was successful
## Why
- The netbox secrets engine mount + roles land in a follow-up PR (#115); its manage policy must exist first so the deployer can create the engine config and roles the moment that PR applies (policy-first split).

## How
- Add `policies/netbox/admin.yaml` granting the deployer `netbox/config`, `netbox/config/rotate` and `netbox/roles/*` (deliberately excludes `netbox/creds/*`), bound to the `tf_vault` AppRole and `woodpecker_terraform_vault` k8s role, mirroring the gpg/gitea admin policies.

Land this before #115 (the engine mount + roles).

Reviewed-on: #116
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-09 12:02:13 +10:00
unkinben c20e7e4664 Let the agents AppRole mint unkin-agent Gitea tokens (#114)
ci/woodpecker/push/apply Pipeline was successful
Why: AI coding agents authenticate to Gitea as Ben using Ben's token. With the unkin-agent identity now in place (terraform-git PR #59), the agents AppRole should issue that account's tokens directly so agent commits and PRs are attributable and carry only least-privilege scopes.

How:
- add a gitea secrets-engine role minting ephemeral tokens for unkin-agent scoped to write:repository, write:issue, read:user — push branches and open PRs, never merge or administer
- add a policy granting read on gitea/creds/unkin-agent, bound to the agents AppRole, mirroring the agent-* Kubernetes creds bindings

Depends on terraform-git PR #59: the unkin-agent Gitea account must exist before minted tokens work. The vault-plugin-secrets-gitea engine is already live (plugin v0.1.0 registered, gitea mount configured), so no engine/plugin change is needed here.

Reviewed-on: #114
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-08 23:41:53 +10:00
unkinben aac651a5e4 Grant terraform-infra kv metadata read (#113)
ci/woodpecker/push/apply Pipeline was successful
Follow-up to the merged #111 (which shipped `kv/data/service/terraform/infra` read only).

`terraform-infra`'s providers.tf uses a `vault_kv_secret_v2` **data source**, which reads the kv-v2 **metadata** path on every plan/apply (same behaviour that 403'd a prior terraform-git apply — see `policies/kv/service/vault/.../gitea/config_write.yaml`). Add `kv/metadata/service/terraform/infra` read so the plan doesn't 403 once the secret is seeded.

Verified against terraform-infra PR #5: `skip_child_token` cleared the child-token 403 and the data-read policy works (plan now reaches "no secret found"); metadata read is the remaining policy gap before a seeded plan can pass.

https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
Reviewed-on: #113
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-06 23:17:35 +10:00
unkinben 95927202ba Rename terraform-ipam CI Vault access -> terraform-infra (#111)
ci/woodpecker/push/apply Pipeline was successful
Follows the `terraform-ipam` -> `terraform-infra` repo rename. Renames the k8s auth role (`woodpecker_terraform_infra`), consul secret-backend role + ACL policy (`terraform-infra`, state path `infra/terraform/infra/*`), consul creds read policy, and kv read policy (`kv/service/terraform/infra`).

https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
Reviewed-on: #111
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-06 22:25:38 +10:00
unkinben d61e985ef2 Add terraform-ipam CI Vault access (#110)
ci/woodpecker/push/apply Pipeline was successful
Grants the `terraform-ipam` Woodpecker pipeline its Vault footprint, mirroring the `terraform-artifactapi` set plus a KV read for provider tokens.

- k8s auth role `woodpecker_terraform_ipam` (SA `terraform-ipam`, ns `woodpecker`)
- consul secret backend role `terraform-ipam` + ACL policy writing `infra/terraform/ipam/*`
- consul creds read policy
- kv read policy for `kv/service/terraform/ipam` (`netbox_token` + `kea_token`)

The KV secret itself still needs seeding before the pipeline can auth to NetBox/Kea.

https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
Reviewed-on: #110
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-03 00:16:34 +10:00
unkinben 0e6da5cdd3 Add Vault-scoped agent kubernetes roles + agents AppRole (#109)
ci/woodpecker/push/apply Pipeline was successful
## Why
Agentic workloads currently need cluster-admin/root kubeconfig contexts to do routine per-domain work. This adds domain-scoped, Vault-issued kubernetes credentials plus an `agents` AppRole so agents get least-privilege access instead of escalating.

## Changes
- Add `kubernetes_secret_backend_role` configs `agent-dhcp`, `agent-dns`, `agent-certs`, `agent-storage` (au/syd1):
  - **agent-dhcp** (Role, ns `dhcp-system`): full verbs on `kea.unkin.net` CRDs; get/list/watch pods/services/configmaps/events + pods/log.
  - **agent-dns** (`service_account_name` mode): mints tokens for the static `agent-dns` SA (argocd-apps#332) whose per-namespace RoleBindings confine access to bind-system/bind-internal/bind-external/externaldns. `allowed_kubernetes_namespaces: [bind-system]` (the SA's namespace).
  - **agent-certs** (Role, ns `cert-manager`): full verbs on `cert-manager.io` + `acme.cert-manager.io` (closes the orders/challenges debugging gap); get/list/watch/delete secrets; get/list/watch pods + pods/log. Secret delete confined to `cert-manager`.
  - **agent-storage** (Role, ns `cephrgw-system`): full verbs on `ceph.unkin.net` CRDs (buckets/bucketaccesses/objectstoreusers); get/list/watch pods + pods/log.
- Extend the `kubernetes_secret_backend_role` module with an optional `service_account_name`; when set, `generated_role_rules`/`kubernetes_role_type` are omitted (the SA's own bindings supply RBAC).
- Add creds policies for each role, bound to the `kubernetes_au_syd1_cluster_operator` ldap group (human kubectl use) and the `agents` AppRole (programmatic use).
- Add the `agents` AppRole (mirrors the certmanager approle schema): `bind_secret_id: false` (role_id-only login), `token_bound_cidrs: [10.10.12.200/32]` (agent workstation wg0 addr), deterministic role_id, 1h/4h TTLs.
- Add `policies/kv/kubernetes/agents.yaml` granting the AppRole create/read/update/list on `kv/data/kubernetes/*` + read/list on `kv/metadata/kubernetes/*` (no delete).

## Ordering
argocd-apps#332 (the `agent-dns` SA + ClusterRole + per-namespace RoleBindings) must sync **before** the `agent-dns` creds here are usable — Vault mints tokens for a service account that must already exist.

https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
Reviewed-on: #109
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-08-02 21:55:05 +10:00
unkinben c0cc74927c Add logarchive gpg key + logging_logarchiver read access (#106)
ci/woodpecker/push/apply Pipeline was successful
## Why

logarchiver encrypts archived logs to an OpenPGP key held in Vault's gpg engine so the private key never leaves Vault (retrieval delegates decryption to `gpg/decrypt/logarchive`, operator-only). This provisions the key and lets the service read only its public key.

## Changes

- Create gpg key `logarchive` (rsa-4096, non-exportable) in the `gpg` mount.
- Add k8s auth role `logging_logarchiver` bound to SA `logarchiver` in the `logging` namespace.
- Add policy granting `read` on `gpg/keys/logarchive` to that role (public key only; no decrypt/export).

Cross-repo: this must apply before the argocd-apps logarchiver Deployment (unkin/argocd-apps) can fetch the key.

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
---------

Co-authored-by: benvin <neotheo@gmail.com>
Reviewed-on: #106
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-29 20:39:41 +10:00
unkinben 31f32aba0f gitea roles: add read:user scope for API login validation (#105)
ci/woodpecker/push/apply Pipeline was successful
End-to-end verification of the freshly-applied gitea engine (mint → API call → revoke) surfaced that tokens without read:user get 403 from GET /api/v1/user — the endpoint tea and most Gitea API clients use to validate a login. teabot's personalities would fail their auth check with the current scope sets, while in-scope calls (repo/issue) already work and lease revocation correctly kills tokens (verified 401 after revoke).

- add read:user to the teabot-implementer role scopes
- add read:user to the teabot-reviewer role scopes

Reviewed-on: #105
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-28 18:07:29 +10:00
unkinben 96a6a7d728 gitea: add the gitea token secrets engine (mount, config, teabot roles) (#101)
ci/woodpecker/push/apply Pipeline was successful
## Why

teabot's implementer and reviewer bot users should mint **ephemeral, scoped Gitea tokens** on demand rather than holding standing personal access tokens — Gitea tokens never expire on their own, so a leak lasts until someone notices. This registers and mounts the new `vault-plugin-secrets-gitea` engine (released v0.1.0) 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 8f67fbc216effada5fd7399888a710b62fad83be0b31761a439e7dec3d56509b` (sha256 of `/opt/openbao-plugins/vault-plugin-secrets-gitea` from the released `openbao-plugin-secrets-gitea-0.1.0` RPM).
- Add `gitea_secret_backend` + `gitea_secret_backend_role` modules and wire them through `config.hcl`, `environments/au/syd1/terragrunt.hcl`, and `modules/vault_cluster` variables/main, using the `giteavaultsecret` provider from the `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`, keys `admin_username`/`admin_password`) — not stored in git.
- **The seed is consumed create-only**: `lifecycle ignore_changes` on `admin_username`/`admin_password` means the engine reads the KV seed only when first creating `gitea/config`. After `rotate-root` diverges the live password from the seed, a later apply never pushes the stale seed back.
- Add roles with conservative, minimal scopes (write: implies read:):
  - `teabot-implementer` — `write:repository`, `write:issue` (clone/push, open PRs, comment).
  - `teabot-reviewer` — `read:repository`, `write:issue` (read diffs, post PR reviews/comments).
- TTLs: `ttl` 1h / `max_ttl` 4h on both roles.

## The site-admin bot + KV seed are now provisioned by Terraform (no manual gap)

Per Ben's review, creating the site-admin bot and seeding its credential is no longer a manual step:

- **terraform-git #46** creates the `gitea-vault-admin` site-admin bot and writes its generated password **once** to `kv/service/vault/au/syd1/secret_backend/gitea/config` (create-only KV write; never updated).
- **terraform-vault #102** grants terraform-git write access to that KV path.

## Ordering (merge + apply)

1. **puppet-prod #498** — installs the plugin binary on the vault nodes (Puppet must run).
2. **terraform-vault #100** (`benvin/gitea-deployer-access`) — deployer access to the gitea mount.
3. **terraform-vault #102** (`benvin/gitea-kv-writer`) — terraform-git KV write grant.
4. **terraform-git #46** (`benvin/gitea-vault-admin`) — creates the bot + seeds KV.
5. **This PR** — mounts the engine (reads the seed) and declares roles.

Files here are disjoint from #100 and #102 (no conflict).

**CI note:** the plan for this PR may hard-fail in CI if the plugin isn't yet registered/installed or the KV seed isn't present in the plan's target. If CI plan fails for that ordering reason, that is expected — do not force; apply only once steps 1–4 are live.

## Remaining manual step (one, ordered)

After this PR's first apply, run `vault write -f gitea/config/rotate-root` so the standing seed password is replaced by one only Vault holds. (On future binary upgrades, bump the RPM version in puppet-prod and the catalog `sha256` here together, then `vault write sys/plugins/reload/backend plugin=vault-plugin-secrets-gitea`.)

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #101
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-27 23:42:13 +10:00
unkinben bf9c785281 policies: allow terraform-git to delete the gitea config seed for taint recovery (#104)
ci/woodpecker/push/apply Pipeline was successful
terraform-git's apply (pipeline 108) still fails: pipeline 107 actually wrote the seed but the post-create metadata read 403'd, so terraform tainted the resource — recovery is replace (delete+create), and delete was deliberately not granted. Withholding delete doesn't provide the write-once property anyway (that's lifecycle ignore_changes in terraform-git); it just breaks taint recovery and destroy.

- add delete on the kv data path for the gitea config seed
- add delete on the matching kv metadata path (full destroy support)

After merge+apply, restart the terraform-git apply once more — it will replace the tainted seed and go green, unblocking #101.

Reviewed-on: #104
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-27 23:36:53 +10:00
unkinben d82580f1af policies: grant terraform-git read on the gitea config KV metadata path (#103)
ci/woodpecker/push/apply Pipeline was successful
terraform-git's main apply still fails after the skip_child_token fix (tfgit #48): the vault_kv_secret_v2 seed resource reads the kv-v2 metadata path during plan/apply, and the grant added in #102 covered kv/data only — Vault returns 403 on GET kv/metadata/.../secret_backend/gitea/config (terraform-git pipeline 107). This is the last blocker before the KV seed lands and terraform-vault #101 can apply.

- add read on kv/metadata/service/vault/au/syd1/secret_backend/gitea/config to the terraform-git seed policy

Reviewed-on: #103
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-27 23:21:43 +10:00
unkinben 2c27395613 policies: let terraform-git seed the gitea engine admin credential to KV (#102)
ci/woodpecker/push/apply Pipeline was successful
## Why

terraform-git now provisions the `gitea-vault-admin` site-admin bot and writes its generated password to `kv/service/vault/au/syd1/secret_backend/gitea/config` (as `admin_username` + `admin_password`) so the gitea secrets engine can consume it at creation time. The `woodpecker_terraform_git` / `terraform_git` identity has no write access to that KV path, so its apply would 403 without this grant.

The deployer that *reads* the seed already has read access via `policies/kv/service/vault/secret_backends_read.yaml` (`kv/data/service/vault/+/+/secret_backend/*`), so only the write side is added here.

## Change

- Add `policies/kv/service/vault/au/syd1/secret_backend/gitea/config_write.yaml` granting `create`/`read`/`update` on the gitea config KV path to the `terraform_git` approle and `woodpecker_terraform_git` k8s role.

## Ordering

Merge + apply this before the terraform-git `benvin/gitea-vault-admin` PR applies (which performs the write). Files are disjoint from the other gitea terraform-vault PRs (#100, #101).

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #102
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-27 20:22:40 +10:00
unkinben d289775e38 policies: grant the vault deployer access to the gitea secrets engine (#100)
ci/woodpecker/push/apply Pipeline was successful
## Why

The forthcoming `gitea_secret_backend` + role configuration (separate PR, `benvin/gitea-secret-engine`) is applied by terraform-vault under the deployment identity (`tf_vault` approle / `woodpecker_terraform_vault` k8s role). That identity has no access to the `gitea/` mount yet, so writing the engine's config and roles would 403. This mirrors `policies/rancher/admin.yaml`.

## Change

- Add `policies/gitea/admin.yaml` granting the deployer:
  - create/read/update/delete on `gitea/config`
  - create/update on `gitea/config/rotate-root` (write-only rotation trigger)
  - full manage + list on `gitea/roles/*` (and list on `gitea/roles`)
- Deliberately excludes `gitea/creds/*` — minting tokens is for consumers, not the deployer.
- No new catalog or mount grant: plugin registration is already covered by the shared, sudo-protected wildcard in `policies/sys/plugins/catalog/admin.yaml`, and mounting uses the deployer's existing `sys/mounts/*` access — same as the rancher engine.

## Order

Merge and apply this **before** the `benvin/gitea-secret-engine` PR, so the deployer can write the engine config/roles on that apply.

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #100
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-27 19:10:21 +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
unkinben bb5f6922fa feat: add vault policy for terraform-git webhook secrets (#75)
ci/woodpecker/push/apply Pipeline was successful
## Summary
- Add read policy for kv/data/service/gitea/webhook/* path
- Assigned to terraform_git approle and woodpecker_terraform_git k8s auth role
- Webhook URLs are stored in Vault KV and read at plan/apply time

## Test plan
- [ ] Verify terragrunt plan succeeds for terraform-git after merge

Reviewed-on: #75
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-06-08 22:56:30 +10:00
benvin 346cf9fa43 feat: manage gitadmin token (#74)
ci/woodpecker/push/apply Pipeline was successful
- add approle for terraform-git
- add policy to read gitadmin token
- update access to the terraform-git consul token

---------

Co-authored-by: Ben Vincent <ben@unkin.net>
Reviewed-on: #74
2026-06-08 15:17:58 +10:00
unkinben 1288057b81 feat: add vault and consul roles for terraform-git (#73)
ci/woodpecker/push/apply Pipeline was successful
## Summary
- Add K8s auth role woodpecker_terraform_git for CI pipeline authentication
- Add consul secret backend role terraform-git for consul state storage tokens
- Add consul ACL policy granting write access to infra/terraform/git/ key prefix
- Add vault policy for reading consul creds at consul_root/au/syd1/creds/terraform-git

## Test plan
- [ ] Verify terragrunt plan succeeds
- [ ] Verify consul ACL policy is created correctly
- [ ] Verify K8s auth role can authenticate from woodpecker namespace

Reviewed-on: #73
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-06-07 20:36:35 +10:00
unkinben 3876fa818d chore: bump almalinux9 image tags (#72)
ci/woodpecker/push/apply Pipeline was successful
Bump almalinux9 image tags to 20260606

Reviewed-on: #72
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-06-07 00:35:30 +10:00
unkinben a548bf1cb1 fix: apply requires plan (#71)
ci/woodpecker/push/apply Pipeline was successful
- ensure make plan runs before make apply when deploying

Reviewed-on: #71
2026-05-22 00:03:08 +10:00
unkinben 93ba86baf3 feat: add apply workflow (#70)
ci/woodpecker/push/apply Pipeline was successful
Reviewed-on: #70
2026-05-21 23:57:25 +10:00
unkinben 098830c10b Merge pull request 'feat: add plan workflow' (#69) from benvin/make-plan-buildwq into master
Reviewed-on: #69
2026-05-21 23:54:07 +10:00
unkinben 9cbac6d3ef feat: add plan workflow
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
- update makefile to enable kubernetes auth or roleid auth
- add plan workflow
- update all policies to allow the terraform-vault kubernetes role
2026-05-21 23:52:30 +10:00
unkinben 73aaaaeb99 Merge pull request 'chore: enable access to gateway.networking.k8s.io' (#68) from benvin/gatewayapi into master
Reviewed-on: #68
2026-05-21 22:42:28 +10:00
unkinben 7c60a5fd53 chore: enable access to gateway.networking.k8s.io
ci/woodpecker/pr/pre-commit Pipeline was successful
2026-05-21 22:39:57 +10:00
unkinben 27f12f183e Merge pull request 'chore: change to specific ci image' (#67) from benvin/ci_image into master
Reviewed-on: #67
2026-03-09 01:16:59 +11:00
unkinben c61434b692 chore: change to specific ci image
ci/woodpecker/pr/pre-commit Pipeline was successful
- almalinux9-opentofu image contains all required tools
2026-03-09 01:14:41 +11:00
unkinben 172ceac2fc Merge pull request 'feat: add templated policies for kubernetes' (#66) from benvin/kubernetes_structured_paths into master
Reviewed-on: #66
2026-03-08 12:57:58 +11:00
unkinben 48a4fd0dd1 feat: add templated policies for kubernetes
ci/woodpecker/pr/pre-commit Pipeline was successful
- add default kubernetes auth role
- add templated access kv/kubernetes/*
2026-03-08 12:48:08 +11:00
unkinben 4dc09547ef Merge pull request 'fix: update audience for rpmbuilder' (#65) from benvin/default_aud into master
Reviewed-on: #65
2026-03-08 12:29:43 +11:00
unkinben 546a9efe44 fix: update audience for rpmbuilder
ci/woodpecker/pr/pre-commit Pipeline was successful
when using using the service account jwt directly, the default audience
is the api servers url
2026-03-07 11:31:36 +11:00
unkinben 679cec4bc1 Merge pull request 'feat: add rpmbuilder k8s role' (#64) from benvin/rpmbuilder-in-k8s into master
Reviewed-on: #64
2026-03-07 11:11:23 +11:00
unkinben 71789f9f32 feat: add rpmbuilder k8s role
ci/woodpecker/pr/pre-commit Pipeline was successful
- create rpmbuilder role
- enable access to gitea/github ro-tokens
- enable access to rpmbuilder role from woodpeckerci
2026-03-07 11:06:27 +11:00
unkinben 4cbcec58d3 Merge pull request 'feat: enable woodpecker access to ro tokens' (#63) from benvin/woodpecker_task_access into master
Reviewed-on: #63
2026-03-07 10:52:38 +11:00
unkinben 9c93e185f8 feat: enable woodpecker access to ro tokens
ci/woodpecker/pr/pre-commit Pipeline was successful
- enable woodpecker tasks to access gitea/github read-only tokens
2026-03-07 10:49:39 +11:00
unkinben d6c8474bd3 Merge pull request 'chore: move pgsql password to vault' (#62) from benvin/artifactapi_postgrespassword into master
Reviewed-on: #62
2026-03-06 19:51:25 +11:00
unkinben 42351000ee chore: move pgsql password to vault
ci/woodpecker/pr/pre-commit Pipeline was successful
- no more storing secrets in configmaps
2026-03-06 19:39:36 +11:00
unkinben f7d1330c37 Merge pull request 'chore: add artifactapi k8s role' (#61) from benvin/artifactapi into master
Reviewed-on: #61
2026-03-06 18:57:05 +11:00
unkinben d9e07e432e chore: add artifactapi k8s role
ci/woodpecker/pr/pre-commit Pipeline was successful
- enable access to read artifactapi secrets
2026-03-06 18:53:42 +11:00
unkinben 14a258de7d Merge pull request 'chore: enable access woodpecker-agent-secret' (#60) from benvin/woodpecker_agent_secret into master
Reviewed-on: #60
2026-03-03 23:34:32 +11:00
unkinben be8bcc3743 chore: enable access woodpecker-agent-secret
ci/woodpecker/pr/pre-commit Pipeline was successful
- add policy to access woodpecker-agent-secret
2026-03-03 23:30:49 +11:00
unkinben dc257b1bcd Merge pull request 'feat: add pre-commit check in ci' (#59) from benvin/woodpecker_integration into master
Reviewed-on: #59
2026-02-28 22:28:21 +11:00
unkinben 66119e5207 feat: add pre-commit check in ci
ci/woodpecker/pr/pre-commit Pipeline was successful
- add a ci workflow to verify pre-commit passes
- fix pre-commit errors/warnings:
  - missing required_version
  - missing required_providers
  - fixed terraform_deprecated_interpolation
  - removed terraform_unused_declarations
2026-02-28 21:42:47 +11:00
unkinben 9e6de4dc32 Merge pull request 'feat: set max token life for auth_kubernetes_role' (#58) from benvin/token_max_ttl into master
Reviewed-on: #58
2026-02-22 22:30:18 +11:00
unkinben 7cafafd483 feat: set max token life for auth_kubernetes_role
found kubernetes vaultauth resources never picking up new policies,
because they would infinitely renew their token.

- set default max token length for roles to 1 day
- changed all existing role token_max_ttl to match their token_ttl
2026-02-22 22:28:21 +11:00
unkinben c94b2af196 Merge pull request 'feat: add woodpecker secrets' (#57) from benvin/woodpecker into master
Reviewed-on: #57
2026-02-22 22:27:50 +11:00
unkinben dd44146d88 feat: add woodpecker secrets
- add secrets required to integrate woodpecker into gitea/pgsql
2026-02-22 22:27:30 +11:00
unkinben 18a62332f6 Merge pull request 'chore: enable access to openldap admin creds' (#56) from benvin/ldap_admin_pass_terraform_ldap into master
Reviewed-on: #56
2026-02-15 20:17:35 +11:00
unkinben 8fa68e2670 chore: enable access to openldap admin creds
- ensure terraform_ldap can read ldap admin credentials
2026-02-15 20:16:58 +11:00
unkinben 4cad39989f Merge pull request 'chore: add default_user_password credentials policy' (#55) from benvin/openldap_default_pass into master
Reviewed-on: #55
2026-02-15 13:45:45 +11:00
unkinben c825962490 chore: add default_user_password credentials policy
- fix the comment for ldap_admin_password
- add policy to read default_user_password
2026-02-15 13:43:02 +11:00
unkinben 51bc3fffc0 Merge pull request 'feat: add terraform-ldap service' (#54) from benvin/terraform-ldap into master
Reviewed-on: #54
2026-02-15 13:40:32 +11:00
unkinben dca26029c0 feat: add terraform-ldap service
- add consul role/policy/acls to allow terraform-ldap state management
- add approle to generate tokens for consul
2026-02-15 13:38:31 +11:00
unkinben d398911108 Merge pull request 'fix: kubernetes auth fixes' (#53) from benvin/kubernetes_fixes into master
Reviewed-on: #53
2026-02-15 13:08:43 +11:00
unkinben c093d5830d fix: kubernetes auth fixes
- annotations as alias metadata does not work with openbao (idempotency issue)
- set token_ttl to be 600 for all auth roles for kubernetes (min)
2026-02-15 13:06:08 +11:00
unkinben 4b176846f2 Merge pull request 'feat: add identity secrets' (#52) from benvin/identity into master
Reviewed-on: #52
2026-02-15 13:02:01 +11:00
unkinben 90b765d713 feat: add identity secrets
- add kubernetes auth role for identity namespace
- add policy to access openldap bootstrap credentials
2026-02-15 13:01:06 +11:00
unkinben 3fb5a64a17 Merge pull request 'feat: add kubernetes ldap groups' (#51) from benvin/kubernetes_ldap_groups into master
Reviewed-on: #51
2026-02-14 19:48:56 +11:00
unkinben 33a746e545 feat: add kubernetes ldap groups
vault's terraform approle doesnt need to access all of these kubernetes
roles, it was just added as a placeholder and access to the kubernetes
roles was via the `vault_admin` to-much-access account. this is an
effort to roll back that and make access more targeted.

- add kubernetes* ldap groups for specific cluster/role combinations
- remove tf_vault from kubernetes* roles
2026-02-14 19:46:39 +11:00
unkinben 4fe0e0de73 Merge pull request 'feat: add terraform_k8s approle' (#50) from benvin/terraform_k8s_approle into master
Reviewed-on: #50
2026-02-14 19:38:46 +11:00
unkinben a47f841028 feat: add terraform_k8s approle
- add approle for kubernetes terraform
- ensure it can access consul token for state storage
- ensure it can generate root token for managing kubernetes
2026-02-14 19:37:22 +11:00
unkinben 9192879c03 Merge pull request 'feat: use ephemeral consul token' (#49) from benvin/use_consul_creds into master
Reviewed-on: #49
2026-02-14 18:59:56 +11:00
unkinben 5cdf6b410d feat: use ephemeral consul token
- add vault_env to makefile
- retrieve a consul_http_token on demand from vault
2026-02-14 18:59:05 +11:00
unkinben b51617c009 Merge pull request 'feat: implement consul ACL management with provider aliases' (#48) from benvin/consul_backend into master
Reviewed-on: #48
2026-02-14 18:41:49 +11:00
unkinben 66ee6430fa Merge pull request 'feat: add tf_vault required policies' (#47) from benvin/tf-vault-policy-updates into master
Reviewed-on: #47
2026-02-14 18:41:33 +11:00
unkinben fd03727ec2 feat: add tf_vault required policies
move management of Vault back to tf_vault approle. for this, we need to
create a number of policies that are missing.

- add policies to manage consul secret engines
- add policies to manage pki secret engines
- add policies to manage kv secret engines
- add policies to manage ssh secret engines
2026-02-14 18:39:21 +11:00
unkinben 5536869a38 feat: implement consul ACL management with provider aliases
This commit message captures the major architectural change of implementing Consul ACL management
with proper provider aliasing, along with the supporting configuration files and policy definitions
for various terraform services.

- add consul_acl_management module to manage consul acl policies and roles
- add consul backend roles and policies for terraform services (incus, k8s, nomad, repoflow, vault)
- add consul provider configuration to root.hcl
- add policies to generate credentials for each role
- simplify consul_secret_backend_role module to reference acl-managed roles
- switch to opentofu for provider foreach support
- update terragrunt configuration to support consul backend aliases
- update pre-commit hooks to use opentofu instead of terraform
- configure tflint exceptions for consul acl management module
2026-02-14 18:13:50 +11:00
unkinben f8f1185b42 Merge pull request 'chore: add puppet k8s role' (#46) from benvin/puppet_secrets into master
Reviewed-on: #46
2026-02-01 14:54:45 +11:00
unkinben 75e9db1aa6 chore: add puppet k8s role
- add role and policies
2026-02-01 14:54:23 +11:00
unkinben f47804ffdf Merge pull request 'chore: rancher pods use rancher service account' (#45) from benvin/rancher_role into master
Reviewed-on: #45
2026-01-30 22:11:53 +11:00
unkinben 24c124d6eb chore: rancher pods use rancher service account
- update bound service account names to be `rancher`
- update namespace to cattle-system (do not run rancher in another namespace)
2026-01-30 22:11:08 +11:00
unkinben 9d54b4cfcc Merge pull request 'chore: add rancher role' (#44) from benvin/rancher_role into master
Reviewed-on: #44
2026-01-30 19:46:19 +11:00
unkinben 33af7010fb chore: add rancher role
- add kubernetes role for rancher
- add policy to enable access to bootstrap-password
2026-01-30 19:43:06 +11:00
unkinben cb1b383035 Merge pull request 'feat: major restructuring in migration to terragrunt' (#43) from benvin/vault_terragrunt into master
Reviewed-on: #43
2026-01-26 23:53:35 +11:00
unkinben f6d06cb319 chore: cleanup unused config data
- remove token_policies from roles config data, this comes from policies.hcl inputs
- remove policies from ldap groups
- remove backend data from roles, this comes from config.hcl inputs
2026-01-26 23:51:50 +11:00
unkinben 1c9e063310 Merge branch 'master' into benvin/vault_terragrunt 2026-01-26 23:07:13 +11:00
unkinben 8070b6f66b feat: major restructuring in migration to terragrunt
- migrate from individual terraform files to config-driven terragrunt module structure
- add vault_cluster module with config discovery system
- replace individual .tf files with centralized config.hcl
- restructure auth and secret backends as configurable modules
- move auth roles and secret backends to yaml-based configuration
- convert policies from .hcl to .yaml format, add rules/auth definition
- add pre-commit hooks for yaml formatting and file cleanup
- add terragrunt cache to gitignore
- update makefile with terragrunt commands and format target
2026-01-26 23:02:44 +11:00
unkinben b115b7d28a Merge pull request 'chore: add nzbget secrets' (#42) from benvin/nzbget into master
Reviewed-on: #42
2026-01-26 18:31:48 +11:00
unkinben 25e3d48337 chore: add nzbget secrets
- add policy for nzbget secrets
- enable the media-apps kubernetes role to use policy
2026-01-26 18:30:49 +11:00
unkinben fdc801739f Merge pull request 'feat: add prowlarr access' (#41) from benvin/prowlarr_policy into master
Reviewed-on: #41
2026-01-04 23:37:23 +11:00
unkinben 56d858f900 feat: add prowlarr access
- enable kubernetes access to prowlarr secrets
2026-01-04 23:36:43 +11:00
unkinben bd112181f5 Merge pull request 'feat: add policy to read terraform vars' (#40) from benvin/repoflow_terraform into master
Reviewed-on: #40
2025-12-13 10:57:33 +11:00
unkinben 4f185d5e28 feat: add policy to read terraform vars
- read variables required for terraform-repoflow
2025-12-13 10:56:58 +11:00
unkinben 65ad53e24c Merge pull request 'feat: add repoflow service vault configuration' (#39) from benvin/repoflow into master
Reviewed-on: #39
2025-12-13 10:13:33 +11:00
unkinben d217f6e42d Merge pull request 'feat: add repoflow tokens' (#38) from benvin/repoflow_tokens into master
Reviewed-on: #38
2025-12-13 10:10:07 +11:00
unkinben 9814b8fc1a feat: add repoflow tokens
- add approle for terraform-repoflow
- add policies to access repoflow tokens
2025-12-13 10:09:29 +11:00
unkinben 7b81abfa9e feat: add repoflow service vault configuration
- add secrets for s3, elasticsearch, hasura, postgres and repoflow
2025-12-13 09:20:58 +11:00
unkinben 2466a6fe5c Merge pull request 'feat: label kubernetes ephemeral serviceaccounts' (#37) from benvin/k8s_roles_labelling into master
Reviewed-on: #37
2025-12-07 12:42:45 +11:00
unkinben c88b19a216 feat: label kubernetes ephemeral serviceaccounts
- ensure all service accounts are labelled with role/cluster
- add additional api endpoints to cluster roles
2025-12-07 12:41:37 +11:00
unkinben 3bada72838 Merge pull request 'chore: allow long lines in yamllint' (#36) from benvin/yamlint-args into master
Reviewed-on: #36
2025-12-01 21:51:11 +11:00
unkinben 8961ba3748 chore: allow long lines in yamllint 2025-12-01 21:50:49 +11:00
unkinben 26b3ee84d6 Merge pull request 'chore: fix policies for rpmbuilder' (#35) from benvin/fix_rpmbuilder into master
Reviewed-on: #35
2025-11-30 21:24:52 +11:00
unkinben 0776fac6eb chore: fix policies for rpmbuilder
- missed the `/read` on the end
2025-11-30 21:24:06 +11:00
unkinben 3a2ecc9b23 Merge pull request 'feat: add rpmbuilder approle' (#34) from benvin/rpmbuilder into master
Reviewed-on: #34
2025-11-29 18:01:37 +11:00
unkinben 5afd1ad9c1 feat: add rpmbuilder approle
- add rpmbuilder approle
- add policies to acces gitea/github read-only tokens
2025-11-29 18:00:20 +11:00
unkinben 756286c231 chore: update name, role type for k8s
- ensure cluster roles are able to be created as ClusterRole
- prefix all vault managed roles with `vault-`
2025-11-29 00:09:57 +11:00
unkinben 9cc482d471 Merge pull request 'feat: add kubernetes secrets engine with RBAC roles for au-syd1 cluster' (#33) from benvin/au-syd1-k8s-roles into master
Reviewed-on: #33
2025-11-27 23:31:04 +11:00
unkinben 6624f7aed1 feat: add kubernetes secrets engine with RBAC roles for au-syd1 cluster
- Add Kubernetes secrets engine at kubernetes/au/syd1 path
  - Create four RBAC roles with external YAML configuration:
    * media-apps-operator: namespaced role for media-apps with selective permissions
    * cluster-operator: cluster-wide read-only access to specific API groups
    * cluster-admin: cluster-wide full access to specific API groups
    * cluster-root: cluster-wide superuser access to all resources
  - Add Vault policies for credential generation for each role
  - Add admin policies for kubernetes auth backend configuration and role management
  - Refactor kubernetes auth backend to use shared locals for CA certificate
  - Update terraform-vault approle with required kubernetes policies
2025-11-27 23:22:13 +11:00
unkinben ad1118af85 Merge pull request 'chore: remove references k8s pki policy' (#32) from benvin/cleanup_k8s_pki_policy_reference into master
Reviewed-on: #32
2025-11-27 21:08:29 +11:00
unkinben cafa887cdc chore: remove references k8s pki policy
- missed from previous pr
- policy no longer exists, remove it from the approle
2025-11-27 21:07:50 +11:00
unkinben f10f96d19c Merge pull request 'feat: move state path in consul' (#31) from benvin/move-state-path into master
Reviewed-on: #31
2025-11-27 21:05:55 +11:00
unkinben da0e0e4239 feat: move state path in consul
- move state to the infra/terraform/vault subdir
2025-11-27 21:04:44 +11:00
unkinben 2efbf7cc6e Merge pull request 'chore: remove k8s pki policy' (#30) from benvin/cleanup_k8s_pki into master
Reviewed-on: #30
2025-11-27 20:43:08 +11:00
unkinben b9deb02cfb chore: remove k8s pki policy
- k8s pki engine was removed some time ago
- also cleanup policy files
2025-11-27 20:42:27 +11:00
505 changed files with 9229 additions and 848 deletions
+1
View File
@@ -1,3 +1,4 @@
.terraform
.terraform.lock.hcl
env
.terragrunt-cache
+14 -2
View File
@@ -1,12 +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: terraform-fmt
- id: terraform-validate
- 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",
]
+24
View File
@@ -0,0 +1,24 @@
when:
- event: push
branch: master
steps:
- name: apply
image: git.unkin.net/unkin/almalinux9-opentofu:20260606
environment:
VAULT_AUTH_METHOD: kubernetes
VAULT_VERSION: "1.20.0"
commands:
- 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:
kubernetes:
serviceAccountName: terraform-vault
resources:
requests:
memory: 512Mi
cpu: 1
limits:
memory: 2Gi
cpu: 2
+22
View File
@@ -0,0 +1,22 @@
when:
- event: pull_request
steps:
- name: plan
image: git.unkin.net/unkin/almalinux9-opentofu:20260606
environment:
VAULT_AUTH_METHOD: kubernetes
VAULT_VERSION: "1.20.0"
commands:
- 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:
serviceAccountName: terraform-vault
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
+30 -15
View File
@@ -1,20 +1,35 @@
.PHONY: init plan apply help
.PHONY: init plan apply format
# Default target
help:
@echo "Available targets:"
@echo " init - Initialize Terraform"
@echo " plan - Plan Terraform changes"
@echo " apply - Apply Terraform changes"
VAULT_AUTH_METHOD ?= approle
VAULT_K8S_ROLE ?= woodpecker_terraform_vault
VAULT_K8S_MOUNT ?= auth/k8s/au/syd1
VAULT_K8S_JWT_PATH ?= /var/run/secrets/kubernetes.io/serviceaccount/token
# Define vault_env function to set up vault environment
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-vault)
endef
init:
@echo "Sourcing environment and initializing Terraform..."
@source ./env && terraform init
@$(call vault_env) && \
terragrunt run --all --non-interactive init -- -upgrade
plan:
@echo "Sourcing environment and planning Terraform changes..."
@source ./env && terraform plan
plan: init
@$(call vault_env) && \
terragrunt run --all --parallelism 4 --non-interactive plan -- -lock=false
apply:
@echo "Sourcing environment and applying Terraform changes..."
@source ./env && terraform apply -auto-approve
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
-15
View File
@@ -1,15 +0,0 @@
resource "vault_approle_auth_backend_role" "certmanager" {
role_name = "certmanager"
bind_secret_id = false
token_policies = ["pki_int/certmanager"]
token_ttl = 30
token_max_ttl = 30
token_bound_cidrs = [
"198.18.25.5/32", # ausyd1nxvm2052.main.unkin.net
"198.18.26.3/32", # ausyd1nxvm2053.main.unkin.net
"198.18.27.89/32", # ausyd1nxvm2054.main.unkin.net
"198.18.28.8/32", # ausyd1nxvm2055.main.unkin.net
"198.18.29.33/32", # ausyd1nxvm2056.main.unkin.net
"198.18.29.239/32", # ausyd1nxvm2097.main.unkin.net
]
}
-16
View File
@@ -1,16 +0,0 @@
resource "vault_approle_auth_backend_role" "incus_cluster" {
role_name = "incus_cluster"
bind_secret_id = false
token_policies = [
"default_access",
"kv/service/incus/incus-cluster-join-tokens"
]
token_ttl = 60
token_max_ttl = 120
token_bound_cidrs = [
"10.10.12.200/32",
"198.18.13.77/32",
"198.18.13.78/32",
"198.18.13.79/32"
]
}
-16
View File
@@ -1,16 +0,0 @@
resource "vault_approle_auth_backend_role" "packer_builder" {
role_name = "packer_builder"
bind_secret_id = false
token_policies = [
"default_access",
"kv/service/packer/packer_builder",
]
token_ttl = 300 # builds can take a few minutes
token_max_ttl = 600
token_bound_cidrs = [
"10.10.12.200/32",
"198.18.25.102/32",
"198.18.26.91/32",
"198.18.27.40/32",
]
}
-15
View File
@@ -1,15 +0,0 @@
resource "vault_approle_auth_backend_role" "puppetapi" {
role_name = "puppetapi"
bind_secret_id = false
token_policies = ["kv/service/puppetapi/puppetapi_read_tokens"]
token_ttl = 30
token_max_ttl = 30
token_bound_cidrs = [
"198.18.25.5/32", # ausyd1nxvm2052.main.unkin.net
"198.18.26.3/32", # ausyd1nxvm2053.main.unkin.net
"198.18.27.89/32", # ausyd1nxvm2054.main.unkin.net
"198.18.28.8/32", # ausyd1nxvm2055.main.unkin.net
"198.18.29.33/32", # ausyd1nxvm2056.main.unkin.net
"198.18.29.239/32", # ausyd1nxvm2097.main.unkin.net
]
}
-9
View File
@@ -1,9 +0,0 @@
resource "vault_approle_auth_backend_role" "rundeck-role" {
role_name = "rundeck-role"
bind_secret_id = true
token_policies = ["rundeck/rundeck"]
token_ttl = 1 * 3600
token_max_ttl = 4 * 3600
token_bound_cidrs = ["198.18.13.59/32"]
secret_id_bound_cidrs = ["198.18.13.59/32"]
}
-15
View File
@@ -1,15 +0,0 @@
resource "vault_approle_auth_backend_role" "sshsign-host-role" {
role_name = "sshsign-host-role"
bind_secret_id = false
token_policies = ["ssh-host-signer/sshsign-host-policy"]
token_ttl = 30
token_max_ttl = 30
token_bound_cidrs = [
"198.18.25.5/32", # ausyd1nxvm2052.main.unkin.net
"198.18.26.3/32", # ausyd1nxvm2053.main.unkin.net
"198.18.27.89/32", # ausyd1nxvm2054.main.unkin.net
"198.18.28.8/32", # ausyd1nxvm2055.main.unkin.net
"198.18.29.33/32", # ausyd1nxvm2056.main.unkin.net
"198.18.29.239/32", # ausyd1nxvm2097.main.unkin.net
]
}
-18
View File
@@ -1,18 +0,0 @@
resource "vault_approle_auth_backend_role" "sshsigner" {
role_name = "sshsigner"
bind_secret_id = false
token_policies = [
"ssh-host-signer/sshsigner",
"sshca_signhost"
]
token_ttl = 30
token_max_ttl = 30
token_bound_cidrs = [
"198.18.25.5/32", # ausyd1nxvm2052.main.unkin.net
"198.18.26.3/32", # ausyd1nxvm2053.main.unkin.net
"198.18.27.89/32", # ausyd1nxvm2054.main.unkin.net
"198.18.28.8/32", # ausyd1nxvm2055.main.unkin.net
"198.18.29.33/32", # ausyd1nxvm2056.main.unkin.net
"198.18.29.239/32", # ausyd1nxvm2097.main.unkin.net
]
}
-17
View File
@@ -1,17 +0,0 @@
resource "vault_approle_auth_backend_role" "terraform_incus" {
role_name = "terraform_incus"
bind_secret_id = false
token_policies = [
"default_access",
"kv/service/terraform/incus",
"kv/service/puppet/certificates/terraform_puppet_cert",
]
token_ttl = 60
token_max_ttl = 120
token_bound_cidrs = [
"10.10.12.200/32",
"198.18.25.102/32",
"198.18.26.91/32",
"198.18.27.40/32",
]
}
-16
View File
@@ -1,16 +0,0 @@
resource "vault_approle_auth_backend_role" "terraform_nomad" {
role_name = "terraform_nomad"
bind_secret_id = false
token_policies = [
"default_access",
"kv/service/terraform/nomad",
]
token_ttl = 60
token_max_ttl = 120
token_bound_cidrs = [
"10.10.12.200/32",
"198.18.25.102/32",
"198.18.26.91/32",
"198.18.27.40/32",
]
}
-31
View File
@@ -1,31 +0,0 @@
resource "vault_approle_auth_backend_role" "tf_vault" {
role_name = "tf_vault"
bind_secret_id = false
token_policies = [
"default_access",
"auth/token/auth_token_create",
"auth/token/auth_token_self",
"auth/token/auth_token_roles_admin",
"auth/approle/approle_role_admin",
"auth/approle/approle_role_login",
"approle_token_create",
"auth/kubernetes/k8s_auth_admin",
"k8s/k8s_pki_roles_admin",
"auth/ldap/ldap_admin",
"pki_int/pki_int_roles_admin",
"pki_root/pki_root_roles_admin",
"ssh-host-signer/ssh-host-signer_roles_admin",
"sshca/sshca_roles_admin",
"kv/service/glauth/services/svc_vault_read",
"sys/sys_auth_admin",
"sys/sys_mounts_admin",
"sys/sys_policy_admin",
"transit/keys/admin",
"kv/service/kubernetes/au/syd1/token_reviewer_jwt/read",
]
token_ttl = 60
token_max_ttl = 120
token_bound_cidrs = [
"10.10.12.200/32",
]
}
-7
View File
@@ -1,7 +0,0 @@
#----------------------------
# Enable approle auth method
#----------------------------
resource "vault_auth_backend" "approle" {
type = "approle"
path = "approle"
}
-38
View File
@@ -1,38 +0,0 @@
#-----------------------------------
# Enable kubernetes auth method
#-----------------------------------
resource "vault_auth_backend" "kubernetes" {
type = "kubernetes"
path = "kubernetes"
}
locals {
kubernetes_ca_cert = <<-EOT
-----BEGIN CERTIFICATE-----
MIIBejCCAR+gAwIBAgIBADAKBggqhkjOPQQDAjAkMSIwIAYDVQQDDBlya2UyLXNl
cnZlci1jYUAxNzU5MDI3NTg0MB4XDTI1MDkyODAyNDYyNFoXDTM1MDkyNjAyNDYy
NFowJDEiMCAGA1UEAwwZcmtlMi1zZXJ2ZXItY2FAMTc1OTAyNzU4NDBZMBMGByqG
SM49AgEGCCqGSM49AwEHA0IABKfsTD4tKzKcnHyubWseKjlIPphBVveV1n6RUxmi
a3H6s9qMmT3dldYJyaalZI0NctSdW4ucPhBN5THCUr8sOmejQjBAMA4GA1UdDwEB
/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRFb0pf+pC/voWvbs1z
fU/dqB0RxjAKBggqhkjOPQQDAgNJADBGAiEA/0zeJRrgwpHFPRsqgO+EhmwBx1Y8
NH3FcktF9J6PfPQCIQD4/IpOhdjf9rmo0ckG1npNEx5V8+OQ8ZTM7s1DL6+DfA==
-----END CERTIFICATE-----
EOT
}
# Data source to read the token_reviewer_jwt from Vault KV
data "vault_kv_secret_v2" "token_reviewer_jwt" {
mount = "kv"
name = "service/kubernetes/au/syd1/token_reviewer_jwt"
}
# Configure Kubernetes auth backend
resource "vault_kubernetes_auth_backend_config" "config" {
backend = vault_auth_backend.kubernetes.path
kubernetes_host = "https://api-k8s.service.consul:6443"
kubernetes_ca_cert = local.kubernetes_ca_cert
token_reviewer_jwt = data.vault_kv_secret_v2.token_reviewer_jwt.data["token"]
disable_iss_validation = true
use_annotations_as_alias_metadata = true
}
-40
View File
@@ -1,40 +0,0 @@
#--------------------------------
# Enable ldap auth method
#--------------------------------
# retrieve the bindpass from Vault
data "vault_generic_secret" "svc_vault" {
path = "kv/service/glauth/services/svc_vault"
}
# create the ldap backend
resource "vault_ldap_auth_backend" "ldap" {
path = "ldap"
url = "ldap://ldap.service.consul"
userdn = "ou=people,ou=users,dc=main,dc=unkin,dc=net"
userattr = "uid"
upndomain = "users.main.unkin.net"
discoverdn = false
groupdn = "ou=users,dc=main,dc=unkin,dc=net"
groupfilter = "(&(objectClass=posixGroup)(memberUid={{.Username}}))"
groupattr = "uid"
binddn = data.vault_generic_secret.svc_vault.data["distinguishedName"]
bindpass = data.vault_generic_secret.svc_vault.data["pass"]
}
resource "vault_ldap_auth_backend_group" "vault_access" {
groupname = "vault_access"
policies = [
"default_access",
]
backend = vault_ldap_auth_backend.ldap.path
}
resource "vault_ldap_auth_backend_group" "vault_admin" {
groupname = "vault_admin"
policies = [
"default_access",
"global-admin",
]
backend = vault_ldap_auth_backend.ldap.path
}
-97
View File
@@ -1,97 +0,0 @@
resource "vault_kubernetes_auth_backend_role" "default" {
backend = vault_auth_backend.kubernetes.path
role_name = "default"
bound_service_account_names = ["default"]
bound_service_account_namespaces = ["*"]
token_ttl = 3600
token_policies = [
"default"
]
audience = "vault"
}
resource "vault_kubernetes_auth_backend_role" "demo_default" {
backend = vault_auth_backend.kubernetes.path
role_name = "demo_default"
bound_service_account_names = ["default"]
bound_service_account_namespaces = ["demo"]
token_ttl = 60
token_policies = [
"kv/service/terraform/nomad"
]
audience = "vault"
}
resource "vault_kubernetes_auth_backend_role" "huntarr-default" {
backend = vault_auth_backend.kubernetes.path
role_name = "huntarr-default"
bound_service_account_names = ["default"]
bound_service_account_namespaces = ["huntarr"]
token_ttl = 60
token_policies = [
"pki_int/sign/servers_default",
"pki_int/issue/servers_default",
]
audience = "vault"
}
resource "vault_kubernetes_auth_backend_role" "externaldns" {
backend = vault_auth_backend.kubernetes.path
role_name = "externaldns"
bound_service_account_names = ["externaldns"]
bound_service_account_namespaces = ["externaldns"]
token_ttl = 60
token_policies = [
"kv/service/kubernetes/au/syd1/externaldns/tsig/read",
]
audience = "vault"
}
resource "vault_kubernetes_auth_backend_role" "cert_manager_issuer" {
backend = vault_auth_backend.kubernetes.path
role_name = "cert-manager-issuer"
bound_service_account_names = ["cert-manager-vault-issuer"]
bound_service_account_namespaces = ["cert-manager"]
token_ttl = 60
token_policies = [
"pki_int/sign/servers_default",
"pki_int/issue/servers_default",
]
audience = "vault"
}
resource "vault_kubernetes_auth_backend_role" "ceph-csi" {
backend = vault_auth_backend.kubernetes.path
role_name = "ceph-csi"
bound_service_account_names = [
"ceph-csi-rbd-csi-rbd-provisioner",
"ceph-csi-cephfs-csi-cephfs-provisioner",
]
bound_service_account_namespaces = [
"csi-cephrbd",
"csi-cephfs",
]
token_ttl = 60
token_policies = [
"kv/service/kubernetes/au/syd1/csi/ceph-rbd-secret/read",
"kv/service/kubernetes/au/syd1/csi/ceph-cephfs-secret/read",
]
audience = "vault"
}
resource "vault_kubernetes_auth_backend_role" "media-apps" {
backend = vault_auth_backend.kubernetes.path
role_name = "media-apps"
bound_service_account_names = [
"media-apps-vault-reader",
]
bound_service_account_namespaces = [
"media-apps",
]
token_ttl = 60
token_policies = [
"kv/service/media-apps/radarr/read",
"kv/service/media-apps/sonarr/read",
]
audience = "vault"
}
@@ -0,0 +1,9 @@
# Mounts the arrstack dynamic secrets engine at "arrstack" and writes its config.
# The arrproxy admin token is sensitive and read from KV, not stored here:
# kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token -> key "token"
# (seeded by argocd-apps #384). arrstack.unkin.net terminates on traefik-external
# with an internal-CA cert the OpenBao nodes already trust, so ca_cert is omitted
# (system trust store), mirroring the gitea engine against git.unkin.net.
description: "arrstack dynamic arrproxy API keys"
base_url: "https://arrstack.unkin.net"
request_timeout_seconds: 30
@@ -0,0 +1,9 @@
---
# Mints an arrproxy API key scoped to all three arr apps.
apps:
- prowlarr
- radarr
- sonarr
ttl: 60 # seconds (1m); short-lived by design, renewed on demand
max_ttl: 86400 # seconds (24h); the engine additionally caps renewal at the
# arrproxy admin token's fixed mint expiry.
@@ -0,0 +1,17 @@
---
# Mints an arrproxy API key for the mediamark kids-content marking UI, scoped to
# Sonarr and Radarr only (no Prowlarr: mediamark never touches indexers).
#
# mediamark only lists series/movies and reads metadata + artwork, so its key is
# read-only in intent. The engine role cannot express that today: an
# arrstack_secret_backend_role carries apps/ttl/max_ttl only, and arrproxy scopes
# machine tokens by app, not by HTTP method (the GET/HEAD restriction on the
# cheeztv/kids tier is a grant on an OIDC *group*, not on a minted token). Until
# the engine and arrproxy grow per-token method scoping, mediamark's read-only
# posture is enforced by the app itself and by the arrproxy tier it routes to.
apps:
- radarr
- sonarr
ttl: 60 # seconds (1m); short-lived by design, renewed on demand
max_ttl: 86400 # seconds (24h); the engine additionally caps renewal at the
# arrproxy admin token's fixed mint expiry.
@@ -0,0 +1,7 @@
---
# Mints an arrproxy API key scoped to Prowlarr only.
apps:
- prowlarr
ttl: 60 # seconds (1m); short-lived by design, renewed on demand
max_ttl: 86400 # seconds (24h); the engine additionally caps renewal at the
# arrproxy admin token's fixed mint expiry.
@@ -0,0 +1,7 @@
---
# Mints an arrproxy API key scoped to Radarr only.
apps:
- radarr
ttl: 60 # seconds (1m); short-lived by design, renewed on demand
max_ttl: 86400 # seconds (24h); the engine additionally caps renewal at the
# arrproxy admin token's fixed mint expiry.
@@ -0,0 +1,7 @@
---
# Mints an arrproxy API key scoped to Sonarr only.
apps:
- sonarr
ttl: 60 # seconds (1m); short-lived by design, renewed on demand
max_ttl: 86400 # seconds (24h); the engine additionally caps renewal at the
# arrproxy admin token's fixed mint expiry.
+2
View File
@@ -0,0 +1,2 @@
default_lease_ttl: 60s
max_lease_ttl: 24h
@@ -0,0 +1,6 @@
token_ttl: 3600
token_max_ttl: 14400
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
use_deterministic_role_id: true
@@ -0,0 +1,11 @@
token_ttl: 30
token_max_ttl: 30
bind_secret_id: false
token_bound_cidrs:
- "198.18.25.5/32"
- "198.18.26.3/32"
- "198.18.27.89/32"
- "198.18.28.8/32"
- "198.18.29.33/32"
- "198.18.29.239/32"
use_deterministic_role_id: false
@@ -0,0 +1,9 @@
token_ttl: 60
token_max_ttl: 120
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
- "198.18.13.77/32"
- "198.18.13.78/32"
- "198.18.13.79/32"
use_deterministic_role_id: false
@@ -0,0 +1,9 @@
token_ttl: 300
token_max_ttl: 600
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: false
@@ -0,0 +1,11 @@
token_ttl: 30
token_max_ttl: 30
bind_secret_id: false
token_bound_cidrs:
- "198.18.25.5/32"
- "198.18.26.3/32"
- "198.18.27.89/32"
- "198.18.28.8/32"
- "198.18.29.33/32"
- "198.18.29.239/32"
use_deterministic_role_id: false
@@ -0,0 +1,9 @@
token_ttl: 30
token_max_ttl: 30
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: false
@@ -0,0 +1,6 @@
token_ttl: 3600
token_max_ttl: 14400
bind_secret_id: true
token_bound_cidrs:
- "198.18.13.59/32"
use_deterministic_role_id: false
@@ -0,0 +1,11 @@
token_ttl: 30
token_max_ttl: 30
bind_secret_id: false
token_bound_cidrs:
- "198.18.25.5/32"
- "198.18.26.3/32"
- "198.18.27.89/32"
- "198.18.28.8/32"
- "198.18.29.33/32"
- "198.18.29.239/32"
use_deterministic_role_id: false
@@ -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: 60
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: false
@@ -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: 60
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: 60
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: false
@@ -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: 60
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: false
@@ -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,6 @@
token_ttl: 60
token_max_ttl: 120
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
use_deterministic_role_id: false
@@ -0,0 +1,5 @@
kubernetes_host: https://api-k8s.service.consul:6443
disable_iss_validation: true
use_annotations_as_alias_metadata: false # doesnt work with openbao yet
default_lease_ttl: 1h
max_lease_ttl: 24h
@@ -0,0 +1,7 @@
bound_service_account_names:
- default
bound_service_account_namespaces:
- artifactapi
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,9 @@
bound_service_account_names:
- ceph-csi-rbd-csi-rbd-provisioner
- ceph-csi-cephfs-csi-cephfs-provisioner
bound_service_account_namespaces:
- csi-cephrbd
- csi-cephfs
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,7 @@
bound_service_account_names:
- cert-manager-vault-issuer
bound_service_account_namespaces:
- cert-manager
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,6 @@
bound_service_account_names:
- default
bound_service_account_namespaces: ['*']
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,7 @@
bound_service_account_names:
- externaldns
bound_service_account_namespaces:
- externaldns
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,6 @@
bound_service_account_names:
- default
bound_service_account_namespaces:
- huntarr
token_ttl: 600
audience: vault
@@ -0,0 +1,7 @@
bound_service_account_names:
- default
bound_service_account_namespaces:
- identity
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,7 @@
bound_service_account_names:
- logarchiver
bound_service_account_namespaces:
- logging
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,7 @@
bound_service_account_names:
- media-apps-vault-reader
bound_service_account_namespaces:
- media-apps
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,10 @@
---
# Kubernetes auth role for the mediamark app: the default service account in the
# mediamark namespace, used by VaultDynamicSecret to read arrstack/creds/mediamark.
bound_service_account_names:
- default
bound_service_account_namespaces:
- mediamark
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,7 @@
bound_service_account_names:
- default
bound_service_account_namespaces:
- puppet
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,11 @@
---
# Kubernetes auth for the puppetserver compilers, which run certmanager
# server-side during catalog compilation. The AppRole equivalent is pinned to
# the legacy VM master IPs and cannot be used from autoscaled compiler pods.
bound_service_account_names:
- default
bound_service_account_namespaces:
- puppet
token_ttl: 30
token_max_ttl: 30
audience: https://kubernetes.default.svc.cluster.local
@@ -0,0 +1,10 @@
---
# Kubernetes auth for the puppetserver compilers, which run sshsignhost
# server-side during catalog compilation.
bound_service_account_names:
- default
bound_service_account_namespaces:
- puppet
token_ttl: 30
token_max_ttl: 30
audience: https://kubernetes.default.svc.cluster.local
@@ -0,0 +1,7 @@
bound_service_account_names:
- rancher
bound_service_account_namespaces:
- cattle-system
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,7 @@
bound_service_account_names:
- default
bound_service_account_namespaces:
- repoflow
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,7 @@
bound_service_account_names:
- repospawner
bound_service_account_namespaces:
- repospawner
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -0,0 +1,8 @@
# rpmbuilder is deployed in woodpeckerci
bound_service_account_names:
- default
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:
- default
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -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-git
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-infra
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
@@ -0,0 +1,7 @@
bound_service_account_names:
- terraform-vault
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
+10
View File
@@ -0,0 +1,10 @@
userdn: "ou=people,ou=users,dc=main,dc=unkin,dc=net"
userattr: "uid"
upndomain: "users.main.unkin.net"
discoverdn: false
groupdn: "ou=users,dc=main,dc=unkin,dc=net"
groupfilter: "(&(objectClass=posixGroup)(memberUid={{.Username}}))"
groupattr: "uid"
username_as_alias: true
default_lease_ttl: 24h
max_lease_ttl: 168h
@@ -0,0 +1,3 @@
---
# this file doesnt need anything in it, so this data is just to make sure yamlencode reads some yaml data
description: foo
@@ -0,0 +1,3 @@
---
# this file doesnt need anything in it, so this data is just to make sure yamlencode reads some yaml data
description: foo
@@ -0,0 +1,3 @@
---
# this file doesnt need anything in it, so this data is just to make sure yamlencode reads some yaml data
description: foo
@@ -0,0 +1,3 @@
---
# this file doesnt need anything in it, so this data is just to make sure yamlencode reads some yaml data
description: foo
+17
View File
@@ -0,0 +1,17 @@
# Authentik (OIDC) human SSO. The mount path is the filename and MUST stay
# "oidc": the Authentik provider registers strict redirect URIs containing
# /ui/vault/auth/oidc/oidc/callback, which encode the mount path.
#
# identity.unkin.net serves the public LetsEncrypt wildcard, so the OpenBao VMs
# validate discovery against the system roots with no CA bundle work. Do not
# swap it for identity.k8s.syd1.au.unkin.net (internal CA).
#
# listing_visibility unauth makes OIDC the offered default on the UI login page.
# client_id/client_secret are read from kv/service/authentik/oidc-vault.
---
oidc_discovery_url: "https://identity.unkin.net/application/o/vault/"
default_role: "default"
description: "Authentik SSO - default human login"
listing_visibility: "unauth"
default_lease_ttl: 24h
max_lease_ttl: 168h
@@ -0,0 +1,5 @@
---
# this file doesnt need anything in it, so this data is just to make sure yamlencode reads some yaml data
# The filename is the Authentik group name asserted in the ak_groups claim.
# Only akP-* permission groups may appear here; akR-* roles are grouping-only.
description: foo
+25
View File
@@ -0,0 +1,25 @@
# Default OIDC login role (the mount's default_role), used by both the web UI
# and `bao login -method=oidc`.
#
# The role grants no policies of its own: authorization comes from the external
# identity groups under config/auth_oidc_group, matched on the ak_groups claim.
# ak_groups is Authentik's hierarchy-expanding claim (plain `groups` only carries
# direct memberships), so nested akP-* permission groups resolve.
#
# allowed_redirect_uris must match the provider's strict URIs exactly.
---
user_claim: "email"
groups_claim: "ak_groups"
oidc_scopes:
- openid
- profile
- email
- ak_groups
bound_audiences:
- vault
allowed_redirect_uris:
- "http://localhost:8250/oidc/callback"
- "https://vault.k8s.syd1.au.unkin.net/ui/vault/auth/oidc/oidc/callback"
- "https://vault.service.consul:8200/ui/vault/auth/oidc/oidc/callback"
token_ttl: 86400
token_max_ttl: 604800
+317
View File
@@ -0,0 +1,317 @@
# =============================================================================
# VAULT MODULE CONFIGURATION SYSTEM
# =============================================================================
#
# This file automatically discovers and organizes YAML configuration files
# for Vault modules, creating structured configuration maps for Terraform.
#
# HOW IT WORKS:
# 1. Scans all subdirectories for *.yaml files
# 2. Groups files by module type based on directory structure
# 3. Creates unique resource keys to prevent naming conflicts
# 4. Adds computed fields like name, backend, etc. from file paths
#
# DIRECTORY STRUCTURE:
# config/
# ├── auth_approle_role/
# │ └── approle/
# │ ├── certmanager.yaml # Creates key: "approle/certmanager"
# │ └── myapp.yaml # Creates key: "approle/myapp"
# ├── auth_kubernetes_role/
# │ └── k8s/au/syd1/
# │ ├── default.yaml # Creates key: "k8s/au/syd1/default"
# │ └── myapp.yaml # Creates key: "k8s/au/syd1/myapp"
# └── kv_secret_backend/
# ├── kv.yaml # Creates key: "kv"
# └── secrets.yaml # Creates key: "secrets"
#
# EXAMPLE YAML FILE (config/auth_approle_role/approle/myapp.yaml):
# ```yaml
# token_ttl: 3600
# token_max_ttl: 7200
# bind_secret_id: true
# token_bound_cidrs:
# - "10.0.0.0/8"
# ```
#
# This becomes:
# ```hcl
# auth_approle_role = {
# "approle/myapp" = {
# approle_name = "myapp" # Auto-computed from filename
# mount_path = "approle" # Auto-computed from directory
# token_ttl = 3600 # From YAML content
# token_max_ttl = 7200 # From YAML content
# bind_secret_id = true # From YAML content
# token_bound_cidrs = ["10.0.0.0/8"]
# }
# }
# ```
#
# KEY NAMING PATTERNS:
# - Simple backends: filename only (e.g., "kv", "transit")
# - Role-based resources: full path without extension (e.g., "approle/myapp")
# - This ensures uniqueness when multiple backends have similar role names
#
# GENERATED OUTPUTS:
# - config.auth_approle_backend, config.auth_approle_role, etc.
# - Each module gets its own map with properly structured configuration
#
# =============================================================================
locals {
# Find all YAML files in subdirectories
config_files = fileset(".", "**/*.yaml")
# Create a flat map of all files with their content
all_configs = {
for file_path in local.config_files :
file_path => yamldecode(file(file_path))
}
# Group by module directory (first part of path)
config = {
auth_approle_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "auth_approle_backend/")
}
auth_approle_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_approle_role/", ""), ".yaml") => merge(content, {
approle_name = trimsuffix(basename(file_path), ".yaml")
mount_path = split("/", replace(file_path, "auth_approle_role/", ""))[0]
})
if startswith(file_path, "auth_approle_role/")
}
auth_ldap_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "auth_ldap_backend/")
}
auth_ldap_group = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_ldap_group/", ""), ".yaml") => merge(content, {
groupname = trimsuffix(basename(file_path), ".yaml")
backend = split("/", replace(file_path, "auth_ldap_group/", ""))[0]
})
if startswith(file_path, "auth_ldap_group/")
}
auth_oidc_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "auth_oidc_backend/")
}
auth_oidc_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_oidc_role/", ""), ".yaml") => merge(content, {
role_name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "auth_oidc_role/", ""))
})
if startswith(file_path, "auth_oidc_role/")
}
auth_oidc_group = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_oidc_group/", ""), ".yaml") => merge(content, {
groupname = trimsuffix(basename(file_path), ".yaml")
backend = split("/", replace(file_path, "auth_oidc_group/", ""))[0]
})
if startswith(file_path, "auth_oidc_group/")
}
auth_kubernetes_backend = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_kubernetes_backend/", ""), ".yaml") => content
if startswith(file_path, "auth_kubernetes_backend/")
}
auth_kubernetes_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_kubernetes_role/", ""), ".yaml") => merge(content, {
role_name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "auth_kubernetes_role/", ""))
})
if startswith(file_path, "auth_kubernetes_role/")
}
kv_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "kv_secret_backend/")
}
transit_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "transit_secret_backend/")
}
transit_secret_backend_key = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "transit_secret_backend_key/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "transit_secret_backend_key/", ""))
})
if startswith(file_path, "transit_secret_backend_key/")
}
ssh_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "ssh_secret_backend/")
}
ssh_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "ssh_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "ssh_secret_backend_role/", ""))
})
if startswith(file_path, "ssh_secret_backend_role/")
}
pki_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "pki_secret_backend/", ""), ".yaml") => content
if startswith(file_path, "pki_secret_backend/")
}
pki_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "pki_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "pki_secret_backend_role/", ""))
})
if startswith(file_path, "pki_secret_backend_role/")
}
kubernetes_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "kubernetes_secret_backend/", ""), ".yaml") => content
if startswith(file_path, "kubernetes_secret_backend/")
}
kubernetes_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "kubernetes_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "kubernetes_secret_backend_role/", ""))
})
if startswith(file_path, "kubernetes_secret_backend_role/")
}
consul_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "consul_secret_backend/", ""), ".yaml") => content
if startswith(file_path, "consul_secret_backend/")
}
consul_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "consul_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "consul_secret_backend_role/", ""))
})
if startswith(file_path, "consul_secret_backend_role/")
}
pki_mount_only = {
for file_path, content in local.all_configs :
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/")
}
arrstack_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "arrstack_secret_backend/")
}
arrstack_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "arrstack_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "arrstack_secret_backend_role/", ""))
})
if startswith(file_path, "arrstack_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/")
}
netbox_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "netbox_secret_backend/")
}
netbox_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "netbox_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
netbox_username = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "netbox_secret_backend_role/", ""))
})
if startswith(file_path, "netbox_secret_backend_role/")
}
ghp_secret_backend = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "ghp_secret_backend/")
}
ghp_secret_backend_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "ghp_secret_backend_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "ghp_secret_backend_role/", ""))
})
if startswith(file_path, "ghp_secret_backend_role/")
}
}
}
@@ -0,0 +1,7 @@
description: "consul secret engine for au-syd1 cluster"
default_lease_ttl_seconds: 600
max_lease_ttl_seconds: 86400
address: "consul.service.au-syd1.consul"
scheme: https
bootstrap: false
datacenter: au-syd1
@@ -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-git
ttl: 120
max_ttl: 300
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-incus
ttl: 300
max_ttl: 600
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-infra
ttl: 120
max_ttl: 300
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-k8s
ttl: 120
max_ttl: 300
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-ldap
ttl: 60
max_ttl: 60
datacenters: []
@@ -0,0 +1,5 @@
consul_roles:
- terraform-nomad
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: []

Some files were not shown because too many files have changed in this diff Show More