How to Author, Sign, and Load Signed Policy Bundles
Ship custom Rego rules as a cosign-signed bundle that the server verifies at load — author, sign, verify-at-load, promote — with the same signed bundle enforced at PR, install, K8s admission, and runtime, and the bundle digest stamped on each policy decision and carried into the audit trail.
Overview
The Policy DSL bundle is a directory of .rego files that the server
loads from CHAINSAW_POLICY_BUNDLE. The server recursively reads
*.rego from that directory and compiles them into the OPA decision
engine on every reload. Without a signature gate, anyone with shell
access to the host could drop a .rego file in that directory and have
it loaded on the next reload — which makes the bundle path the primary
lateral-movement surface for the firewall.
Signed bundles close that gap: every reload must come with a Sigstore signature minted by a known OIDC identity. A reload whose signature doesn’t verify is rejected, and the previously-loaded bundle keeps serving traffic.
.sigstore
bundle does not verify against the pinned signer identity is rejected.
You author the rules in
Policy DSL Reference;
this page is how you get them safely into the running server.Prerequisites
- Admin access to the Chainsaw host filesystem (the bundle directory)
- cosign installed
- A CI signing identity (GitHub Actions OIDC) for production bundles
- Rules authored against the Policy DSL
The lifecycle at a glance
- Author the bundle — a directory of
.regofiles. - Sign it with cosign, producing a sibling
.sigstorebundle. - Verify at load — the server checks the signature against the pinned identity on every reload (enforced by default).
- Promote — the same signed bundle is enforced across surfaces.
Step 1: Author the bundle
A bundle is a directory of .rego files, each declaring
package chainsaw.policy and contributing decision rules. Use the
demo bundle in the repo as a starting point:
policies/chainsaw_policy.rego.
/etc/chainsaw/policies/
├── install-scripts.rego
├── publish-gates.rego
└── geo.rego
See Policy DSL Reference for the input fields, decision shape, and worked rules.
Step 2: Sign the bundle
Sign it with the repo’s helper, which reproduces the canonical content hash and (in CI) mints the signature under your org’s signing identity:
bash scripts/sign_policy_bundle.sh /etc/chainsaw/policies
Run this in your CI under your organization’s signing identity. The
canonical Chainsaw bundle is signed by the sign-policy-bundle.yml
workflow running in Chain305/chainsaw (an org-owned workflow ref IS the
Sigstore identity — there is no bot account),
using GitHub Actions OIDC (issuer
https://token.actions.githubusercontent.com).
Verify a signed bundle by hand
To inspect a signed bundle manually, reproduce the content hash and
verify the .sigstore bundle against the signer identity:
# Reproduce the canonical content hash used at sign time.
bash scripts/sign_policy_bundle.sh policies # emits policies.sha256
# Verify the .sigstore bundle against the signer identity.
cosign verify-blob \
--bundle policies.sigstore \
--certificate-identity-regexp '^https://github\.com/Chain305/chainsaw/\.github/workflows/sign-policy-bundle\.yml@refs/heads/.+$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
<(xxd -r -p policies.sha256)
The server runs the equivalent check via sigstore-go, hitting the live Sigstore trust root via TUF (cached for 6 hours).
Step 3: Verify at load
Point the server at the bundle directory and (re)load. Verification is enforced by default — no extra flag is needed to turn it on.
export CHAINSAW_POLICY_BUNDLE=/etc/chainsaw/policies
On a successful load the server logs the verified bundle with its SHA-256, signer identity, and issuer. The reload fails when:
- the
.sigstoresibling is missing, - the signature doesn’t cover the current bundle hash,
- the cert chain doesn’t verify against the live Fulcio root,
- the cert subject doesn’t match the pinned identity regexp,
- the Rekor inclusion proof is invalid or missing.
Fail-old-good
When a reload fails verification, the in-memory engine is not
swapped — the previously-loaded bundle keeps serving every proxy /
publish / admission decision. The failure is logged at ERROR level with
alert=opa_bundle_verification_failed, and a BundleVerifyFailureCount
counter increments, which the admin metrics endpoint surfaces. The
failure mode of the policy plane must never become a denial-of-service
on the data plane.
Publishing your own (custom) bundle
The default trust root is the canonical Chainsaw bundle. To enforce your own org-specific rules, ship a custom bundle and pin the server to your signing identity:
export CHAINSAW_POLICY_BUNDLE=/etc/chainsaw/policies
export CHAINSAW_OPA_BUNDLE_IDENTITY='^https://github\.com/your-org/.+$'
The chainsaw default identity regexp rejects your bundle otherwise — that is intentional. Your bundle is a different artifact with a different trust root, and the server should know the difference.
Configuration reference
| Env var | Default | Effect |
|---|---|---|
CHAINSAW_POLICY_BUNDLE | (unset) | Directory of .rego files (or a single .rego file). Empty / unset → DSL path is a no-op. |
CHAINSAW_OPA_BUNDLE_IDENTITY | (canonical signer regexp) | Override the pinned OIDC subject regexp for operators publishing their own signed bundles. |
CHAINSAW_OPA_SKIP_VERIFY | off (verification ENFORCED) | Do not use in production. Incident-recovery escape hatch that skips Sigstore verification on every reload. Truthy values: 1, true, TRUE. Each skipped reload emits a loud warning. |
CHAINSAW_OPA_BUNDLE_SKIP_VERIFY | off | Legacy alias for CHAINSAW_OPA_SKIP_VERIFY, preserved for runbook compatibility. Same semantics, same loud warning. New configuration should use the canonical name. |
Step 4: One signed bundle, every surface
The same signed bundle the proxy hot path consults at the proxy
surface is consulted at the other surfaces too. A single Rego rule
authored once is enforced at:
- PR check (
input.surface == "pr") - Registry proxy fetch (
proxy) - Pre-publish gate (
publish) - K8s admission webhook (
deploy) - Package-manager install hook (
runtime) - Env→env promotion gate (
promote)
You do not sign or distribute a separate bundle per surface — the loaded, verified bundle backs all of them. (Which input signals are populated still varies per surface; see Policy DSL Reference.)
Audit: the bundle digest on every row
Each decision the bundle produces is stamped with the bundle’s content digest, so a verdict is reproducible against the exact rule-set version that produced it. On a verified load, the server records the bundle’s SHA-256 and signer identity, and that digest is carried into the policy audit trail. That lets you answer “which rules were live when this package was blocked?” against an append-only record.
The audit trail is append-only except erasure: a database trigger refuses
every update and every truncation of audit_events, and permits deletion
only to the right-to-erasure org-purge cascade. Rows are hash-chained to
their predecessors, so an edit or a removal made around the trigger still
shows up — run SELECT * FROM chainsaw_verify_audit_chain(); to check.
It is not immutable, and it is deliberately not described that way: an
audit trail that cannot be erased cannot satisfy a right-to-erasure
request.
Next Steps
- Policy DSL Reference — author the rules you sign here
- YAML Conditions vs the Rego DSL — pick the right policy surface
- How to Enable Checksum Fail-Closed Enforcement — another fail-closed supply-chain gate