Apus can auto-configure Tailscale Serve (tailnet) or Funnel (public) for the Gateway dashboard and WebSocket port. This keeps the gateway bound to loopback while Tailscale provides HTTPS, routing, and (for Serve) identity headers.

Looking for the step-by-step setup? See Give your Gateway a stable HTTPS URL.

Modes

gateway.tailscale.mode:

Mode Behavior
serve Tailnet-only Serve via tailscale serve. The gateway stays on 127.0.0.1.
funnel Public HTTPS via tailscale funnel. Requires a shared password.
off (default) No Tailscale automation.

Status and audit output use Tailscale exposure for this Apus Serve/Funnel mode. off means Apus is not managing Serve or Funnel. It does not mean the local Tailscale daemon is stopped or logged out.

Config examples

Tailnet-only (Serve)

{
  gateway: {
    bind: "loopback",
    tailscale: { mode: "serve" },
  },
}

Open: https://<magicdns>/ (or your configured gateway.controlUi.basePath)

Tailnet-only (bind to Tailnet IP)

Use this to have the gateway listen directly on the Tailnet IP, with no Serve/Funnel:

{
  gateway: {
    bind: "tailnet",
    auth: { mode: "token", token: "your-token" },
  },
}

Connect a native or CLI client from another Tailnet device:

The browser Control UI can create and sign device identity over the direct plain-HTTP address using pure-JavaScript Ed25519. With token/password auth, the shared secret still does not replace browser device identity.

Plain HTTP remains a downgraded transport: identity signing does not encrypt the page, shared secret, or Gateway traffic. Prefer Tailscale Serve for HTTPS. See Insecure HTTP.

Loading the Control UI from the Tailnet address itself is a private same-origin request and needs no gateway.controlUi.allowedOrigins entry. Public or cross-origin browser deployments need an allowed origin.

When a bindable Tailnet IPv4 is present, the Gateway also requires http://127.0.0.1:18789 for authenticated same-host clients. If no Tailnet address is available at startup, it falls back to loopback only. Restart after Tailscale becomes available to add direct Tailnet access. Neither path adds LAN or public exposure.

Public internet (Funnel + shared password)

{
  gateway: {
    bind: "loopback",
    tailscale: { mode: "funnel" },
    auth: { mode: "password", password: "replace-me" },
  },
}

Prefer APUS_GATEWAY_PASSWORD over committing a password to disk.

The Funnel URL also remains usable from devices inside the tailnet. Tailscale marks public requests as Funnel traffic but sends tailnet peers through its Serve identity path instead. Apus recognizes both paths on its dedicated listener and still requires the configured Funnel password.

CLI examples

apus gateway --tailscale serve
apus gateway --tailscale funnel --auth password

Auth

gateway.auth.mode controls the handshake:

Mode Use case
none Private ingress only
token (default) Shared token via APUS_GATEWAY_TOKEN or config
password Shared secret via APUS_GATEWAY_PASSWORD or config
trusted-proxy Identity-aware reverse proxy; see Trusted Proxy Auth

When gateway.auth.mode is unset, a resolved password selects password mode. Without one, token is the default. If both gateway.auth.token and gateway.auth.password are configured, set the mode explicitly.

When token mode has no credential, loopback startup generates a runtime-only token without writing it to config. A separate apus qr command still needs usable configured or supplied token/password authentication. That startup-only token does not configure pairing. See QR auth resolution.

Tailscale identity headers (Serve only)

When tailscale.mode: "serve" and gateway.auth.allowTailscale is true, Control UI/WebSocket auth can use Tailscale identity headers (tailscale-user-login) instead of a token/password. Apus verifies the header by resolving the request's x-forwarded-for address via the local Tailscale daemon (tailscale whois) and matching it to the header login before accepting it. A request only qualifies when it reaches Apus's dedicated managed-Tailscale listener with Tailscale's x-forwarded-for, x-forwarded-proto, and x-forwarded-host headers. Those headers never establish managed Serve provenance or tokenless auth on the ordinary Gateway listener.

This tokenless flow assumes the gateway host is trusted. If untrusted local code may run on the same host, set gateway.auth.allowTailscale: false and require token/password auth instead.

Scope of the bypass:

Externally managed Serve and Funnel

You can point a native Tailscale Serve or Funnel route at the ordinary Gateway listener when another service owns the route. Configure the route's immediate source narrowly in gateway.trustedProxies, and ensure it overwrites or safely rebuilds X-Forwarded-For. Apus then treats the request as generic trusted-proxy ingress, uses the forwarded client address for rate limits, and applies the configured gateway auth mode normally. Because Funnel is public, Gateway-protected routes reject externally managed Funnel ingress when gateway.auth.mode is none. Configure token, password, or trusted-proxy authentication. The aggregate health, readiness, and startup checks retain their existing unauthenticated responses, without exposing detailed readiness or startup data. See Health and readiness.

This compatibility path does not grant managed Tailscale semantics. gateway.auth.allowTailscale cannot provide tokenless auth. Apus does not call tailscale whois. It does not own or clean up the external route. Without an explicitly trusted source and a valid non-loopback forwarded client address, Gateway-authenticated routes fail with proxy_attribution_required. If the proxy connects over loopback, adding 127.0.0.1 to trustedProxies explicitly trusts same-host processes to supply proxy attribution. Keep token or password auth enabled unless every process on the host belongs to the same trust boundary.

Notes

Tailscale prerequisites and limits

Recover an orphaned foreground claim

Older Gateways could leave a foreground Tailscale claim running after a forced shutdown or an interrupted sudo-backed start. Updating fixes cleanup for new claims but does not automatically remove existing claims with unproven ownership.

If startup reports an occupied HTTPS port, run tailscale serve status --json. Check Foreground for the reported session, hostname, path, and proxy target. On macOS or Linux, inspect candidate CLI processes with ps -axo pid,ppid,args | grep '[t]ailscale'. Confirm which process created that route before stopping it with kill -TERM <confirmed-pid> (or sudo kill -TERM <confirmed-pid> for a root process). Tailscale status does not report the claimant PID. A backend listener PID or an orphaned parent alone does not prove ownership. If another application owns the claim, leave it alone and keep Apus managed ingress off until you resolve the conflict.

Verify that the foreground session disappears from tailscale serve status --json, then restart the Gateway. tailscale serve --https=443 --set-path=/ off removes a background Web handler. It does not release a foreground claim.

Browser control (remote Gateway + local browser)

To run the Gateway on one machine but drive a browser on another, run a node host on the browser machine. Keep both machines on the same tailnet. The Gateway proxies browser actions to the node. No separate control server or Serve URL is needed.

Avoid Funnel for browser control. Treat node pairing like operator access.

Learn more

Related