{"record":{"id":"cae2506ee4a1f849","repo":"coleam00/Archon","slug":"github-app-mode-is-active-but-the-server-is-bound","errorCode":null,"errorMessage":"GitHub App mode is active but the server is bound to a non-loopback interface (${hostname}). The /internal/git-credential endpoint hands out live installation tokens — exposing it would leak credentials to the network. Either bind to 127.0.0.1 (HOST=127.0.0.1), or, if your reverse proxy already drops /internal/* and the upstream needs a non-loopback bind, opt out by setting ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1.","messagePattern":"GitHub App mode is active but the server is bound to a non-loopback interface \\((.+?)\\)\\. The /internal/git-credential endpoint hands out live installation tokens — exposing it would leak credentials to the network\\. Either bind to 127\\.0\\.0\\.1 \\(HOST=127\\.0\\.0\\.1\\), or, if your reverse proxy already drops /internal/\\* and the upstream needs a non-loopback bind, opt out by setting ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"packages/server/src/index.ts","lineNumber":893,"sourceCode":"\n  const hostname = process.env.HOST || '0.0.0.0';\n\n  // Security guardrail: /internal/git-credential hands out live installation\n  // access tokens. Fail fast (not just WARN) when App mode is active and the\n  // server is bound to a non-loopback interface — a WARN line in startup\n  // logs is too easy to scroll past, and the failure mode is \"anyone on the\n  // network who can hit the port pulls a live token\". Operators who deliberately\n  // firewall externally (so loopback bind would block their reverse proxy's\n  // upstream) can opt out via ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1.\n  //\n  // Runs BEFORE Bun.serve so a rejected config never opens the listening\n  // socket — even briefly — and `server_listening` is never logged.\n  if (githubAppAuthProvider && hostname !== '127.0.0.1' && hostname !== 'localhost') {\n    if (process.env.ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND === '1') {\n      getLog().warn({ hostname }, 'github_app.internal_endpoint_exposed_acknowledged');\n    } else {\n      getLog().fatal({ hostname }, 'github_app.internal_endpoint_public_bind_rejected');\n      throw new Error(\n        'GitHub App mode is active but the server is bound to a non-loopback ' +\n          `interface (${hostname}). The /internal/git-credential endpoint hands out ` +\n          'live installation tokens — exposing it would leak credentials to the network. ' +\n          'Either bind to 127.0.0.1 (HOST=127.0.0.1), or, if your reverse proxy already ' +\n          'drops /internal/* and the upstream needs a non-loopback bind, opt out by ' +\n          'setting ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1.'\n      );\n    }\n  }\n\n  // Security guardrail (advisory): the web identity header (ARCHON_WEB_AUTH_HEADER,\n  // default X-Archon-User) is trusted as-is — Archon attributes web requests to\n  // whoever the header names. That is only sound when Archon is reachable SOLELY\n  // through a reverse proxy that authenticates and sets the header (loopback bind).\n  // On a non-loopback bind any client that can reach the port can forge it:\n  // cosmetic misattribution without per-user GitHub, but in per-user mode a forged\n  // header can read/disconnect another user's GitHub connection or bind a\n  // device-flow token under their identity. WARN (not fatal) so existing exposed","sourceCodeStart":875,"sourceCodeEnd":911,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/server/src/index.ts#L875-L911","documentation":"When GitHub App mode is active, the server exposes /internal/git-credential, which hands out live GitHub installation tokens. startServer refuses to bind that endpoint on any non-loopback interface: it logs a fatal 'github_app.internal_endpoint_public_bind_rejected' and throws unless the operator explicitly acknowledges the risk with ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1. This prevents accidentally leaking credentials to the network.","triggerScenarios":"GitHub App mode is enabled, the HOST env var binds the HTTP server to something other than 127.0.0.1/localhost (e.g. 0.0.0.0 or a LAN IP), and ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND is not set to '1'.","commonSituations":"Running Archon in Docker with the container bound to 0.0.0.0; hosting behind a reverse proxy on another machine and setting HOST to a public/LAN interface; deploying to a VPS where the default HOST is the external IP; k8s port bindings using non-loopback addresses.","solutions":["Set HOST=127.0.0.1 (or 'localhost') so the server binds to loopback, then let a same-host reverse proxy forward traffic.","If the reverse proxy already strips /internal/* and a non-loopback upstream bind is required, set ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1 to acknowledge the exposure.","Verify the proxy actually blocks /internal/git-credential before enabling the opt-out — the endpoint issues live installation tokens.","After changing HOST, restart the server; the check runs at bind time and must log 'server_listening' to succeed."],"exampleFix":"# before\nHOST=0.0.0.0\n# after: loopback bind behind a local proxy\nHOST=127.0.0.1\n# or, explicitly acknowledged:\n# ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1","handlingStrategy":"validation","validationCode":"const host = process.env.HOST ?? '127.0.0.1';\nconst loopback = host === '127.0.0.1' || host === 'localhost';\nif (!loopback && process.env.GITHUB_APP_ID && process.env.ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND !== '1') {\n  throw new Error('App mode requires HOST=127.0.0.1 or ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await startServer(config);\n} catch (e) {\n  if (/internal endpoint public bind/i.test(e?.message ?? '')) {\n    logFatal('Bind HOST=127.0.0.1, or set ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1 behind a proxy that drops /internal/*.');\n    process.exit(1);\n  }\n  throw e;\n}","preventionTips":["Default container deployments to HOST=127.0.0.1 and publish ports via a proxy.","Never set ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1 unless the proxy provably blocks /internal/*.","Include the bind-host check in deployment runbooks for App mode.","Alert on the 'github_app.internal_endpoint_public_bind_rejected' fatal log."],"tags":["security","github-app","network","configuration","credential-leak"],"backgroundTag":"internal-endpoint-public-bind","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}