Isolate internal bot-config route from the public listener (#33) #36
Reference in New Issue
Block a user
No description provided.
Delete Branch "bugfix/internal-token-endpoint-33"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes #33.
Problem
GET /internal/bot-configreturns the decrypted Discord bot token (server/src/router/v1/internal/internal.controller.js). It was mounted under the public/apiapp —app.js→apiRouter→v1.router.jsmounted/internal— so the full path/api/v1/internal/bot-configlived on the same port 3000 listener that Pangolin proxies to the world. The only access control wasrequireInternalKey, a timing-safe compare againstBOT_INTERNAL_KEY, and.env.exampleshipped the placeholderchange-me-to-a-long-random-string.The code comments asserted the route was "never exposed through the public reverse proxy," but nothing in the repo enforced that. The guarantee rested entirely on (a) Pangolin being configured to not forward
/api/v1/internal/*, and (b) the shared secret being strong. If either failed, an unauthenticated remote attacker could retrieve the plaintext token and fully control the bot — defeating the point of encrypting it at rest.By contrast, the bot's own internal API is correctly isolated on an unpublished port (4100); the server's internal route was not.
Fix
Both remediations from the issue, mirroring the bot's port-4100 pattern:
1. Move
/internalonto its own, unpublished listenerserver/src/internalApp.js— a standalone Express app mountingrequireInternalKey+/internal(and a no-secret/health).server.jsstarts a second listener onINTERNAL_PORT(default3001), bound0.0.0.0but never published in compose, and closed on graceful shutdown./internalmount from the publicv1.router.js. The public app now 404s/api/v1/internal/bot-configeven with a valid key — the decrypted-token route is gone from the public attack surface.docker-compose.yml: botSITE_INTERNAL_URL→http://app:3001/internal/bot-config; documented thatINTERNAL_PORTstays unpublished.2. Fail fast on a weak
BOT_INTERNAL_KEYserver/src/utils/botInternalKey.jsrejects an empty, documented-placeholder, or<16-char key: fatal in production (process.exit(1)at boot), warning in dev.3. Docs / defense in depth
.env.example(root/server/bot): documentINTERNAL_PORT, the fail-fast behavior, and an explicit Pangolin deny rule for/api/v1/internalas belt-and-braces.Deployment note ⚠️
The bot's
SITE_INTERNAL_URLport changed (3000 → 3001). Server and bot must be redeployed together — a partial rollout will 401 the bot's boot-time config fetch until both sides match. Anyone running with a placeholder/shortBOT_INTERNAL_KEYin production will now fail to start (intentional) and must set a strong key.Verification
npm test(server): 93 pass / 0 fail, incl. newrequireInternalKey.test.jsandbotInternalKey.test.js./internal(401 without key) and serves/health; public listener 404s the old path with a valid key header;/api/healthstill 200.🤖 Generated with Claude Code
https://claude.ai/code/session_019rao86n5cXpwAyjdBFEshV