1 Commits

Author SHA1 Message Date
unkinben 4ec2f92ca5 Grant terraform-incus approle read on encapi environment secret
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline failed
terraform-incus dual-writes ENC data to encapi and needs the
ENCAPI_WRITE_TOKEN from Vault at apply time. Add a policy granting the
terraform_incus approle read on kv/data/kubernetes/namespace/encapi/default/environment.

Prerequisite for terraform-incus PR #39.
2026-07-24 22:55:09 +10:00
222 changed files with 58 additions and 4708 deletions
-15
View File
@@ -22,18 +22,3 @@ repos:
"-d {extends: relaxed, rules: {line-length: disable}, ignore: chart}",
"-s",
]
- repo: local
hooks:
- id: vault-yaml-tests
name: vault yaml definitions pass their unit tests
entry: python3 -m unittest discover -s tests -t .
language: python
additional_dependencies: [pyyaml, pydantic]
pass_filenames: false
always_run: true
- id: tofu-module-tests
name: tofu module tests pass
entry: sh -c 'for m in auth_jwt_backend auth_jwt_role; do (cd modules/vault_cluster/modules/$m && tofu init -backend=false -input=false >/dev/null && tofu test) || exit 1; done'
language: system
pass_filenames: false
always_run: true
+2 -3
View File
@@ -4,12 +4,11 @@ when:
steps:
- name: apply
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/opentofu:0.1.0
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
- dnf install vault -y
- make plan
- make apply
backend_options:
+2 -3
View File
@@ -3,12 +3,11 @@ when:
steps:
- name: plan
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/opentofu:0.1.0
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
- dnf install vault -y
- make plan
backend_options:
kubernetes:
+1 -1
View File
@@ -3,7 +3,7 @@ when:
steps:
- name: pre-commit
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/opentofu:0.1.0
image: git.unkin.net/unkin/almalinux9-opentofu:20260606
commands:
- uvx pre-commit run --all-files
backend_options:
+2 -6
View File
@@ -1,4 +1,4 @@
.PHONY: init plan apply format test
.PHONY: init plan apply format
VAULT_AUTH_METHOD ?= approle
VAULT_K8S_ROLE ?= woodpecker_terraform_vault
@@ -22,16 +22,12 @@ init:
plan: init
@$(call vault_env) && \
terragrunt run --all --parallelism 4 --non-interactive plan -- -lock=false
terragrunt run --all --parallelism 4 --non-interactive plan
apply: init
@$(call vault_env) && \
terragrunt run --all --parallelism 2 --non-interactive apply
test:
@uv run --with pyyaml --with pydantic python -m unittest discover -s tests -t .
@for m in auth_jwt_backend auth_jwt_role; do (cd modules/vault_cluster/modules/$$m && tofu init -backend=false -input=false >/dev/null && tofu test) || exit 1; done
format:
@echo "Formatting OpenTofu files..."
@tofu fmt -recursive .
-32
View File
@@ -35,35 +35,3 @@ You can check the Consul KV store by accessing the Consul UI or using the consul
```bash
consul kv get terraform/state
```
# GitHub Actions JWT roles
`auth/jwt/github/iuclabs` trusts GitHub Actions OIDC tokens. One org-wide role, `default` (`config/auth_jwt_role/jwt/github/iuclabs/default.yaml`, the mount's `default_role`, so workflows needn't name it), accepts any IUCLabs repo for `push`, `release`, `schedule` and `pull_request` runs on `refs/heads/*`, `refs/tags/*` or `refs/pull/*/merge`. Role validation rejects any other event (`pull_request_target`, `workflow_run`, comment events), any other ref glob, and globs in any other claim. Fork pull requests can't mint OIDC tokens unless "Send write tokens to workflows from pull requests" is enabled on the repo.
Workflows log in with audience `https://vault.service.consul:8200`:
```yaml
permissions:
id-token: write
contents: read
steps:
- uses: hashicorp/vault-action@v3
with:
url: https://vault.service.consul:8200
method: jwt
path: jwt/github/iuclabs
jwtGithubAudience: https://vault.service.consul:8200
secrets: |
kv/data/github/IUCLabs/<repo>/ref/refs/heads/main/.secrets/deploy token | DEPLOY_TOKEN
```
The role attaches a templated read-only policy, so each run reads only its own KV-v2 secrets:
- `kv/github/<org>/<repo>/event/<event_name>/*` is not ref-scoped: any run of that event in the repo, on any branch or tag a writer pushes, can read it; keep it for secrets every such run may use
- `kv/github/<org>/<repo>/ref/<full ref>/.secrets/*`, e.g. `ref/refs/heads/main/.secrets/`, `ref/refs/tags/v1.2.3/.secrets/`; git refnames can't contain a `.`-prefixed component, so branch `x` can never read branch `x/y`'s secrets
On OpenBao older than the fork's v2.6, a ref path component of exactly `+` acts as a single-segment wildcard in the templated policy. Public IUCLabs repos block such refs via a terraform-git ruleset; private repos are not protected, so a writer in a private repo can read that repo's other refs' secrets.
`user_claim` is `jti`, so every run creates its own Vault identity entity; Vault never garbage-collects these, so entity count grows with CI volume.
Extra policies are granted via a policy yaml `auth: {jwt/github/iuclabs: [default]}`; the KV read policy is attached automatically.
@@ -1,9 +0,0 @@
# 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
@@ -1,9 +0,0 @@
---
# 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.
@@ -1,17 +0,0 @@
---
# 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.
@@ -1,7 +0,0 @@
---
# 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.
@@ -1,7 +0,0 @@
---
# 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.
@@ -1,7 +0,0 @@
---
# 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.
@@ -1,6 +0,0 @@
token_ttl: 3600
token_max_ttl: 14400
bind_secret_id: false
token_bound_cidrs:
- "10.10.12.200/32"
use_deterministic_role_id: true
@@ -1,9 +0,0 @@
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
@@ -1,9 +0,0 @@
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
@@ -1,9 +0,0 @@
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
@@ -1,7 +0,0 @@
# GitHub Actions OIDC for the IUCLabs org; the org-wide role lives in
# config/auth_jwt_role/jwt/github/iuclabs/.
---
oidc_discovery_url: "https://token.actions.githubusercontent.com"
bound_issuer: "https://token.actions.githubusercontent.com"
description: "GitHub Actions OIDC - IUCLabs"
default_role: "default"
@@ -1,22 +0,0 @@
---
bound_audiences: ["https://vault.service.consul:8200"]
bound_claims:
repository_owner: IUCLabs
event_name: push,release,schedule,pull_request
ref: refs/heads/*,refs/tags/*,refs/pull/*/merge
claim_mappings:
repository: repository
repository_owner: repository_owner
ref: ref
ref_type: ref_type
event_name: event_name
workflow: workflow
job_workflow_ref: job_workflow_ref
run_id: run_id
run_attempt: run_attempt
sha: sha
actor: actor
environment: environment
user_claim: jti
token_ttl: 900
token_max_ttl: 3600
@@ -1,7 +0,0 @@
bound_service_account_names:
- logarchiver
bound_service_account_namespaces:
- logging
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -1,10 +0,0 @@
---
# 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
@@ -1,11 +0,0 @@
---
# 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
@@ -1,10 +0,0 @@
---
# 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
@@ -1,7 +0,0 @@
bound_service_account_names:
- repospawner
bound_service_account_namespaces:
- repospawner
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -1,10 +0,0 @@
---
# Kubernetes auth role for rspamd: the rspamd service account in the mail
# namespace, used by VaultDynamicSecret to read litellm/creds/rspamd.
bound_service_account_names:
- rspamd
bound_service_account_namespaces:
- mail
token_ttl: 600
token_max_ttl: 600
audience: vault
@@ -1,15 +0,0 @@
bound_service_account_names:
- default
- arrproxy-ci
- autobackup-operator-ci
- ghp
- jellyfin-plugin-sso
- kea-operator-ci
- mediamark-ci
- plugin-docker-buildx
- repospawner-ci
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
@@ -1,7 +0,0 @@
bound_service_account_names:
- terraform-binarylane
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
@@ -1,7 +0,0 @@
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
@@ -1,7 +0,0 @@
bound_service_account_names:
- terraform-netbox
bound_service_account_namespaces:
- woodpecker
token_ttl: 600
token_max_ttl: 600
audience: https://kubernetes.default.svc.cluster.local
-17
View File
@@ -1,17 +0,0 @@
# 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
@@ -1,5 +0,0 @@
---
# 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
@@ -1,25 +0,0 @@
# 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
-128
View File
@@ -97,40 +97,6 @@ locals {
})
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_jwt_backend = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_jwt_backend/", ""), ".yaml") => content
if startswith(file_path, "auth_jwt_backend/")
}
auth_jwt_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_jwt_role/", ""), ".yaml") => merge(content, {
role_name = trimsuffix(basename(file_path), ".yaml")
backend = dirname(replace(file_path, "auth_jwt_role/", ""))
})
if startswith(file_path, "auth_jwt_role/")
}
auth_kubernetes_backend = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "auth_kubernetes_backend/", ""), ".yaml") => content
@@ -232,19 +198,6 @@ locals {
})
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, {
@@ -286,86 +239,5 @@ locals {
})
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/")
}
identity_oidc = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "identity_oidc/")
}
identity_oidc_key = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "identity_oidc_key/")
}
identity_oidc_role = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "identity_oidc_role/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
key = dirname(replace(file_path, "identity_oidc_role/", ""))
})
if startswith(file_path, "identity_oidc_role/")
}
identity_oidc_client = {
for file_path, content in local.all_configs :
trimsuffix(replace(file_path, "identity_oidc_client/", ""), ".yaml") => merge(content, {
name = trimsuffix(basename(file_path), ".yaml")
key = dirname(replace(file_path, "identity_oidc_client/", ""))
})
if startswith(file_path, "identity_oidc_client/")
}
identity_oidc_scope = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "identity_oidc_scope/")
}
identity_oidc_assignment = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "identity_oidc_assignment/")
}
identity_oidc_provider = {
for file_path, content in local.all_configs :
trimsuffix(basename(file_path), ".yaml") => content
if startswith(file_path, "identity_oidc_provider/")
}
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/")
}
}
}
@@ -1,5 +0,0 @@
consul_roles:
- terraform-binarylane
ttl: 120
max_ttl: 300
datacenters: []
@@ -1,5 +0,0 @@
consul_roles:
- terraform-enc
ttl: 120
max_ttl: 300
datacenters: []
@@ -1,5 +0,0 @@
consul_roles:
- terraform-netbox
ttl: 120
max_ttl: 300
datacenters: []
-15
View File
@@ -1,15 +0,0 @@
# Mounts the ghp token secrets engine at "ghp" and writes its config.
# The seeded ghp service token is sensitive and read from KV, not stored here:
# kv/service/vault/au/syd1/secret_backend/ghp/config
# -> key: admin_token (required) the shared ghpsvc_... service token
#
# admin_token is a static shared secret provisioned into KV by an operator. The
# SAME token value must also be present in the running ghp deployment's accepted
# service tokens (GHP_AUTH_SERVICE_TOKENS) so ghp ACCEPTS what this engine
# PRESENTS. ghp has no rotate endpoint, so the engine never rotates it in place;
# the mount uses ignore_changes=[admin_token], making the KV seed create-only
# (re-reading a stale KV value never re-pushes it to a live mount).
description: "ghp ephemeral scoped agent token engine"
base_url: "https://ghp.unkin.net"
tls_skip_verify: false
request_timeout_seconds: 30
-11
View File
@@ -1,11 +0,0 @@
# Mounts the gitea token secrets engine at "gitea" and writes its config.
# The seeded site-admin credentials are sensitive and read from KV, not stored
# here:
# kv/service/vault/au/syd1/secret_backend/gitea/config
# -> keys: admin_username (required), admin_password (required)
# Populate that KV path with a purpose-built Gitea site-admin bot (2FA disabled)
# BEFORE applying, then run `vault write -f gitea/config/rotate-root` after the
# first apply so only Vault holds the admin password.
description: "Gitea ephemeral scoped access token engine"
gitea_url: "https://git.unkin.net"
request_timeout_seconds: 30
@@ -1,19 +0,0 @@
# 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
@@ -1,17 +0,0 @@
# Role minting ephemeral tokens for the teabot-implementer bot user.
# The implementer clones/pushes code and opens pull requests, so it gets write
# on repositories (clone + push + PR create) and write on issues (PR/issue
# comments). Read is implied by write. No admin/org/user-write scopes.
# read:user is required because tea (and most API clients) validate the login
# via GET /api/v1/user, which 403s without it (verified against a minted token).
# Reading gitea/creds/teabot-implementer mints a lease-bound token deleted from
# Gitea on revoke/expiry.
---
username: teabot-implementer
scopes:
- write:repository
- write:issue
- read:user
token_name_prefix: vault-teabot-implementer
ttl: 3600 # 1h
max_ttl: 14400 # 4h
@@ -1,17 +0,0 @@
# Role minting ephemeral tokens for the teabot-reviewer bot user.
# The reviewer reads code and posts pull-request reviews/comments, so it gets
# read on repositories (fetch diffs) and write on issues (PR reviews + issue/PR
# comments). No repository-write, admin, org, or user-write scopes.
# read:user is required because tea (and most API clients) validate the login
# via GET /api/v1/user, which 403s without it (verified against a minted token).
# Reading gitea/creds/teabot-reviewer mints a lease-bound token deleted from
# Gitea on revoke/expiry.
---
username: teabot-reviewer
scopes:
- read:repository
- write:issue
- read:user
token_name_prefix: vault-teabot-reviewer
ttl: 3600 # 1h
max_ttl: 14400 # 4h
@@ -1,20 +0,0 @@
# Role minting ephemeral tokens for the unkin-agent bot user -- the shared
# identity Ben's AI coding agents use to submit work. The agent clones/pushes
# code and opens pull requests, so it gets write on repositories (clone + push +
# PR create) and write on issues (PR/issue comments). Read is implied by write.
# No admin/org/user-write scopes, so it can never merge via API privilege; merge
# is blocked separately by branch protection (merge whitelist = Owners).
# read:user is required because tea (and most API clients) validate the login
# via GET /api/v1/user, which 403s without it.
# Reading gitea/creds/unkin-agent mints a lease-bound token deleted from Gitea
# on revoke/expiry. Consumed by the "agents" AppRole (see
# policies/gitea/creds/unkin-agent.yaml).
---
username: unkin-agent
scopes:
- write:repository
- write:issue
- read:user
token_name_prefix: vault-unkin-agent
ttl: 3600 # 1h
max_ttl: 14400 # 4h
-8
View File
@@ -1,8 +0,0 @@
# config/gpg_key/gpg/logarchive.yaml
# OpenPGP key in the gpg engine for the logarchiver service. The private key
# stays in Vault; logarchiver reads only the exported public key
# (gpg/keys/logarchive) to encrypt archived logs, and retrieval delegates
# decryption back to gpg/decrypt/logarchive. Key name = "logarchive", backend = "gpg".
algorithm: rsa-4096
identity: "logarchive <logarchive@unkin.net>"
exportable: false
-2
View File
@@ -1,2 +0,0 @@
---
issuer: https://vault.service.consul:8200
@@ -1,2 +0,0 @@
---
algorithm: RS256
-2
View File
@@ -1,2 +0,0 @@
---
algorithm: RS256
@@ -1,3 +0,0 @@
---
client_id: artifactapi
ttl: 900
@@ -1,3 +0,0 @@
---
client_id: artifactapi
ttl: 900
@@ -1,3 +0,0 @@
---
client_id: encapi-testing
ttl: 900
@@ -1,3 +0,0 @@
---
client_id: encapi
ttl: 900
@@ -1,4 +0,0 @@
allowed_kubernetes_namespaces:
- "cert-manager"
kubernetes_role_type: "Role"
extra_labels: {}
@@ -1,4 +0,0 @@
allowed_kubernetes_namespaces:
- "dhcp-system"
kubernetes_role_type: "Role"
extra_labels: {}
@@ -1,4 +0,0 @@
allowed_kubernetes_namespaces:
- "bind-system"
service_account_name: "agent-dns"
extra_labels: {}
@@ -1,6 +0,0 @@
allowed_kubernetes_namespaces:
- "vm-system"
- "observability"
- "logging"
kubernetes_role_type: "Role"
extra_labels: {}
@@ -1,4 +0,0 @@
allowed_kubernetes_namespaces:
- "cephrgw-system"
kubernetes_role_type: "Role"
extra_labels: {}
@@ -1,6 +1,7 @@
---
models:
- claude-haiku-4-5
- claude-sonnet-4-6
max_budget: 50
ttl: 3600 # seconds (1h)
max_ttl: 86400 # seconds (24h)
-48
View File
@@ -1,48 +0,0 @@
# Mounts the netbox token secrets engine at "netbox" and writes its config.
# The seeded NetBox admin token is sensitive and read from KV, not stored here:
# kv/service/vault/au/syd1/secret_backend/netbox/config
# -> key: admin_token (required) the SINGLE static admin credential
#
# admin_token must be a BARE NetBox token with NO scheme prefix: do not prepend
# "Bearer " or "Token ". NetBox infers the version from the value's nbt_ prefix,
# so one bare token authenticates under either scheme; the plugin adds the keyword
# itself. A prefixed value yields a malformed header + 403.
#
# Populate admin_token with a purpose-built NetBox superuser token (add_user +
# add_token + grant_token, or superuser) BEFORE applying, then run
# `vault write -f netbox/config/rotate` after the first apply so only Vault holds
# the live admin token.
#
# Only ONE static admin token exists. netbox_user_management does NOT re-read this
# token; instead the engine mints it a short-lived user-admin token per apply from
# netbox/roles/vault-user-mgmt (see user_mgmt_username below), so rotating
# admin_token never breaks user management. Set user_mgmt_username to the
# pre-existing NetBox superuser the static admin_token belongs to (or another
# superuser). Leaving it unset falls back to using admin_token directly, which is
# only a bootstrap/degraded path and breaks after rotation.
#
# Bootstrap ordering: the vault-user-mgmt role must exist before the netbox
# provider is configured from its creds, so on a brand-new backend apply the mount
# + role first (e.g. `tofu apply -target=...netbox_secret_backend
# -target=...netbox_user_mgmt_role`) once, then apply normally.
#
# token_version 2 is the NetBox 4.6.5 default and requires API_TOKEN_PEPPERS to
# be configured on the NetBox server; set token_version: 1 here if the server
# has no peppers. token_version does NOT change how the plugin authenticates its
# own calls (that scheme comes from the admin_token value's nbt_ prefix); it only
# sets the version of the per-user tokens the engine mints. It must still MATCH
# the admin_token kind: nbt_ v2 token -> token_version 2; bare v1 token -> 1.
#
# The mount uses ignore_changes=[token], so editing KV alone does NOT reach the
# live mount. To push a corrected/rotated admin token into a running mount:
# vault write netbox/config token=<BARE_TOKEN>
# (netbox_url/token_version are preserved on a partial update). Do NOT -replace
# the mount to force a re-read - that recreates it and drops all roles/config.
description: "NetBox ephemeral scoped API token engine"
netbox_url: "https://netbox.k8s.syd1.au.unkin.net"
token_version: 2
request_timeout_seconds: 30
# Set to the pre-existing NetBox superuser admin_token belongs to, to mint the
# user-management credential dynamically (recommended). Until set, user management
# uses admin_token directly and a check block warns that rotation will break it.
# user_mgmt_username: "vault-netbox-admin"
@@ -1,44 +0,0 @@
# Single declarative source for the terraform-netbox NetBox service identity. The
# filename stem is the engine role name AND the NetBox username (1:1); config.hcl
# derives both from it, so neither is repeated below. Creating this file creates
# the user: the netbox_user_management module synthesizes the NetBox user + object
# permissions from the permissions block, and the engine role mints ephemeral
# tokens for that same user. write_enabled true because terraform-netbox manages
# NetBox IPAM/DCIM; very short TTLs because a token is minted per plan/apply and
# revoked when the run's lease ends.
#
# extras.customfield is what lets the runner declare the custom fields bootapi
# reads (pxe_enabled, provision_template, domain, gateway, nameservers), and
# dcim.platform is how a device gets the platform slug bootapi selects a distro
# catalog entry by.
---
write_enabled: true
ttl: 120 # 2m
max_ttl: 300 # 5m
permissions:
- name: terraform-netbox-write
object_types:
- ipam.prefix
- ipam.ipaddress
- ipam.iprange
- ipam.role
- dcim.device
- dcim.devicerole
- dcim.devicetype
- dcim.interface
- dcim.macaddress
- dcim.manufacturer
- dcim.platform
- extras.customfield
- extras.tag
actions:
- view
- add
- change
- delete
# Sites are looked up with a data source, never created here.
- name: terraform-netbox-read
object_types:
- dcim.site
actions:
- view
@@ -1,19 +0,0 @@
# 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.
#
# Terraform retunes mounts onto a bumped version; after apply, run
# `bao write sys/plugins/reload/backend mounts=arrstack scope=global`.
#
# Once versioned, keep `version` set: dropping it leaves the mount pinned to a
# deregistered version.
type: secret
command: vault-plugin-secrets-arrstack
version: "0.2.0"
sha256: "9ea7f16013b3bcbadfcb15b08fd1545f8069893cf864bfdaa57e7e7e72612fa1"
@@ -1,11 +0,0 @@
# config/plugins/vault-plugin-secrets-ghp.yaml
# Imports (registers) the ghp secrets plugin in the catalog. Filename =
# catalog name = mount type. The binary is installed on the OpenBao nodes by
# Puppet (openbao-plugin-secrets-ghp RPM ->
# /opt/openbao-plugins/vault-plugin-secrets-ghp).
#
# sha256 pins the released v0.1.0 binary; bump it in lockstep with any RPM
# upgrade or OpenBao will refuse to launch the plugin.
type: secret
command: vault-plugin-secrets-ghp
sha256: "85761421cd532788ed28fb57e93d3868f3577320a538289936d9ed3be5f396de"
@@ -1,11 +0,0 @@
# config/plugins/vault-plugin-secrets-gitea.yaml
# Imports (registers) the gitea secrets plugin in the catalog. Filename =
# catalog name = mount type. The binary is installed on the OpenBao nodes by
# Puppet (openbao-plugin-secrets-gitea RPM ->
# /opt/openbao-plugins/vault-plugin-secrets-gitea).
#
# sha256 pins the released v0.1.0 binary; bump it in lockstep with any RPM
# upgrade or OpenBao will refuse to launch the plugin.
type: secret
command: vault-plugin-secrets-gitea
sha256: "8f67fbc216effada5fd7399888a710b62fad83be0b31761a439e7dec3d56509b"
@@ -1,11 +0,0 @@
# config/plugins/vault-plugin-secrets-netbox.yaml
# Imports (registers) the netbox secrets plugin in the catalog. Filename =
# catalog name = mount type. The binary is installed on the OpenBao nodes by
# Puppet (openbao-plugin-secrets-netbox RPM ->
# /opt/openbao-plugins/vault-plugin-secrets-netbox).
#
# sha256 pins the released v0.1.0 binary; bump it in lockstep with any RPM
# upgrade or OpenBao will refuse to launch the plugin.
type: secret
command: vault-plugin-secrets-netbox
sha256: "362b7f6c9e21179ad51d2d810684d9387fe50e3a1887f171700122a0b2a05cef"
@@ -3,8 +3,6 @@ algorithm_signer: rsa-sha2-256
ttl: 315360000 # 87600 * 3600
allow_host_certificates: true
allow_user_certificates: false
# "*" must be the sole entry: OpenBao only treats allowed_domains as unrestricted
# when the whole string is "*", and otherwise matches entries exactly or by suffix.
allowed_domains: "*"
allowed_domains: "main.unkin.net,consul"
allow_subdomains: true
allow_bare_domains: false
-33
View File
@@ -39,12 +39,6 @@ locals {
for backend_name, _ in local.config.consul_secret_backend :
backend_name => replace(backend_name, "/", "_")
}
# Same sanitized alias mapping for the NetBox providers.
netbox_backend_aliases = {
for backend_name, _ in local.config.netbox_secret_backend :
backend_name => replace(backend_name, "/", "_")
}
}
terraform {
@@ -60,11 +54,6 @@ inputs = {
auth_approle_role = local.config.auth_approle_role
auth_ldap_backend = local.config.auth_ldap_backend
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_jwt_backend = local.config.auth_jwt_backend
auth_jwt_role = local.config.auth_jwt_role
auth_kubernetes_backend = local.config.auth_kubernetes_backend
auth_kubernetes_role = local.config.auth_kubernetes_role
kv_secret_backend = local.config.kv_secret_backend
@@ -81,8 +70,6 @@ inputs = {
pki_mount_only = local.config.pki_mount_only
litellm_secret_backend = local.config.litellm_secret_backend
litellm_secret_backend_role = local.config.litellm_secret_backend_role
arrstack_secret_backend = local.config.arrstack_secret_backend
arrstack_secret_backend_role = local.config.arrstack_secret_backend_role
plugins = local.config.plugins
gpg_secret_backend = local.config.gpg_secret_backend
gpg_key = local.config.gpg_key
@@ -91,30 +78,10 @@ inputs = {
rancher_secret_backend_service_account = local.config.rancher_secret_backend_service_account
rancher_secret_backend_role = local.config.rancher_secret_backend_role
gitea_secret_backend = local.config.gitea_secret_backend
gitea_secret_backend_role = local.config.gitea_secret_backend_role
netbox_secret_backend = local.config.netbox_secret_backend
netbox_secret_backend_role = local.config.netbox_secret_backend_role
ghp_secret_backend = local.config.ghp_secret_backend
ghp_secret_backend_role = local.config.ghp_secret_backend_role
identity_oidc = local.config.identity_oidc
identity_oidc_key = local.config.identity_oidc_key
identity_oidc_role = local.config.identity_oidc_role
identity_oidc_client = local.config.identity_oidc_client
identity_oidc_scope = local.config.identity_oidc_scope
identity_oidc_assignment = local.config.identity_oidc_assignment
identity_oidc_provider = local.config.identity_oidc_provider
# Pass policy maps to vault_cluster module
policy_auth_map = local.policies.policy_auth_map
policy_rules_map = local.policies.policy_rules_map
# Pass sanitized consul backend aliases for provider configuration
consul_backend_aliases = local.consul_backend_aliases
# Pass sanitized netbox backend aliases for provider configuration
netbox_backend_aliases = local.netbox_backend_aliases
}
-10
View File
@@ -29,12 +29,6 @@ provider "rancher" {
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 {
backend "consul" {
address = "https://consul.service.consul"
@@ -65,10 +59,6 @@ terraform {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret"
version = "0.1.0"
}
arrstack = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-arrstack"
version = "0.2.0"
}
}
}
EOF
-404
View File
@@ -64,89 +64,6 @@ module "auth_ldap_group" {
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_jwt_backend" {
source = "./modules/auth_jwt_backend"
for_each = var.auth_jwt_backend
path = each.key
description = each.value.description
oidc_discovery_url = each.value.oidc_discovery_url
bound_issuer = each.value.bound_issuer
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_jwt_role" {
source = "./modules/auth_jwt_role"
for_each = var.auth_jwt_role
backend = each.value.backend
role_name = each.value.role_name
user_claim = each.value.user_claim
bound_audiences = each.value.bound_audiences
backend_accessor = module.auth_jwt_backend[each.value.backend].accessor
bound_claims = each.value.bound_claims
claim_mappings = each.value.claim_mappings
token_ttl = each.value.token_ttl
token_max_ttl = each.value.token_max_ttl
token_policies = try(var.policy_auth_map[each.value.backend][each.value.role_name], [])
depends_on = [module.auth_jwt_backend]
}
module "auth_kubernetes_backend" {
source = "./modules/auth_kubernetes_backend"
@@ -382,7 +299,6 @@ module "kubernetes_secret_backend_role" {
allowed_kubernetes_namespaces = each.value.allowed_kubernetes_namespaces
kubernetes_role_type = each.value.kubernetes_role_type
extra_labels = each.value.extra_labels
service_account_name = each.value.service_account_name
depends_on = [module.kubernetes_secret_backend]
}
@@ -430,73 +346,6 @@ module "plugin" {
plugin_version = each.value.version
}
locals {
plugin_mounts = merge(
{ for path, mount in var.arrstack_secret_backend : path => mount.plugin },
{ for path, mount in var.ghp_secret_backend : path => mount.plugin },
{ for path, mount in var.gitea_secret_backend : path => mount.plugin },
{ for path, mount in var.gpg_secret_backend : path => mount.plugin },
{ for path, mount in var.litellm_secret_backend : path => mount.plugin },
{ for path, mount in var.netbox_secret_backend : path => mount.plugin },
{ for path, mount in var.rancher_secret_backend : path => mount.plugin },
)
}
# Pins each plugin-backed mount to its plugin's catalog version. Unversioned
# catalog entries need no pin (OpenBao rejects an empty plugin_version tune).
module "mount_plugin_version" {
source = "./modules/mount_plugin_version"
for_each = {
for path, plugin in local.plugin_mounts : path => var.plugins[plugin].version
if try(var.plugins[plugin].version, null) != null
}
path = each.key
plugin_version = each.value
depends_on = [
module.plugin,
module.arrstack_secret_backend,
module.ghp_secret_backend,
module.gitea_secret_backend,
module.gpg_secret_backend,
module.litellm_secret_backend,
module.netbox_secret_backend,
module.rancher_secret_backend,
]
}
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" {
source = "./modules/gpg_secret_backend"
@@ -572,157 +421,6 @@ module "rancher_secret_backend_role" {
depends_on = [module.rancher_secret_backend_service_account]
}
module "gitea_secret_backend" {
source = "./modules/gitea_secret_backend"
for_each = var.gitea_secret_backend
path = each.key
plugin = each.value.plugin
description = each.value.description
gitea_url = each.value.gitea_url
country = var.country
region = var.region
ca_cert = each.value.ca_cert
tls_skip_verify = each.value.tls_skip_verify
request_timeout_seconds = each.value.request_timeout_seconds
depends_on = [module.plugin]
}
module "gitea_secret_backend_role" {
source = "./modules/gitea_secret_backend_role"
for_each = var.gitea_secret_backend_role
backend = each.value.backend
name = each.value.name
username = each.value.username
scopes = each.value.scopes
token_name_prefix = each.value.token_name_prefix
ttl = each.value.ttl
max_ttl = each.value.max_ttl
depends_on = [module.gitea_secret_backend]
}
module "netbox_secret_backend" {
source = "./modules/netbox_secret_backend"
for_each = var.netbox_secret_backend
path = each.key
plugin = each.value.plugin
description = each.value.description
netbox_url = each.value.netbox_url
token_version = each.value.token_version
country = var.country
region = var.region
ca_cert = each.value.ca_cert
tls_skip_verify = each.value.tls_skip_verify
request_timeout_seconds = each.value.request_timeout_seconds
depends_on = [module.plugin]
}
# Dedicated engine role that mints an ephemeral, user-admin-capable token for the
# pre-existing NetBox superuser named on each backend (user_mgmt_username).
# netbox_user_management reads netbox/creds/vault-user-mgmt from it, so it
# authenticates with a short-lived Vault-minted token derived from the single
# static admin token - never a second static credential, and unaffected by
# rotation of the engine's admin seed. Created before user management so the role
# exists when it reads creds.
module "netbox_user_mgmt_role" {
source = "./modules/netbox_secret_backend_role"
for_each = { for k, v in var.netbox_secret_backend : k => v if v.user_mgmt_username != null }
backend = each.key
name = "vault-user-mgmt"
netbox_username = each.value.user_mgmt_username
write_enabled = true
description = "Ephemeral user-admin token for netbox_user_management (Vault-minted per apply)"
ttl = 600
max_ttl = 1200
depends_on = [module.netbox_secret_backend]
}
# Declaratively manage the NetBox service users + object permissions the engine
# roles mint tokens for, authenticating with the Vault-minted user-admin token
# above (mirrors consul_acl_management). Consumes the SAME role config as
# netbox_secret_backend_role: one file per identity, filename-derived username,
# inline permissions.
module "netbox_user_management" {
source = "./modules/netbox_user_management"
country = var.country
region = var.region
netbox_backends = var.netbox_secret_backend
netbox_roles = var.netbox_secret_backend_role
netbox_backend_aliases = var.netbox_backend_aliases
# This module declares its own netbox provider, so it is a legacy module and
# cannot take depends_on. Ordering vs the vault-user-mgmt role is not needed on
# steady state (the role pre-exists, so reading its creds succeeds regardless);
# on first enablement the role must be created first via the one-time targeted
# bootstrap documented in config/netbox_secret_backend/netbox.yaml.
}
module "netbox_secret_backend_role" {
source = "./modules/netbox_secret_backend_role"
for_each = var.netbox_secret_backend_role
backend = each.value.backend
name = each.value.name
netbox_username = each.value.netbox_username
netbox_user_id = each.value.netbox_user_id
write_enabled = each.value.write_enabled
description = each.value.description
ttl = each.value.ttl
max_ttl = each.value.max_ttl
depends_on = [module.netbox_secret_backend, module.netbox_user_management]
}
module "ghp_secret_backend" {
source = "./modules/ghp_secret_backend"
for_each = var.ghp_secret_backend
path = each.key
plugin = each.value.plugin
description = each.value.description
base_url = each.value.base_url
country = var.country
region = var.region
ca_cert = each.value.ca_cert
tls_skip_verify = each.value.tls_skip_verify
request_timeout_seconds = each.value.request_timeout_seconds
depends_on = [module.plugin]
}
module "ghp_secret_backend_role" {
source = "./modules/ghp_secret_backend_role"
for_each = var.ghp_secret_backend_role
backend = each.value.backend
name = each.value.name
token_type = each.value.token_type
installation_id = each.value.installation_id
app_record_id = each.value.app_record_id
repositories = each.value.repositories
scopes = each.value.scopes
session_prefix = each.value.session_prefix
ttl = each.value.ttl
max_ttl = each.value.max_ttl
depends_on = [module.ghp_secret_backend]
}
module "vault_policy" {
source = "./modules/vault_policy"
@@ -754,105 +452,3 @@ module "pki_mount_only" {
delta_rebuild_interval = each.value.delta_rebuild_interval
}
module "identity_oidc" {
source = "./modules/identity_oidc"
for_each = var.identity_oidc
issuer = each.value.issuer
}
module "identity_oidc_key" {
source = "./modules/identity_oidc_key"
for_each = var.identity_oidc_key
name = each.key
algorithm = each.value.algorithm
rotation_period = each.value.rotation_period
verification_ttl = each.value.verification_ttl
depends_on = [module.identity_oidc]
}
module "identity_oidc_role" {
source = "./modules/identity_oidc_role"
for_each = var.identity_oidc_role
name = each.value.name
key = each.value.key
template = each.value.template
ttl = each.value.ttl
client_id = each.value.client_id
depends_on = [module.identity_oidc, module.identity_oidc_key]
}
module "identity_oidc_assignment" {
source = "./modules/identity_oidc_assignment"
for_each = var.identity_oidc_assignment
name = each.key
entity_ids = each.value.entity_ids
group_ids = each.value.group_ids
}
module "identity_oidc_client" {
source = "./modules/identity_oidc_client"
for_each = var.identity_oidc_client
name = each.value.name
key = each.value.key
redirect_uris = each.value.redirect_uris
assignments = each.value.assignments
client_type = each.value.client_type
id_token_ttl = each.value.id_token_ttl
access_token_ttl = each.value.access_token_ttl
depends_on = [module.identity_oidc, module.identity_oidc_key, module.identity_oidc_assignment]
}
module "identity_oidc_key_allowed_client_id" {
source = "./modules/identity_oidc_key_allowed_client_id"
for_each = merge(
{ for k, v in var.identity_oidc_role : "role/${k}" => { key = v.key, client_id = module.identity_oidc_role[k].client_id } },
{ for k, v in var.identity_oidc_client : "client/${k}" => { key = v.key, client_id = module.identity_oidc_client[k].client_id } },
)
key_name = each.value.key
allowed_client_id = each.value.client_id
depends_on = [module.identity_oidc_key]
}
module "identity_oidc_scope" {
source = "./modules/identity_oidc_scope"
for_each = var.identity_oidc_scope
name = each.key
description = each.value.description
template = each.value.template
}
module "identity_oidc_provider" {
source = "./modules/identity_oidc_provider"
for_each = var.identity_oidc_provider
name = each.key
issuer_host = each.value.issuer_host
https_enabled = each.value.https_enabled
scopes_supported = each.value.scopes_supported
allowed_client_ids = [
for c in each.value.allowed_clients :
c == "*" ? c : one([for k, v in var.identity_oidc_client : module.identity_oidc_client[k].client_id if v.name == c])
]
depends_on = [module.identity_oidc, module.identity_oidc_scope, module.identity_oidc_client]
}
@@ -1,20 +0,0 @@
# 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
}
@@ -1,13 +0,0 @@
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"
}
}
}
@@ -1,45 +0,0 @@
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
}
@@ -1,8 +0,0 @@
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
}
@@ -1,9 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
arrstack = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-arrstack"
version = "0.2.0"
}
}
}
@@ -1,33 +0,0 @@
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
}
@@ -1,18 +0,0 @@
resource "vault_jwt_auth_backend" "jwt" {
path = var.path
type = "jwt"
description = var.description
oidc_discovery_url = var.oidc_discovery_url
bound_issuer = var.bound_issuer
default_role = var.default_role
# provider 5.6.0 panics on an all-null tune block
dynamic "tune" {
for_each = anytrue([for v in [var.default_lease_ttl, var.max_lease_ttl, var.listing_visibility] : v != null]) ? [1] : []
content {
default_lease_ttl = var.default_lease_ttl
max_lease_ttl = var.max_lease_ttl
listing_visibility = var.listing_visibility
}
}
}
@@ -1,3 +0,0 @@
output "accessor" {
value = vault_jwt_auth_backend.jwt.accessor
}
@@ -1,9 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -1,26 +0,0 @@
mock_provider "vault" {}
variables {
path = "jwt/github/iuclabs"
oidc_discovery_url = "https://token.actions.githubusercontent.com"
bound_issuer = "https://token.actions.githubusercontent.com"
}
run "omit_tune_when_unset" {
command = plan
assert {
condition = length(vault_jwt_auth_backend.jwt.tune) == 0
error_message = "no tune block may render when every tune value is null"
}
}
run "render_tune_when_set" {
command = plan
variables { max_lease_ttl = "1h" }
assert {
condition = length(vault_jwt_auth_backend.jwt.tune) == 1 && vault_jwt_auth_backend.jwt.tune[0].max_lease_ttl == "1h"
error_message = "tune block must render with the configured values"
}
}
@@ -1,48 +0,0 @@
variable "path" {
description = "Mount path of the JWT auth backend"
type = string
}
variable "description" {
description = "Human-readable description of the auth mount"
type = string
default = null
}
variable "oidc_discovery_url" {
description = "OIDC discovery URL used to fetch the issuer's signing keys"
type = string
}
variable "bound_issuer" {
description = "Value the iss claim of every presented token must match"
type = string
}
variable "default_role" {
description = "Role used when a login request names none"
type = string
default = null
}
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
}
@@ -1,29 +0,0 @@
locals {
alias = "identity.entity.aliases.${var.backend_accessor}.metadata"
kv_prefixes = [
"github/{{${local.alias}.repository}}/event/{{${local.alias}.event_name}}",
"github/{{${local.alias}.repository}}/ref/{{${local.alias}.ref}}/.secrets",
]
}
resource "vault_policy" "kv" {
name = "${var.backend}/${var.role_name}/kv-read"
policy = join("\n", flatten([for p in local.kv_prefixes : [
"path \"kv/data/${p}/*\" {\n capabilities = [\"read\"]\n}",
"path \"kv/metadata/${p}/*\" {\n capabilities = [\"read\", \"list\"]\n}",
]]))
}
resource "vault_jwt_auth_backend_role" "role" {
backend = var.backend
role_name = var.role_name
role_type = "jwt"
user_claim = var.user_claim
bound_audiences = var.bound_audiences
bound_claims = var.bound_claims
bound_claims_type = "glob"
claim_mappings = var.claim_mappings
token_ttl = var.token_ttl
token_max_ttl = var.token_max_ttl
token_policies = concat(var.token_policies, [vault_policy.kv.name])
}
@@ -1,9 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -1,167 +0,0 @@
mock_provider "vault" {}
variables {
backend = "jwt/github/iuclabs"
role_name = "default"
backend_accessor = "auth_jwt_test"
bound_audiences = ["https://vault.service.consul:8200"]
}
run "accept_org_role" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge" } }
assert {
condition = vault_policy.kv.policy == chomp(<<-EOT
path "kv/data/github/{{identity.entity.aliases.auth_jwt_test.metadata.repository}}/event/{{identity.entity.aliases.auth_jwt_test.metadata.event_name}}/*" {
capabilities = ["read"]
}
path "kv/metadata/github/{{identity.entity.aliases.auth_jwt_test.metadata.repository}}/event/{{identity.entity.aliases.auth_jwt_test.metadata.event_name}}/*" {
capabilities = ["read", "list"]
}
path "kv/data/github/{{identity.entity.aliases.auth_jwt_test.metadata.repository}}/ref/{{identity.entity.aliases.auth_jwt_test.metadata.ref}}/.secrets/*" {
capabilities = ["read"]
}
path "kv/metadata/github/{{identity.entity.aliases.auth_jwt_test.metadata.repository}}/ref/{{identity.entity.aliases.auth_jwt_test.metadata.ref}}/.secrets/*" {
capabilities = ["read", "list"]
}
EOT
)
error_message = "rendered KV policy paths changed"
}
assert {
condition = contains(vault_jwt_auth_backend_role.role.token_policies, "jwt/github/iuclabs/default/kv-read")
error_message = "role must carry the KV read policy"
}
assert {
condition = vault_jwt_auth_backend_role.role.bound_claims_type == "glob" && vault_jwt_auth_backend_role.role.user_claim == "jti"
error_message = "role must glob-match claims and alias on jti"
}
}
run "ref_paths_cannot_overlap_child_refs" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push", ref = "refs/heads/*" } }
assert {
condition = length(regexall("metadata\\.ref}}/[^.]", vault_policy.kv.policy)) == 0
error_message = "ref paths must end in a dot-prefixed segment git refnames cannot contain"
}
}
run "accept_exact_refs" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "refs/heads/main,refs/tags/v1.2.3,refs/pull/7/merge" } }
assert {
condition = vault_jwt_auth_backend_role.role.bound_claims.ref == "refs/heads/main,refs/tags/v1.2.3,refs/pull/7/merge"
error_message = "exact refs must pass through to bound_claims unchanged"
}
}
run "accept_with_repository" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge", repository = "IUCLabs/app" } }
}
run "reject_missing_repository_owner" {
command = plan
variables { bound_claims = { event_name = "push,release,schedule,pull_request", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge" } }
expect_failures = [var.bound_claims]
}
run "reject_missing_ref" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request" } }
expect_failures = [var.bound_claims]
}
run "reject_missing_event_name" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge" } }
expect_failures = [var.bound_claims]
}
run "reject_repository_owner_wildcard" {
command = plan
variables { bound_claims = { repository_owner = "*", event_name = "push,release,schedule,pull_request", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge" } }
expect_failures = [var.bound_claims]
}
run "reject_repository_owner_glob" {
command = plan
variables { bound_claims = { repository_owner = "IUC*", event_name = "push,release,schedule,pull_request", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge" } }
expect_failures = [var.bound_claims]
}
run "reject_event_pull_request_target" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,pull_request_target", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge" } }
expect_failures = [var.bound_claims]
}
run "reject_event_workflow_run" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "workflow_run", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge" } }
expect_failures = [var.bound_claims]
}
run "reject_event_issue_comment" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "issue_comment", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge" } }
expect_failures = [var.bound_claims]
}
run "reject_event_workflow_dispatch" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "workflow_dispatch", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge" } }
expect_failures = [var.bound_claims]
}
run "reject_ref_star" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "refs/*" } }
expect_failures = [var.bound_claims]
}
run "reject_ref_bare_star" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "*" } }
expect_failures = [var.bound_claims]
}
run "reject_ref_pull_star" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "refs/pull/*" } }
expect_failures = [var.bound_claims]
}
run "reject_ref_pull_head" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "refs/pull/*/head" } }
expect_failures = [var.bound_claims]
}
run "reject_ref_heads_prefix_glob" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "refs/heads/rel*" } }
expect_failures = [var.bound_claims]
}
run "reject_ref_empty" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "" } }
expect_failures = [var.bound_claims]
}
run "reject_repository_glob" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge", repository = "IUCLabs/*" } }
expect_failures = [var.bound_claims]
}
run "reject_environment_empty" {
command = plan
variables { bound_claims = { repository_owner = "IUCLabs", event_name = "push,release,schedule,pull_request", ref = "refs/heads/*,refs/tags/*,refs/pull/*/merge", environment = "" } }
expect_failures = [var.bound_claims]
}
@@ -1,79 +0,0 @@
variable "backend" {
description = "The unique path of the JWT auth backend to configure"
type = string
}
variable "role_name" {
description = "The name of the role"
type = string
}
variable "backend_accessor" {
description = "Accessor of the JWT auth mount, used to template the KV read policy"
type = string
}
variable "user_claim" {
description = "Claim used as the entity alias name"
type = string
default = "jti"
}
variable "claim_mappings" {
description = "Token claims copied into entity alias metadata"
type = map(string)
default = {}
}
variable "bound_audiences" {
description = "List of audiences (aud claim) accepted in the token"
type = list(string)
validation {
condition = length(var.bound_audiences) > 0
error_message = "bound_audiences must not be empty."
}
}
variable "bound_claims" {
description = "Claims the token must carry, glob-matched; only ref may use the allowed glob patterns, comma-separated values form a list"
type = map(string)
validation {
condition = can(regex("^[A-Za-z0-9-]+$", lookup(var.bound_claims, "repository_owner", "")))
error_message = "bound_claims.repository_owner must be one exact org name."
}
validation {
condition = alltrue([for v in split(",", lookup(var.bound_claims, "event_name", "")) : contains(["push", "release", "schedule", "pull_request"], trimspace(v))])
error_message = "bound_claims.event_name must only list push, release, schedule or pull_request."
}
validation {
condition = alltrue([for v in split(",", lookup(var.bound_claims, "ref", "")) :
contains(["refs/heads/*", "refs/tags/*", "refs/pull/*/merge"], trimspace(v)) ||
can(regex("^(refs/(heads|tags)/[A-Za-z0-9._/-]+|refs/pull/[0-9]+/merge)$", trimspace(v)))
])
error_message = "bound_claims.ref must be refs/heads/*, refs/tags/*, refs/pull/*/merge or exact refs of those shapes."
}
validation {
condition = alltrue(flatten([for k, v in var.bound_claims : [
for e in split(",", v) : trimspace(e) != "" && !strcontains(e, "*")
] if k != "ref"]))
error_message = "bound_claims values other than ref must be exact and non-empty."
}
}
variable "token_ttl" {
description = "The TTL period of tokens issued using this role, in seconds"
type = number
default = 900
}
variable "token_max_ttl" {
description = "The maximum lifetime for generated tokens in number of seconds"
type = number
default = 3600
}
variable "token_policies" {
description = "List of policies to assign to the role (passed from policy_auth_map)"
type = list(string)
default = []
}
@@ -1,22 +0,0 @@
# 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
}
}
@@ -1,9 +0,0 @@
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
}
@@ -1,9 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -1,56 +0,0 @@
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
}
@@ -1,13 +0,0 @@
# 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
}
@@ -1,9 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -1,14 +0,0 @@
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
}
@@ -1,13 +0,0 @@
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
}
@@ -1,9 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -1,56 +0,0 @@
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 = []
}
@@ -1,31 +0,0 @@
# Mounts the ghp secrets engine and writes its connection config via the
# vault-secrets-ghp provider. The plugin is registered ("imported") in the
# catalog separately (config/plugins/vault-plugin-secrets-ghp.yaml). The seeded
# ghp service token is sensitive and read from KV, not stored in git:
# kv/service/vault/<country>/<region>/secret_backend/<path>/config
# Expected key: admin_token (a ghpsvc_... service token that ghp accepts via its
# GHP_AUTH_SERVICE_TOKENS list). ghp has no rotate endpoint, so this static
# shared secret is the single credential the engine authenticates with.
data "vault_kv_secret_v2" "config" {
mount = "kv"
name = "service/vault/${var.country}/${var.region}/secret_backend/${var.path}/config"
}
resource "ghp_secret_backend" "this" {
path = var.path
plugin = var.plugin
description = var.description
base_url = var.base_url
admin_token = data.vault_kv_secret_v2.config.data["admin_token"]
ca_cert = var.ca_cert
tls_skip_verify = var.tls_skip_verify
request_timeout_seconds = var.request_timeout_seconds
lifecycle {
# The KV seed is a bootstrap credential consumed only when the engine config
# is first created. ghp has no rotate endpoint, so re-reading a (possibly
# stale) KV value must never re-push it into the live mount. Ignoring the
# token makes this module create-only for it (mirrors gitea/netbox config).
ignore_changes = [admin_token]
}
}
@@ -1,13 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
ghp = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-ghp"
version = "0.1.0"
}
}
}
@@ -1,49 +0,0 @@
variable "path" {
description = "Mount path of the ghp secrets engine (e.g. \"ghp\")"
type = string
}
variable "plugin" {
description = "Registered plugin name to mount (the catalog name = mount type)"
type = string
default = "vault-plugin-secrets-ghp"
}
variable "description" {
description = "Human-friendly description of the mount"
type = string
default = null
}
variable "base_url" {
description = "Base URL of the ghp server (e.g. https://ghp.unkin.net)"
type = string
}
variable "country" {
description = "Country segment of the KV path holding the seeded admin token"
type = string
}
variable "region" {
description = "Region segment of the KV path holding the seeded admin token"
type = string
}
variable "ca_cert" {
description = "PEM CA certificate that signed the ghp server's TLS cert (optional; omit to use the system trust store)"
type = string
default = null
}
variable "tls_skip_verify" {
description = "Skip TLS verification of the ghp server (not recommended)"
type = bool
default = false
}
variable "request_timeout_seconds" {
description = "HTTP timeout in seconds for calls from the plugin to ghp"
type = number
default = 30
}
@@ -1,15 +0,0 @@
# A role that mints short-lived, scoped ghp tokens. Reading ghp/creds/<name>
# produces a lease-bound token that is revoked from ghp when the lease is
# revoked or reaches max_ttl.
resource "ghp_secret_role" "this" {
backend = var.backend
name = var.name
token_type = var.token_type
installation_id = var.installation_id
app_record_id = var.app_record_id
repositories = var.repositories
scopes = var.scopes
session_prefix = var.session_prefix
ttl = var.ttl
max_ttl = var.max_ttl
}
@@ -1,9 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
ghp = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-ghp"
version = "0.1.0"
}
}
}
@@ -1,57 +0,0 @@
variable "backend" {
description = "Mount path of the ghp secrets engine this role belongs to"
type = string
}
variable "name" {
description = "Role name (read ghp/creds/<name> to mint a token)"
type = string
}
variable "token_type" {
description = "ghp token type to mint: \"agent\" (default) or \"proxy\""
type = string
default = null
}
variable "installation_id" {
description = "ghp App installation id the minted agent token is bound to (required when token_type is \"agent\")"
type = number
default = null
}
variable "app_record_id" {
description = "Optional ghp App record id (UUID) to pin agent tokens to; empty selects ghp's default app"
type = string
default = null
}
variable "repositories" {
description = "Optional repositories the minted token is restricted to; empty is open-scoped (all repositories)"
type = list(string)
default = null
}
variable "scopes" {
description = "Optional ghp permission:level scopes (e.g. [\"contents:read\"]); empty is open-scoped"
type = list(string)
default = null
}
variable "session_prefix" {
description = "Prefix for the ghp session id of each minted token (default \"vault\")"
type = string
default = null
}
variable "ttl" {
description = "Default lease TTL in seconds for minted tokens"
type = number
default = null
}
variable "max_ttl" {
description = "Maximum lease TTL in seconds for minted tokens"
type = number
default = null
}
@@ -1,34 +0,0 @@
# Mounts the gitea secrets engine and writes its connection config via the
# giteavaultsecret provider. The plugin is registered ("imported") in the
# catalog separately (config/plugins/vault-plugin-secrets-gitea.yaml). The
# seeded site-admin credentials are sensitive and read from KV, not stored in
# git:
# kv/service/vault/<country>/<region>/secret_backend/<path>/config
# Expected keys: admin_username (required), admin_password (required).
data "vault_kv_secret_v2" "config" {
mount = "kv"
name = "service/vault/${var.country}/${var.region}/secret_backend/${var.path}/config"
}
resource "gitea_secret_backend" "this" {
path = var.path
plugin = var.plugin
description = var.description
gitea_url = var.gitea_url
admin_username = data.vault_kv_secret_v2.config.data["admin_username"]
admin_password = data.vault_kv_secret_v2.config.data["admin_password"]
ca_cert = var.ca_cert
tls_skip_verify = var.tls_skip_verify
request_timeout_seconds = var.request_timeout_seconds
lifecycle {
# The KV seed is a bootstrap credential: it is consumed only when the engine
# config is first created. After creation the live admin password is rotated
# in place (vault write -f gitea/config/rotate-root) and diverges from the
# seed, so re-reading the (possibly stale) KV value must never push it back.
# Ignoring the credential attributes makes this module create-only for them.
# (The sibling rancher/litellm seed modules do not yet do this and would
# re-push their seed on a subsequent apply.)
ignore_changes = [admin_username, admin_password]
}
}
@@ -1,13 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
gitea = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/giteavaultsecret"
version = "0.1.0"
}
}
}
@@ -1,49 +0,0 @@
variable "path" {
description = "Mount path of the gitea secrets engine (e.g. \"gitea\")"
type = string
}
variable "plugin" {
description = "Registered plugin name to mount (the catalog name = mount type)"
type = string
default = "vault-plugin-secrets-gitea"
}
variable "description" {
description = "Human-friendly description of the mount"
type = string
default = null
}
variable "gitea_url" {
description = "Base URL of the Gitea server (e.g. https://git.unkin.net)"
type = string
}
variable "country" {
description = "Country segment of the KV path holding the seeded admin credentials"
type = string
}
variable "region" {
description = "Region segment of the KV path holding the seeded admin credentials"
type = string
}
variable "ca_cert" {
description = "PEM CA certificate that signed the Gitea server's TLS cert (optional; omit to use the system trust store)"
type = string
default = null
}
variable "tls_skip_verify" {
description = "Skip TLS verification of the Gitea server (not recommended)"
type = bool
default = false
}
variable "request_timeout_seconds" {
description = "HTTP timeout in seconds for calls from the plugin to Gitea"
type = number
default = 30
}
@@ -1,12 +0,0 @@
# A role that mints short-lived, scoped gitea tokens for a target Gitea user.
# Reading gitea/creds/<name> produces a lease-bound token that is deleted from
# Gitea when the lease is revoked or reaches max_ttl.
resource "gitea_secret_backend_role" "this" {
backend = var.backend
name = var.name
username = var.username
scopes = var.scopes
token_name_prefix = var.token_name_prefix
ttl = var.ttl
max_ttl = var.max_ttl
}

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