feat(security): activate caddy security-event pipeline — bounded first-start replay, self-noise filter, host fidelity (DC-113) [glm-grade=A]
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

The caddy tail worker (DC-112) was 100% dead in prod: no /var/log/caddy
mount, no CADDY_ACCESS_LOG env, no global access log in the Caddyfile.
Store census 45,912 events, 100% source_type 'api', ZERO 'caddy'.

- createTail: firstStartMaxBytes (5 MiB) bounds first-ever-start replay
  against a long-lived access.log; multi-chunk partial-line discard on
  the jump; normal restarts resume at exact persisted offset (judge r1
  fix-first fold)
- worker: self-noise filter drops our own probe UAs
  (DashCaddy-Probe/1.0, DashCaddy-HealthCheck/1.0) from the derived
  store — raw log keeps everything; ~50-100 events/min of probe noise
  would otherwise bury perimeter signal in the 100k-cap store
- worker: metadata.host reads request.host (real caddy JSON nests it;
  verified against live /var/log/caddy/seeds.log — the DC-112 read was
  always null on live lines); top-level fallback kept
- worker: onAppear recovery log (DC-112 judge polish fold)
- start.sh: -v /var/log/caddy:/var/log/caddy:ro + CADDY_ACCESS_LOG env
- README: dead caddy-api/ dir refs -> dashcaddy-api/ (queue item e)
- tests: +12 DC-113 pins (hermetic, real worker + store); DC-112
  fixtures corrected to the real nested request.host shape

126 suites / 2841 tests green. Judge: GLM-5.3 round-1 B, round-2 A
(SHIP). Verdict urn:ump:mccln523fptotuvrmddlqpf4zpkrxf273tg4kyytuyy3776rj3pq
This commit is contained in:
Hermes
2026-08-23 12:40:32 -07:00
parent a29a59a320
commit 65457ff8e0
5 changed files with 402 additions and 17 deletions
@@ -128,11 +128,11 @@ describe('caddy worker end-to-end (real tail + real store)', () => {
test('gate hit is named, escalated, and carries array-normalized UA + host', async () => {
// A realistic forward_auth gate miss, exactly as caddy logs it:
// headers as arrays, host at top level, duration in seconds.
// headers as arrays, host nested in request, duration in seconds.
fs.appendFileSync(ACCESS_LOG, JSON.stringify({
ts: 1787500800,
host: 'plex.sami',
request: {
request: { host: 'plex.sami',
remote_ip: '10.9.9.9',
method: 'GET',
uri: '/api/auth/gate/plex',
@@ -163,15 +163,15 @@ describe('caddy worker end-to-end (real tail + real store)', () => {
test('canonical gate hit and sso-exchange POST are named too', async () => {
fs.appendFileSync(ACCESS_LOG, JSON.stringify({
ts: 1787500801,
host: 'status.sami',
request: { remote_ip: '10.9.9.8', method: 'GET', uri: '/api/v1/auth/gate/sonarr', proto: 'HTTP/2.0', headers: { 'User-Agent': ['Mozilla/5.0'] } },
request: { host: 'status.sami',
remote_ip: '10.9.9.8', method: 'GET', uri: '/api/v1/auth/gate/sonarr', proto: 'HTTP/2.0', headers: { 'User-Agent': ['Mozilla/5.0'] } },
status: 401,
duration: 0.002,
}) + '\n');
fs.appendFileSync(ACCESS_LOG, JSON.stringify({
ts: 1787500802,
host: 'status.sami',
request: { remote_ip: '10.9.9.8', method: 'POST', uri: '/api/auth/sso-exchange', proto: 'HTTP/2.0', headers: { 'user-agent': ['DashCaddy-Login/1.0'] } },
request: { host: 'status.sami',
remote_ip: '10.9.9.8', method: 'POST', uri: '/api/auth/sso-exchange', proto: 'HTTP/2.0', headers: { 'user-agent': ['DashCaddy-Login/1.0'] } },
status: 200,
duration: 0.084,
}) + '\n');
@@ -192,8 +192,8 @@ describe('caddy worker end-to-end (real tail + real store)', () => {
test('ordinary traffic keeps http.<status> naming and default severity', async () => {
fs.appendFileSync(ACCESS_LOG, JSON.stringify({
ts: 1787500803,
host: 'status.sami',
request: { remote_ip: '100.121.150.22', method: 'GET', uri: '/api/health', proto: 'HTTP/2.0', headers: { 'User-Agent': ['watchdog'] } },
request: { host: 'status.sami',
remote_ip: '100.121.150.22', method: 'GET', uri: '/api/health', proto: 'HTTP/2.0', headers: { 'User-Agent': ['watchdog'] } },
status: 401,
duration: 0.004,
}) + '\n');
@@ -216,8 +216,8 @@ describe('caddy worker end-to-end (real tail + real store)', () => {
test('restart does not re-emit: offset persistence across worker instances', async () => {
fs.appendFileSync(ACCESS_LOG, JSON.stringify({
ts: 1787500804,
host: 'plex.sami',
request: { remote_ip: '10.9.9.9', method: 'GET', uri: '/api/auth/gate/plex', headers: {} },
request: { host: 'plex.sami',
remote_ip: '10.9.9.9', method: 'GET', uri: '/api/auth/gate/plex', headers: {} },
status: 401,
}) + '\n');