Status: historical / pre-cutover validation phase. This runbook documents
the shadow-validation step that preceded controlled-authority cutover. Once
CUTOVER.md has been completed, cf-sync.service (Go) is the
live authority for Cloudflare enforcement and AbuseIPDB reporting; the
legacy Python daemon (crowdsec-cf-sync.service) continues running in
reduced mode for the duties Go does not yet replace (Lua push, ModSecurity,
recidive) — see CUTOVER.md's "What Python KEEPS" table. The steps below
describe the read-only validation that ran before that promotion and are
retained for re-running shadow validation against a new environment, not as
the current authority model.
Run the security-automation-go shadow validator while the Python daemon
remains authoritative. This runbook is read-only: no new providers, no new
mutation paths, and no new shadow-status runtime mode.
cf-shadow.service- Go shadow validator. This is the service to start for the shadow run.crowdsec-sync.service- supporting Go service for CrowdSec state observation/sync; keep it in the existing deployment posture.cf-allowlist-sync.timer- read-only allowlist sync cadence; keep it on the existing schedule if already deployed.cf-cleanup.service- cleanup helper. During shadow, use dry-run only.cf-sync.service- main Go authority path for later cutover. Keep it stopped and disabled during the shadow run unless you are explicitly rehearsing authority promotion.crowdsec-cf-sync.service- Python legacy authority, if still present on the host. This remains the authority during shadow.
/etc/security-automation-go/cf-shadow.env- shadow environment file./etc/security-automation-go/cf-shadow.yaml- shadow runtime config./etc/security-automation-go/crowdsec-sync.env- environment for the CrowdSec sync service, if used by the host deployment./etc/security-automation-go/cf-cleanup.env- environment for cleanup, if the host uses the installed cleanup unit./etc/security-automation-go/cf-allowlist-sync.env- environment for the allowlist sync service, if the host uses the installed timer/service pair./var/lib/security-automation-go/secrets.local- UI secret file used bycf-sync -mode ui.
Provider-specific AI config is environment-only and fail-closed:
AI_EXPLAIN_ENABLEDAI_PROVIDER_OPENAI_ENABLEDAI_PROVIDER_OPENAI_API_KEY_FILEAI_PROVIDER_ANTHROPIC_ENABLEDAI_PROVIDER_ANTHROPIC_API_KEY_FILEAI_PROVIDER_GEMINI_ENABLEDAI_PROVIDER_GEMINI_API_KEY_FILE
GOTOOLCHAIN=go1.25.0 go build ./cmd/cf-sync
GOTOOLCHAIN=go1.25.0 go build ./cmd/cf-shadow
GOTOOLCHAIN=go1.25.0 go build ./cmd/security-automation-mcpRecommended install path:
sudo install -d -m 0755 /etc/security-automation-go
sudo install -d -m 0755 /opt/security-automation-go/bin
sudo install -m 0640 deployments/shadow/cf-shadow.env.example /etc/security-automation-go/cf-shadow.env
sudo install -m 0644 deployments/shadow/cf-shadow.yaml.example /etc/security-automation-go/cf-shadow.yaml
sudo install -m 0644 deployments/shadow/cf-shadow.service /etc/systemd/system/cf-shadow.service
sudo systemctl daemon-reload
sudo systemctl enable --now cf-shadow.serviceIf the deployment also manages the supporting read-only services on this host, bring them up in the same maintenance window without promoting Go authority:
sudo systemctl enable --now crowdsec-sync.service
sudo systemctl enable --now cf-allowlist-sync.timerThe cleanup helper must remain dry-run only during shadow:
GOTOOLCHAIN=go1.25.0 go run ./cmd/cf-cleanup --dry-runsudo systemctl stop cf-shadow.service
sudo systemctl disable cf-shadow.service
sudo systemctl daemon-reloadDo not start cf-sync.service during rollback unless you are explicitly moving
to authority rehearsal.
Python authority is considered present if the legacy daemon is still active:
systemctl status crowdsec-cf-sync.service --no-pager
journalctl -u crowdsec-cf-sync.service --no-pager -n 100If the Python service is absent, do not promote Go automatically. Treat that as a deployment issue and restore the legacy authority path first.
systemctl status cf-shadow.service --no-pager
journalctl -u cf-shadow.service --no-pager -n 100
grep -E '^(CF_API_TOKEN|CF_ZONE_ID|STATE_DIR|CLOUDFLARE_MUTATIONS_ENABLED|AI_EXPLAIN_ENABLED|AI_PROVIDER_.*_ENABLED)=' /etc/security-automation-go/cf-shadow.env
test -f /var/lib/security-automation-go/shadow/SHADOW_MODE_REPORT.md && tail -n 20 /var/lib/security-automation-go/shadow/SHADOW_MODE_REPORT.mdExpected posture:
- Go reports
SHADOW MODE: Go will compute plans but NOT mutate Cloudflare. - No Cloudflare write path is active from the shadow unit.
- Shadow report files continue to advance.
grep -E '^CLOUDFLARE_MUTATIONS_ENABLED=0$' /etc/security-automation-go/cf-shadow.env
grep -E '^mutations_enabled: false$' /etc/security-automation-go/cf-shadow.yaml
systemctl status cf-cleanup.service --no-pager || trueAlso confirm the UI mutation gate remains off:
rg -n 'MutationsEnabled|handleCloudflareBanPreview|ui mutations disabled|cloudflare mutations disabled' internal/ui internal/config -g '!vendor/**'During shadow, no new CrowdSec writer may appear in AI, MCP, or UI layers:
rg -n 'cscli|allowlist|ban|deban|decision|write|mutat' internal/ai internal/mcpserver internal/ui cmd/security-automation-mcp -g '!vendor/**'
systemctl status crowdsec-sync.service --no-pager
systemctl status cf-allowlist-sync.timer --no-pagerThe goal is to keep the existing CrowdSec boundary exactly as it was before the shadow launch.
Start the local UI if needed:
GOTOOLCHAIN=go1.25.0 go run ./cmd/cf-sync -config /etc/security-automation-go/cf-sync.yaml -mode uiUseful read-only checks:
curl -skI http://127.0.0.1:9091/
curl -skI http://127.0.0.1:9091/providers
curl -skI http://127.0.0.1:9091/audit
curl -skI http://127.0.0.1:9091/timeline
curl -skI http://127.0.0.1:9091/intelligence
curl -skI http://127.0.0.1:9091/trusted-networks
curl -skI http://127.0.0.1:9091/cloudflare/diff
curl -skI http://127.0.0.1:9091/replay
curl -skI http://127.0.0.1:9091/recovery
curl -skI http://127.0.0.1:9091/driftTo inspect AI Explain gating from the UI, use an unauthenticated POST and expect CSRF/auth failure when the gate is working:
curl -sk -X POST http://127.0.0.1:9091/ui/ai/explain \
-H 'Content-Type: application/json' \
-d '{"subject_type":"provider","subject_id":"cloudflare","provider_preference":"auto"}'If AI providers are disabled, the /providers page should show masked keys and
disabled provider state. If a provider is enabled, the provider config must be
backed by its AI_PROVIDER_*_API_KEY_FILE path and must never print the raw
key.
The MCP entrypoint is stdio-first and read-only:
timeout 3s GOTOOLCHAIN=go1.25.0 go run ./cmd/security-automation-mcp
GOTOOLCHAIN=go1.25.0 go test ./internal/mcpserver
GOTOOLCHAIN=go1.25.0 go test ./internal/mcpserver -run 'TestServerExposesReadOnlyTools|TestServerRedactsSecrets|TestServerRejectsForbiddenImports' -vExpected posture:
- The process starts on stdio.
- Only read-only tools are registered.
- The audit sink redacts secrets.
Check the environment-driven provider posture:
grep -E '^(AI_EXPLAIN_ENABLED|AI_PROVIDER_OPENAI_ENABLED|AI_PROVIDER_OPENAI_API_KEY_FILE|AI_PROVIDER_ANTHROPIC_ENABLED|AI_PROVIDER_ANTHROPIC_API_KEY_FILE|AI_PROVIDER_GEMINI_ENABLED|AI_PROVIDER_GEMINI_API_KEY_FILE)=' /etc/security-automation-go/cf-shadow.envThen inspect the UI provider page:
curl -skI http://127.0.0.1:9091/providersExpected result:
- AI Explain is disabled by default if
AI_EXPLAIN_ENABLEDis unset or false. - Missing provider secrets keep the provider disabled.
- No raw provider key is rendered in the UI or logs.
journalctl -u cf-shadow.servicejournalctl -u crowdsec-sync.servicejournalctl -u cf-allowlist-sync.timerjournalctl -u cf-cleanup.service/var/lib/security-automation-go/shadow/SHADOW_MODE_REPORT.md/var/lib/security-automation-go/shadow/PYTHON_GO_PARITY_REPORT.md/var/lib/security-automation-go/shadow/SHADOW_DRIFT_ANALYSIS.mdif presentdu -sh /var/lib/security-automation-go /var/lib/security-automation-go/shadow
Watch for:
- agreement percentage
- false positives and false negatives
- report growth
- unexpected drift class changes
- shadow process restarts
journalctl -u cf-shadow.service --no-pager -n 100journalctl -u crowdsec-sync.service --no-pager -n 100journalctl -u cf-cleanup.service --no-pager -n 100journalctl -u cf-allowlist-sync.service --no-pager -n 100if the host uses a service unit instead of a timer/var/lib/security-automation-go/shadow/SHADOW_MODE_REPORT.md/var/lib/security-automation-go/shadow/PYTHON_GO_PARITY_REPORT.md
If the shadow run needs to stop:
sudo systemctl stop cf-shadow.service
sudo systemctl disable cf-shadow.service
sudo systemctl daemon-reloadThen confirm the Python authority path is still available:
systemctl status crowdsec-cf-sync.service --no-pagerDo not delete the report directory unless you are intentionally discarding the shadow evidence.
GO SHADOW.