From 0a896f5740c09b533a1d1d4d527767b9f102582f Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 17 Apr 2026 10:05:49 -0400 Subject: [PATCH] security(encryptid): gate /api/internal/* against public reach The /api/internal/* routes (user lookups, space invites, fund claims, agent mailbox creds) were publicly reachable via auth.rspace.online because Traefik forwards everything under the encryptid host rule. They were designed for service-to-service calls over the Docker network only. Add a Hono middleware that rejects /api/internal/* with 404 when the request arrives through the public edge (X-Forwarded-For / X-Real-IP set by Traefik) unless it carries a valid X-Internal-Key matching INTERNAL_API_KEY. Direct container-to-container fetches don't set forwarded headers so existing callers keep working untouched. --- docker-compose.yml | 1 + src/encryptid/server.ts | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 806c5ade..0d4059f3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -245,6 +245,7 @@ services: - MAILCOW_API_URL=${MAILCOW_API_URL:-http://nginx-mailcow:8080} - MAILCOW_API_KEY=${MAILCOW_API_KEY:-} - ADMIN_DIDS=${ADMIN_DIDS} + - INTERNAL_API_KEY=${INTERNAL_API_KEY} labels: - "traefik.enable=true" - "traefik.http.routers.encryptid.rule=Host(`auth.rspace.online`) || Host(`auth.ridentity.online`) || Host(`encryptid.jeffemmett.com`)" diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 0887e5b8..6da79f68 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -480,10 +480,25 @@ app.use('*', cors({ return undefined; }, allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowHeaders: ['Content-Type', 'Authorization'], + allowHeaders: ['Content-Type', 'Authorization', 'X-Internal-Key'], credentials: true, })); +// /api/internal/* is for service-to-service calls over the Docker network only. +// Traefik adds X-Forwarded-For for any request arriving through the public edge +// (CF tunnel, direct HTTPS). Direct container-to-container fetches do not set it. +// A shared INTERNAL_API_KEY also works for callers outside the Docker net. +app.use('/api/internal/*', async (c, next) => { + const forwarded = c.req.header('X-Forwarded-For') || c.req.header('X-Real-IP'); + const providedKey = c.req.header('X-Internal-Key'); + const expectedKey = process.env.INTERNAL_API_KEY; + const hasValidKey = !!(expectedKey && providedKey && providedKey === expectedKey); + if (forwarded && !hasValidKey) { + return c.json({ error: 'Not found' }, 404); + } + return next(); +}); + // ============================================================================ // STATIC FILES & WELL-KNOWN // ============================================================================