Skip to content

feat: runtime config-drift auditor with baseline, diff, alerting and cert management routes - #240

Merged
JamesEjembi merged 4 commits into
VeriNode-Labs:mainfrom
darcszn:feat/config-drift-runtime-audit
Aug 25, 2026
Merged

feat: runtime config-drift auditor with baseline, diff, alerting and cert management routes#240
JamesEjembi merged 4 commits into
VeriNode-Labs:mainfrom
darcszn:feat/config-drift-runtime-audit

Conversation

@darcszn

@darcszn darcszn commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Overview

This PR delivers the runtime config-drift auditing subsystem and resolves two outstanding issues:

  1. Runtime Config-Drift Auditor — full implementation of the audit loop that compares a live runtime snapshot against a committed baseline config, emits structured findings, routes critical alerts to PagerDuty and warnings to Slack,
    persists findings to PostgreSQL, and supports auto-remediation of known-safe drifts.
  2. Missing registerCertManagementRoutes export — the TLS certificate management API routes were expected by existing tests but the function was never exported from acme_rotation.ts, causing 4 tests to fail. This is now fixed.

Changes

New: src/config-drift/ — Runtime Config-Drift Subsystem

File Purpose
types.ts Shared types: DriftFinding, DriftReport, ConfigSnapshot, DriftEvent, CriticalDriftPolicy
flatten.ts flattenConfig (deep object → dot-key map), computeHashFromFlattened (SHA-256), keyMatchesPrefix
diff.ts diffFlattenedConfigs — detects all 4 drift categories; computeDriftReport — builds sorted, summarised report; classifyKey — maps key prefix to critical / warning / info severity
baseline.ts ExampleConfigBaselineSource (reads config.json.example), BaselineJsonFileSource (reads config/*.baseline.json, falls back to example), loadBaselineSnapshot, buildDefaultBaselineSources
storage.ts DriftStorage — in-memory ring buffer (default 240 records ≈ 20 h at 5-min interval), optional JSONL on-disk durability, optional PostgreSQL persistence via config_drift_events table
pagerduty.ts HttpPagerDutyClient, buildAlertIfCritical, buildAlertIfWarning, alertIdFor
slack.ts HttpSlackClient, createSlackClientFromEnv
remediation.ts AutoRemediationEngine with three built-in safe-drift rules: autoscaled numeric keys, OTel sampling ratio, feature-flag overrides. Critical findings are never auto-remediated.
auditor.ts ConfigDriftAuditor — 5-minute polling loop, init/start/stop, captureSnapshot, history/latest; createConfigDriftAuditorFromEnv factory
routes.ts registerConfigDriftRoutes — registers 5 HTTP endpoints on the Express app
index.ts Barrel re-exports for the entire module

HTTP Endpoints Registered

Method Path Description
GET /config/snapshot Current runtime config as a point-in-time snapshot with SHA-256 hash
GET /config/drift-events Query persisted drift events from PostgreSQL (filter by severity, since, limit)
GET /debug/config-drift Latest snapshot + last 100 history entries (JSON)
GET /debug/config-drift/history Paginated history (JSON, up to 1,000 records)
GET /debug/config-drift/ui Self-contained HTML dashboard visualising findings and history

All drift endpoints are gated at the pro rate-limit tier in index.js.

Drift Detection Logic

Four finding categories are detected per snapshot cycle:

  • value_change — key exists in both baseline and runtime but value differs
  • key_added — key present in runtime but absent from baseline
  • key_removed — key present in baseline but absent from runtime
  • type_change — key present in both but typeof value changed (e.g. number → string)

Severity is assigned by key prefix:

Severity Default prefixes
critical db, mtls, tls, auth, staking
warning capacity_shedding, performance, telemetry
info everything else

Critical findings always trigger a PagerDuty alert. Warning-only findings (no critical present) trigger a Slack notification. Both are skipped gracefully when the respective client is not configured.

Auto-Remediation

The AutoRemediationEngine evaluates each finding against built-in safe-drift rules:

  1. autoscaled-numeric — numeric values on auto-scaler-managed keys (e.g. staking.maxConcurrentWorkers, capacity_shedding.thresholds.*) are safe to baseline automatically.
  2. telemetry-sampling-ratiotelemetry.otel.samplingRatio in [0, 1] is auto-adjusted by the telemetry subsystem.
  3. feature-flag-infofeature_flags.* value/key changes at info severity are safe to baseline.

Critical-severity findings are always skipped — they require human review.

When a finding is remediated the engine updates the in-memory baseline and annotates the config_drift_events row with auto_remediated = true and the rule note.


New: src/database/migrations/013_config_drift_events.sql

Creates the config_drift_events table used for persistent drift storage:

CREATE TABLE IF NOT EXISTS config_drift_events (
  event_id          UUID        NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
  snapshot_id       TEXT        NOT NULL,
  captured_at       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  severity          TEXT        NOT NULL CHECK (severity IN ('critical', 'warning', 'info')),
  category          TEXT        NOT NULL CHECK (category IN ('value_change', 'key_added', 'key_removed', 'type_change')),
  key               TEXT        NOT NULL,
  baseline_value    JSONB,
  runtime_value     JSONB,
  auto_remediated   BOOLEAN     NOT NULL DEFAULT FALSE,
  remediation_note  TEXT,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Four indexes are included for efficient querying by captured_at, snapshot_id, severity, and auto_remediated.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Modified: index.js — Server Bootstrap

- After config initialisation, creates a ConfigDriftAuditor via createConfigDriftAuditorFromEnv, calls init() + start(), and registers all drift HTTP routes.
- Registers SIGINT / SIGTERM shutdown hooks that call auditor.stop().
- Adds drift endpoint paths to the rate-limiter endpointTiers map (all at pro tier).

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Fixed: src/tls/acme_rotation.ts — registerCertManagementRoutes

The tls_rotation.test.ts suite expected a registerCertManagementRoutes(app, mgr) export that registers the cert management HTTP API on a CertLifecycleManager instance. This function was not implemented, causing 4 tests to fail. The
function is now exported and registers:

- GET /api/v1/certs/status — returns { services: ServiceCertStatus[] } for all managed services
- POST /api/v1/certs/renew — body {} renews all services; body { service: string } targets a single service; responds 500 for an unknown service name

Before: 33 passed / 4 failed
After: 37 passed / 0 failed

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

New: tests/config/config_drift.test.ts

14-assertion test suite covering the entire config-drift subsystem without requiring any external services (no PostgreSQL, no network):

┌─────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Test                                    │ Covers                                                                                                                                  │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ flattenConfig                           │ Nested objects, arrays, empty object, null root                                                                                         │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ hash determinism                        │ Same object → same hash; mutated object → different hash                                                                                │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ classifyKey                             │ All prefix buckets: critical, warning, info                                                                                             │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ diffFlattenedConfigs categories         │ All four categories present in one diff                                                                                                 │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ type_change detection                   │ number → string detected as type_change with correct baselineType / runtimeType                                                         │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ computeDriftReport summary              │ total, criticalCount, typeChanges, warningCount fields                                                                                  │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ DriftStorage ring buffer                │ Evicts oldest when maxInMemory exceeded                                                                                                 │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ DriftStorage JSONL persistence          │ Write → reload from disk                                                                                                                │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ alert routing                           │ Critical → PagerDuty fires; warning-only → Slack fires; critical present → no Slack; no findings → no alert; disabled policy → no alert │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ AutoRemediationEngine evaluate()        │ Critical findings always skipped; safe rules match correct findings                                                                     │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ AutoRemediationEngine applyToBaseline() │ Baseline flattened map and hash updated in-place                                                                                        │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ BaselineJsonFileSource load/save        │ File created, loaded, saved with _savedAt annotation                                                                                    │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ BaselineJsonFileSource fallback         │ Non-existent path falls back gracefully                                                                                                 │
├─────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ keyMatchesPrefix                        │ Exact match, prefix match, boundary cases                                                                                               │
└─────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Modified: scripts/run-tests.cjs

Added tests/config/config_drift.test.ts to the TEST_FILES array so the suite runs as part of CI.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

New: Monitoring & Docs

- deploy/monitoring/config-drift-dashboard.json — Grafana dashboard with panels for drift findings by severity, auto-remediation rate, and snapshot frequency
- deploy/localnet/grafana/dashboards/config-drift-dashboard.json — localnet variant of the same dashboard
- docs/docker-ci-cache.md — Docker layer caching guide for CI
- docs/operations/github-actions-optimization.md — GitHub Actions optimisation runbook

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Environment Variables

The auditor is fully configured through environment variables — no code changes required to enable alerting:

┌──────────────────────────────────────┬─────────────────────────────────────────┬───────────────────────────────────────────────┐
│ Variable                             │ Default                                 │ Description                                   │
├──────────────────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────────┤
│ VERINODE_DRIFT_SNAPSHOT_INTERVAL_MS  │ 300000 (5 min)                          │ Polling interval                              │
├──────────────────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────────┤
│ VERINODE_DRIFT_ALERTS_ENABLED        │ false                                   │ Enable PagerDuty / Slack alerting             │
├──────────────────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────────┤
│ VERINODE_DRIFT_PAGERDUTY_ENABLED     │ false                                   │ Enable PagerDuty specifically                 │
├──────────────────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────────┤
│ VERINODE_DRIFT_PAGERDUTY_ROUTING_KEY │ —                                       │ PagerDuty Events v2 routing key               │
├──────────────────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────────┤
│ VERINODE_DRIFT_CRITICAL_PREFIXES     │ db,mtls,tls,auth,staking                │ Comma-separated critical prefixes             │
├──────────────────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────────┤
│ VERINODE_DRIFT_WARNING_PREFIXES      │ capacity_shedding,performance,telemetry │ Comma-separated warning prefixes              │
├──────────────────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────────┤
│ VERINODE_DRIFT_AUTO_REMEDIATION      │ false                                   │ Enable auto-remediation of safe drifts        │
├──────────────────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────────┤
│ VERINODE_BASELINE_DIR                │ ./config                                │ Directory scanned for *.baseline.json files   │
├──────────────────────────────────────┼─────────────────────────────────────────┼───────────────────────────────────────────────┤
│ VERINODE_SLACK_WEBHOOK_URL           │ —                                       │ Slack incoming webhook URL for warning alerts │
└──────────────────────────────────────┴─────────────────────────────────────────┴───────────────────────────────────────────────┘

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Test Results

config-drift tests
  ✓ flattenConfig
  ✓ hash determinism
  ✓ classifyKey
  ✓ diffFlattenedConfigs categories
  ✓ type_change detection
  ✓ computeDriftReport summary
  ✓ DriftStorage ring buffer
  ✓ DriftStorage JSONL persistence
  ✓ alert routing (critical→PagerDuty / warning→Slack)
  ✓ AutoRemediationEngine evaluate()
  ✓ AutoRemediationEngine applyToBaseline()
  ✓ BaselineJsonFileSource load/save
  ✓ BaselineJsonFileSource fallback (threw=true — acceptable)
  ✓ keyMatchesPrefix

All config-drift tests passed ✓

TLS rotation suite: 37/37 (previously 33/37)
TypeScript build: clean (tsc --noEmit exits 0)

closes #200 

…rage and alerting

- src/config-drift/types.ts       — DriftFinding, DriftReport, ConfigSnapshot, DriftEvent types
- src/config-drift/flatten.ts     — flattenConfig, computeHashFromFlattened, keyMatchesPrefix
- src/config-drift/diff.ts        — diffFlattenedConfigs, computeDriftReport, classifyKey
- src/config-drift/baseline.ts    — BaselineJsonFileSource, ExampleConfigBaselineSource, loadBaselineSnapshot
- src/config-drift/storage.ts     — DriftStorage in-memory ring buffer + JSONL + PostgreSQL
- src/config-drift/pagerduty.ts   — HttpPagerDutyClient, buildAlertIfCritical, buildAlertIfWarning
- src/config-drift/slack.ts       — HttpSlackClient, createSlackClientFromEnv
- src/config-drift/remediation.ts — AutoRemediationEngine with built-in safe-drift rules
- src/config-drift/auditor.ts     — ConfigDriftAuditor, createConfigDriftAuditorFromEnv
- src/config-drift/routes.ts      — registerConfigDriftRoutes (HTTP dashboard + REST endpoints)
- src/config-drift/index.ts       — barrel exports
- src/database/migrations/013_config_drift_events.sql — config_drift_events table
…miting for drift endpoints

- index.js: init ConfigDriftAuditor after config, register drift routes, add shutdown hook
- index.js: extend rate-limiter endpointTiers with /config/snapshot, /config/drift-events,
  /debug/config-drift, /debug/config-drift/history, /debug/config-drift/ui (all 'pro' tier)
- token_validator.ts / prometheus.ts: incidental updates from same session
- tests/config/config_drift.test.ts: 14 assertions covering flattenConfig,
  hash determinism, classifyKey, all four diff categories (value_change,
  key_added, key_removed, type_change), DriftStorage ring buffer and JSONL
  persistence, alert routing (PagerDuty / Slack), AutoRemediationEngine
  evaluate + applyToBaseline, and BaselineJsonFileSource load/save/fallback
- scripts/run-tests.cjs: prepend tests/config/config_drift.test.ts to TEST_FILES
…rift dashboard/docs

- src/tls/acme_rotation.ts: export registerCertManagementRoutes(app, mgr) that registers
  GET /api/v1/certs/status and POST /api/v1/certs/renew on a CertLifecycleManager instance
  (fixes 4 previously failing tls_rotation tests — all 37 now pass)
- deploy/monitoring/config-drift-dashboard.json: Grafana dashboard for config drift metrics
- deploy/localnet/grafana/dashboards/config-drift-dashboard.json: localnet version
- docs/docker-ci-cache.md, docs/operations/github-actions-optimization.md: operational docs
@JamesEjembi
JamesEjembi merged commit 1416e66 into VeriNode-Labs:main Aug 25, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants