Plan: Security Culture Hardening
On this page
Status
COMPLETE — archived 2026-04-17 (plan-hygiene sweep). All six security features are shipped and active in the pre-push battery. The one residual (SBOM-on-release-tags in CI) is tracked as a standalone follow-up issue (see Errata below).
| Step | Description | Status |
|---|---|---|
1 |
Plan document + GitLab issue + branch |
Done (pre-ADR-030) |
2 |
cargo-deny in pre-push validate |
Done (pre-ADR-030) — |
3 |
Content-Security-Policy headers |
Done (pre-ADR-030) — |
4 |
API rate limiting via shared middleware |
Done (pre-ADR-030) — |
5 |
gitleaks secret scanning in pre-push |
Done (pre-ADR-030) — soft-fail integration in |
6 |
SBOM generation on release tags |
Done (pre-ADR-030) — partial — |
7 |
Security regression tests in pre-push |
Done (pre-ADR-030) — |
8 |
Unit tests for new code |
Done (pre-ADR-030) — rate_limit has 4 tests; cargo-deny and gitleaks are CLI tools (no unit tests needed); sbom generator tested via E2E xtask run |
9 |
Documentation updates + verification |
Done (pre-ADR-030) — |
Issues: #168 (closed as the work shipped across multiple MRs)
Branch: feature/security-culture-hardening (unused — work landed incrementally)
Context
CRAIG passes ZAP DAST + ffuf with 0 High/Medium/Low findings. The architecture prevents most vulnerability classes at compile time (forbid unsafe, parameterized queries, strum enums, strict serde). This plan addresses 5 remaining gaps to prevent regressions, catch supply chain issues, and add defense-in-depth.
4 items from the original 9-item audit are already done: dependency pinning (Cargo.lock committed + --locked), signed commits (validate enforces gpgsign), security scan persistence (test-results/security/ .gitignored), and Cargo.lock management.
Scope
In scope:
-
Content-Security-Policy header on all services + craig-web
-
Per-IP rate limiting on all 8 API services via shared craig-api middleware
-
cargo-deny license/advisory check in pre-push validate step
-
gitleaks secret scanning in pre-push (soft-fail if not installed)
-
SBOM (CycloneDX) generation on release tags + local xtask command
-
Security regression tests (phases 2-4) in pre-push hook
Out of scope:
-
Shared cross-instance rate limiting (would need Redis — deferred)
-
CSP nonce-based script loading (would require refactoring Alpine.js usage)
-
Signed SBOM attestation (deferred to production deployment)
Potential Improvements
-
Trusted proxy IP extraction (#169): Rate limiter uses X-Forwarded-For which can be spoofed. Production deployments behind a reverse proxy should use ConnectInfo (peer address) or configure trusted proxy IPs.
-
CSP nonce-based script-src: Replace
'unsafe-inline'with per-request nonces for Alpine.js scripts. Requires template changes to inject nonce on every<script>tag. -
Shared rate limiting: Redis-backed rate limiter for horizontal scaling — all instances share the same quota per IP. Deferred to ADR-012 (service modularity) timeline.
-
SBOM attestation: Sign SBOM with cosign/sigstore for supply chain verification. Required for FedRAMP.
Errata
-
Step 4 deviation: Plan specified moving intake’s rate_limit.rs to craig-api as a shared module. Instead, created a new separate rate_limit.rs in craig-api. Reason: intake’s rate limiter is per-hour for unauthenticated public endpoints; craig-api’s is per-minute for authenticated API endpoints. Different quotas, different IP extraction strategies. Keeping them separate is cleaner.