diff --git a/policies/gpg/admin.yaml b/policies/gpg/admin.yaml index 23d7955..bc962dd 100644 --- a/policies/gpg/admin.yaml +++ b/policies/gpg/admin.yaml @@ -1,20 +1,8 @@ -# Allow the vault deployer to import the gpg plugin and manage its OpenPGP keys. -# -# terraform-vault registers the plugin itself (vault_plugin -> sys/plugins/catalog, -# a sudo-protected path) and manages keys via the gpgvaultsecret provider, so the -# deployer needs catalog access on top of the mount access it already has -# (sys/mounts/*). Without this, apply 403s on the plugin registration and on -# gpg/keys writes. +# Allow the vault deployer to manage the gpg mount's OpenPGP keys via the +# gpgvaultsecret provider. Registering the plugin itself is a sys/plugins/catalog +# grant, carried by policies/sys/plugins/catalog/admin.yaml. --- rules: - # Import / register (and deregister) the gpg plugin in the catalog. - - path: "sys/plugins/catalog/secret/vault-plugin-secrets-gpg" - capabilities: - - create - - read - - update - - delete - - sudo # Manage keys (create/rotate/config/delete) in the gpg mount. - path: "gpg/keys/*" capabilities: diff --git a/policies/identity/group-alias/admin.yaml b/policies/identity/group-alias/admin.yaml new file mode 100644 index 0000000..437c8f9 --- /dev/null +++ b/policies/identity/group-alias/admin.yaml @@ -0,0 +1,24 @@ +# Allow the deployer to manage the identity group aliases that map external OIDC +# group membership (the ak_groups claim) onto the Vault groups managed under +# policies/identity/group/. Both endpoints are needed: create posts to +# identity/group-alias, subsequent reads and updates address +# identity/group-alias/id/. +--- +rules: + - path: "identity/group-alias" + capabilities: + - create + - update + - path: "identity/group-alias/*" + capabilities: + - create + - update + - read + - delete + - list + +auth: + approle: + - tf_vault + k8s/au/syd1: + - woodpecker_terraform_vault diff --git a/policies/identity/group/admin.yaml b/policies/identity/group/admin.yaml index d077a51..524decf 100644 --- a/policies/identity/group/admin.yaml +++ b/policies/identity/group/admin.yaml @@ -1,7 +1,7 @@ -# 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/. +# Allow the deployer to manage the external identity groups that OIDC group +# membership (the ak_groups claim) maps onto Vault policies through. Both the +# collection endpoint and the per-id endpoints are needed: create posts to +# identity/group, subsequent reads and updates address identity/group/id/. --- rules: - path: "identity/group" @@ -15,21 +15,6 @@ rules: - 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: diff --git a/policies/identity/lookup/group/admin.yaml b/policies/identity/lookup/group/admin.yaml new file mode 100644 index 0000000..e56e5a3 --- /dev/null +++ b/policies/identity/lookup/group/admin.yaml @@ -0,0 +1,14 @@ +# Allow the deployer to look up an identity group by name, which is how it +# resolves the group it is about to update under policies/identity/group/. +--- +rules: + - path: "identity/lookup/group" + capabilities: + - create + - update + +auth: + approle: + - tf_vault + k8s/au/syd1: + - woodpecker_terraform_vault diff --git a/policies/kv/kubernetes/namespace/+/default/oauth-credentials/read.yaml b/policies/kv/kubernetes/namespace/+/default/oauth-credentials/read.yaml new file mode 100644 index 0000000..e6c7451 --- /dev/null +++ b/policies/kv/kubernetes/namespace/+/default/oauth-credentials/read.yaml @@ -0,0 +1,15 @@ +# Allow the Terraform Authentik runner to read the OAuth2/OIDC client secret that +# backs an authentik provider, in whichever namespace the target service lives +# (the module's data.vault_kv_secret_v2). The literal + is a Vault single-segment +# wildcard: one oauth-credentials secret per onboarded namespace. +--- +rules: + - path: "kv/data/kubernetes/namespace/+/default/oauth-credentials" + capabilities: + - read + +auth: + approle: + - terraform_authentik + k8s/au/syd1: + - woodpecker_terraform_authentik diff --git a/policies/kv/kubernetes/namespace/cattle-system/default/oauth-credentials/read.yaml b/policies/kv/kubernetes/namespace/cattle-system/default/oauth-credentials/read.yaml new file mode 100644 index 0000000..e89ed59 --- /dev/null +++ b/policies/kv/kubernetes/namespace/cattle-system/default/oauth-credentials/read.yaml @@ -0,0 +1,14 @@ +# Allow the Terraform Rancher runner to read the OAuth2/OIDC client secret backing +# the Rancher keycloakoidc AuthConfig (the same secret Authentik sets on the +# provider). +--- +rules: + - path: "kv/data/kubernetes/namespace/cattle-system/default/oauth-credentials" + capabilities: + - read + +auth: + approle: + - terraform_rancher + k8s/au/syd1: + - woodpecker_terraform_rancher diff --git a/policies/kv/service/terraform/enc-encapi-environment.yaml b/policies/kv/kubernetes/namespace/encapi/default/environment/read.yaml similarity index 100% rename from policies/kv/service/terraform/enc-encapi-environment.yaml rename to policies/kv/kubernetes/namespace/encapi/default/environment/read.yaml diff --git a/policies/kv/kubernetes/namespace/logging/default/vlogs-oauth-credentials/read.yaml b/policies/kv/kubernetes/namespace/logging/default/vlogs-oauth-credentials/read.yaml new file mode 100644 index 0000000..02aba75 --- /dev/null +++ b/policies/kv/kubernetes/namespace/logging/default/vlogs-oauth-credentials/read.yaml @@ -0,0 +1,14 @@ +# Allow the Terraform Authentik runner to read the vlogs OIDC client secret. It is +# a second OIDC client in an already-onboarded namespace, so it cannot use the +# one-per-namespace oauth-credentials path. +--- +rules: + - path: "kv/data/kubernetes/namespace/logging/default/vlogs-oauth-credentials" + capabilities: + - read + +auth: + approle: + - terraform_authentik + k8s/au/syd1: + - woodpecker_terraform_authentik diff --git a/policies/kv/service/terraform/authentik.yaml b/policies/kv/service/terraform/authentik.yaml index 09ba06a..80a96a5 100644 --- a/policies/kv/service/terraform/authentik.yaml +++ b/policies/kv/service/terraform/authentik.yaml @@ -1,20 +1,11 @@ -# Allow the Terraform Authentik runner to read: -# - its own Authentik API token (provider auth), and -# - OAuth2/OIDC client secrets that back authentik providers (per target -# service's kv namespace path, via the module's data.vault_kv_secret_v2). +# Allow the Terraform Authentik runner to read its own Authentik API token +# (provider auth). The OAuth2/OIDC client secrets backing authentik providers are +# granted per secret under policies/kv/kubernetes/namespace/. --- rules: - path: "kv/data/service/terraform/authentik" capabilities: - read - - path: "kv/data/kubernetes/namespace/+/default/oauth-credentials" - capabilities: - - read - # Second OIDC client in an already-onboarded namespace, so it cannot use the - # one-per-namespace oauth-credentials path above. - - path: "kv/data/kubernetes/namespace/logging/default/vlogs-oauth-credentials" - capabilities: - - read auth: approle: diff --git a/policies/kv/service/terraform/rancher.yaml b/policies/kv/service/terraform/rancher.yaml index 30352ab..c3f069b 100644 --- a/policies/kv/service/terraform/rancher.yaml +++ b/policies/kv/service/terraform/rancher.yaml @@ -1,15 +1,11 @@ -# Allow the Terraform Rancher runner to read: -# - its own Rancher admin API token (rancher2 provider auth), and -# - the OAuth2/OIDC client secret backing the Rancher keycloakoidc AuthConfig -# (same secret Authentik sets on the provider). +# Allow the Terraform Rancher runner to read its own Rancher admin API token +# (rancher2 provider auth). The OAuth2/OIDC client secret backing the Rancher +# keycloakoidc AuthConfig is granted under policies/kv/kubernetes/namespace/. --- rules: - path: "kv/data/service/terraform/rancher" capabilities: - read - - path: "kv/data/kubernetes/namespace/cattle-system/default/oauth-credentials" - capabilities: - - read auth: approle: diff --git a/policies/sys/mounts-tune/admin.yaml b/policies/sys/mounts-tune/admin.yaml new file mode 100644 index 0000000..ad27986 --- /dev/null +++ b/policies/sys/mounts-tune/admin.yaml @@ -0,0 +1,15 @@ +# Allow the deployer to read and tune the configuration of a mounted secret +# engine. sys/mounts-tune is the tuning endpoint beside the mount management +# granted by policies/sys/mounts/admin.yaml. +--- +rules: + - path: "sys/mounts-tune/*" + capabilities: + - update + - read + +auth: + approle: + - tf_vault + k8s/au/syd1: + - woodpecker_terraform_vault diff --git a/policies/sys/mounts/admin.yaml b/policies/sys/mounts/admin.yaml index 6c3c938..98b2649 100644 --- a/policies/sys/mounts/admin.yaml +++ b/policies/sys/mounts/admin.yaml @@ -8,10 +8,6 @@ rules: - delete - read - list - - path: "sys/mounts-tune/*" - capabilities: - - update - - read - path: "sys/mounts" capabilities: - read diff --git a/tests/test_policies.py b/tests/test_policies.py index 31cf9e8..e2fc47c 100644 --- a/tests/test_policies.py +++ b/tests/test_policies.py @@ -11,21 +11,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent # kv-v2 inserts one of these directly after the mount; it is not part of the scope. KV_API_SEGMENTS = {"data", "metadata", "delete", "undelete", "destroy"} -ALLOWED = { - ("policies/global-root.yaml", "*"), # root policy - ("policies/gpg/admin.yaml", "sys/plugins/catalog/secret/vault-plugin-secrets-gpg"), # plugin catalog registration - ("policies/kv/service/terraform/authentik.yaml", "kv/data/kubernetes/namespace/+/default/oauth-credentials"), # terraform writes k8s namespace secrets - ("policies/kv/service/terraform/authentik.yaml", "kv/data/kubernetes/namespace/logging/default/vlogs-oauth-credentials"), # terraform writes k8s namespace secrets - ("policies/kv/service/terraform/enc-encapi-environment.yaml", "kv/data/kubernetes/namespace/encapi/default/environment"), # terraform writes k8s namespace secrets - ("policies/kv/service/terraform/rancher.yaml", "kv/data/kubernetes/namespace/cattle-system/default/oauth-credentials"), # terraform writes k8s namespace secrets - ("policies/identity/group/admin.yaml", "identity/group-alias"), # group-alias endpoint for the same groups - ("policies/identity/group/admin.yaml", "identity/group-alias/*"), # group-alias endpoint for the same groups - ("policies/identity/group/admin.yaml", "identity/lookup/group"), # group lookup endpoint - ("policies/sys/policy/admin.yaml", "sys/policies/acl"), # dir is policy, API path is policies - ("policies/sys/policy/admin.yaml", "sys/policies/acl/*"), # dir is policy, API path is policies - ("policies/sys/mounts/admin.yaml", "sys/mounts-tune/*"), # sibling API path of the mounts endpoint -} - class Rule(BaseModel): model_config = ConfigDict(extra="forbid") @@ -49,12 +34,9 @@ def policy_files(): def escaping_scope(policy_file, rule_path): """The policy directory a rule path reaches outside of, or None when in scope.""" - if (policy_file, rule_path) in ALLOWED: - return None parts = PurePosixPath(policy_file).parts - below_policies = PurePosixPath(*parts[parts.index("policies") + 1:]) - # a policy at the policies/ root has no directory, so its own name is the scope - scope = below_policies.parent if below_policies.parent.parts else PurePosixPath(below_policies.stem) + # a policy at the policies/ root is located at the root, so its scope is the whole tree + scope = PurePosixPath(*parts[parts.index("policies") + 1:]).parent path = PurePosixPath(rule_path) if len(path.parts) > 1 and path.parts[1] in KV_API_SEGMENTS: path = PurePosixPath(path.parts[0], *path.parts[2:]) @@ -94,6 +76,7 @@ class RuleScopeTests(unittest.TestCase): ("policies/kv/service/authentik/oidc-vault/read.yaml", "kv/data/service/authentik/oidc-vault"), ("policies/kv/service/vault/read.yaml", "kv/data/service/vault/+/+/auth_backend/*"), ("policies/kubernetes/au/admin.yaml", "kubernetes/au/+/config"), + ("policies/global-root.yaml", "*"), # a root-level policy is scoped to the whole tree ]: with self.subTest(policy=policy_file, rule=rule_path): self.assertIsNone(escaping_scope(policy_file, rule_path)) @@ -107,16 +90,6 @@ class RuleScopeTests(unittest.TestCase): with self.subTest(policy=policy_file, rule=rule_path): self.assertEqual(escaping_scope(policy_file, rule_path), scope) - def test_allowlisted(self): - for policy_file, rule_path in [ - ("policies/global-root.yaml", "*"), - ("policies/sys/policy/admin.yaml", "sys/policies/acl"), - ("policies/sys/policy/admin.yaml", "sys/policies/acl/*"), - ("policies/sys/mounts/admin.yaml", "sys/mounts-tune/*"), - ]: - with self.subTest(policy=policy_file, rule=rule_path): - self.assertIsNone(escaping_scope(policy_file, rule_path)) - def test_schema_rejects_malformed(self): auth = {"approle": ["tf_vault"]} rule = {"path": "kv/data/x", "capabilities": ["read"]}