{"record":{"id":"e734eab4644c3080","repo":"coleam00/Archon","slug":"github-app-internal-endpoint-public-bind-rejected","errorCode":"github_app.internal_endpoint_public_bind_rejected","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":"Error","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":"In GitHub App mode, startServer refuses to bind the HTTP server to a non-loopback interface because /internal/git-credential hands out live installation tokens. This fatal check (getLog().fatal + throw) prevents the credential endpoint from ever being network-exposed, unless the operator explicitly acknowledges the risk via ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1.","triggerScenarios":"Server starts with a GitHub App auth provider configured while the HOST/hostname is anything other than 127.0.0.1 or localhost, and ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND is not '1'. serveCommand -> startServer throws before the listening log is emitted.","commonSituations":"Running in Docker/Kubernetes where the default bind is 0.0.0.0; exposing Archon through a reverse proxy; switching to GitHub App mode on an existing deployment that already used a public bind.","solutions":["Set HOST=127.0.0.1 so the server binds to loopback","If a trusted reverse proxy already strips /internal/* and a non-loopback bind is required, set ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1 — only after verifying the proxy blocks that path","Verify the proxy config actually drops /internal/* before opting out; a leaked git-credential endpoint exposes live installation tokens","Check the startup error/log for the offending hostname value and correct the deployment's bind configuration"],"exampleFix":"// before: binds 0.0.0.0 with GitHub App mode\nHOST=0.0.0.0 archon serve\n// after: bind loopback, or acknowledge exposure only behind a proxy that drops /internal/*\nHOST=127.0.0.1 archon serve\n# or, deliberately:\nHOST=0.0.0.0 ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1 archon serve","handlingStrategy":"validation","validationCode":"const isLoopback = ['127.0.0.1', 'localhost', '::1'].includes(process.env.HOST ?? '');\nconst githubAppMode = Boolean(process.env.GITHUB_APP_ID);\nif (githubAppMode && !isLoopback && process.env.ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND !== '1') {\n  throw new Error('GitHub App mode requires HOST=127.0.0.1 (or explicit ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1)');\n}","typeGuard":"null","tryCatchPattern":"try {\n  await startServer(config);\n} catch (e) {\n  if (String(e.message).includes('internal_endpoint_public_bind_rejected') || String(e.message).includes('non-loopback')) {\n    console.error('Bind rejected: set HOST=127.0.0.1, or verify your proxy drops /internal/* and set ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1');\n    process.exit(1);\n  }\n  throw e;\n}","preventionTips":["Default HOST=127.0.0.1 for all deployments running in GitHub App mode","Only set ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1 after verifying the reverse proxy drops /internal/*","Never expose the Archon process directly on a public interface in GitHub App mode","Include the internal-endpoint bind check in deployment smoke tests so misconfiguration fails in CI, not production"],"tags":["security","github-app","startup","bind","credentials"],"backgroundTag":"internal-endpoint-exposed","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}