Plan: Operational Infrastructure Remediation
On this page
- Status
- Context
- Scope
- Design
- Steps
- Step 1: Add CI test and lint jobs
- Step 2: Implement column-level encryption
- Step 3: Backup and disaster recovery tooling
- Step 4: Migration rollback strategy
- Step 5: Secret management integration
- Step 6: API versioning and deprecation
- Step 7: Retry with exponential backoff
- Step 8: Database connection pool tuning
- Step 9: SSE for real-time portal updates
- Step 10: Expand automated accessibility testing
- Step 11: General-purpose data export API
- Step 12: Distributed idempotency and cache store
- Step 13: Documentation testing
- Step 14: Full validation
- Files Touched
- Execution Priority
- Verification
- Documentation Updates
Status
| Step | Description | Status |
|---|---|---|
1 |
Add test, lint, and build jobs to GitLab CI pipeline |
Not started |
2 |
Implement column-level encryption for sensitive PII fields |
Not started |
3 |
Create backup/disaster recovery tooling and runbooks |
Not started |
4 |
Establish migration rollback strategy with pre-migration snapshots |
Not started |
5 |
Integrate secret management (Vault or sealed-secrets) |
Not started |
6 |
Add API versioning and deprecation headers |
Not started |
7 |
Implement retry with exponential backoff and jitter for external calls |
Not started |
8 |
Tune database connection pool per service and export pool metrics |
Not started |
9 |
Add SSE endpoint for real-time portal updates |
Not started |
10 |
Expand automated accessibility testing (axe-core) across all pages |
Not started |
11 |
Build general-purpose data export API for FOIA and audit portability |
Not started |
12 |
Replace in-memory idempotency/JWKS cache with PostgreSQL or Redis-backed store |
Not started |
13 |
Add documentation testing for API examples and CLI commands |
Not started |
14 |
Full validation pass |
Not started |
Epic: TBD
Branch: chore/operational-infrastructure
Labels: type::chore, priority::critical
Context
A cross-project audit of CRAIG and its sibling project Canopy identified 13 shared operational infrastructure gaps. Both projects invested heavily in application architecture (service isolation, rules engines, event buses, E2E testing) but underinvested in operational concerns (backups, encryption, CI enforcement, retry resilience, observability under failure).
The most critical findings:
-
CI does not run tests. Both projects defer entirely to optional pre-push hooks. A developer pushing with
--no-verifyor from a machine without hooks configured can land broken code inmainwith zero automated test signal. For a CCWIS handling child welfare case records, this is an unacceptable risk. -
No encryption at rest. Child names, SSNs, case narratives, placement records, and court orders sit in PostgreSQL as plaintext. HHS requires encryption at rest for child welfare data. Application-level column encryption provides defense-in-depth beyond filesystem encryption.
-
No backup or disaster recovery tooling. CRAIG is a system of record for child welfare cases. Data loss from ransomware, accidental deletion, or failed migrations has no recovery path today.
-
Forward-only migrations with no rollback. Failed deployments cannot revert schema changes. Combined with no backups, a bad migration could leave the system in an unrecoverable state.
CRAIG is further along than Canopy in some areas (working portal, E2E tests, rate limiting, security headers, i18n, k6 load tests) but shares all 13 operational gaps. This plan addresses them for CRAIG specifically.
Scope
In scope:
-
CI pipeline:
cargo fmt,cargo clippy,cargo nextest runas blocking merge jobs -
JUnit XML artifact consumption and test reporting in CI
-
Docker build validation on feature branches
-
Column-level encryption for SSN, DOB, case narratives, and court order details using
aes-gcm-siv -
Encryption key management via environment variable (phase 1) with Vault integration (phase 2)
-
pg_basebackupwrapper script with WAL archiving configuration -
Documented RTO/RPO targets and tested restore procedure
-
Pre-migration snapshot tooling in
cargo xtask -
Down migration templates for critical tables
-
HashiCorp Vault integration or sealed-secrets operator support
-
Accept-Versionheader support withSunsetandDeprecationheaders (RFC 8594) -
backoffcrate integration for JWKS refresh, inter-service HTTP, and RabbitMQ reconnect -
Per-service pool sizing with
test_before_acquire, pool metrics exported to Prometheus -
SSE endpoint in craig-web for real-time case/placement event updates
-
axe-core coverage expansion across all craig-web pages (building on existing ARIA work)
-
General-purpose
/v1/exportendpoints beyond AFCARS reporting -
PostgreSQL-backed idempotency key store (replacing in-memory DashMap)
-
API example validation in CI; CLI example validation
Out of scope:
-
Mobile/offline client (separate plan exists)
-
Full Redis deployment (PostgreSQL-backed cache is sufficient for phase 1)
-
Multi-jurisdiction deployment orchestration (Kubernetes operator)
-
HSM-backed encryption keys (phase 3, post-production)
-
ICPC partner integration changes (separate plan)
Design
CI Pipeline Architecture
Add a test stage to .gitlab-ci.yml that runs before promote.
Use the existing rust:1.94-alpine builder image.
Three jobs run in parallel:
cargo-fmt:
stage: test
script: cargo fmt --check --all
cargo-clippy:
stage: test
script: cargo clippy --workspace -- -D warnings
cargo-test:
stage: test
script: cargo nextest run --workspace --profile ci
artifacts:
reports:
junit: test-results/**/*.xml
The docker-promote job gains a needs: [cargo-fmt, cargo-clippy, cargo-test] dependency so broken code cannot be promoted.
Encryption at Rest
Use aes-gcm-siv (AEAD, nonce-misuse resistant) for column-level encryption.
Create a craig-crypto shared crate:
// crates/craig-crypto/src/lib.rs
pub struct FieldEncryptor { key: aes_gcm_siv::Aes256GcmSiv }
impl FieldEncryptor {
pub fn from_env(var: &str) -> Result<Self, Error>;
pub fn encrypt(&self, plaintext: &[u8]) -> Vec<u8>; // nonce || ciphertext || tag
pub fn decrypt(&self, blob: &[u8]) -> Result<Vec<u8>, Error>;
}
Encryption key loaded from CRAIG_FIELD_ENCRYPTION_KEY (base64-encoded 256-bit key).
Phase 2 replaces env var with Vault transit engine.
CRAIG-specific encrypted fields:
* persons.ssn — Social Security Number
* persons.date_of_birth — Date of birth
* case_narratives.content — Free-text case narratives (may contain sensitive details)
* court_orders.order_text — Court order content
Retry with Backoff
Add backoff crate to workspace dependencies.
Wrap all external calls (JWKS refresh, rules client, inter-service HTTP, exchange partner APIs) with exponential backoff:
backoff::future::retry(
backoff::ExponentialBackoffBuilder::new()
.with_initial_interval(Duration::from_secs(1))
.with_max_interval(Duration::from_secs(300))
.with_randomization_factor(0.3)
.with_max_elapsed_time(Some(Duration::from_secs(3600)))
.build(),
|| async { /* call */ },
).await
Particularly important for craig-exchange which calls external partner APIs (ICPC, SSA) that may be intermittently unreachable.
SSE for Real-Time Portal Updates
craig-web already has a mature Askama + htmx portal. Add an SSE endpoint that subscribes to relevant RabbitMQ events and pushes to connected browser sessions:
// services/craig-web/src/api/sse.rs
async fn event_stream(
session: AuthenticatedWorker,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
// Subscribe to events for this worker's caseload
// Map RabbitMQ EventEnvelope → SSE Event
}
htmx natively supports SSE via hx-sse="connect:/sse".
Events to push: case.updated, referral.created, placement.changed, court_order.filed, investigation.completed.
Data Export Beyond AFCARS
CRAIG already has AFCARS export in craig-reporting.
Add general-purpose export endpoints for non-federal use cases:
-
GET /v1/export/cases— Case records (CSV/JSON, admin/supervisor only) -
GET /v1/export/persons— Person records with PII redaction option for FOIA -
GET /v1/export/audit-events— Audit log export (admin only) -
GET /v1/export/placements— Placement history (supervisor only)
All exports require admin or supervisor role, produce audit log entries, and support Accept: text/csv content negotiation.
Steps
Step 1: Add CI test and lint jobs
Files: .gitlab-ci.yml, .config/nextest.toml
-
Add
teststage to stages list (beforepromote) -
Add
cargo-fmtjob:cargo fmt --check --all -
Add
cargo-clippyjob:cargo clippy --workspace — -D warnings -
Add
cargo-testjob:cargo nextest run --workspace --profile ci -
Publish
test-results/*/.xmlas JUnit artifacts -
Add
cargo-build-dockerjob on MR branches (build only, no push) -
Gate
docker-promoteon test jobs:needs: [cargo-fmt, cargo-clippy, cargo-test]
Step 2: Implement column-level encryption
Files: New crates/craig-crypto/, services/craig-cases/src/store/, Cargo.toml
-
Create
craig-cryptocrate withFieldEncryptor(encrypt/decrypt using AES-256-GCM-SIV) -
Add
craig-cryptoto workspace members and dependencies -
Update
craig-casesperson store: encrypt SSN and DOB on write, decrypt on read -
Update case narrative store: encrypt narrative content
-
Add migration to backfill existing plaintext data (encrypt in place)
-
Add unit tests for encrypt/decrypt roundtrip, tamper detection, and key rotation
-
Document key management in
.claude/docs/security.md
Step 3: Backup and disaster recovery tooling
Files: New tools/craig-backup/, new docs/modules/ROOT/pages/disaster-recovery.adoc
-
Create
tools/craig-backup/backup.sh: wrapper aroundpg_basebackupfor craig database -
Configure WAL archiving in devstack PostgreSQL container
-
Create
tools/craig-backup/restore.sh: tested point-in-time recovery -
Document RTO (4 hours) and RPO (1 hour) targets
-
Add quarterly restore test procedure to operations documentation
-
Add
cargo xtask backupcommand that invokes the script
Step 4: Migration rollback strategy
Files: xtask/src/cmd/migrate.rs (new), .claude/docs/coding-conventions.md
-
Add
cargo xtask migrate snapshotcommand that takes apg_dumpbefore running pending migrations -
Add
cargo xtask migrate rollbackcommand that restores from the most recent snapshot -
Document the rollback strategy in coding conventions
-
Create down migration templates for critical tables (cases, persons, placements, court_orders)
-
Add snapshot step to
cargo xtask validatebefore running migrations in integration tests
Step 5: Secret management integration
Files: New crates/craig-secrets/, crates/craig-common/src/settings.rs, .env.example
-
Create
craig-secretscrate with trait-based secret provider:EnvSecretProvider(phase 1),VaultSecretProvider(phase 2) -
Settings loader uses
SecretProviderto resolvedatabase_url,rabbitmq_url,encryption_key -
Phase 1:
EnvSecretProviderreads from env vars (current behavior, wrapped in trait) -
Phase 2:
VaultSecretProviderreads from HashiCorp Vault via HTTP API -
Add secret access audit logging
-
Document secret rotation procedure
Step 6: API versioning and deprecation
Files: crates/craig-api/src/versioning.rs (new), crates/craig-api/src/lib.rs
-
Add
Accept-Versionheader extraction middleware -
Default to
v1when header is absent -
Add
SunsetandDeprecationresponse headers (RFC 8594) for deprecated endpoints -
Add
/v1/api-versionsendpoint listing available versions with sunset dates -
Document versioning strategy in developer guide
Step 7: Retry with exponential backoff
Files: Cargo.toml, crates/craig-auth/src/jwks.rs, services/craig-exchange/src/, crates/craig-mq/src/subscriber.rs
-
Add
backoff = "0.4"to workspace dependencies -
Replace infinite loop in JWKS refresh with exponential backoff (1s → 300s max, 30% jitter)
-
Wrap exchange partner API calls with retry (3 attempts, 1s → 4s)
-
Add retry on RabbitMQ reconnect in subscriber
-
Add tests for retry behavior
Step 8: Database connection pool tuning
Files: crates/craig-db/src/lib.rs, crates/craig-common/src/settings.rs
-
Add per-service pool configuration:
db_max_connections,db_min_connections,db_acquire_timeout_secs -
Enable
test_before_acquire(true)for connection health checks -
Export pool metrics to Prometheus:
db_pool_active,db_pool_idle,db_pool_waiting,db_pool_acquire_duration_seconds -
Set production-appropriate defaults: max 25 connections, min 5, 10s acquire timeout
-
Add pool exhaustion alert threshold
Step 9: SSE for real-time portal updates
Files: services/craig-web/src/api/sse.rs (new), services/craig-web/src/api/mod.rs, services/craig-web/templates/base.html
-
Add SSE route:
GET /ssereturningSse<impl Stream> -
Subscribe to RabbitMQ events filtered by worker’s assigned caseload
-
Map
EventEnvelopeto SSEEventwith JSON data -
Add
hx-sse="connect:/sse"to base template for auto-reconnect -
Add SSE event handlers for:
case.updated,referral.created,placement.changed,investigation.completed -
Add connection keepalive (30s heartbeat)
-
Add E2E test: trigger case update, verify SSE event received in Playwright
Step 10: Expand automated accessibility testing
Files: tests/e2e/package.json, tests/e2e/fixtures/a11y.ts (new), tests/e2e/specs/*.spec.ts
-
Add
@axe-core/playwrightto E2E dev dependencies -
Create shared fixture that runs
checkA11y()after each page load -
Assert zero WCAG 2.1 AA violations on every page render
-
Expand existing ARIA patterns to all forms (not just tabs)
-
Add color contrast validation for all theme colors
-
Run a11y tests as part of
cargo xtask e2e
Step 11: General-purpose data export API
Files: services/craig-cases/src/api/export.rs (new), services/craig-security/src/api/export.rs (new)
-
Add
GET /v1/export/casesto craig-cases (CSV and JSON, supervisor+ role) -
Add
GET /v1/export/personsto craig-cases (with PII redaction for FOIA) -
Add
GET /v1/export/audit-eventsto craig-security (admin role required) -
Add
GET /v1/export/placementsto craig-placement (supervisor+ role) -
Add
Accept: text/csvcontent negotiation -
Add audit log entry for every export request
-
Add integration tests for export endpoints
Step 12: Distributed idempotency and cache store
Files: crates/craig-api/src/idempotency.rs, new migration in craig-cases
-
Create
idempotency_keystable in service database:(cache_key TEXT PRIMARY KEY, response_status INT, response_headers JSONB, response_body BYTEA, created_at TIMESTAMPTZ) -
Replace
DashMapin idempotency middleware with PostgreSQL queries -
Add TTL cleanup:
DELETE FROM idempotency_keys WHERE created_at < now() - interval '24 hours' -
JWKS cache can remain in-memory (per-instance, refreshes hourly)
-
Add tests for idempotency across simulated pod restarts
Step 13: Documentation testing
Files: .gitlab-ci.yml, new tests/doc-validation/
-
Extract API request/response examples from AsciiDoc into testable fixtures
-
Add CI job that validates fixture requests against running devstack
-
Add
cargo test --docto CI for Rust doc examples -
Validate CLI examples (from
cli.adoc) produce expected output -
Add
cargo xtask check-docs --examplescommand
Step 14: Full validation
-
cargo fmt --check --all -
cargo clippy --workspace — -D warnings -
cargo nextest run --workspace --profile ci— all tests pass (existing + new) -
cargo xtask e2e— E2E tests pass (including new a11y and SSE tests) -
cargo xtask validate— full pre-push validation -
CI pipeline successfully runs all new jobs on feature branch
-
Verify encryption roundtrip: encrypt SSN, decrypt, compare
-
Verify backup/restore: take backup, corrupt data, restore, verify integrity
-
Verify retry: mock failing Keycloak, confirm backoff intervals in logs
-
Verify SSE: open browser, trigger case update, confirm real-time notification
Files Touched
| File | Change |
|---|---|
|
Add test stage with fmt, clippy, test, docker-build jobs |
|
Add aes-gcm-siv, backoff, craig-crypto to workspace |
|
New crate: AES-256-GCM-SIV field encryption |
|
Pool tuning, test_before_acquire, metrics export |
|
Per-service pool config, secret provider integration |
|
Versioning middleware, SSE wiring |
|
Replace DashMap with PostgreSQL-backed store |
|
New: Accept-Version header, Sunset/Deprecation headers |
|
Replace infinite loop with backoff crate |
|
Add connection retry with backoff |
|
New crate: trait-based secret provider |
|
Encrypt/decrypt SSN, DOB, narratives via craig-crypto |
|
New: SSE endpoint for real-time updates |
|
Add hx-sse connection |
|
New: data export endpoints |
|
New: audit event export |
|
Add retry with backoff on partner API calls |
|
New: pg_basebackup wrapper + restore script |
|
New: snapshot and rollback commands |
|
New: axe-core accessibility fixture |
|
New: API and CLI example validation tests |
Execution Priority
| Priority | Step | Effort | Reason |
|---|---|---|---|
P0 |
Step 1 (CI tests) |
Small |
Highest-impact single change; blocks broken code from merging |
P0 |
Step 2 (Encryption) |
Medium |
Regulatory requirement for child welfare PII |
P0 |
Step 3 (Backups) |
Medium |
No recovery path today; existential risk for system of record |
P1 |
Step 4 (Migration rollback) |
Small |
Prevents unrecoverable deployment failures |
P1 |
Step 7 (Retry/backoff) |
Small |
Low effort; prevents cascading failures; critical for craig-exchange |
P1 |
Step 8 (Pool tuning) |
Small |
Low effort; prevents connection exhaustion under load |
P2 |
Step 5 (Secret management) |
Medium |
Env vars acceptable short-term; Vault needed for production |
P2 |
Step 6 (API versioning) |
Medium |
Not urgent until v2 is needed, but foundation should exist |
P2 |
Step 9 (SSE) |
Medium |
UX improvement; caseworkers will benefit immediately |
P2 |
Step 10 (a11y expansion) |
Small |
Section 508 requirement; builds on existing ARIA work |
P3 |
Step 11 (Data export) |
Medium |
Needed for FOIA and auditors; not blocking for initial deployment |
P3 |
Step 12 (Distributed cache) |
Medium |
Only matters at multi-pod scale |
P3 |
Step 13 (Doc testing) |
Small |
Quality-of-life; prevents doc drift |
Verification
-
cargo fmt --check --all— no formatting issues -
cargo clippy --workspace — -D warnings— zero warnings -
cargo nextest run --workspace --profile ci— all tests pass -
cargo xtask e2e— E2E tests pass (including a11y and SSE) -
cargo xtask validate— full pre-push validation passes -
CI pipeline runs fmt + clippy + test jobs and blocks promote on failure
-
cargo xtask backupcreates valid backup;cargo xtask migrate rollbackrestores from snapshot -
Encrypted SSN roundtrip: insert person with SSN, retrieve, verify match
-
Retry test: stop Keycloak, verify JWKS refresh backs off (1s, 2s, 4s… in logs)
-
SSE test: open portal, update case via API, verify browser receives update < 2s
-
a11y test:
cargo xtask e2ereports zero WCAG 2.1 AA violations across all pages
Documentation Updates
-
.claude/docs/services.md— export endpoint tables, SSE endpoint -
.claude/docs/security.md— encryption at rest, secret management, key rotation -
docs/modules/ROOT/pages/deployment-guide.adoc— backup/restore commands, pool tuning, secret rotation -
docs/modules/ROOT/pages/developer-guide.adoc— migration rollback strategy, retry patterns -
docs/modules/ROOT/pages/configuration-reference.adoc— new pool and encryption settings -
CHANGELOG.adoc— entry under== Unreleased -
Antora pages — new disaster-recovery.adoc