diff --git a/docs/pages/config/contributors.json b/docs/pages/config/contributors.json
index 46958d626..8078c7e88 100644
--- a/docs/pages/config/contributors.json
+++ b/docs/pages/config/contributors.json
@@ -428,6 +428,19 @@
"description": "Industry leading OpSec audits, security tools, and code reviews performed by true security wizards",
"badges": []
},
+ "s1ns3nz0": {
+ "slug": "s1ns3nz0",
+ "name": "s1ns3nz0",
+ "avatar": "https://avatars.githubusercontent.com/s1ns3nz0",
+ "github": "https://github.com/s1ns3nz0",
+ "twitter": null,
+ "website": "https://miata.cloud",
+ "company": null,
+ "job_title": null,
+ "role": "contributor",
+ "description": "Frameworks Contributor",
+ "badges": []
+ },
"scode2277": {
"slug": "scode2277",
"name": "Sara Russo",
diff --git a/docs/pages/devsecops/index.mdx b/docs/pages/devsecops/index.mdx
index 77399cac9..13a83ad1e 100644
--- a/docs/pages/devsecops/index.mdx
+++ b/docs/pages/devsecops/index.mdx
@@ -20,3 +20,4 @@ title: "Devsecops"
- [Repository Hardening](/devsecops/repository-hardening)
- [Security Testing](/devsecops/security-testing)
- [Isolation](/devsecops/isolation)
+- [Policy As Code](/devsecops/policy-as-code)
diff --git a/docs/pages/devsecops/overview.mdx b/docs/pages/devsecops/overview.mdx
index 9bcbe02b9..18a4f0a71 100644
--- a/docs/pages/devsecops/overview.mdx
+++ b/docs/pages/devsecops/overview.mdx
@@ -51,6 +51,8 @@ Some of the key areas to consider are:
- [Developer Machine Confinement](/devsecops/isolation/developer-machine-sandboxing) - Sandbox configurations for
Claude Code, Codex CLI, and VS Code dev containers
- [Isolation & Sandboxing](/devsecops/isolation) - Containment patterns for CI/CD, tool execution, and build pipelines
+- [Policy as Code](/devsecops/policy-as-code/overview) - Policy engines and NIST SP 800-204D applied across the
+ CI/CD pipeline, from commit through runtime
- [Repository Hardening](/devsecops/repository-hardening) - Protect your code repositories
- [Security Testing](/devsecops/security-testing) - Integrate security testing into your development workflow
diff --git a/docs/pages/devsecops/policy-as-code/ci-pipeline.mdx b/docs/pages/devsecops/policy-as-code/ci-pipeline.mdx
new file mode 100644
index 000000000..f3e029024
--- /dev/null
+++ b/docs/pages/devsecops/policy-as-code/ci-pipeline.mdx
@@ -0,0 +1,524 @@
+---
+title: "Policy in the CI Pipeline | SEAL"
+description: "Commit, merge, and build policy gates: what the engine evaluates at each stage, which violations block, and what evidence each stage records."
+tags:
+ - Engineer/Developer
+ - Security Specialist
+ - DevOps
+ - SRE
+contributors:
+ - role: wrote
+ users: [s1ns3nz0]
+ - role: reviewed
+ users: []
+---
+
+import { TagList, AttributionList, ContributeFooter, Checklist } from '../../../../components'
+
+# Policy in the CI Pipeline
+
+
+
+
+> 🔑 **Key Takeaway**: Each CI stage verifies the previous one and records what it found. A
+> stage that verifies thoroughly but records nothing has given the next gate no reason to trust
+> it, which is the same position as having verified nothing.
+
+Commit, merge, and build are the first three stages of the pipeline. They start with a
+developer identity and end with an artifact and its attestation, and everything downstream
+reads that attestation instead of re-checking the source.
+
+Each stage below is described the same way: what arrives and who asserts it, what document the
+engine evaluates, where the decision is enforced, what is checked, and what the stage records
+for the next one.
+
+For the policy engine itself, the two evaluation modes, and what the standard requires, see
+[Policy as Code: Overview](/devsecops/policy-as-code/overview).
+
+Two terms recur below. A check is **deterministic** when it has one correct answer and no
+override is reasonable. A check is a matter of **judgment** when the correct result depends on
+context the engine does not have.
+[Governing the Policy Set](/devsecops/policy-as-code/governance) maps that distinction to
+override authority.
+
+## Commit
+
+What arrives is a set of source changes and a developer identity asserted by the source-code
+management system. Nothing upstream attests to that identity, and a phished session token
+satisfies it. Every check downstream inherits this assumption.
+
+What the engine evaluates? A commit is not JSON, so the input has to be assembled:
+
+- changed file paths and signature status, from the SCM API
+- secret scanner output, as JSON
+- SCA output, as JSON
+
+Teams that skip the assembly step enforce this stage entirely through platform settings, which
+holds until a setting is changed.
+
+Where the decision is enforced? Two enforcement points share this stage. Push protection at the
+SCM rejects the push before it reaches the repository. A CI step covers what push protection
+cannot evaluate, and functions as an enforcement point only if it exits non-zero.
+
+### Commits are signed, and the signature is verified
+
+Result: `Deterministic`
+
+Requiring a signature and verifying it are separate controls. A platform setting can require
+that a signature be present. Confirming that the signing key belongs to a current member
+requires resolving the identity against the roster.
+
+The roster is policy data. It changes at every join and departure, so the rule reads it at
+evaluation time through `data.roster`. A hand-maintained list goes stale at the first
+offboarding.
+
+```python
+package commit.signing
+
+import rego.v1
+
+deny contains msg if {
+ not input.commit.signature.verified
+ msg := sprintf("commit %s is not signed by a verified key", [input.commit.sha])
+}
+
+deny contains msg if {
+ input.commit.signature.verified
+ not input.commit.author.login in data.roster.active_members
+ msg := sprintf("commit %s signed by %q, who is not a current member", [input.commit.sha, input.commit.author.login])
+}
+```
+
+### Sensitive paths require their owning team
+
+Result: `Deterministic`
+
+CODEOWNERS handles review routing. Paths that determine pipeline behavior need more than
+routing:
+
+- `.github/workflows/` determines what runs with access to secrets
+- the policy directory determines what blocks a release
+
+Encoding the path-to-owner map as policy allows the same rule to run at merge and in a
+scheduled audit, and records the decision.
+
+```python
+package commit.paths
+
+import rego.v1
+
+protected := {
+ ".github/workflows/": "ci-platform",
+ "policy/": "security",
+}
+
+deny contains msg if {
+ some path in input.commit.changed_files
+ some prefix, owner in protected
+ startswith(path, prefix)
+ not owner in input.commit.approving_teams
+ msg := sprintf("%q requires approval from %q", [path, owner])
+}
+```
+
+### No secrets enter the repository
+
+Result: `Deterministic`
+
+Enable push protection at the SCM and run a scanner in CI. Scan the repository before it holds
+code as well: a credential already present may already be exposed, depending on repository
+visibility.
+
+There is no threshold to configure. The rule reads scanner output and fails on any finding.
+
+### Dependencies are fully resolved, and truncation is a failure
+
+Result: `Judgment`
+
+The severity threshold is a judgment call, covered in
+[Governing the Policy Set](/devsecops/policy-as-code/governance).
+
+Resolution depth is not. If the SCA tool stopped before resolving the full transitive graph,
+the result is an incomplete scan, and it is indistinguishable from a clean one unless the rule
+checks for it. Encode truncation as a violation.
+
+```python
+package commit.dependencies
+
+import rego.v1
+
+deny contains msg if {
+ input.sca.max_depth_reached
+ msg := "SCA stopped before resolving all transitive dependencies"
+}
+
+deny contains msg if {
+ some dep in input.sca.dependencies
+ dep.severity in data.reachability[input.repo].blocking_severities
+ msg := sprintf("%s in %s@%s blocks at this tier", [dep.severity, dep.name, dep.version])
+}
+```
+
+### Analysis covers every language actually in use
+
+Result: `Judgment`
+
+A pipeline that runs SAST on the primary language and skips Solidity, shell scripts, or
+Terraform reports a clean run without having examined those files. Encode the expected language
+set as data and fail when actual scanner coverage does not match it.
+
+### Every result is recorded and keyed to the commit SHA
+
+Result: `Deterministic`
+
+Three records leave this stage.
+
+| Record | Consumed by |
+| --- | --- |
+| Commit signature, as a Git object | Merge gate |
+| Secret-scan result | Merge gate |
+| Dependency inventory, as an SBOM fragment or lockfile digest | Build, deploy |
+
+The next gate can only read records that persist outside the CI job.
+
+Every check above rests on an identity the SCM asserts and nothing attests to. A phished
+session token satisfies all of them. This is the weakest link in the sequence, and no later
+stage can recover it.
+
+Controls: [Security Testing](/devsecops/security-testing),
+[Repository Hardening](/devsecops/repository-hardening).
+
+## Merge
+
+What arrives is a proposed change at a known commit SHA, its signature, and the results the
+commit stage produced. Treat fork contributions as untrusted: they are attacker-controlled
+content requesting execution inside the pipeline.
+
+What the engine evaluates? Two documents.
+
+The workflow definitions themselves. `.github/workflows/**` is parsed from YAML into JSON and
+evaluated before anything runs. This catches conditions platform settings cannot express: a
+workflow can satisfy every branch protection rule while checking out untrusted code with
+secrets in scope.
+
+The SCM account's own configuration, pulled through the platform API or taken from an OpenSSF
+Scorecard run, and shaped into a record the engine can walk.
+
+```python
+package ci.workflow
+
+import rego.v1
+
+deny contains v if {
+ some job_name, i
+ uses := input.jobs[job_name].steps[i].uses
+ not startswith(uses, "./")
+ not regex.match(`@[0-9a-f]{40}$`, uses)
+ v := {
+ "id": "build-tooling.pinned-actions",
+ "class": "deterministic",
+ "msg": sprintf("job %q step %d uses %q, which is not pinned to a commit SHA", [job_name, i, uses]),
+ }
+}
+```
+
+Where the decision is enforced? A required status check. Branch protection refuses the merge
+while the check fails. The enforcement point is a platform feature you configure; the decision
+behind it is yours.
+
+The posture check uses a different enforcement point. It runs on a schedule, and its verdict
+raises an alert against the repository whose setting drifted.
+
+### No one approves their own change, and the approval count fits what it can reach
+
+Result: `Deterministic` for self-approval, `Judgment` for the count
+
+Blocking self-approval is a platform setting. Deciding that a library loaded by a signing page
+requires two independent approvals while an internal tool requires one is a policy.
+
+Store the count as tier data, so one rule serves every
+repository and the difference between them is visible as configuration.
+
+### Required checks ran against the commit being merged
+
+Result: `Deterministic`
+
+A check bound to a branch rather than a commit can pass against a state that no longer exists.
+The failure is silent: the check ran, it succeeded, and it examined different code.
+
+```python
+package merge.review
+
+import rego.v1
+
+deny contains msg if {
+ input.pull_request.author in input.pull_request.approvers
+ msg := sprintf("PR %d approved by its own author %q", [input.pull_request.number, input.pull_request.author])
+}
+
+deny contains msg if {
+ count(input.pull_request.approvers) < data.reachability[input.repo].min_approvals
+ msg := sprintf("PR %d has %d approvals; this tier requires %d", [input.pull_request.number, count(input.pull_request.approvers), data.reachability[input.repo].min_approvals])
+}
+
+deny contains msg if {
+ some check in input.pull_request.required_checks
+ check.head_sha != input.pull_request.head_sha
+ msg := sprintf("check %q ran against %s, not the head commit %s", [check.name, check.head_sha, input.pull_request.head_sha])
+}
+```
+
+### Workflow definitions are evaluated before they execute
+
+Result: `Deterministic`
+
+Require the following in every workflow file:
+
+- third-party actions pinned to full commit SHAs
+- `permissions` declared explicitly rather than inherited
+- no untrusted checkout under a privileged trigger
+
+`pull_request_target` combined with an untrusted checkout grants repository secrets to whoever
+opened the pull request. Branch protection does not detect this.
+
+### Untrusted contributions cannot reach secrets
+
+Result: `Deterministic`
+
+Choose one of two configurations. Both are acceptable; a fork workflow with secrets in scope
+and no approval gate is not.
+
+- Fork workflows run sandboxed, with no network access, no privileged access, and no ability to
+ read secrets.
+- Fork workflows wait for a maintainer with write access to approve the run, with the approval
+ setting at the strictest level the platform offers.
+
+### The SCM account's configuration is assessed against policy on a schedule
+
+Result: `Deterministic`
+
+The settings involved are the same ones
+[Repository Hardening](/devsecops/repository-hardening) documents. The difference is that this
+check runs across every repository on a cadence and produces a record, which converts an
+enabled setting into a verified one.
+
+NIST SP 800-204D requires this where an SCM lacks sufficient built-in protection, naming Open
+Policy Agent as the example mechanism and OpenSSF Scorecard as the example tool.
+
+```python
+package scm.posture
+
+import rego.v1
+
+deny contains v if {
+ some repo in input.repositories
+ repo.settings.allow_self_approval
+ v := {
+ "id": "scm-posture.self-approval",
+ "class": "deterministic",
+ "subject": repo.name,
+ "msg": "merge approval by the change author is permitted",
+ }
+}
+
+deny contains v if {
+ some repo in input.repositories
+ not repo.settings.require_signed_commits
+ v := {
+ "id": "scm-posture.unsigned-commits",
+ "class": "deterministic",
+ "subject": repo.name,
+ "msg": "signed commits are not required on the default branch",
+ }
+}
+```
+
+### Build tooling comes from a trusted origin
+
+Result: `Judgment`
+
+Run pipelines only with tools whose source-code origin can be established. How much provenance
+is sufficient is an organizational decision and should be documented.
+
+### The merge record carries reviewer identities alongside check results
+
+Result: `Deterministic`
+
+What leaves this stage:
+
+| Record | Consumed by |
+| --- | --- |
+| Merge commit at a known ref, with reviewer identities | Build |
+| Status check records bound to the head SHA | Build, audit |
+| SCM posture report | Scheduled review |
+
+The posture report feeds a scheduled review. A drifted setting raises an alert against the
+repository it belongs to, leaving unrelated releases unaffected.
+
+Controls: [Repository Hardening](/devsecops/repository-hardening).
+
+## Build
+
+What arrives is reviewed source at an approved ref and the resolved dependency set. Anything
+the build touches that is not declared here is an unrecorded input.
+
+What the engine evaluates? Before the build, the workflow document again, read for runner
+labels, container privileges, and trigger conditions. After the build, the attestation, which
+is consumed at the deploy gate. This stage specifies policy and emits
+evidence; most verification happens on either side of it.
+
+Where the decision is enforced? The build job. A failing verdict stops the job before it
+produces an artifact. If the artifact is produced first, it can be referenced by digest, and
+the deploy gate becomes the only remaining control.
+
+### Builds run only on approved, non-privileged, ephemeral runners
+
+Result: `Deterministic`
+
+Three requirements:
+
+- the runner is on an allowlist you maintain
+- the job does not run privileged
+- the runner is ephemeral and re-provisioned per job
+
+The allowlist is policy data (`data.approved_runners`). It describes your infrastructure rather
+than a general security property, and will differ between organizations.
+
+```python
+package build.platform
+
+import rego.v1
+
+deny contains msg if {
+ some job_name, job in input.jobs
+ not job["runs-on"] in data.approved_runners
+ msg := sprintf("job %q runs on %v, which is not an approved runner", [job_name, job["runs-on"]])
+}
+
+deny contains msg if {
+ some job_name, job in input.jobs
+ job.container.options
+ contains(job.container.options, "--privileged")
+ msg := sprintf("job %q requests a privileged container", [job_name])
+}
+```
+
+### Everything the build consumes is pinned immutably
+
+Result: `Deterministic`
+
+Pin compiler and toolchain versions, base images, lockfiles, and third-party actions.
+
+A tag is a mutable pointer. It can be moved to different content without any change in your
+repository, so a build that passed previously can produce different output from identical
+source.
+
+```python
+package build.pinning
+
+import rego.v1
+
+deny contains msg if {
+ some job_name, job in input.jobs
+ image := job.container.image
+ not contains(image, "@sha256:")
+ msg := sprintf("job %q uses image %q, which is pinned by tag rather than digest", [job_name, image])
+}
+```
+
+In March 2025 an attacker gained write access to `tj-actions/changed-files`, a widely used
+GitHub Action, and moved its version tags to a malicious commit that dumped runner memory into
+build logs. Secrets from thousands of repositories were exposed
+([CVE-2025-30066](https://nvd.nist.gov/vuln/detail/CVE-2025-30066)). Every repository that
+pinned the action by commit SHA was unaffected.
+
+### Release builds run only from protected refs, under a purpose-scoped identity
+
+Result: `Deterministic`
+
+The identity that builds a release should not be able to deploy, publish, or read unrelated
+secrets. How finely identities are scoped is an organizational decision and should be
+documented.
+
+### Network egress is default-deny with an explicit allowlist
+
+Result: `Judgment`
+
+Which registries and APIs a build requires changes over time. The allowlist is policy data and
+requires periodic review.
+
+### An attestation covers environment, process, materials, and artifacts
+
+Result: `Deterministic`
+
+| Component | Contents |
+| --- | --- |
+| Environment | Build system inventory: compiler, interpreter, platform |
+| Process | Programs that transformed source into artifact, and those that tested it |
+| Materials | Configuration, source, dependencies |
+| Artifacts | Output of the step: a binary, or a scan result |
+
+### The attestation is signed, stored tamper-proof, and produced by something more trusted than the build
+
+Result: `Deterministic`
+
+Three requirements on the evidence:
+
+- cryptographically signed with a secure key
+- stored tamper-proof, under access control
+- generated by a process at a higher level of trust or isolation than the build itself
+
+The third requirement is the one that carries weight. A signature identifies who wrote a
+statement; it does not establish that the statement is true. If the build step can sign its own
+attestation, an attacker who controls the build also controls the evidence describing it, and
+the signature verifies correctly the whole time.
+
+Treat every workflow job as untrusted, and treat a self-hosted runner as compromised once it
+has executed one.
+
+Controls: [Securing CI/CD Pipelines](/devsecops/continuous-integration-continuous-deployment),
+[Sandboxing & Isolation](/devsecops/isolation/sandboxing-and-isolation).
+
+## CI pipeline checklist
+
+
+- [ ] Commit signatures verified, not merely required
+- [ ] Signing identities resolved against the current roster
+- [ ] Protected paths require their owning team
+- [ ] Secret scanning runs at push and in CI, and the repository was scanned before it held code
+- [ ] SCA resolves the full transitive graph, and truncation is treated as a failure
+- [ ] Analysis covers every language in use, verified against an expected set
+- [ ] Commit-stage results recorded and keyed to the commit SHA
+- [ ] Self-approval blocked; approval count set per tier
+- [ ] Required checks bound to the head SHA, not the branch
+- [ ] Workflow definitions evaluated by policy before they execute
+- [ ] `pull_request_target` never combined with an untrusted checkout
+- [ ] Fork workflows either sandboxed or held for maintainer approval
+- [ ] SCM posture assessed on a schedule and recorded
+- [ ] Runners restricted to an approved allowlist, non-privileged and ephemeral
+- [ ] Images pinned by digest; toolchains, lockfiles, and actions pinned immutably
+- [ ] Release builds restricted to protected refs under a purpose-scoped identity
+- [ ] Egress default-deny with a documented allowlist
+- [ ] Attestation emitted, signed, stored tamper-proof
+- [ ] Attestation generated by a process more trusted than the build it describes
+
+
+## Further reading
+
+- [Policy as Code: Overview](/devsecops/policy-as-code/overview): the engine, the two modes,
+ and what the standard requires
+- [Policy in Release and Runtime](/devsecops/policy-as-code/release-and-runtime): what happens
+ to the artifact and attestation this section produces
+- [Governing the Policy Set](/devsecops/policy-as-code/governance): rule class, thresholds,
+ exceptions, and central ownership
+- [Securing CI/CD Pipelines](/devsecops/continuous-integration-continuous-deployment):
+ configuration for the controls gated here
+- [Repository Hardening](/devsecops/repository-hardening): the settings the posture checks read
+- [Security Testing](/devsecops/security-testing): SAST, DAST, and SCA tooling
+- [Conftest](https://www.conftest.dev/): evaluating structured configuration against Rego
+- [zizmor](https://docs.zizmor.sh/): static analysis for GitHub Actions workflows
+- [CVE-2025-30066](https://nvd.nist.gov/vuln/detail/CVE-2025-30066): the tj-actions compromise
+
+---
+
+
diff --git a/docs/pages/devsecops/policy-as-code/governance.mdx b/docs/pages/devsecops/policy-as-code/governance.mdx
new file mode 100644
index 000000000..6e306b281
--- /dev/null
+++ b/docs/pages/devsecops/policy-as-code/governance.mdx
@@ -0,0 +1,353 @@
+---
+title: "Governing the Policy Set | SEAL"
+description: "Governing a policy set: which violations block a release, who may override them, where thresholds come from, and how rules are owned and retired."
+tags:
+ - Engineer/Developer
+ - Security Specialist
+ - Operations & Strategy
+ - DevOps
+contributors:
+ - role: wrote
+ users: [s1ns3nz0]
+ - role: reviewed
+ users: []
+---
+
+import { TagList, AttributionList, ContributeFooter, Checklist } from '../../../../components'
+
+# Governing the Policy Set
+
+
+
+
+> 🔑 **Key Takeaway**: A policy engine that can only answer "fail" is not usable for long.
+> What decides whether a gate survives is which violations stop a release outright, which ones
+> a named owner may override, and what that override leaves behind.
+
+The rules in the preceding pages do nothing until three questions are answered: what blocks a
+release, who is allowed to decide otherwise, and how the rule set stays current.
+
+Getting these wrong fails in both directions. A gate where every finding blocks is disabled
+within a quarter, usually during an incident when someone needs to ship. A gate where every new
+rule ships as advisory blocks nothing and accumulates rules nobody has tested.
+
+This page is the organizational half of policy as code. The rules are code; the decisions about
+who may override them, and what happens when nobody maintains them, are not.
+
+## Deciding what blocks
+
+Three decisions govern a gate: how a violation is classified, where the threshold for a
+judgment call comes from, and how an override is recorded.
+
+### Class, not just severity
+
+[Security Testing](/devsecops/security-testing) sets severity thresholds for scanner findings:
+Critical blocks the pull request, Low is tracked. Severity measures how bad a finding is.
+
+Class measures something else: whether anyone is entitled to decide about it. A Critical
+vulnerability and an unpinned action can both block a release, but only the first has a person
+who can reasonably say "ship anyway, and here is why."
+
+The two are independent. A finding can be low severity and still have no override path.
+
+Treating every finding as blocking produces a gate that is eventually disabled. Four outcomes
+cover the cases, and each is decided in advance.
+
+| Violation class | Outcome | Decided by | Recorded | Expiry |
+| --- | --- | --- | --- | --- |
+| Deterministic | No-Go, no in-pipeline override | No one; the input is fixed | Rule id, artifact digest, decision log entry | None |
+| Judgment | No-Go pending explicit decision | Named owner of the rule | Who, why, artifact digest, linked issue | 90 days, then re-decided |
+| Unclassified | No-Go | Rule author and a security reviewer | As for judgment | Until a class is assigned |
+| Engine failure | No-Go | No one; treated as an incident | Alert and the failed evaluation | None |
+
+**Deterministic violations** have no override path because there is nothing to decide. The
+input is corrected.
+
+**Judgment violations** require an explicit decision from the rule's owner, and the record has
+to identify who decided and why. The decision expires, so it is made again at the next
+release.
+
+**Unclassified violations** cover rules that are new or not yet triaged. Blocking is the deliberate
+default. If new rules shipped as advisory until proven necessary, every rule would enter
+service as a no-op and the rule set would document controls that are not enforced.
+
+**Engine failures** cover an unreachable engine, a stale bundle, or an evaluation error. These
+are incidents. The remedy is to repair the engine and re-run the evaluation.
+
+The engine-failure row is where the PDP and PEP separation becomes an operational decision.
+When the decision point is unavailable, the enforcement point acts without a verdict. Fail
+closed and treat an absent verdict as a refusal. An enforcement point that fails open converts
+every engine outage into an organization-wide bypass that leaves no record.
+
+What holds this together is the record. A No-Go that anyone can convert to a Go without leaving
+one behind only delays the release.
+
+### Where thresholds come from
+
+Deterministic rules require no threshold. Judgment rules do, and this is where most policy sets
+become arbitrary. "Block Critical, track Low" appears in a great deal of documentation, this
+framework included, without an explanation of why the line sits at that point. A threshold that
+cannot be derived will not be defended when it blocks a release.
+
+Two properties of the artifact determine the threshold.
+
+**Reachability.** If this artifact is malicious, what does it touch? For a Web3 team the answer
+is frequently user funds. A library loaded by a page where users sign transactions is not
+equivalent to an internal dashboard, and each deserves its own threshold.
+
+**Reversibility.** A cluster deployment rolls back in minutes. A published npm version is
+already on other machines and cannot be recalled. Removing it from the registry does not
+remove it from lockfiles that already pinned it. An on-chain upgrade may be irreversible. The less reversible
+the release, the more the gate has to catch before it runs.
+
+Combining the two produces the tiers. High reachability with low reversibility takes the
+strictest thresholds you can operate. Low reachability with straightforward rollback takes
+thresholds loose enough to keep the gate off the critical path.
+
+Thresholds belong in policy data keyed by repository or artifact class, in the same form as the
+exception register. A rule that hardcodes `severity == "critical"` has to be forked for every
+repository that needs a different line. A rule that reads `tier.blocking_severities` is written
+once and configured per artifact. The deploy gate in
+[Policy in Release and Runtime](/devsecops/policy-as-code/release-and-runtime) works this way.
+
+The standard does not supply these numbers. NIST SP 800-204D requires that a policy exist and
+be enforced; it does not state that a Medium finding should block a wallet library. That
+derivation is yours, which is why the reasoning behind a threshold belongs in the policy
+repository alongside the threshold itself.
+
+### Overrides are data, not edits
+
+An exception carries three fields: the rule it suppresses, an owner, and an expiry. It lives in
+a checked-in file under the same review as the rules, and once the expiry passes it stops
+suppressing anything.
+
+This extends a discipline [Security Testing](/devsecops/security-testing) already applies to
+scanner suppressions, which carry a finding id, a rationale, a linked ticket, and an expiry
+date. The same fields, applied to the policy set.
+
+Structured rule output is what makes this possible. The gate routes on `class`, the exception
+register matches on `id`, and an expired entry returns to the blocking set.
+
+In the rules below, `violation` emits a structured record rather than a string so the gate can
+route on it. `suppressed` reads the exception register and stops matching once the expiry has
+passed. `blocking` collects anything that is neither downgraded to a judgment call nor covered
+by a live exception.
+
+```python
+package build.gate
+
+import rego.v1
+
+violation contains v if {
+ some job_name, i
+ uses := input.workflow.jobs[job_name].steps[i].uses
+ not regex.match(`@[0-9a-f]{40}$`, uses)
+ v := {
+ "id": "build-tooling.pinned-actions",
+ "class": "deterministic",
+ "msg": sprintf("%q is not pinned to a commit SHA", [uses]),
+ }
+}
+
+suppressed(v) if {
+ exception := data.exceptions[v.id]
+ time.parse_rfc3339_ns(exception.expires) > time.now_ns()
+}
+
+blocking contains v if {
+ some v in violation
+ v.class != "judgment"
+ not suppressed(v)
+}
+```
+
+```json
+{
+ "exceptions": {
+ "build-tooling.pinned-actions": {
+ "owner": "@security-team",
+ "reason": "vendor action ships no tags; tracked in SEC-142",
+ "expires": "2026-09-30T00:00:00Z"
+ }
+ }
+}
+```
+
+## Metadata that ties the chain together
+
+Each stage in the preceding pages emits records. Four properties turn those records into
+something answerable during an incident.
+
+**Use the artifact digest as the join key.** Commit SHAs identify source and tags identify
+releases, but only the digest identifies the exact bytes that were built, signed, deployed, and
+are running. Every record should carry it: attestation, SBOM, scan result, admission decision,
+drift event. Records keyed by version string cannot distinguish two builds of `v1.2.3`.
+
+**Store evidence outside the pipeline run that produced it.** Evidence must be tamper-proof and
+access-controlled. A decision log held in CI job output is removed with log retention. An
+attestation held only in the build workspace does not persist. Store evidence alongside the
+artifact it describes, or in a dedicated attestation store.
+
+**Set retention by artifact lifetime.** An SBOM is required when
+a CVE is disclosed against a dependency, which may be years after the build. Retain SBOMs and
+provenance for as long as the artifact may still be running.
+
+**Verify the metadata by answering an incident question.** Starting from a digest running in
+production:
+
+| Question | Record that answers it |
+| --- | --- |
+| What is running? | Deployed digest in declared state |
+| Where did it come from? | Provenance: source repository, ref, builder identity |
+| What is in it? | SBOM |
+| Who allowed it through? | Admission decision log, exception register |
+| Has it changed since? | Drift events |
+
+If answering any of these requires asking a person, the record chain is incomplete at that
+point.
+
+## Owning and maintaining the policy set
+
+Three questions decide whether a policy set survives contact with a growing organization: who
+owns the rules, how the set stays current, and how much of it to adopt at once.
+
+### Policy needs a central owner
+
+These pages assume a single policy set for the whole organization. Without that, policy as code
+reproduces the problem it was introduced to solve.
+
+The failure mode is consistent. Each repository grows its own `policy/` directory, seeded by
+copying whatever the previous team wrote. A rule is fixed in one of them and stays broken in
+the others, because nothing connects them. The organization's enforced posture becomes the
+weakest copy, and no one can state what is actually enforced without auditing every
+repository.
+
+Centralization means four things.
+
+**One source of rules, distributed to consumers.** What is centralized is the rule set. The
+engine still runs at merge, at build, and at deploy. Rules live in one repository
+and ship to consumers as versioned, signed bundles. Consumers pull a version; forking the
+content is not supported. A rule change then reaches every consumer on the next pull, and the
+enforced version is a property you can read. This also follows from the standard's definition of a
+policy as "a signed document that encodes the requirements for an artifact to be validated":
+an unsigned bundle cannot be verified as received intact.
+
+**One owner, with delegation.** A named team owns the rule set and reviews every change through
+CODEOWNERS on the policy repository. Domain teams contribute the rules they understand best;
+the owning team reviews them. No rule enters or leaves the enforced set without an accountable
+reviewer. A policy that can be weakened through an unreviewed pull request offers no
+protection.
+
+**One place decisions land.** Decision logs from every enforcement point go to a common store,
+and there is one enforcement point per stage. A common store makes organization-wide questions
+answerable: which rules fire most, which are overridden most, and which have never fired. Those
+answers drive the review and retirement steps below.
+
+**One exception register.** Exceptions are held with the policy, under the same review as the
+rules. A waiver granted in one repository should be visible to whoever reviews the
+rule set. A rule waived in four places is not being waived; it is not working.
+
+Scale the machinery to the problem. A single-repository team does not need a bundle registry; a
+local `policy/` directory under CODEOWNERS is the proportionate answer. The indirection earns
+its cost once rules span more than one repository or more than one enforcement point. For most
+protocol teams that happens early, because contracts, frontend, infrastructure, and keepers are
+already separate repositories sharing the same signing keys.
+
+### Keeping the policy set alive
+
+Run the policy set through the same lifecycle as any other code.
+
+**Author.** Every rule carries an id, a named owner, and a class before it ships. A rule with
+none of these cannot be routed, overridden, or reviewed.
+
+**Test.** Rego has a built-in test framework. Write a passing fixture and a failing fixture for
+every rule and run both in CI. An untested rule that stopped matching, because a field was
+renamed or a path changed, produces the same green pipeline as a rule that works. Nothing
+distinguishes the two until an incident does.
+
+**Stage.** New rules run in dry-run, then warn, then block, and each staged rule gets an
+enforcement date when it is created. A warn-only rule with no date is a permanent no-op that
+makes the rule set look more complete than it is. For sequencing against the underlying
+controls, the
+[execution sandboxing guide](/devsecops/isolation/execution-sandboxing-practical-guide) sets
+out a 30/60/90 day rollout; enforce each rule as the control it checks lands.
+
+**Enforce and distribute.** Fail closed on engine error, unreachable bundle, or evaluation
+timeout. Serve bundles signed and versioned so a consumer can verify what it is enforcing. Put
+the policy source behind CODEOWNERS with a security reviewer, and treat a pull request that
+weakens a rule with the same scrutiny as one that touches a signing key.
+
+**Review.** Decision logs turn the gate into evidence: which rules fire, how often, how many
+are overridden and by whom. Review the exception register on a fixed cadence. An exception that
+has been renewed three times is a rule that does not match reality, and renewing it a fourth
+time is maintenance work disguised as a risk decision.
+
+**Retire.** A rule that has not fired in a year is either dead or its risk is gone. Both need a
+decision. Rules that accumulate without pruning make the suite slow, and a slow gate is the one
+somebody argues for skipping.
+
+### Adopting this without stalling delivery
+
+SP 800-204D states directly that the full control set "cannot be implemented all at once in the
+SDLC of all enterprises without a great deal of disruption to underlying business processes and
+operational costs." It groups solutions into four types, which work as a sequencing guide.
+
+1. **Per-task pipeline features.** Tamper-proof build pipelines with verified visibility into
+ dependencies and build steps, since compromised dependencies and build tools are the largest
+ source of poisoned workflows. Per-stage checklists that the pipeline enforces.
+2. **Integrity and provenance** through digital signatures and attestations.
+3. **Currency of running code.** Institute a build horizon, where code older than a set period
+ is not launched, keeping production close to the reviewed commit.
+4. **Securing CI/CD clients** against malicious code that steals source, signing keys, or cloud
+ credentials, reads secrets from environment variables, or exfiltrates to an
+ attacker-controlled endpoint.
+
+A workable order: assess against SSDF to find the gaps, map the supply chain, analyze the
+threats, add pipeline controls stage by stage, then connect the resulting evidence to incident
+response. Evidence that nobody reads during an incident is cost without benefit.
+
+## Policy set health checklist
+
+
+- [ ] Every rule carries an id, a named owner, and a class
+- [ ] Rules live in one place and ship as versioned, signed bundles
+- [ ] Every rule has a passing and a failing test fixture, run in CI
+- [ ] New rules go dry-run, then warn, then block, each with an enforcement date set at creation
+- [ ] Unclassified rules block by default
+- [ ] The gate fails closed on engine error, unreachable bundle, or evaluation timeout
+- [ ] The policy source is behind CODEOWNERS with a security reviewer
+- [ ] Thresholds live in policy data keyed by reachability, not hardcoded in rules
+- [ ] Exceptions carry a rule id, an owner, and an expiry, and live in one register
+- [ ] Expired exceptions stop suppressing, verified by a test
+- [ ] Decision logs from every enforcement point land in a common store
+- [ ] The exception register is reviewed on a fixed cadence, not when it hurts
+- [ ] Chronically renewed exceptions are treated as broken rules, not accepted risk
+- [ ] Rules that have not fired in a year get an explicit keep-or-retire decision
+
+
+## Further reading
+
+- [Policy as Code: Overview](/devsecops/policy-as-code/overview): the engine, the two modes,
+ and what the standard requires
+- [Policy in the CI Pipeline](/devsecops/policy-as-code/ci-pipeline): the rules this page governs
+- [Policy in Release and Runtime](/devsecops/policy-as-code/release-and-runtime): where the tier
+ thresholds derived here get applied
+- [Security Testing](/devsecops/security-testing): severity thresholds for scanner findings, and
+ the suppression discipline this page extends to the policy set
+- [Repository Hardening](/devsecops/repository-hardening): CODEOWNERS and branch protection for
+ the policy repository itself
+- [Sandboxing & Policy Enforcement](/devsecops/isolation/sandboxing-and-policy-enforcement):
+ policy checkpoints across pre-execution, runtime, and post-execution
+- [Execution Sandboxing: A Practical Guide](/devsecops/isolation/execution-sandboxing-practical-guide):
+ a 30/60/90 rollout to sequence rule enforcement against
+- [NIST SP 800-204D](https://csrc.nist.gov/pubs/sp/800/204/d/final): the implementation strategy
+ section, and the definition of a policy as a signed document
+- [OPA policy testing](https://www.openpolicyagent.org/docs/latest/policy-testing/): the `opa test`
+ framework
+- [OPA bundles](https://www.openpolicyagent.org/docs/latest/management-bundles/): versioned,
+ signed policy distribution
+- [OPA decision logs](https://www.openpolicyagent.org/docs/latest/management-decision-logs/)
+
+---
+
+
diff --git a/docs/pages/devsecops/policy-as-code/index.mdx b/docs/pages/devsecops/policy-as-code/index.mdx
new file mode 100644
index 000000000..535b030d2
--- /dev/null
+++ b/docs/pages/devsecops/policy-as-code/index.mdx
@@ -0,0 +1,17 @@
+---
+title: "Policy As Code"
+---
+
+{/* AUTOGENERATED: This file is generated by utils/generate-folder-indexes.js */}
+
+# Policy As Code
+
+> _Note:_ This page is auto-generated. Please use the sidebar to explore the docs instead of
+> navigating directory paths directly.
+
+## Pages
+
+- [Policy as Code through the CI/CD Pipeline](/devsecops/policy-as-code/overview)
+- [Policy in the CI Pipeline](/devsecops/policy-as-code/ci-pipeline)
+- [Policy in Release and Runtime](/devsecops/policy-as-code/release-and-runtime)
+- [Governing the Policy Set](/devsecops/policy-as-code/governance)
diff --git a/docs/pages/devsecops/policy-as-code/overview.mdx b/docs/pages/devsecops/policy-as-code/overview.mdx
new file mode 100644
index 000000000..16e68f5d5
--- /dev/null
+++ b/docs/pages/devsecops/policy-as-code/overview.mdx
@@ -0,0 +1,257 @@
+---
+title: "Policy as Code through the CI/CD Pipeline | SEAL"
+description: "Policy as code across the CI/CD pipeline: what a policy engine decides, what NIST SP 800-204D requires of it, and how each gate reads the build evidence."
+tags:
+ - Engineer/Developer
+ - Security Specialist
+ - DevOps
+ - SRE
+ - Protocol
+contributors:
+ - role: wrote
+ users: [s1ns3nz0]
+ - role: reviewed
+ users: []
+---
+
+import { TagList, AttributionList, ContributeFooter } from '../../../../components'
+
+# Policy as Code through the CI/CD Pipeline
+
+
+
+
+> 🔑 **Key Takeaway**: Configuration states an intent. Policy enforces it and leaves a record.
+> Every pipeline stage should verify the previous one against evidence it can read, and the
+> record it leaves is what the next stage has to work with.
+
+A setting is a statement of intent. Anyone with access can change it, and nothing records that
+it changed or who changed it. A policy is a rule the pipeline evaluates on every run, and its
+output is a decision with a reason attached.
+
+That difference is what these pages are about. Other DevSecOps pages document which settings to
+enable; this one covers what a policy engine decides, what evidence each stage produces, and
+which of those decisions a person is allowed to override. A pipeline moves an artifact through
+commit, merge, build, release, deployment, and runtime. Each stage receives something from the
+previous one and either verifies it or assumes it.
+
+For a Web3 team the assumption is expensive. Pipelines hold deploy keys and signing wallets,
+and publish packages that load into wallet frontends. An artifact that reaches production
+without a verifiable origin has taken the shortest available path to user funds.
+
+These pages walk the pipeline in order, state what policy applies at each stage, and link to
+the pages that document the underlying controls.
+
+## The basics
+
+### General concepts
+
+- **Policy Decision Point (PDP)**: evaluates rules, returns a verdict and a reason.
+- **Policy Enforcement Point (PEP)**: acts on the verdict. A CI step exiting non-zero, a
+ deploy gate, a merge block.
+- **Policy data**: what the decision is made against, plus context such as trusted builders,
+ the registry allowlist, and the exception register.
+
+Numerous types of artifacts are created during the CI/CD process. If you don't have any
+policies or security gates throughout your CI/CD pipeline, you can't stop attackers from
+accessing resources through a compromised pipeline. A policy engine is the officially
+recommended tool for preventing and detecting compromised artifacts.
+
+One PDP serves many PEPs. The engine that decides is the same at every stage. The component
+that acts on its verdict differs per stage:
+
+| Stage | Enforcement point |
+| --- | --- |
+| Commit | Push protection at the SCM, and a CI step exiting non-zero |
+| Merge | A required status check that branch protection will not merge without |
+| Build | The build job, failing before it produces an artifact |
+| Release | The signing step, which does not run without a passing verdict |
+| Deploy | An admission controller, or the deploy job refusing promotion |
+| Runtime | The GitOps controller, resyncing or alerting on divergence |
+
+Two consequences follow.
+
+- Most adoption effort goes into wiring enforcement points. Rules are portable across
+ platforms; enforcement points are platform-specific.
+- A step only enforces if it exits non-zero. Printing violations and returning success leaves
+ the pipeline unchanged.
+
+**Policy data** keeps one rule set serving many contexts. The rule states what must be true.
+The data states what is true in a given repository: approved runners, the current roster,
+allowed registries, blocking severities, and live exceptions. In the examples throughout these
+pages, `data.` references mark that boundary.
+
+### Artifacts
+
+- **SBOM** (Software Bill of Materials): what components are in this software?
+- **Provenance**: which source, dependencies, environment, and process produced this artifact?
+- **Attestation**: who claims a check was performed, and can that claim be trusted?
+
+### Associated frameworks
+
+- **NIST SP 800-218 (SSDF)**: the Secure Software Development Framework, applicable to any
+ software architecture.
+ - Provides a core set of secure software development practices that can be integrated into
+ an existing SDLC.
+ - Technology-neutral, organized around the security outcomes an organization should achieve.
+- **NIST SP 800-204D**: *Strategies for the Integration of Software Supply Chain Security in
+ DevSecOps CI/CD Pipelines*.
+ - Explains how software supply chain security can be integrated into the CI/CD pipelines
+ used to develop and deploy cloud-native applications.
+
+**NIST SP 800-204D can therefore be understood as guidance on how to implement SSDF security
+objectives within a CI/CD pipeline.**
+
+Appendix A of SP 800-204D maps each pipeline task to an SSDF practice, so this work is
+reportable as SSDF conformance. Secure build maps to PO.1 and PW.6.
+
+### Associated project
+
+- [Open Policy Agent (OPA)](https://www.openpolicyagent.org/): a policy engine that
+ streamlines policy management across your stack for improved development, security, and
+ audit capability. This engine is officially referred to in NIST SP 800-204D as a policy
+ engine.
+
+## What a policy engine gives you
+
+A policy engine is a component that answers exactly one question: given this input, do the
+rules allow it?
+
+- Works regardless of execution environment
+- Only takes structured data such as workflow files, attestations, and manifests
+- Returns a verdict and a reason, usable as a decision point
+
+A simple `if`-statement script evaluates conditions directly within individual code paths,
+whereas a policy engine separates and centralizes policy evaluation so the same rules can be
+applied consistently across multiple systems.
+
+Rules become reviewable artifacts. They live in Git, change through pull requests, and can be
+owned via CODEOWNERS. They are also testable: write a fixture for the passing case and one for
+the failing case, and run both in CI. An untested rule that stops matching because a field was
+renamed produces the same green pipeline as a rule that works.
+
+Decisions come back structured. The gate can route on the result:
+block, warn, or require a named owner to sign off. Every evaluation can be logged with its
+input and its verdict, which supplies evidence for incident response and conformance reporting.
+
+In addition, policy logic stays separate from policy data. Trusted builders, registry
+allowlists, and the exception register are inputs, not code, so granting an exception does not
+mean editing a rule, and the exception can carry its own owner and expiry.
+
+Finally, a policy rule engine enables organizations to manage their policies as code across
+various environments.
+
+The cost is real. An engine is another component to operate, another language to learn, and a
+dependency the pipeline now fails closed on. It earns that cost once rules span more than one
+repository or more than one enforcement point. Below that, a workflow linter and branch
+protection do the same job with less machinery.
+
+## What NIST SP 800-204D asks for
+
+The standard names the policy engine as the enforcement mechanism in four places.
+
+**Pipeline prerequisites.** Organizations must define pipeline activities and security
+requirements for application code, for infrastructure as code, and for **policy as code and
+configuration code**. Policy as code is listed alongside the application itself.
+
+**Secure build.** The second of three required tasks is to enforce build policies "using
+techniques such as an agent and policy enforcement engine." Appendix A restates the same task
+when mapping it to SSDF: enforce those policies "using an agent or some other means **and a
+policy enforcement engine**."
+
+**Repository operations.** Where a source-code management system lacks the built-in protections
+the standard requires, external tooling must evaluate the SCM account's security settings
+against a policy and report actionable findings. The document names **Open Policy Agent** as
+the example, and OpenSSF Scorecard as the example tool.
+
+**Deployment.** CD measures "can be implemented by defining verification policies for allowing
+or disallowing an artifact for deployment."
+
+The standard also defines what a policy is: "a signed document that encodes the requirements
+for an artifact to be validated." A rule that anyone can edit without review does not meet
+that definition.
+
+## Two enforcement modes
+
+A policy engine operates in two modes. Distinguishing them matters because only the first is
+commonly implemented.
+
+**Mode 1: artifacts flowing through the pipeline.** A workflow file, a build attestation, a
+deployment manifest. The document already exists. The engine reads it and returns a verdict.
+
+**Mode 2: the platform's own configuration.** "Require signed commits" is a setting. "No
+repository in this organization permits self-approval, and the release gate fails when one
+does" is a policy. The first describes an intended state. The second is verified and recorded.
+
+Mode 2 is what distinguishes these pages from
+[Repository Hardening](/devsecops/repository-hardening), which documents which settings to
+enable. The question here is how you confirm they are still enabled next quarter, across every
+repository, without manual review.
+
+Mode 2 requires a collector: a component that calls the platform API, shapes the response into
+JSON, and passes it to the engine. That collector becomes part of the trust base. If its token
+is stolen or its output is modified, the engine evaluates fabricated data and returns a passing
+verdict. The standard's requirement that evidence come from a process more trusted than what it
+describes applies to posture data as well, so the collector belongs inside the pipeline's trust
+boundary.
+
+### About the examples in these pages
+
+Every Rego snippet in this section is illustrative, and represents what the engine reads, what
+it decides, and what it returns.
+
+Two things have to come from your organization.
+
+- The input structure depends on which SCM, registry, and CI platform you run and on how your
+ collectors shape their output. The field names will not match yours.
+- The rule content is a statement of what your organization has decided to require, which is a
+ different question from what a standard permits.
+- The work is deriving your own requirements from your own threat model, organizational
+ regulations, and incident history.
+
+## What this framework covers
+
+Where to start depends on what already exists. If nothing is enforced yet, begin with the merge
+gate: workflow definitions are the highest-leverage document and the cheapest to evaluate. If
+gates exist but releases route around them, the problem is governance. If
+artifacts ship without provenance, start with release.
+
+1. [Policy in the CI Pipeline](/devsecops/policy-as-code/ci-pipeline): what policy applies at
+ commit, merge, and build, and what each stage emits for the next one.
+2. [Policy in Release and Runtime](/devsecops/policy-as-code/release-and-runtime): publishing
+ identity, deployment gates, and holding running state to what was declared.
+3. [Governing the Policy Set](/devsecops/policy-as-code/governance): which violations block,
+ who may override, where thresholds come from, and how a policy set is maintained.
+
+## Further reading
+
+- [Securing CI/CD Pipelines](/devsecops/continuous-integration-continuous-deployment):
+ configuration for the controls these pages gate
+- [Repository Hardening](/devsecops/repository-hardening): the branch protection and
+ organization settings the posture checks evaluate
+- [Security Testing](/devsecops/security-testing): SAST, DAST, and SCA tooling, and severity
+ thresholds
+- [Implementing Code Signing](/devsecops/code-signing): signing mechanics behind the release
+ gate
+- [Sandboxing & Isolation](/devsecops/isolation/sandboxing-and-isolation): runner and build
+ containment
+- [Sandboxing & Policy Enforcement](/devsecops/isolation/sandboxing-and-policy-enforcement):
+ admission-time and runtime enforcement
+- [NIST SP 800-204D](https://csrc.nist.gov/pubs/sp/800/204/d/final): the source document
+- [NIST SP 800-218 (SSDF)](https://csrc.nist.gov/pubs/sp/800/218/final): the practices
+ SP 800-204D implements
+- [Open Policy Agent documentation](https://www.openpolicyagent.org/docs/latest/) and the
+ [Rego policy reference](https://www.openpolicyagent.org/docs/latest/policy-reference/)
+- [Conftest](https://www.conftest.dev/): evaluating structured configuration against Rego
+- [OpenSSF Scorecard](https://securityscorecards.dev/): named in SP 800-204D as the example
+ tool for assessing SCM posture
+- [SLSA specification](https://slsa.dev/spec/v1.0/): provenance format for build evidence
+- [zizmor](https://docs.zizmor.sh/): static analysis for GitHub Actions workflows
+
+---
+
+> *Note:* Rego examples in this section are fenced as `python`. The site's syntax highlighter
+> has no Rego grammar, and an unrecognized language fails the build. Python is the closest
+> available approximation.
+
+
diff --git a/docs/pages/devsecops/policy-as-code/release-and-runtime.mdx b/docs/pages/devsecops/policy-as-code/release-and-runtime.mdx
new file mode 100644
index 000000000..12bddcd83
--- /dev/null
+++ b/docs/pages/devsecops/policy-as-code/release-and-runtime.mdx
@@ -0,0 +1,432 @@
+---
+title: "Policy in Release and Runtime | SEAL"
+description: "Release, deploy, and runtime policy gates: publishing identity, provenance verification at deployment, and drift between declared and running state."
+tags:
+ - Engineer/Developer
+ - Security Specialist
+ - DevOps
+ - SRE
+ - Protocol
+contributors:
+ - role: wrote
+ users: [s1ns3nz0]
+ - role: reviewed
+ users: []
+---
+
+import { TagList, AttributionList, ContributeFooter, Checklist } from '../../../../components'
+
+# Policy in Release and Runtime
+
+
+
+
+> 🔑 **Key Takeaway**: The deploy gate is where the pipeline's work becomes enforceable.
+> Everything the build proved stays advisory until something refuses an artifact that cannot
+> show where it came from.
+
+The artifact and its attestation arrive from
+[Policy in the CI Pipeline](/devsecops/policy-as-code/ci-pipeline). Three stages remain:
+publish it, deploy it, and keep the running state matching what was declared.
+
+The earlier stages evaluate things you control directly. From here the artifact leaves your
+systems, and the gates read signed evidence about it.
+
+The publish stage matters most for Web3 teams and is gated least often. A package published to
+a registry is loaded by wallet frontends within hours, and the registry account that publishes
+it usually sits outside both the repository settings and the pipeline.
+
+## Release and publish
+
+What arrives is the artifact and its build attestation. After this stage the artifact is
+installed by other parties, so it is the last point at which anything can be asserted about it.
+
+Attackers target the evidence generation process as well as the artifact, to remove the record
+that distinguishes a legitimate release from a malicious one. Verifying a signature on package
+metadata is not sufficient, because the signature generation step is itself attackable.
+
+What the engine evaluates? Publishing identity lives in a package registry's account settings
+and not in your repository, so this stage uses mode 2 and requires a second collector. The
+registry's member list, MFA status, and token inventory are pulled and compared against the
+current organization roster, which is the same policy data the commit stage reads. Most
+organizations do not build this collector.
+
+Where the decision is enforced? The signing step. It does not refuse to promote an existing
+artifact; it refuses to produce one. An unsigned artifact cannot be verified downstream, so a
+failure here propagates without further enforcement.
+
+### Signing is gated on the build policy result
+
+Result: `Deterministic`
+
+Signing must be conditional on the policy result. If the check runs after signing, or runs
+where the signing step cannot observe it, the signature records only that a pipeline
+executed.
+
+### Every publishing identity is org-owned, MFA-enforced, and on the current roster
+
+Result: `Deterministic`
+
+Authorization must be evaluated at release time. Account setup is not evidence of current
+authorization. This
+is mode 2 applied to a different platform: the registry's member list and token inventory,
+compared against the organization roster.
+
+```python
+package release.publish
+
+import rego.v1
+
+deny contains msg if {
+ not input.build.policy_passed
+ msg := "signing requested before the build policy result was available"
+}
+
+deny contains msg if {
+ some publisher in input.registry.publishers
+ not publisher.login in data.roster.active_members
+ msg := sprintf("registry publisher %q is not a current member", [publisher.login])
+}
+
+deny contains msg if {
+ some publisher in input.registry.publishers
+ not publisher.mfa_enabled
+ msg := sprintf("registry publisher %q has no MFA enabled", [publisher.login])
+}
+
+deny contains msg if {
+ input.artifact.published_digest != input.attestation.subject_digest
+ msg := sprintf("published digest %s does not match attested digest %s", [input.artifact.published_digest, input.attestation.subject_digest])
+}
+```
+
+In December 2023 a former Ledger employee's npm account was phished using a stolen session
+token that bypassed 2FA. The attacker published malicious versions `1.1.5` through `1.1.7` of
+Ledger Connect Kit, injecting a wallet drainer into every dApp that loaded the library, and
+roughly $600,000 was taken before the code was removed about forty minutes after discovery.
+Ledger's [incident report](https://www.ledger.com/blog/security-incident-report) notes that the
+employee's access to GitHub, SSO, and internal tooling had been revoked at offboarding. Their
+npm access had not.
+
+No build-time policy would have caught this. The build was fine; the authorization on the
+publish path was not. That is the argument for evaluating publishing identity on every release
+instead of configuring it once.
+
+### Signing keys are structured so one compromise is survivable
+
+Result: `Deterministic`
+
+Requirements:
+
+- roles hold multiple keys under threshold or quorum trust
+- online keys, used automatically without a human present, are not the keys clients ultimately
+ trust for what they install
+- where an online key is unavoidable, it is HSM-backed and usable only against an artifact that
+ passed the build policy
+
+Key count and quorum size are organizational decisions, determined by team size and the impact
+of a single compromise.
+
+### The published digest matches the attested digest, and versions are immutable
+
+Result: `Deterministic`
+
+Two checks:
+
+- the published digest equals the attested digest, which catches a mismatch between what was
+ built and what shipped
+- registry version immutability is enabled, so a published version cannot be replaced under
+ consumers that already pinned it
+
+### The release emits evidence bound to the artifact digest
+
+Result: `Deterministic`
+
+Three records leave this stage, all bound to the same digest.
+
+| Record | Format | Stored | Consumed by |
+| --- | --- | --- | --- |
+| Signed provenance | in-toto or SLSA attestation | Attestation store or registry | Deploy gate |
+| SBOM | SPDX or CycloneDX | Artifact store, retained for the artifact's life | Incident response, deploy gate |
+| Signature and certificate | Cosign bundle, transparency log entry | Registry, Rekor | Any external verifier |
+
+The SBOM is what answers whether you are affected when a CVE is disclosed against a dependency,
+which may be years after the build.
+
+Most teams emit nothing here that anything downstream reads. The package is published and the
+record stops. For a team shipping npm packages into wallet frontends, this is the widest gap in
+the sequence.
+
+Controls: [Implementing Code Signing](/devsecops/code-signing).
+
+## Deploy
+
+What arrives is an artifact reference and whatever evidence exists about it. This gate knows
+only what upstream metadata records. When those records are absent, the correct result is
+refusal.
+
+What the engine evaluates? Signed evidence: the provenance predicate and the attested scan
+result. Both are verified for signature first, then evaluated as claims. On a cluster this
+arrives as an admission review; in a pipeline, as a verified attestation bundle. This is the
+only stage where the engine reads signed assertions.
+
+Where the decision is enforced? Either an admission controller, which refuses to start the
+workload, or the deploy job, which refuses to promote. The choice affects coverage. An
+admission controller evaluates everything reaching the cluster, including changes that did not
+come through the pipeline. A deploy-job enforcement point sees only what the pipeline sends it.
+
+Thresholds are not universal at this stage. They are read from policy data, keyed by what the
+artifact can reach:
+
+```python
+package deploy.gate
+
+import rego.v1
+
+tier := data.reachability[input.artifact.repo]
+
+deny contains v if {
+ builder := input.provenance.runDetails.builder.id
+ not builder in tier.trusted_builders
+ v := {
+ "id": "deploy.untrusted-builder",
+ "class": "deterministic",
+ "msg": sprintf("built by %q, which is not a trusted builder for this tier", [builder]),
+ }
+}
+
+deny contains v if {
+ scan_age_days := round((input.now_ns - time.parse_rfc3339_ns(input.scan.completed_at)) / (24 * 60 * 60 * 1000000000))
+ scan_age_days > tier.max_scan_age_days
+ v := {
+ "id": "deploy.stale-scan",
+ "class": "judgment",
+ "msg": sprintf("vulnerability scan is %d days old; this tier allows %d", [scan_age_days, tier.max_scan_age_days]),
+ }
+}
+
+deny contains v if {
+ some finding in input.scan.findings
+ finding.severity in tier.blocking_severities
+ v := {
+ "id": "deploy.blocking-vulnerability",
+ "class": "judgment",
+ "msg": sprintf("%s finding %q blocks release at this tier", [finding.severity, finding.id]),
+ }
+}
+```
+
+```json
+{
+ "reachability": {
+ "wallet-connect-kit": {
+ "reaches_user_funds": true,
+ "reversible": false,
+ "trusted_builders": ["https://github.com/acme/wallet-connect-kit/.github/workflows/release.yml@refs/heads/main"],
+ "max_scan_age_days": 7,
+ "blocking_severities": ["critical", "high", "medium"]
+ },
+ "internal-dashboard": {
+ "reaches_user_funds": false,
+ "reversible": true,
+ "trusted_builders": ["https://github.com/acme/internal-dashboard/.github/workflows/release.yml@refs/heads/main"],
+ "max_scan_age_days": 30,
+ "blocking_severities": ["critical", "high"]
+ }
+ }
+}
+```
+
+One rule set, two outcomes. A Medium finding and a three-week-old scan block the wallet library
+and pass the internal dashboard. The rule did not change; the data did. One of these artifacts
+loads into a page where users sign transactions and cannot be unpublished once released.
+
+Where those numbers come from is covered in
+[Governing the Policy Set](/devsecops/policy-as-code/governance).
+
+### The signature is verified before any claim inside the attestation is evaluated
+
+Result: `Deterministic`
+
+Order matters, and the error is easy to miss because both steps appear to work in isolation.
+Evaluating policy over an unverified document produces a verdict about assertions that anyone
+could have written.
+
+### Provenance matches the approved build process
+
+Result: `Deterministic`
+
+Builder identity, source repository, and source ref must all match policy. The build emits an
+attestation; this gate refuses any artifact that cannot produce one naming an approved
+builder.
+
+This refusal is what makes the CI pipeline enforceable. Without it, the build can be as careful
+as you like and nothing prevents a different artifact from reaching production under the same
+version number.
+
+### Missing evidence produces refusal, not an assumption of good faith
+
+Result: `Deterministic`
+
+The common failure at this gate is an absent verdict: the attestation is missing, the scan
+result was never uploaded, or the evidence store was unreachable. Treat each as a refusal.
+
+### An attested vulnerability scan exists and is recent enough for this tier
+
+Result: `Judgment`
+
+Scanners are updated continuously to detect newly disclosed vulnerabilities, so scan age is
+part of the check. What counts as too old depends on what the artifact can
+reach, and is set per tier.
+
+### Deployable content is scanned for secrets, and dependency risk is visible
+
+Result: `Judgment`
+
+Secret scanning at this point catches what earlier stages could not see: configuration
+assembled during release, and values injected at packaging. Dependency review surfaces
+vulnerable versions before promotion.
+
+### Deployment runs under a short-lived, environment-scoped identity
+
+Result: `Deterministic`
+
+Use a short-lived OIDC credential scoped to the target environment, and require environment
+approval on production paths. Scope granularity is an organizational decision and should be
+documented. Platform defaults are not a scope decision.
+
+### The gate records what it decided and what it read
+
+Result: `Deterministic`
+
+Two records leave the gate.
+
+| Record | Contents | Consumed by |
+| --- | --- | --- |
+| Admission decision | Rule ids evaluated, verdict, artifact digest, evidence consulted | Audit, incident response |
+| Deployed digest | Pinned in the deployment manifest, never a mutable tag | Runtime drift detection |
+
+## Runtime
+
+What arrives is two states that should be identical: the state declared in Git, and the state
+running in the cluster. The control at this stage is continuous comparison between the two.
+
+What the engine evaluates? The rendered manifests in Git as one document, the live cluster
+state as another, and whether they agree. Rules also evaluate the declared state on its own
+terms: images referenced by digest rather than tag, no privileged pods, resource limits
+present.
+
+Where the decision is enforced? The GitOps controller. This enforcement point differs from the
+others because the change has already taken effect. It acts by resyncing or by raising an
+alert, which is the only option available once something is running.
+
+### Declared state pins digests, not tags
+
+Result: `Deterministic`
+
+A manifest that references `:latest` declares nothing verifiable. The same declared state
+resolves to different running code depending on when it was applied, which makes drift
+detection meaningless.
+
+### Drift is detected continuously, and divergence never persists silently
+
+Result: `Deterministic`
+
+Pull the repository, compare against live configuration, then either resync automatically or
+alert for remediation. Choosing between resync and alert is an operational decision. Doing
+neither leaves divergence undetected until it causes an incident.
+
+```python
+package runtime.declared
+
+import rego.v1
+
+deny contains msg if {
+ some c in input.spec.template.spec.containers
+ not contains(c.image, "@sha256:")
+ msg := sprintf("container %q references %q by tag; declared state must pin digests", [c.name, c.image])
+}
+
+deny contains msg if {
+ some d in input.drift
+ d.field != ""
+ msg := sprintf("live state diverges from declared at %q (declared %q, observed %q)", [d.field, d.declared, d.observed])
+}
+```
+
+### Changes arrive through the pipeline, never through an operator's terminal
+
+Result: `Deterministic`
+
+Change the code and trigger a release, so Git commits remain the single source of truth for
+what runs. Roll back by reverting the declared state and letting the controller apply it.
+
+This item is what makes the preceding stages count. Every control across these pages is void if
+an operator can edit the running state afterwards, because the state that was verified and the
+state that is running are then two different things.
+
+### Release data is preserved for every release
+
+Result: `Deterministic`
+
+Retain module versions, configuration files, and operational metadata for every release. This
+is what allows you to determine what was running at a given point in time during a
+post-incident review.
+
+Enforcement at admission time, using tools such as Gatekeeper or the Sigstore policy controller,
+is covered in
+[Sandboxing & Policy Enforcement](/devsecops/isolation/sandboxing-and-policy-enforcement).
+
+## Where the chain actually breaks
+
+Two stages hold by convention alone.
+
+The commit stage rests on an identity the SCM asserts and nothing attests to. A phished session
+token satisfies every check in that stage, and everything downstream inherits the result.
+
+The publish stage usually emits evidence that nothing downstream reads. The package ships, the
+provenance is either absent or unverified by any consumer, and the record stops there. For a
+team shipping npm packages into wallet frontends, this is the widest gap in the sequence.
+
+## Release and runtime checklist
+
+
+- [ ] Signing gated on the build policy result, checked before signing runs
+- [ ] Publishing identities org-owned, MFA-enforced, resolved against the current roster
+- [ ] Signing roles hold multiple keys under threshold or quorum trust
+- [ ] Online keys are not the keys clients ultimately trust; unavoidable ones are HSM-backed
+- [ ] Published digest equals the attested digest
+- [ ] Registry version immutability enabled
+- [ ] Provenance, SBOM, and signature bundle published and bound to the same digest
+- [ ] Signature verified before any attestation claim is evaluated
+- [ ] Provenance matched against builder identity, source repository, and ref
+- [ ] Missing evidence produces refusal
+- [ ] Attested vulnerability scan required, with a tier-specific age threshold
+- [ ] Deployable content scanned for secrets; dependency review before promotion
+- [ ] Deployment identity short-lived and environment-scoped; production requires approval
+- [ ] Admission decisions logged with rule ids, verdict, digest, and evidence consulted
+- [ ] Deployed digest pinned in the manifest, never a mutable tag
+- [ ] Drift detected continuously, with auto-resync or alerting
+- [ ] Manual runtime mutation blocked; rollbacks revert declared state
+- [ ] Release data preserved: module versions, configuration, operational metadata
+
+
+## Further reading
+
+- [Policy as Code: Overview](/devsecops/policy-as-code/overview): the engine, the two modes,
+ and what the standard requires
+- [Policy in the CI Pipeline](/devsecops/policy-as-code/ci-pipeline): where the artifact and
+ its attestation come from
+- [Governing the Policy Set](/devsecops/policy-as-code/governance): where the tier thresholds
+ above come from, and who may override a blocked release
+- [Implementing Code Signing](/devsecops/code-signing): signing mechanics behind the release gate
+- [Sandboxing & Policy Enforcement](/devsecops/isolation/sandboxing-and-policy-enforcement):
+ admission-time and runtime enforcement
+- [SLSA provenance specification](https://slsa.dev/spec/v1.0/provenance)
+- [Sigstore](https://docs.sigstore.dev/): signing, transparency log, and policy controller
+- [Ledger security incident report](https://www.ledger.com/blog/security-incident-report):
+ the Connect Kit compromise
+
+---
+
+
diff --git a/vocs.config.ts b/vocs.config.ts
index 6501bd2d3..e5abd91f7 100644
--- a/vocs.config.ts
+++ b/vocs.config.ts
@@ -121,6 +121,17 @@ const config = {
{ text: 'Data Security Checklist', link: '/devsecops/data-security-upgrade-checklist' },
{ text: 'Governance Proposal Security Across the SDLC', link: '/devsecops/governance-proposal-security' },
{ text: 'Integrated Development Environments', link: '/devsecops/integrated-development-environments' },
+ {
+ text: 'Policy as Code',
+ collapsed: true,
+ dev: true,
+ items: [
+ { text: 'Overview', link: '/devsecops/policy-as-code/overview', dev: true },
+ { text: 'Policy in the CI Pipeline', link: '/devsecops/policy-as-code/ci-pipeline', dev: true },
+ { text: 'Policy in Release and Runtime', link: '/devsecops/policy-as-code/release-and-runtime', dev: true },
+ { text: 'Governing the Policy Set', link: '/devsecops/policy-as-code/governance', dev: true },
+ ]
+ },
{ text: 'Repository Hardening', link: '/devsecops/repository-hardening' },
{ text: 'Security Testing', link: '/devsecops/security-testing' },
]
diff --git a/wordlist.txt b/wordlist.txt
index 08ece144f..1144778c3 100644
--- a/wordlist.txt
+++ b/wordlist.txt
@@ -1,5 +1,6 @@
AADAPT
AccuKnox
+actionlint
Acuvity
Aderyn
adraffy
@@ -13,7 +14,9 @@ anonymization
ASVS
Aublin
authenticator
+authn
Authy
+authz
automod
autorun
axie
@@ -47,6 +50,7 @@ Coinspect
collab
collateralized
Comms
+Conftest
confusables
Consensys
Cowork
@@ -127,6 +131,7 @@ firewalld
Forta
Galxe
Gapped
+Gatekeeper
Ghostery
GitHub
GitLab
@@ -197,6 +202,7 @@ LLVM
LUKS
Mahhouk
Mailvelope
+mainnet
mdlint
MDM
MEE
@@ -281,8 +287,10 @@ Reco
recoverooor
redactions
reentrancy
+Rego
reimage
reinit
+Rekor
Rekt
renderer
reprompt
@@ -308,6 +316,7 @@ Shamir's
shorteners
SIEM
sig
+Sigstore
simbolik
SLSA
slugified