29 Commits

Author SHA1 Message Date
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
62 changed files with 1073 additions and 18 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ init:
plan: init plan: init
@$(call vault_env) && \ @$(call vault_env) && \
terragrunt run --all --parallelism 4 --non-interactive plan terragrunt run --all --parallelism 4 --non-interactive plan -- -lock=false
apply: init apply: init
@$(call vault_env) && \ @$(call vault_env) && \
@@ -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.
@@ -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,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:
- repospawner
bound_service_account_namespaces:
- repospawner
token_ttl: 600
token_max_ttl: 600
audience: vault
+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
+34
View File
@@ -97,6 +97,27 @@ locals {
}) })
if startswith(file_path, "auth_ldap_group/") 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 = { auth_kubernetes_backend = {
for file_path, content in local.all_configs : for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_kubernetes_backend/", ""), ".yaml") => content trimsuffix(replace(file_path, "auth_kubernetes_backend/", ""), ".yaml") => content
@@ -198,6 +219,19 @@ locals {
}) })
if startswith(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 = { plugins = {
for file_path, content in local.all_configs : for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => merge(content, { trimsuffix(basename(file_path), ".yaml") => merge(content, {
@@ -1,15 +0,0 @@
# Role minting ephemeral, scoped ghp agent tokens. Reading ghp/creds/agent mints
# a lease-bound token deleted from ghp on revoke/expiry. token_type "agent" binds
# the minted token to a ghp App installation, so installation_id is REQUIRED.
#
# installation_id below is a PLACEHOLDER (0) and MUST be set to the real ghp App
# installation id before this role can mint usable tokens. scopes are ghp
# permission:level pairs; contents:read is the least-privilege default.
---
token_type: agent
installation_id: 0 # PLACEHOLDER - set to the real ghp App installation id
scopes:
- contents:read
session_prefix: vault
ttl: 3600 # 1h
max_ttl: 86400 # 24h
@@ -0,0 +1,19 @@
# Role minting ephemeral tokens for the repospawner service user. repospawner
# creates and seeds Gitea repositories on demand, so it gets write on
# repositories (create + push) and write on issues (issue/PR comments). Read is
# implied by write. No admin/org/user-write scopes.
# read:user is required because most API clients validate the login via
# GET /api/v1/user, which 403s without it.
# Reading gitea/creds/repospawner mints a lease-bound token deleted from Gitea
# on revoke/expiry. Consumed by the repospawner pods via Kubernetes auth (see
# policies/gitea/creds/repospawner.yaml) -- the agents AppRole is CIDR-bound to
# Ben's workstation and cannot be used from in-cluster.
---
username: repospawner
scopes:
- write:repository
- write:issue
- read:user
token_name_prefix: vault-repospawner
ttl: 3600 # 1h
max_ttl: 14400 # 4h
@@ -0,0 +1,13 @@
# config/plugins/vault-plugin-secrets-arrstack.yaml
# Imports (registers) the arrstack secrets plugin in the catalog. Filename =
# catalog name = mount type. The binary is installed on the OpenBao nodes by
# Puppet (openbao-plugin-secrets-arrstack RPM ->
# /opt/openbao-plugins/vault-plugin-secrets-arrstack).
#
# sha256 pins the released v0.2.0 binary; bump it in lockstep with any RPM
# upgrade or OpenBao will refuse to launch the plugin. Registration only
# succeeds once the Puppet PR has installed the binary on the nodes.
type: secret
command: vault-plugin-secrets-arrstack
version: "0.2.0"
sha256: "9ea7f16013b3bcbadfcb15b08fd1545f8069893cf864bfdaa57e7e7e72612fa1"
@@ -3,6 +3,6 @@ algorithm_signer: rsa-sha2-256
ttl: 315360000 # 87600 * 3600 ttl: 315360000 # 87600 * 3600
allow_host_certificates: true allow_host_certificates: true
allow_user_certificates: false allow_user_certificates: false
allowed_domains: "main.unkin.net,consul" allowed_domains: "unkin.net,main.unkin.net,consul"
allow_subdomains: true allow_subdomains: true
allow_bare_domains: false allow_bare_domains: false
+5
View File
@@ -60,6 +60,9 @@ inputs = {
auth_approle_role = local.config.auth_approle_role auth_approle_role = local.config.auth_approle_role
auth_ldap_backend = local.config.auth_ldap_backend auth_ldap_backend = local.config.auth_ldap_backend
auth_ldap_group = local.config.auth_ldap_group auth_ldap_group = local.config.auth_ldap_group
auth_oidc_backend = local.config.auth_oidc_backend
auth_oidc_role = local.config.auth_oidc_role
auth_oidc_group = local.config.auth_oidc_group
auth_kubernetes_backend = local.config.auth_kubernetes_backend auth_kubernetes_backend = local.config.auth_kubernetes_backend
auth_kubernetes_role = local.config.auth_kubernetes_role auth_kubernetes_role = local.config.auth_kubernetes_role
kv_secret_backend = local.config.kv_secret_backend kv_secret_backend = local.config.kv_secret_backend
@@ -76,6 +79,8 @@ inputs = {
pki_mount_only = local.config.pki_mount_only pki_mount_only = local.config.pki_mount_only
litellm_secret_backend = local.config.litellm_secret_backend litellm_secret_backend = local.config.litellm_secret_backend
litellm_secret_backend_role = local.config.litellm_secret_backend_role litellm_secret_backend_role = local.config.litellm_secret_backend_role
arrstack_secret_backend = local.config.arrstack_secret_backend
arrstack_secret_backend_role = local.config.arrstack_secret_backend_role
plugins = local.config.plugins plugins = local.config.plugins
gpg_secret_backend = local.config.gpg_secret_backend gpg_secret_backend = local.config.gpg_secret_backend
gpg_key = local.config.gpg_key gpg_key = local.config.gpg_key
+10
View File
@@ -29,6 +29,12 @@ provider "rancher" {
address = local.vault_addr address = local.vault_addr
} }
# The arrstack (arrproxy API key) secrets engine is managed through its own
# provider (same Vault server; token falls back to VAULT_TOKEN).
provider "arrstack" {
address = local.vault_addr
}
terraform { terraform {
backend "consul" { backend "consul" {
address = "https://consul.service.consul" address = "https://consul.service.consul"
@@ -59,6 +65,10 @@ terraform {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret" source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret"
version = "0.1.0" version = "0.1.0"
} }
arrstack = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-arrstack"
version = "0.2.0"
}
} }
} }
EOF EOF
+79
View File
@@ -64,6 +64,55 @@ module "auth_ldap_group" {
depends_on = [module.auth_ldap_backend] depends_on = [module.auth_ldap_backend]
} }
module "auth_oidc_backend" {
source = "./modules/auth_oidc_backend"
for_each = var.auth_oidc_backend
path = each.key
description = each.value.description
oidc_discovery_url = each.value.oidc_discovery_url
client_secret_mount = each.value.client_secret_mount
client_secret_path = each.value.client_secret_path
default_role = each.value.default_role
listing_visibility = each.value.listing_visibility
default_lease_ttl = each.value.default_lease_ttl
max_lease_ttl = each.value.max_lease_ttl
}
module "auth_oidc_role" {
source = "./modules/auth_oidc_role"
for_each = var.auth_oidc_role
backend = each.value.backend
role_name = each.value.role_name
user_claim = each.value.user_claim
groups_claim = each.value.groups_claim
oidc_scopes = each.value.oidc_scopes
bound_audiences = each.value.bound_audiences
allowed_redirect_uris = each.value.allowed_redirect_uris
token_ttl = each.value.token_ttl
token_max_ttl = each.value.token_max_ttl
# Human authorization comes from the external identity groups below, so a
# login role carrying no policies is the normal case.
token_policies = try(var.policy_auth_map[each.value.backend][each.value.role_name], [])
depends_on = [module.auth_oidc_backend]
}
module "auth_oidc_group" {
source = "./modules/auth_oidc_group"
for_each = var.auth_oidc_group
groupname = each.value.groupname
policies = var.policy_auth_map[each.value.backend][each.value.groupname]
mount_accessor = module.auth_oidc_backend[each.value.backend].accessor
depends_on = [module.auth_oidc_backend]
}
module "auth_kubernetes_backend" { module "auth_kubernetes_backend" {
source = "./modules/auth_kubernetes_backend" source = "./modules/auth_kubernetes_backend"
@@ -347,6 +396,36 @@ module "plugin" {
plugin_version = each.value.version plugin_version = each.value.version
} }
module "arrstack_secret_backend" {
source = "./modules/arrstack_secret_backend"
for_each = var.arrstack_secret_backend
path = each.key
plugin = each.value.plugin
description = each.value.description
base_url = each.value.base_url
ca_cert = each.value.ca_cert
request_timeout_seconds = each.value.request_timeout_seconds
depends_on = [module.plugin]
}
module "arrstack_secret_backend_role" {
source = "./modules/arrstack_secret_backend_role"
for_each = var.arrstack_secret_backend_role
name = each.value.name
backend = each.value.backend
apps = each.value.apps
methods = each.value.methods
ttl = each.value.ttl
max_ttl = each.value.max_ttl
depends_on = [module.arrstack_secret_backend]
}
module "gpg_secret_backend" { module "gpg_secret_backend" {
source = "./modules/gpg_secret_backend" source = "./modules/gpg_secret_backend"
@@ -0,0 +1,20 @@
# Mounts the arrstack dynamic secrets engine and writes its config via the
# vault-secrets-arrstack provider. The plugin is registered in the catalog
# separately (config/plugins/vault-plugin-secrets-arrstack.yaml). The arrproxy
# admin token is sensitive and read from KV, not stored in git:
# kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token -> key "token"
# (seeded by argocd-apps #384).
data "vault_kv_secret_v2" "admin_token" {
mount = "kv"
name = var.admin_token_kv_name
}
resource "arrstack_secret_backend" "this" {
path = var.path
plugin = var.plugin
description = var.description
base_url = var.base_url
admin_token = data.vault_kv_secret_v2.admin_token.data[var.admin_token_kv_key]
ca_cert = var.ca_cert
request_timeout_seconds = var.request_timeout_seconds
}
@@ -0,0 +1,13 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
arrstack = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-arrstack"
version = "0.2.0"
}
}
}
@@ -0,0 +1,45 @@
variable "path" {
description = "Mount path of the arrstack secrets engine (e.g. \"arrstack\")"
type = string
}
variable "plugin" {
description = "Registered plugin name/type to mount (the catalog name = mount type)"
type = string
default = "vault-plugin-secrets-arrstack"
}
variable "description" {
description = "Human-friendly description of the mount"
type = string
default = null
}
variable "base_url" {
description = "Base URL of the arrproxy front door (e.g. https://arrstack.unkin.net)"
type = string
}
variable "admin_token_kv_name" {
description = "kv-v2 secret name (relative to the \"kv\" mount) holding the seeded arrproxy admin token"
type = string
default = "kubernetes/namespace/arrstack/default/arrproxy-admin-token"
}
variable "admin_token_kv_key" {
description = "Key within the KV secret that holds the arrproxy admin token"
type = string
default = "token"
}
variable "ca_cert" {
description = "PEM CA certificate that signed the arrproxy server's TLS cert (optional; omit to use the system trust store)"
type = string
default = null
}
variable "request_timeout_seconds" {
description = "HTTP timeout in seconds for calls from the plugin to arrproxy"
type = number
default = 30
}
@@ -0,0 +1,8 @@
resource "arrstack_secret_backend_role" "this" {
backend = var.backend
name = var.name
apps = var.apps
methods = var.methods
ttl = var.ttl
max_ttl = var.max_ttl
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
arrstack = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-arrstack"
version = "0.2.0"
}
}
}
@@ -0,0 +1,33 @@
variable "name" {
description = "Name of the role"
type = string
}
variable "backend" {
description = "Mount path of the arrstack secrets engine this role belongs to"
type = string
}
variable "apps" {
description = "arr apps a generated key may access (subset of sonarr, radarr, prowlarr)"
type = list(string)
}
variable "methods" {
description = "HTTP methods a generated key is limited to (subset of GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS). Null leaves the role unrestricted"
type = set(string)
# null, not [], so a role yaml that omits methods matches the provider's null read-back and shows no drift.
default = null
}
variable "ttl" {
description = "Default lease TTL in seconds for keys generated from this role"
type = number
default = null
}
variable "max_ttl" {
description = "Maximum lease TTL in seconds for keys generated from this role"
type = number
default = null
}
@@ -0,0 +1,22 @@
# Expected keys in KV secret: client_id, client_secret (generated and written by
# terraform-authentik's oauth2 provider module, never seeded by hand).
data "vault_kv_secret_v2" "oidc_client" {
mount = var.client_secret_mount
name = var.client_secret_path
}
resource "vault_jwt_auth_backend" "oidc" {
path = var.path
type = "oidc"
description = var.description
oidc_discovery_url = var.oidc_discovery_url
oidc_client_id = data.vault_kv_secret_v2.oidc_client.data["client_id"]
oidc_client_secret = data.vault_kv_secret_v2.oidc_client.data["client_secret"]
default_role = var.default_role
tune {
default_lease_ttl = var.default_lease_ttl
max_lease_ttl = var.max_lease_ttl
listing_visibility = var.listing_visibility
}
}
@@ -0,0 +1,9 @@
output "accessor" {
description = "Accessor of the OIDC auth mount, used to bind identity group aliases"
value = vault_jwt_auth_backend.oidc.accessor
}
output "path" {
description = "Mount path of the OIDC auth backend"
value = vault_jwt_auth_backend.oidc.path
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -0,0 +1,56 @@
variable "path" {
description = "Mount path of the OIDC auth backend"
type = string
default = "oidc"
}
variable "description" {
description = "Human-readable description of the auth mount"
type = string
default = null
}
variable "oidc_discovery_url" {
description = "OIDC issuer discovery URL of the identity provider"
type = string
}
variable "client_secret_mount" {
description = "KV-v2 mount holding the OIDC client credentials"
type = string
default = "kv"
}
variable "client_secret_path" {
description = "Path within the KV-v2 mount holding client_id and client_secret"
type = string
default = "service/authentik/oidc-vault"
}
variable "default_role" {
description = "Role used when none is supplied at login (the UI/CLI default)"
type = string
default = "default"
}
variable "listing_visibility" {
description = "Specifies whether to show this mount in the UI-specific listing endpoint. Valid values are 'unauth' or 'hidden'"
type = string
default = null
validation {
condition = var.listing_visibility == null || contains(["unauth", "hidden"], var.listing_visibility)
error_message = "listing_visibility must be either 'unauth' or 'hidden'."
}
}
variable "default_lease_ttl" {
description = "Specifies the default time-to-live. If set, this overrides the global default. Must be a valid duration string"
type = string
default = null
}
variable "max_lease_ttl" {
description = "Specifies the maximum time-to-live. If set, this overrides the global default. Must be a valid duration string"
type = string
default = null
}
@@ -0,0 +1,13 @@
# External identity group: membership is asserted by the IdP through the role's
# groups_claim, matched by the alias name below.
resource "vault_identity_group" "group" {
name = var.groupname
type = "external"
policies = var.policies
}
resource "vault_identity_group_alias" "alias" {
name = var.groupname
mount_accessor = var.mount_accessor
canonical_id = vault_identity_group.group.id
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -0,0 +1,14 @@
variable "groupname" {
description = "Name of the IdP group, as it appears in the groups claim"
type = string
}
variable "policies" {
description = "List of policies to assign to the identity group"
type = list(string)
}
variable "mount_accessor" {
description = "Accessor of the OIDC auth mount the alias is bound to"
type = string
}
@@ -0,0 +1,13 @@
resource "vault_jwt_auth_backend_role" "role" {
backend = var.backend
role_name = var.role_name
role_type = "oidc"
user_claim = var.user_claim
groups_claim = var.groups_claim
oidc_scopes = var.oidc_scopes
bound_audiences = var.bound_audiences
allowed_redirect_uris = var.allowed_redirect_uris
token_ttl = var.token_ttl
token_max_ttl = var.token_max_ttl
token_policies = var.token_policies
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -0,0 +1,56 @@
variable "backend" {
description = "The unique path of the OIDC auth backend to configure"
type = string
}
variable "role_name" {
description = "The name of the role"
type = string
}
variable "user_claim" {
description = "Claim used as the entity alias name (the Vault identity of the human)"
type = string
default = "email"
}
variable "groups_claim" {
description = "Claim holding the caller's group memberships, matched against identity group aliases"
type = string
default = "ak_groups"
}
variable "oidc_scopes" {
description = "Scopes requested from the identity provider during the authorization request"
type = list(string)
default = []
}
variable "bound_audiences" {
description = "List of audiences (aud claim) accepted in the ID token"
type = list(string)
default = []
}
variable "allowed_redirect_uris" {
description = "Redirect URIs accepted for this role. Must match the provider's registered URIs exactly"
type = list(string)
}
variable "token_ttl" {
description = "The TTL period of tokens issued using this role, in seconds"
type = number
default = 3600
}
variable "token_max_ttl" {
description = "The maximum lifetime for generated tokens in number of seconds. Its current value will be referenced at renewal time."
type = number
default = 28800
}
variable "token_policies" {
description = "List of policies to assign to the role (passed from policy_auth_map). Human authorization normally comes from external identity groups instead"
type = list(string)
default = []
}
+65
View File
@@ -62,6 +62,46 @@ variable "auth_ldap_group" {
default = {} default = {}
} }
variable "auth_oidc_backend" {
description = "Map of OIDC (JWT) auth backends to create"
type = map(object({
oidc_discovery_url = string
description = optional(string)
client_secret_mount = optional(string, "kv")
client_secret_path = optional(string, "service/authentik/oidc-vault")
default_role = optional(string, "default")
listing_visibility = optional(string)
default_lease_ttl = optional(string)
max_lease_ttl = optional(string)
}))
default = {}
}
variable "auth_oidc_role" {
description = "Map of OIDC auth roles to create"
type = map(object({
role_name = string
backend = string
allowed_redirect_uris = list(string)
user_claim = optional(string, "email")
groups_claim = optional(string, "ak_groups")
oidc_scopes = optional(list(string), [])
bound_audiences = optional(list(string), [])
token_ttl = optional(number, 3600)
token_max_ttl = optional(number, 28800)
}))
default = {}
}
variable "auth_oidc_group" {
description = "Map of external identity groups bound to an OIDC auth mount"
type = map(object({
groupname = string
backend = string
}))
default = {}
}
variable "auth_kubernetes_backend" { variable "auth_kubernetes_backend" {
description = "Map of Kubernetes auth backends to create" description = "Map of Kubernetes auth backends to create"
type = map(object({ type = map(object({
@@ -316,6 +356,31 @@ variable "litellm_secret_backend_role" {
default = {} default = {}
} }
variable "arrstack_secret_backend" {
description = "Map of arrstack secret engines to create (mount + config). The arrproxy admin token is read from KV"
type = map(object({
plugin = optional(string, "vault-plugin-secrets-arrstack")
description = optional(string)
base_url = string
ca_cert = optional(string)
request_timeout_seconds = optional(number, 30)
}))
default = {}
}
variable "arrstack_secret_backend_role" {
description = "Map of arrstack roles to create"
type = map(object({
name = string
backend = string
apps = list(string)
methods = optional(set(string))
ttl = optional(number)
max_ttl = optional(number)
}))
default = {}
}
variable "plugins" { variable "plugins" {
description = "Map of plugins to import (register) in the catalog, keyed by catalog name" description = "Map of plugins to import (register) in the catalog, keyed by catalog name"
type = map(object({ type = map(object({
+27
View File
@@ -0,0 +1,27 @@
# Allow management of the arrstack secrets engine (config and roles) by the
# terraform-vault deployer.
---
rules:
- path: "arrstack/config"
capabilities:
- create
- update
- read
- delete
- path: "arrstack/roles/*"
capabilities:
- create
- update
- delete
- read
- list
- path: "arrstack/roles"
capabilities:
- read
- list
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
+12
View File
@@ -0,0 +1,12 @@
# Allow the mediamark app to mint its own Sonarr/Radarr-scoped arrproxy key.
# Bound to the dedicated "mediamark" k8s role only, never the shared "default"
# role, so no other namespace can read these creds.
---
rules:
- path: "arrstack/creds/mediamark"
capabilities:
- read
auth:
k8s/au/syd1:
- mediamark
+12
View File
@@ -0,0 +1,12 @@
# Allow the terraform-prowlarr run to mint a Prowlarr-scoped arrproxy key.
---
rules:
- path: "arrstack/creds/prowlarr"
capabilities:
- read
auth:
approle:
- terraform_prowlarr
k8s/au/syd1:
- woodpecker_terraform_prowlarr
+12
View File
@@ -0,0 +1,12 @@
# Allow the terraform-radarr run to mint a Radarr-scoped arrproxy key.
---
rules:
- path: "arrstack/creds/radarr"
capabilities:
- read
auth:
approle:
- terraform_radarr
k8s/au/syd1:
- woodpecker_terraform_radarr
+12
View File
@@ -0,0 +1,12 @@
# Allow the terraform-sonarr run to mint a Sonarr-scoped arrproxy key.
---
rules:
- path: "arrstack/creds/sonarr"
capabilities:
- read
auth:
approle:
- terraform_sonarr
k8s/au/syd1:
- woodpecker_terraform_sonarr
+19
View File
@@ -0,0 +1,19 @@
# Allow full administration of the OIDC auth backend (mount config and login
# roles), mirroring policies/auth/ldap/admin.yaml. sys/auth/* already covers
# enabling the mount itself; this covers writing auth/oidc/config and
# auth/oidc/role/*.
---
rules:
- path: "auth/oidc/*"
capabilities:
- create
- update
- read
- delete
- list
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
+16
View File
@@ -0,0 +1,16 @@
# Lets the repospawner pods mint ephemeral Gitea tokens for the repospawner
# service user, so the service authenticates to git.unkin.net as its own
# least-privilege identity. Reading gitea/creds/repospawner returns a
# lease-bound token scoped by the role (write:repository, write:issue,
# read:user -- never merge/admin).
# Kubernetes auth only: repospawner runs in-cluster and the agents AppRole is
# CIDR-bound to Ben's workstation.
---
rules:
- path: "gitea/creds/repospawner"
capabilities:
- read
auth:
k8s/au/syd1:
- repospawner
+6
View File
@@ -10,6 +10,12 @@ rules:
- list - list
- sudo - sudo
# The oidc entry is an Authentik akP-* permission group, not an LDAP group name:
# it names the external identity group under config/auth_oidc_group, so a human
# who logs in via Authentik SSO lands on exactly the policy the LDAP vault_admin
# group already carries. akR-* roles are grouping-only and never named here.
auth: auth:
ldap: ldap:
- vault_admin - vault_admin
oidc:
- akP-vault-admin
+38
View File
@@ -0,0 +1,38 @@
# Allow the deployer to manage external identity groups and their aliases, which
# is how OIDC group membership (the ak_groups claim) maps onto Vault policies.
# Both the collection endpoints and the per-id endpoints are needed: create posts
# to identity/group, subsequent reads and updates address identity/group/id/<id>.
---
rules:
- path: "identity/group"
capabilities:
- create
- update
- path: "identity/group/*"
capabilities:
- create
- update
- read
- delete
- list
- path: "identity/group-alias"
capabilities:
- create
- update
- path: "identity/group-alias/*"
capabilities:
- create
- update
- read
- delete
- list
- path: "identity/lookup/group"
capabilities:
- create
- update
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
@@ -0,0 +1,22 @@
# Allow the terraform-vault deployer to read the seeded arrproxy admin token so
# the arrstack engine config module can source it. The token is seeded by
# argocd-apps #384 at kv/kubernetes/namespace/arrstack/default/arrproxy-admin-token.
# The deployer's existing secret_backends_read policy only covers
# kv/data/service/vault/+/+/secret_backend/*, which does not match this
# kubernetes/namespace path, so this adds the minimal read grant rather than
# duplicating the secret into the litellm-style path. vault_kv_secret_v2 also
# reads the kv-v2 metadata path on every plan/apply, so grant that too.
---
rules:
- path: "kv/data/kubernetes/namespace/arrstack/default/arrproxy-admin-token"
capabilities:
- read
- path: "kv/metadata/kubernetes/namespace/arrstack/default/arrproxy-admin-token"
capabilities:
- read
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
@@ -0,0 +1,14 @@
# Lets the agents AppRole read a dedicated, scoped Authentik API token seeded by
# the operator. Automation seeds OAuth2 client secrets and LDAP outpost tokens as
# part of normal IaC workflows; fetching an outpost token requires calling the
# Authentik API, so a scoped token (not the CI admin credential) is read here.
# Mirrors the gitea/creds/unkin-agent agents-approle read grant pattern.
---
rules:
- path: "kv/data/service/authentik/agent-api-token"
capabilities:
- read
auth:
approle:
- agents
@@ -0,0 +1,21 @@
# Let the terraform-vault deployer read the OpenBao OIDC client credentials that
# Authentik's provider module generates and writes here (terraform_authentik owns
# kv/service/authentik/* — see policies/kv/service/authentik/write.yaml). The
# deployer consumes client_id/client_secret to configure the auth/oidc backend.
#
# OIDC becomes the default human auth path; approle and k8s (CI and agents) plus
# the break-glass root path are unchanged.
#
# AppRole capabilities are fixed at login, so this grant must be applied before
# the PR that adds the auth/oidc modules.
---
rules:
- path: "kv/data/service/authentik/oidc-vault"
capabilities:
- read
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
+32
View File
@@ -0,0 +1,32 @@
# Let the terraform-authentik runner manage the Authentik service KV subtree as
# IaC (vault_kv_secret_v2), starting with kv/service/authentik/agent-api-token,
# which is currently seeded by hand.
#
# Scoping: the ask was to grant only *-token paths, but Vault ACL paths support
# a trailing glob only (no infix/suffix wildcards), so the grant is the whole
# kv/service/authentik/ subtree. That is acceptable because terraform-authentik
# is the owner of every secret under kv/service/authentik/.
#
# delete is included (unlike the agents kv/kubernetes grant) so `terraform
# destroy` and resource replacement can remove a secret and its metadata
# cleanly; vault_kv_secret_v2 also reads the kv-v2 metadata path on every
# plan/apply, so metadata read/list is required for the plan to succeed.
---
rules:
- path: "kv/data/service/authentik/*"
capabilities:
- create
- read
- update
- delete
- path: "kv/metadata/service/authentik/*"
capabilities:
- read
- list
- delete
auth:
approle:
- terraform_authentik
k8s/au/syd1:
- woodpecker_terraform_authentik
@@ -0,0 +1,10 @@
# Allow reading Ceph mediafs CSI secrets
---
rules:
- path: "kv/data/service/kubernetes/au/syd1/csi/ceph-mediafs-secret"
capabilities:
- read
auth:
k8s/au/syd1:
- ceph-csi
@@ -0,0 +1,24 @@
# Allow the agents AppRole to seed the ghp secrets engine's admin credentials.
# The ghp engine (managed by the tf_vault deployer) reads its ghpsvc_ service
# token from admin_token at ghp/config creation time; that key is currently
# unseeded, so the master apply aborts on the ghp data-source read. This grants
# the agents AppRole scoped write here so an agent can seed the value. Deployer
# read is already granted via policies/kv/service/vault/secret_backends_read.yaml,
# so this only adds the write side for the agents identity.
---
rules:
# Seed value: admin_token (a ghpsvc_ service token). create/update to seed,
# read so the agent can verify what it wrote. No delete (least privilege).
- path: "kv/data/service/vault/au/syd1/secret_backend/ghp/config"
capabilities:
- create
- update
- read
# vault_kv_secret_v2 also reads the kv-v2 metadata path on every plan/apply.
- path: "kv/metadata/service/vault/au/syd1/secret_backend/ghp/config"
capabilities:
- read
auth:
approle:
- agents
@@ -0,0 +1,21 @@
# Lets the agents AppRole read and maintain a dedicated Woodpecker API token.
# Agents query the Woodpecker API to inspect pipeline runs and failing steps when
# reviewing PRs; today that token is pasted into agent config by hand. Granting
# create/update as well as read lets automation seed and rotate it in place,
# mirroring the agents-approle grant on kv/kubernetes/*. delete is excluded, as
# it is there.
---
rules:
- path: "kv/data/service/woodpecker/tokens/agents"
capabilities:
- create
- read
- update
- path: "kv/metadata/service/woodpecker/tokens/agents"
capabilities:
- read
- list
auth:
approle:
- agents
+2
View File
@@ -17,3 +17,5 @@ rules:
auth: auth:
approle: approle:
- certmanager - certmanager
k8s/au/syd1:
- puppet_certmanager
+2
View File
@@ -17,3 +17,5 @@ rules:
auth: auth:
approle: approle:
- certmanager - certmanager
k8s/au/syd1:
- puppet_certmanager
+20
View File
@@ -0,0 +1,20 @@
# Allow the terraform-rancher runner to mint an ephemeral Rancher API token
# from the "ci" role, so its Makefile can swap
# vault kv get kv/service/terraform/rancher
# for
# vault read -field=token rancher/creds/ci
# and retire the static 90-day admin token. The minted token is lease-bound
# (ttl 1h, max 8h) and deleted from Rancher on revoke; it inherits the seeded
# admin service account's RBAC, so this is the same privilege as the static
# token it replaces, just short-lived.
---
rules:
- path: "rancher/creds/ci"
capabilities:
- read
auth:
approle:
- terraform_rancher
k8s/au/syd1:
- woodpecker_terraform_rancher
@@ -1,7 +1,7 @@
# Allow signing SSH host certificates # Allow signing SSH host certificates
--- ---
rules: rules:
- path: "sshca/sign/host" - path: "sshca/sign/signhost"
capabilities: capabilities:
- create - create
- update - update
@@ -9,3 +9,5 @@ rules:
auth: auth:
approle: approle:
- sshsigner - sshsigner
k8s/au/syd1:
- puppet_sshsigner
+2
View File
@@ -9,3 +9,5 @@ rules:
auth: auth:
approle: approle:
- sshsigner - sshsigner
k8s/au/syd1:
- puppet_sshsigner
+18
View File
@@ -8,6 +8,24 @@ rules:
- delete - delete
- read - read
- list - list
# Enabling/tuning an auth mount is sudo-protected, and an exact path match
# wins over the wildcard above, so both rules repeat the full capability set.
- path: "sys/auth/oidc"
capabilities:
- create
- update
- delete
- read
- list
- sudo
- path: "sys/auth/oidc/tune"
capabilities:
- create
- update
- delete
- read
- list
- sudo
auth: auth:
approle: approle:
@@ -22,6 +22,28 @@ rules:
- "elasticsearch.k8s.elastic.co" - "elasticsearch.k8s.elastic.co"
- "kibana.k8s.elastic.co" - "kibana.k8s.elastic.co"
- "gateway.networking.k8s.io" - "gateway.networking.k8s.io"
- "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"
- "valkey.io"
- "ceph.unkin.net"
- "bind.unkin.net"
- "kea.unkin.net"
- "k8up.io"
- "grafana.integreatly.org"
- "operator.victoriametrics.com"
- "clickhouse.altinity.com"
- "clickhouse-keeper.altinity.com"
- "acme.cert-manager.io"
- "deviceplugin.intel.com"
- "fpga.intel.com"
- "autoscaling.k8s.io"
- "snapshot.storage.k8s.io"
- "groupsnapshot.storage.k8s.io"
resources: resources:
- "*" - "*"
verbs: verbs: