Your Policies Don’t Count Until They Compile: Least‑Privilege, Secret Rotation, and Dependency Risk as Code
If you can’t enforce it in CI and prove it in an audit, it’s not a policy—it’s a wish. Here’s how we codify least‑privilege, rotation, and dependency risk without tanking delivery speed.
Policies don’t count until they compile.Back to all posts
The night the auditor asked for a commit hash
We were wrapping a SOC 2 walkthrough when the auditor asked, “Show me the commit that proves wildcards are blocked in IAM policies.” Not the wiki page, not the slide deck—the commit. I’ve seen teams scramble with screenshots and spreadsheets. That’s not evidence. Evidence is a passing CI run, an admission log, and an attestation tied to a release SHA.
If your security policies can’t be enforced and proven by code, they’re just aspirations. Let’s talk about turning least‑privilege, secret rotation, and dependency risk into guardrails, checks, and automated proofs—without killing delivery speed.
Translate policies into guardrails, checks, and proofs
Policies only stick when you implement them three ways:
- Guardrails (prevent): Admission policies in
Kyverno/OPA Gatekeeper, permissions boundaries in IAM, Git branch protections. - Checks (detect): CI steps with
Conftest,Checkov,Trivy,Grype, unit tests for infra, and drift monitors. - Proofs (attest): Signed attestations via
cosign, SBOMs viasyft, OPA decision logs, CI artifacts stored immutably.
Pattern we use at GitPlumbers:
- Write the policy in plain English.
- Codify as
rego/yaml/pipeline steps. - Enforce progressively: warn → block in non‑prod → block in prod.
- Emit evidence as artifacts: logs, attestations, and waiver files with expiry.
Least‑privilege that actually ships
Start by killing wildcards and scoping blast radius. Terraform plus OPA (or Sentinel if you’re in Terraform Cloud) works well. Then backstop with runtime admission in Kubernetes.
- Block IAM wildcards at plan time
# policy/iam_wildcards.rego
package terraform.aws.iam
# Input is tfplan JSON from: terraform show -json plan.out | conftest test -
deny[msg] {
some r
input.resource_changes[r].type == "aws_iam_policy"
after := input.resource_changes[r].change.after
contains_wildcard(after.policy)
msg := sprintf("IAM policy %s contains wildcard actions or resources", [input.resource_changes[r].name])
}
contains_wildcard(policy_json) {
policy := json.unmarshal(policy_json)
some s
stmt := policy.Statement[s]
stmt.Action == "*"; stmt.Effect == "Allow"
} {
policy := json.unmarshal(policy_json)
some s
stmt := policy.Statement[s]
stmt.Resource == "*"; stmt.Effect == "Allow"
}CI snippet:
terraform init
terraform plan -out=plan.out
terraform show -json plan.out | conftest test - --policy policy/- Force roles into a permissions boundary
# terraform
resource "aws_iam_policy" "boundary" {
name = "gp-permissions-boundary"
policy = file("boundary.json")
}
resource "aws_iam_role" "ci" {
name = "ci-deploy"
assume_role_policy = data.aws_iam_policy_document.gha_oidc.json
permissions_boundary = aws_iam_policy.boundary.arn
}Boundary idea: allow only ecs:* on specific ARNs, deny iam:*, and require aws:RequestTag/ChangeID to be present.
- Admission guardrail in Kubernetes: no wildcard
ClusterRoleverbs.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: deny-wildcard-clusterrole
spec:
validationFailureAction: enforce
rules:
- name: deny-wildcard
match:
resources:
kinds: [ClusterRole]
validate:
message: "ClusterRole must not use wildcard verbs or resources"
pattern:
rules:
- verbs: ["!*"]
resources: ["!*"]- Result we’ve hit: teams move from “we think it’s least‑privilege” to a plan-time block rate that drops from ~30% to <5% within two sprints, while prod deploy lead time remains flat because dev finds the issues at PR time.
Secret rotation you can trust and prove
Long‑lived secrets are compliance debt. Prefer short‑lived credentials and automated rotation with artifacts showing it happened.
- Short‑lived CI/CD creds with GitHub OIDC + AWS
# .github/workflows/deploy.yaml
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/ci-deploy
role-session-name: gha-${{ github.run_id }}
aws-region: us-east-1
- run: aws sts get-caller-identityEvidence: CloudTrail shows the AssumeRoleWithWebIdentity with sub=repo:org/repo:ref:refs/heads/main.
- Managed rotation for databases and API keys
# Terraform: AWS Secrets Manager with rotation
resource "aws_secretsmanager_secret" "db" {
name = "rds-app-cred"
description = "App creds for prod RDS"
rotation_rules { automatically_after_days = 30 }
kms_key_id = aws_kms_key.secrets.arn
}
resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db.id
rotation_lambda_arn = aws_lambda_function.rotate_rds.arn
}Record the rotation in logs and forward to SIEM. Store last rotation timestamp as an evidence artifact alongside the release notes.
- Encrypt at rest in Git with SOPS + age
# secrets.enc.yaml (SOPS encrypted)
apiVersion: v1
kind: Secret
metadata:
name: app
stringData:
API_KEY: ENC[AES256_GCM,data:...,type:str]
sops:
age:
- recipient: age1yoursopskey...
encrypted_regex: '^(data|stringData)$'
version: 3.8.1Rotate recipients and re-encrypt:
sops updatekeys secrets.enc.yamlWe prefer SOPS for GitOps overlays and Vault/Secrets Manager for runtime; use External Secrets Operator to bridge.
- JIT human access: Okta ASA or AWS IAM Identity Center with 15–60 min sessions, justification tags (
aws:RequestTag/Ticket), and session recording where feasible. Auditors love the tag trail.
Dependency risk without breaking builds
Software supply chain controls that block real risks but don’t brick every PR.
- Generate SBOMs and attach as attestations
syft packages dir:. -o spdx-json > sbom.spdx.json
cosign attest --predicate sbom.spdx.json --type spdx $IMAGE_REF- Scan and fail on high/critical with exceptions-as-code
grype $IMAGE_REF --fail-on high --only-fixed --add-cpe-if-noneAdd a waiver file with expiry for justified exceptions:
# .security/waivers.yaml
vulns:
- id: CVE-2023-12345
image: ghcr.io/org/api:sha-abc
expires: 2025-01-31
justification: "No reachable path; upstream patch scheduled"
ticket: SEC-421- Sign images and verify at admission (Sigstore + Kyverno)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: enforce
rules:
- name: verify-signature-and-slsa
match:
resources:
kinds: [Pod]
verifyImages:
- image: "ghcr.io/yourorg/*"
keyless:
issuer: "https://token.actions.githubusercontent.com"
subject: "repo:yourorg/yourrepo:ref:refs/heads/main"
attestations:
- predicateType: https://slsa.dev/provenance/v1
attestors:
- entries:
- keyless:
issuer: https://token.actions.githubusercontent.com
subject: "repo:yourorg/yourrepo:ref:refs/heads/main"- Pin and update smartly: Lockfiles and Renovate bots with rate limits per service. For Python, pair
requirements.txtwithconstraints.txt. For Node, enforce--prefer-offline --frozen-lockfile.
pip install -r requirements.txt -c constraints.txt
npm ci --ignore-scriptsWe typically see unsigned image blocks catch issues within the first week; by week two, teams have signing in CI and block rates fall near zero.
Automated proofs for regulated workloads
Auditors want evidence mapped to controls. Make it push‑button.
- Attest policies ran: Use
cosign attestto attach policy results.
conftest test plan.json -o json > policy-results.json
cosign attest --predicate policy-results.json --type application/vnd.gp.policy+json $IMAGE- Store OPA decision logs: Enable decision logging and ship to S3/CloudWatch with retention.
# opa-config.yaml
services:
s3log: { url: https://logs.example.com }
decision_logs:
console: false
reporting:
min_delay_seconds: 10
max_delay_seconds: 30- Map to controls: Keep an OSCAL/NIST 800‑53 mapping alongside code.
# controls.map.yaml
AC-6(1):
guardrails:
- kyverno: deny-wildcard-clusterrole
- iam: gp-permissions-boundary
checks:
- rego: policy/iam_wildcards.rego
proofs:
- attestation: application/vnd.gp.policy+json
- logs: opa-decision-logs S3 bucket- Immutable evidence bucket: Versioned, write‑once S3 bucket (
S3 Object Lock) for CI artifacts: SBOMs, attestations, plan files, waiver history.
Tip: Make the CI job fail if evidence upload fails. If you didn’t store proof, it didn’t happen.
Balance regulated‑data constraints with delivery speed
I’ve seen “security freeze” kill more roadmaps than outages. Controls can be fast if you design for flow.
- Environment tiers: Full enforcement in prod; warn-only in dev; block explicit violations in staging.
- Data minimization: Use production‑like masked datasets or Tonic/Redgate for synthetic data; block PII egress with egress gateways and DLP in CI artifact scanning.
- JIT approvals: Time‑boxed policy waivers in code reviewed via PR. Rego reads
.security/waivers.yamland checksexpires. - Fast feedback: Pre‑commit hooks for Terraform (
tflint,checkov) and Docker (hadolint) so engineers get fixes locally.
Velocity metric we watch: change lead time before vs. after. If it bumps more than 10–15% after enforcement, tune gates or move checks earlier in the flow.
Rollout plan and what to measure
- Inventory controls and map to guardrail/check/proof. Write the plain English first.
- Add CI checks for IaC, containers, SBOMs, and signing. Start warn‑only.
- Introduce admission policies in non‑prod; fix noise; then enforce.
- Flip CI to block on criticals; add waiver workflow with expiry.
- Turn on short‑lived creds; remove long‑lived keys from repos and CI.
- Stand up evidence storage and dashboards (failed policies, waivers, unsigned blocks).
Track:
- Failed policy rate per repo/service; time to remediate.
- Secrets rotation success rate and max secret age.
- Unsigned/Unscanned image block count over time (should trend to zero).
- Change lead time and deployment frequency (DORA) pre/post rollout.
- Exception MTTR and % expired waivers.
When we run this playbook, most orgs stabilize within 4–6 weeks, with audit readiness moving from “ad hoc” to “we can export everything by commit.”
Key takeaways
- Translate policies into code: guardrails (admission), checks (CI), and proofs (attestations/artifacts).
- Enforce least‑privilege with Terraform + OPA/Kyverno and permissions boundaries; deny wildcards by default.
- Rotate secrets by design: prefer short‑lived, OIDC‑based creds; automate vault/manager rotation and SOPS key updates.
- Control dependency risk with SBOMs, signature verification, and block unsigned/unscanned images at admission.
- Automate evidence: store policy decisions, attestations, and waiver files with expiry for audit trails.
- Roll out progressively: warn → block → enforce; measure change lead time, failed policy rates, MTTR for waivers.
Implementation checklist
- Map each policy to a control: guardrail, check, proof.
- Stand up OPA/Kyverno and wire into CI + admission.
- Add Terraform policy checks (Conftest/OPA or TFC Sentinel) for IAM wildcards and boundary usage.
- Move CI/CD to short‑lived cloud creds via OIDC; remove stored long‑lived keys.
- Enable managed secret rotation or Vault; document runbooks and evidence artifacts.
- Generate SBOMs with Syft; sign images and attest provenance with Cosign; enforce verification in admission.
- Create an exception/waiver workflow with expiry and Jira linkage.
- Publish control metrics: failed checks, exceptions, rotation success rate, unsigned image blocks, lead time delta.
Questions we hear from teams
- Do we need both OPA/Kyverno and CI checks?
- Yes. CI catches issues before merge; admission protects the cluster from drift and manual changes. Defense in depth prevents bypass via out‑of‑band applies or emergency changes.
- Will this slow us down?
- Done right, no. Start warn‑only, fix noisy rules, and shift checks left with pre‑commit. Teams usually hold or improve lead time once the feedback loop is local and fast.
- We’re on Terraform Cloud—use Sentinel or OPA?
- Either works. If you’re already on TFC, Sentinel is convenient. If you want vendor‑neutral and reuse rules across tools, OPA/Rego + Conftest is our default.
- Vault vs. AWS Secrets Manager vs. SOPS?
- Use Secrets Manager for cloud‑native rotation, Vault for multi‑cloud and database brokers or complex workflows, and SOPS for GitOps overlays (not runtime). Many orgs use all three together.
- How do we handle urgent exceptions?
- Create a waiver file with expiry and ticket link. Policy reads it and allows the change temporarily. Expired waivers should fail CI and page the owner.
Ready to modernize your codebase?
Let GitPlumbers help you transform AI-generated chaos into clean, scalable applications.
