1 Commits

Author SHA1 Message Date
unkin-agent afbea53d94 Grant the puppet k8s role PKI issue and SSH host signing
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
2026-09-22 22:32:52 +10:00
8 changed files with 26 additions and 137 deletions
-8
View File
@@ -22,11 +22,3 @@ repos:
"-d {extends: relaxed, rules: {line-length: disable}, ignore: chart}",
"-s",
]
- repo: local
hooks:
- id: policy-path-scope
name: policy rule paths stay within their own directory
entry: python3 scripts/check_policy_paths.py
language: python
additional_dependencies: [pyyaml]
files: ^policies/.*\.yaml$
+1 -4
View File
@@ -1,4 +1,4 @@
.PHONY: init plan apply format check-policies
.PHONY: init plan apply format
VAULT_AUTH_METHOD ?= approle
VAULT_K8S_ROLE ?= woodpecker_terraform_vault
@@ -28,9 +28,6 @@ apply: init
@$(call vault_env) && \
terragrunt run --all --parallelism 2 --non-interactive apply
check-policies:
@python3 scripts/check_policy_paths.py
format:
@echo "Formatting OpenTofu files..."
@tofu fmt -recursive .
@@ -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: "unkin.net,main.unkin.net,consul"
allow_subdomains: true
allow_bare_domains: false
@@ -10,11 +10,6 @@ rules:
- 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:
+12
View File
@@ -0,0 +1,12 @@
# Allow the puppet catalog compilers to issue host certificates
# via certmanager during catalog compilation
---
rules:
- path: "pki_int/issue/servers_default"
capabilities:
- create
- update
auth:
k8s/au/syd1:
- puppet
+12
View File
@@ -0,0 +1,12 @@
# Allow the puppet catalog compilers to sign SSH host certificates
# via sshsignhost during catalog compilation
---
rules:
- path: "sshca/sign/signhost"
capabilities:
- create
- update
auth:
k8s/au/syd1:
- puppet
-61
View File
@@ -1,61 +0,0 @@
#!/usr/bin/env python3
"""Fail when a rule path in policies/**/*.yaml grants outside the policy's own directory."""
import glob
import sys
from pathlib import PurePosixPath
import yaml
# 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
}
def policy_scope(path):
parts = PurePosixPath(path).parts
rel = PurePosixPath(*parts[parts.index("policies") + 1:])
# a policy at the policies/ root has no directory, so its own name is the scope
return "policies" / rel, rel.parent if rel.parent.parts else PurePosixPath(rel.stem)
def rule_scope(rule_path):
path = PurePosixPath(rule_path)
if len(path.parts) > 1 and path.parts[1] in KV_API_SEGMENTS:
return PurePosixPath(path.parts[0], *path.parts[2:])
return path
def violations(files):
for f in files:
key, scope = policy_scope(f)
rules = (yaml.safe_load(open(f)) or {}).get("rules")
if not rules:
yield f"{f}: no rules"
continue
for rule in rules:
path = rule.get("path")
if not path:
yield f"{f}: rule without a path"
elif (str(key), path) not in ALLOWED and not rule_scope(path).is_relative_to(scope):
yield f'{f}: rule path "{path}" escapes policy scope "{scope}"'
if __name__ == "__main__":
found = list(violations(sys.argv[1:] or sorted(glob.glob("policies/**/*.yaml", recursive=True))))
for v in found:
print(v)
sys.exit(1 if found else 0)
-56
View File
@@ -1,56 +0,0 @@
#!/usr/bin/env python3
"""Run with: python3 scripts/test_check_policy_paths.py"""
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from check_policy_paths import violations
TMP = Path(tempfile.mkdtemp())
def check(rel, *rule_paths):
f = TMP / rel
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text("rules:\n" + "".join(f' - path: "{p}"\n' for p in rule_paths))
return [v.replace(f"{TMP}/", "") for v in violations([str(f)])]
# in scope, including kv-v2 data/metadata segments and any kv-v2 mount
assert check("policies/sys/mounts/admin.yaml", "sys/mounts", "sys/mounts/*") == []
assert check("policies/rundeck/rundeck.yaml", "rundeck/data/*", "rundeck/metadata/*") == []
assert check("policies/kv/service/authentik/oidc-vault/read.yaml", "kv/data/service/authentik/oidc-vault") == []
# + and * below the directory are fine
assert check("policies/kv/service/vault/read.yaml", "kv/data/service/vault/+/+/auth_backend/*") == []
assert check("policies/kubernetes/au/admin.yaml", "kubernetes/au/+/config") == []
# + standing in for a literal directory segment is not
assert check("policies/kv/service/vault/au/syd1/ghp/write.yaml", "kv/data/service/vault/+/+/ghp/config") == [
'policies/kv/service/vault/au/syd1/ghp/write.yaml: rule path '
'"kv/data/service/vault/+/+/ghp/config" escapes policy scope "kv/service/vault/au/syd1/ghp"'
]
# a sibling API path is not a prefix match
assert check("policies/sys/thing/admin.yaml", "sys/thing-tune/*") == [
'policies/sys/thing/admin.yaml: rule path "sys/thing-tune/*" escapes policy scope "sys/thing"'
]
# out of tree entirely
assert check("policies/kv/foo/bar/baz.yaml", "kv/data/foo/baz/bar") == [
'policies/kv/foo/bar/baz.yaml: rule path "kv/data/foo/baz/bar" escapes policy scope "kv/foo/bar"'
]
# allowlisted outliers pass
assert check("policies/global-root.yaml", "*") == []
assert check("policies/sys/policy/admin.yaml", "sys/policies/acl", "sys/policies/acl/*") == []
assert check("policies/sys/mounts/admin.yaml", "sys/mounts-tune/*") == []
# malformed policies fail
(TMP / "policies/empty.yaml").write_text("auth:\n approle:\n - x\n")
assert list(violations([str(TMP / "policies/empty.yaml")]))[0].endswith(": no rules")
(TMP / "policies/nopath.yaml").write_text("rules:\n - capabilities:\n - read\n")
assert list(violations([str(TMP / "policies/nopath.yaml")]))[0].endswith(": rule without a path")
print("ok")