How to Author, Sign, and Load Signed Policy Bundles

Advanced 30 minutes Platform Engineers / Security Engineers Security & Policy

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.

Verification is ENFORCED by default. A reload whose .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

  1. Author the bundle — a directory of .rego files.
  2. Sign it with cosign, producing a sibling .sigstore bundle.
  3. Verify at load — the server checks the signature against the pinned identity on every reload (enforced by default).
  4. 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 .sigstore sibling 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.

First boot is the one exception. On a fresh host with no previously-loaded bundle, a verification failure leaves the DSL path a no-op (no custom rules enforced) rather than failing closed. An unsigned bundle on a new host should not enforce custom rules — your next reload, after you sign, lights the path up. This is intentional; it is not a way to run unsigned in production (see the escape hatch below, which you should not use).

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 varDefaultEffect
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_VERIFYoff (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_VERIFYoffLegacy alias for CHAINSAW_OPA_SKIP_VERIFY, preserved for runbook compatibility. Same semantics, same loud warning. New configuration should use the canonical name.
Do not set the skip-verify env vars in production. They disable the product’s headline supply-chain guarantee. They exist only as an incident-recovery escape hatch, and every skipped reload logs a loud warning. The full config table lives in the Configuration reference.

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