GitOps Policy Sync¶
The M23 GitOps flow lets you manage sandbox policies as YAML in a Git repo and apply them via CI/CD with the same safety rails the UI uses — pins, quorum workflows, audit log, optimistic locking.
Why a separate flow from Terraform?¶
Terraform provisions infrastructure — gateways, groups, approval
workflows, pins. Policy content changes on a different cadence
(every denial flow, often daily) and should route through the same
quorum as a manual policy edit. Putting policy content in Terraform
state would drift every time a denial gets approved, so the M24
Terraform provider deliberately does not expose a
shoreguard_sandbox_policy resource. GitOps fills that gap.
The round-trip¶
GET /export produces a deterministic YAML document:
metadata:
gateway: dev
sandbox: agent-a
version: 42
policy_hash: 8f3a…
exported_at: 2026-04-13T12:00:00Z
policy:
network:
rules: [...]
filesystem:
paths: [...]
process: {...}
Re-exporting a YAML that came out of GET /export produces the
same bytes (the ordering is stable and keys are sorted). That
matters because your Git diff stays signal, not noise.
POST /apply takes {yaml, dry_run, expected_version} and returns
one of:
| Status | Meaning |
|---|---|
200 up_to_date |
Submitted YAML matches the live policy. No write. |
200 dry_run |
Dry-run apply — computed the diff, did not write. |
200 applied |
Policy updated upstream. |
202 vote_recorded |
M19 workflow active — one vote recorded, no write yet. |
409 |
expected_version / policy_hash does not match the live version. |
423 |
Sandbox is pinned (M18). |
400 |
Malformed YAML. |
Optimistic locking: if you omit expected_version, it falls
back to metadata.policy_hash in the YAML document. A mismatch
returns the live current_hash in the response body so CI can
refetch + retry without a second roundtrip.
CLI¶
shoreguard policy wraps all three operations:
# Dump the live policy to stdout
shoreguard policy export --gateway dev --sandbox agent-a > policy.yaml
# Diff (dry-run apply) — exits 1 on drift, 0 if up-to-date
shoreguard policy diff --gateway dev --sandbox agent-a -f policy.yaml
# Apply (writes) — exits 1 if a vote was recorded but quorum not met,
# exit 2 on any error
shoreguard policy apply --gateway dev --sandbox agent-a -f policy.yaml
Credentials: SHOREGUARD_URL + SHOREGUARD_TOKEN env vars or
--url / --token flags.
Typical CI pipeline¶
# .github/workflows/policy-sync.yml
- run: pip install shoreguard-cli
- run: shoreguard policy diff --gateway dev --sandbox agent-a -f agent-a.yaml
- run: shoreguard policy apply --gateway dev --sandbox agent-a -f agent-a.yaml
if: github.ref == 'refs/heads/main'
Under a quorum workflow, the apply on the main branch returns
exit code 1 + vote_recorded. The second human voter approves from
the UI, which reaches quorum and fires UpdateConfig upstream
exactly once. CI does not need to re-run.
Drift detection (optional)¶
DriftDetectionService is a background loop, off by default behind
SHOREGUARD_DRIFT_DETECTION_ENABLED. When enabled, it polls every
registered sandbox every interval, compares the policy hash to the
last snapshot, and fires a policy.drift_detected webhook on any
change between scans. The first scan after restart bootstraps the
snapshot silently. Per-sandbox failures are logged + swallowed.
Subscribe to the webhook from Slack and every out-of-band policy edit becomes a visible event, not a mystery.
Interaction with pins and workflows¶
- Pinned sandbox (M18) —
applyanddry_runboth return HTTP 423. Export stays allowed. - Active workflow (M19) — the first
applyrecords one approve-vote on a synthetic chunk idpolicy.apply:<sha16>. A new tablepolicy_apply_proposalscaches the pending YAML so the second voter does not need to resubmit bytes. Subsequent apply calls with the same YAML body accumulate votes until quorum.
Reference¶
- API:
/api/gateways/{gw}/sandboxes/{name}/policy/export+/apply - Demo:
scripts/m23_demo.py(8 phases: export → no-op → drift → write → vote → quorum → pin → drift). - Runbook:
scripts/m23-gitops.md. - Audit events:
policy.exported,policy.apply.dry_run,policy.apply.noop,policy.apply.voted,policy.applied,policy.drift_detected.