coleam00/Archon · critical

GitHub App mode is active but the server is bound to a non-l

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

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.

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 (or 'localhost') so the server binds to loopback, then let a same-host reverse proxy forward traffic.
  2. 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.
  3. Verify the proxy actually blocks /internal/git-credential before enabling the opt-out — the endpoint issues live installation tokens.
  4. After changing HOST, restart the server; the check runs at bind time and must log 'server_listening' to succeed.

Example fix

# before
HOST=0.0.0.0
# after: loopback bind behind a local proxy
HOST=127.0.0.1
# or, explicitly acknowledged:
# ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1
Defensive patterns

Strategy: validation

Validate before calling

const host = process.env.HOST ?? '127.0.0.1';
const loopback = host === '127.0.0.1' || host === 'localhost';
if (!loopback && process.env.GITHUB_APP_ID && process.env.ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND !== '1') {
  throw new Error('App mode requires HOST=127.0.0.1 or ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1');
}

Try / catch

try {
  await startServer(config);
} catch (e) {
  if (/internal endpoint public bind/i.test(e?.message ?? '')) {
    logFatal('Bind HOST=127.0.0.1, or set ARCHON_ALLOW_INTERNAL_ON_PUBLIC_BIND=1 behind a proxy that drops /internal/*.');
    process.exit(1);
  }
  throw e;
}

Prevention

When it happens

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

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

Related errors


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