coleam00/Archon · critical · Error

github_app.internal_endpoint_public_bind_rejected

github_app.internal_endpoint_public_bind_rejected

Error message

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.

What it means

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.

Source

Thrown at packages/server/src/index.ts:893

  const hostname = process.env.HOST || '0.0.0.0';

  // Security guardrail: /internal/git-credential hands out live installation
  // access tokens. Fail fast (not just WARN) when App mode is active and the
  // server is bound to a non-loopback interface — a WARN line in startup
  // logs is too easy to scroll past, and the failure mode is "anyone on the
  // network who can hit the port pulls a live token". Operators who deliberately
  // firewall externally (so loopback bind would block their reverse proxy's
  // upstream) can opt out via ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1.
  //
  // Runs BEFORE Bun.serve so a rejected config never opens the listening
  // socket — even briefly — and `server_listening` is never logged.
  if (githubAppAuthProvider && hostname !== '127.0.0.1' && hostname !== 'localhost') {
    if (process.env.ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND === '1') {
      getLog().warn({ hostname }, 'github_app.internal_endpoint_exposed_acknowledged');
    } else {
      getLog().fatal({ hostname }, 'github_app.internal_endpoint_public_bind_rejected');
      throw new Error(
        '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.'
      );
    }
  }

  // Security guardrail (advisory): the web identity header (ARCHON_WEB_AUTH_HEADER,
  // default X-Archon-User) is trusted as-is — Archon attributes web requests to
  // whoever the header names. That is only sound when Archon is reachable SOLELY
  // through a reverse proxy that authenticates and sets the header (loopback bind).
  // On a non-loopback bind any client that can reach the port can forge it:
  // cosmetic misattribution without per-user GitHub, but in per-user mode a forged
  // header can read/disconnect another user's GitHub connection or bind a
  // device-flow token under their identity. WARN (not fatal) so existing exposed

View on GitHub (pinned to 0773b97458)

Solutions

  1. Set HOST=127.0.0.1 so the server binds to loopback
  2. 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
  3. Verify the proxy config actually drops /internal/* before opting out; a leaked git-credential endpoint exposes live installation tokens
  4. Check the startup error/log for the offending hostname value and correct the deployment's bind configuration

Example fix

// before: binds 0.0.0.0 with GitHub App mode
HOST=0.0.0.0 archon serve
// after: bind loopback, or acknowledge exposure only behind a proxy that drops /internal/*
HOST=127.0.0.1 archon serve
# or, deliberately:
HOST=0.0.0.0 ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1 archon serve
Defensive patterns

Strategy: validation

Validate before calling

const isLoopback = ['127.0.0.1', 'localhost', '::1'].includes(process.env.HOST ?? '');
const githubAppMode = Boolean(process.env.GITHUB_APP_ID);
if (githubAppMode && !isLoopback && process.env.ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND !== '1') {
  throw new Error('GitHub App mode requires HOST=127.0.0.1 (or explicit ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1)');
}

Type guard

null

Try / catch

try {
  await startServer(config);
} catch (e) {
  if (String(e.message).includes('internal_endpoint_public_bind_rejected') || String(e.message).includes('non-loopback')) {
    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');
    process.exit(1);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/e734eab4644c3080. Report an issue: GitHub.