Skip to main content
Version: Next

Native access

Native access lets a developer's own tools — psql, DBeaver, redis-cli, a Kafka client, curl — reach an instance's services on their native ports, at addresses that do not change, from a laptop running the stock Tailscale client. No port is published on any host, two instances of the same environment never collide, and it works the same on a plain Docker host and on an elastic runtime.

It is available on every tier, with no switch beyond the settings below. The first half of this page is for the administrator who connects Fadebox to a tailnet; the second is for the developer who uses it.

There is a second way in that needs no tailnet: the instance agent, which Fadebox runs in every instance, and fadebox connect, which tunnels the services to localhost with Fadebox's own access rules.

How it works​

  • A gateway per instance. When an environment has native access on, every deploy of its instances adds one container, <namespace>-fadebox-tailnet: the stock tailscale/tailscale image in userspace networking mode — no capability, no /dev/net/tun — on the instance's own network, publishing nothing. It joins the tailnet as a node named after the instance's namespace (shop-staging-pr-42) and advertises the instance's subnet as a subnet route.
  • A subnet per instance. Each instance gets a /24 of the installation's address block, and each of its services a fixed address in it. The tunnel ends inside the instance's network, so a route into pr-42 reaches pr-42 and nothing else.
  • Fadebox never joins the tailnet. It calls the control server's API — Tailscale's, or a Headscale you run — to mint a key for each gateway and to delete nodes, nothing more.
  • A node per deploy. A deploy mints a single-use, ephemeral, tagged key valid for ten minutes, deletes the node the previous deploy left, and starts the gateway with the key. Fadebox remembers which node each deploy's gateway joined as and only ever acts on that node, never on whatever happens to carry its name. A stop deletes the node, and so does deleting the instance, its environment or its project — which also frees the subnet. An hourly sweep deletes the node of every instance that is not running, starting or failed, and of every instance that was deleted while its node could not be.
  • The gateway only dials out: to the control server over HTTPS, to STUN and to peers over UDP, and to a DERP relay over HTTPS when UDP cannot get through. That is the whole networking requirement on a runtime host; nothing is configured per runtime.

:::warning Who may connect is the tailnet's policy, and it is coarse

The policy grants a group of the tailnet's users the whole address block, so a member of that group can route to every instance. What protects a given database is its own password, and the fact that an outsider is not on the tailnet. An instance's sign-in gate protects its URLs, not this route.

:::

For administrators​

What it costs​

  • A container and a node per running instance of an environment with native access. A stopped instance has no node. On Tailscale's plans every node counts toward the tailnet's devices, and every developer who joins is a user of the tailnet, under whatever the plan charges for them.
  • The switch is per environment, so only the environments that need it spend nodes.
  • The sweep keeps the count honest. A node that a stop or a delete failed to remove is deleted within the hour (FADEBOX_TAILNET_RECONCILE_INTERVAL). Every gateway's node is ephemeral as well, so the control server drops one Fadebox has lost track of once it has been offline a while.

Choosing Tailscale or Headscale​

  • Tailscale — the hosted control plane and its relays. Nothing to run; it needs an OAuth client and internet egress from the Fadebox server and the runtime hosts.
  • Headscale — a control server you run, which is how native access works offline. Fadebox speaks Headscale 0.28.0 up to, but not including, 0.30.0.

Either way, give each Fadebox installation a tag and an address block of its own (tag:fadebox-instance and 10.224.0.0/12 by default), and put the tag on nothing else. Two installations on one block hand out the same subnets, and a laptop routes a subnet through one gateway only. Two on one tag mostly stay out of each other's way — Fadebox deletes only nodes it recorded for its own deploys — but on Tailscale, where a node does not say which key registered it, two installations whose instances share a name and deploy within the same minutes can still take each other's node. Verify warns when nodes carrying the tag are not gateways of this installation — under any name, its own instances' included — and when such nodes advertise routes inside the block. A node without the tag is never touched.

Tailscale​

The OAuth client. Create it in the Tailscale admin console on the Trust credentials page, tagged with the gateways' tag, with these scopes:

ScopeWhy
auth_keysMinting each gateway's key.
devices:coreFinding and deleting gateway nodes; Verify reads the devices.
policy_file:readVerify reads the policy. Without it, Verify warns that it could not check.

An OAuth client does not expire like a personal access token does. Verify cannot see which tags the client may mint keys for: a client tagged with something else passes Verify and is refused at the first deploy, with Tailscale's requested tags … are invalid or not permitted.

The policy needs three entries, shown here for the default tag and block:

"tagOwners": { "tag:fadebox-instance": ["autogroup:admin"] },
"autoApprovers": { "routes": { "10.224.0.0/12": ["tag:fadebox-instance"] } },
"grants": [ { "src": ["group:developers"], "dst": ["10.224.0.0/12"], "ip": ["*"] } ]
  • tagOwners declares the tag. Verify fails without it.
  • autoApprovers.routes approves every gateway's subnet as it is advertised. Without it, each gateway waits for an administrator to approve its route — after every deploy, since every deploy is a new node. Verify warns when the block is not covered; several adjoining entries count, as does one wider one.
  • The grant lets the developers route to the block. It names the block and not the tag, because traffic through a subnet router is matched by its destination. group:developers stands for a group of your own — whichever of the tailnet's users may reach instances, defined under groups in the policy. Fadebox never reads it, and Verify does not check the grant.

Headscale​

Versions. 0.28.0 up to, but not including, 0.30.0; Verify refuses a server outside that range and names its version. A server that reports no version Fadebox can place — a development build, or a Headscale behind an authenticating proxy that lets only /api/v1 through — passes with a warning if its API answers like a supported release. The policy below uses grants, which needs 0.29: on 0.28, write the same rule — the group to the block, on every port — in the form that release's policy takes.

The control URL is Headscale's server_url, an origin such as https://headscale.example.com with no path. Fadebox calls the API there and every gateway joins with --login-server=<control URL>, so the Fadebox server, the runtime hosts and the laptops all have to reach it. Fadebox follows no redirect, so it must be the URL Headscale itself serves. If Fadebox goes out through an egress proxy, a Headscale on an internal network must be among its non-proxy hosts.

The API key comes from headscale apikeys create. It has no scopes — it may do anything on the server — and it expires, after 90 days unless --expiration says otherwise. When it lapses, deploys of environments with native access fail with the control server's refusal until a new key is saved.

The policy:

{
"groups": {
"group:developers": ["dev@"],
},
"tagOwners": {
"tag:fadebox-instance": [],
},
"autoApprovers": {
"routes": {
"10.224.0.0/12": ["tag:fadebox-instance"],
},
},
"grants": [
{ "src": ["group:developers"], "dst": ["10.224.0.0/12"], "ip": ["*"] },
],
}
  • The tag has no owner. Headscale refuses autogroup:admin in a policy, and a tag needs no owner when the only way onto it is a pre-auth key that carries it. Headscale does not check a key's tag against the policy itself, so Verify does: it fails unless tagOwners declares the tag.
  • No user. The keys Fadebox mints carry the tag and no user, and the gateway nodes belong to the tag. Gateways join with --login-server alone, since Headscale refuses a node that also sends --advertise-tags beside a tagged key.
  • autoApprovers.routes: Verify warns unless the entries for the tag cover the whole block, exactly as on Tailscale. A Headscale running without any policy approves no route at all, and Verify says so.
  • The group is your own: dev@ is the Headscale user dev. A developer's laptop joins as a Headscale user you create (headscale users create <name>) — with a pre-auth key minted for that user, for instance (headscale preauthkeys create --user <id>).

TLS, so the relay works. A Tailscale client dials a DERP relay over TLS only. A Headscale on plain HTTP hands its nodes STUN but no relay, and a gateway with no direct path to a laptop then has no path at all. Serve Headscale over HTTPS, /derp included, and keep its STUN port (stun_listen_addr) reachable over UDP. Verify warns when the control URL is plain HTTP to anything but a loopback address, since the API key then crosses the network unencrypted.

Offline. For an installation without internet access, turn on Headscale's embedded DERP server and clear the default DERP map, which points at Tailscale's relays. In config.yaml:

derp:
server:
enabled: true
region_id: 999
region_code: headscale
region_name: Headscale Embedded DERP
stun_listen_addr: 0.0.0.0:3478
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
urls: []
paths: []
auto_update_enabled: false

policy:
mode: file
path: /etc/headscale/policy.hujson

This assumes a server_url that is HTTPS — served by Headscale itself, or by a proxy that passes /derp through — so the region Headscale adds for itself is reached at that URL. If a separate front terminates TLS for the relay on another address, set automatically_add_embedded_derp_region: false and load a DERP map of your own through derp.paths, whose region points at that front.

The runtime hosts also pull the gateway image, tailscale/tailscale at the tag the release pins. With an image registry prefix set on Administration → System, it is pulled from your mirror like Fadebox's other system images: <prefix>/tailscale/tailscale:<tag>.

The settings​

Native access is configured under Administration → System, on the Native access through a tailnet card, by a global admin. The settings are installation-wide, and their presence is what switches native access on.

  • Provider — Tailscale or Headscale. Saving under another provider replaces the stored settings, credential included.
  • The provider's fields. Tailscale: Tailnet (the tailnet ID from the admin console's General settings, or -, the default, for the tailnet the OAuth client belongs to), OAuth client ID and OAuth client secret (tskey-client-…). Headscale: Control URL and API key.
  • Tag — what every gateway node is tagged with. Empty means tag:fadebox-instance; otherwise tag:, a letter, then letters, digits and dashes.
  • Address block — the IPv4 block each instance's /24 is carved from. Empty means 10.224.0.0/12. Read the address block before the first instance deploys.

Secrets are write-only. The OAuth client secret and the API key are stored encrypted and never shown again. Once one is stored, the form shows Set — stored encrypted, never shown and a Replace button; left alone, the stored value is kept on Save and used by Verify — as long as the provider's other fields are unchanged. Change the control URL, the tailnet or the OAuth client ID, and every stored secret has to be entered again or cleared: Save and Verify refuse, naming each one left out, and nothing is sent to the server the new values name. A stored credential never goes to a server it was not entered for. Emptying a field and saving clears it, falling back to its default where it has one.

The same rules hold over the REST API (PUT /api/system/tailnet, POST /api/system/tailnet/verify): a provider field left out or sent as null keeps its stored value, one sent blank clears it, and a request that changes a plain field while leaving a stored secret out is refused with 400 tailnet_config_invalid, naming each such secret. GET /api/tailnet-providers lists the fields each provider takes.

Verify​

Verify asks the control server whether the values in the form work, without saving them. It answers that the control server accepted the settings, that it refused them and why, or that it accepted them with something for the tailnet's admin — a list of warnings. Editing the form clears the answer.

Verify saysOutcome
The tag is not declared under tagOwnersRefused: add it to the policy.
Routes in the block are not auto-approved for the tagWarning: add the autoApprovers.routes entry, or every gateway waits for approval.
Headscale runs without a policyWarning: give it one with the entries above.
Headscale older than 0.28.0, or 0.30.0 and laterRefused, naming the version.
The control URL is plain HTTPWarning: the API key crosses the network unencrypted.
The OAuth client lacks auth_keys or devices:coreRefused.
The policy file could not be readWarning: the client lacks policy_file:read, so the block was not checked.
Nodes carrying the tag are not gateways of this installationWarning: another installation may share the tag — give each its own.
Such nodes advertise routes inside the blockWarning: give each installation its own address block.
The tagged nodes could not be listedWarning: whether another installation shares the tag was not checked.
A stored secret was left out while a plain field changedRefused, naming each secret to enter again.

Save and Remove​

Save stores the settings without calling the control server — that is Verify's job. A Save is written to the audit log, naming the secret fields without their values and the plain fields that changed with their new ones. A Verify whose provider or plain fields differ from the stored settings is written too, before anything is called, as system.tailnet_settings_verified: the differing fields by name, and whether the Verify was refused before it reached the control server. Every Save and every Remove sets each gateway's state back to checking and has Fadebox read the gateways' nodes again at once, since a node read under the old settings says nothing about the new ones.

Changing the control server — another Headscale control URL, another Tailscale tailnet, or the other provider — leaves running gateways on the old one, since Fadebox never acts on a node by an ID another server issued. Those instances read offline until a redeploy joins their gateways to the new server; their old nodes expire with the old server's ephemeral cleanup. Switching back before then brings them back without a redeploy. Rotating a credential alone changes nothing.

Remove switches native access off and deletes the provider, its credential and the tag. Turn native access off on the environments and redeploy or stop their instances first: without settings, a deploy of an environment that still has it on fails, and Fadebox can no longer delete a gateway's node. Instances keep their subnets and their services' addresses through a Remove, and while any instance still holds a subnet, the removed block stays in force — configuring native access again is held to it.

The address block​

  • The default is 10.224.0.0/12, a range Docker's default pools and most office networks leave alone. A block must lie inside the RFC 1918 private ranges — 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16 — which also keeps it clear of 100.64.0.0/10, where the tailnet numbers its own nodes. It is a network address with a prefix no longer than /24, since each instance gets a /24 of it: a /12 holds 4096 instance subnets.
  • Pick one no developer's laptop can already see. A route to a subnet the laptop reaches some other way — an office network, a home network, another VPN — loses. On the runtime hosts, Docker refuses a subnet that overlaps a network already there, and the deploy fails saying so.
  • An instance takes a subnet on its first deploy with native access on, and keeps it across stops, redeploys and resets — and when native access is turned off again — until the instance is deleted. An instance of an environment without native access takes none and keeps Docker's subnets. When every subnet is held, the deploy that would take one is refused with 409 address_block_exhausted.
  • It cannot move while instances hold subnets. A save that would move one is refused with 409 address_block_in_use, because the instances' networks and their services' addresses come from the block. Deleting an instance frees its subnet; a stop or a redeploy does not.
  • It can grow from the same start address. With subnets held, a save may keep the start address with a shorter prefix that still holds every subnet in use — 10.224.0.0/12 to 10.224.0.0/11, say. Nothing moves, and that is how a full block grows.
  • A deploy that finds an instance's subnet outside the block in force fails with 409 address_block_moved, naming the block. A save cannot cause that — it is refused first — so it means the block was changed behind Fadebox's back. Restore the block, or delete the instance.
tip

Settle the block before the first instance deploys. Changing it later means deleting every instance that holds a subnet.

Turning it on for an environment​

Native access is switched on per environment by an editor of the project (a maintainer, if the environment is protected): the Native access checkbox in the environment's edit drawer, or in the composer's Launch card for a new environment. Until the settings exist, the checkbox is disabled with a hint that an admin configures native access on the System page, and the API refuses turning it on with 409 tailnet_not_configured. Turning it off always works.

A running instance picks the change up on its next deploy. Turned on, the deploy adds the gateway — and the first time, also takes the instance's subnet and recreates its services on their fixed addresses. Turned off, it deletes the node and removes the gateway; the instance keeps its subnet and addresses. Cloning an environment copies the switch.

In the REST API the switch is the environment's nativeAccess field, on create and on update; left out of an update, it stays as it is. An update replaces name, description, ttlSeconds, blueprint and sharedInstances with what it is sent, so send those back as they are — here with an API key whose account is an editor of the project:

HOST=https://fadebox.example.com
AUTH="Authorization: Bearer $FADEBOX_API_KEY"
P=shop; E=staging

curl -sf -H "$AUTH" "$HOST/api/projects/$P/environments/$E" \
| jq '{name, description, ttlSeconds, blueprint, sharedInstances, nativeAccess: true}' \
| curl -sf -X PUT -H "$AUTH" -H 'Content-Type: application/json' -d @- \
"$HOST/api/projects/$P/environments/$E"

Reserved service names​

fadebox- is a prefix Fadebox keeps for the containers it adds to an instance itself, the gateway among them. These are refused with 400 service_name_reserved:

  • a template whose service key starts with fadebox-, in any case — at save, catalog imports and updates included;
  • an environment service named with that prefix, or named fadebox exactly;
  • an environment service whose name ends in fadebox-tailnet or fadebox-agent, since its container would be named like another instance's gateway or agent;
  • at deploy, any container of the stack whose name starts with fadebox-, or whose full name ends in -fadebox-tailnet or -fadebox-agent — a service tailnet in an instance whose namespace ends in -fadebox, say. Rename the service, or the environment or instance.

Upgrading​

Nothing changes until an admin saves the settings: native access is off, and every instance keeps Docker's subnets. One thing can bite on the first deploy after an upgrade — a template or an environment service stored earlier under a now-reserved name. A template keeps deploying and is refused at its next save; an environment service named fadebox, or whose containers run under the fadebox- prefix, is refused at its next deploy and has to be composed again under another name. The server logs a warning at every startup naming each such template and environment service, so you can rename them ahead of time.

When a gateway does not come up​

  • Waiting for route approval. The gateway is on the tailnet, but the control server has not approved the subnet it advertises, so no laptop routes to it. The policy lacks an autoApprovers.routes entry covering the block for the tag, or a Headscale runs without a policy. Fix the policy rather than approving the route by hand: a hand-approved route belongs to one node, and the next deploy brings a new one.
  • The deploy failed. A control server that refuses — an expired API key, a rejected OAuth client, a tag the client may not mint for — fails the deploy with The tailnet control server refused the gateway's node: and its reason. An instance with no subnet for its gateway fails the deploy saying what is missing: native access on for the environment, the tailnet settings, or both.
  • The gateway's container. The instance's status reports it under access.gateway, and its log is read like a service's, under the key fadebox-tailnet — by a maintainer of the project or a global admin. The log export carries it under the same key beside the services. Anyone else gets 403 for the gateway's log — its tail, the search and the assistant's log tool alike — and a viewer's export of every log leaves the gateway out.

For developers​

Join the tailnet​

Install the stock Tailscale client and join the tailnet your Fadebox administrator uses. On a tailnet run by Tailscale, sign in as usual. On a Headscale, point the client at it:

tailscale up --login-server=https://headscale.example.com --accept-routes

Your user has to be in the group the tailnet's policy grants the address block; ask the tailnet's administrator if you are not sure.

On Linux, accept subnet routes once:

tailscale set --accept-routes

macOS and Windows accept subnet routes by default. No Fadebox software is involved on the laptop.

Your instance's addresses​

Each instance has a /24 of the address block, and each service of the environment a fixed address in it. On the instance's first deploy the services take .2, .3, … in the sorted order of their names; after that each keeps its address. A redeploy, a reset, a stop and a start, or a service added to the environment moves none of them — a new service takes the lowest free address. The subnet stays with the instance until it is deleted. So for pr-42 of shop/staging, on 10.224.0.0/24:

ServiceAddress
keycloak10.224.0.2
postgres10.224.0.3

The other containers of an instance — init containers, sidecars, the gateway — take addresses from .128 up, which may change; nothing on the tailnet connects to them. The addresses do not move, so save them in your tools' connections. The tailnet names nodes, not services: the node shop-staging-pr-42 is the gateway, not the services behind it, and names for the services are not part of this version.

The instance's status, GET /api/projects/{project}/environments/{environment}/instances/{instance}/status, carries the addresses under access.services, keyed by service — .access.services.postgres.address — and the gateway's state as access.state: CONNECTED, WAITING_FOR_ROUTE_APPROVAL, OFFLINE or NOT_CONFIGURED, and null while it is checking. access.services lists a service's other containers too, with a null address, because the instance agent tunnels to them; the drawer and fadebox instance connect leave them out.

In the instance drawer​

An instance that runs a gateway has a Connect section in its drawer:

  • The gateway's state as a badge — connected, waiting for route approval, offline or not configured (see When it does not connect); hovering it says what the state means. Right after a deploy it reads checking until Fadebox has read the gateway's node, which takes up to one health sweep — five minutes by default. A change to the tailnet settings also sets it back to checking, and Fadebox reads every gateway's node again at once. When the last read failed, the badge is dimmed and the time of the reading is shown beside it.
  • The node's name, as Tailnet node: …, when it is not the instance's namespace — a namespace longer than 63 bytes is cut short and ends in a hash.
  • A row per service: its fixed address, the ports its container exposes (5432/tcp), and a copy button that copies address:port for the first port, or the address alone.
  • Who may connect: the tailnet's policy line, the same one the CLI prints.
  • The Linux hint: accept subnet routes once, with tailscale set --accept-routes.

An instance without a gateway — not deployed, stopped, deployed before native access was turned on, or of an environment with it off — has none of this. While it runs an instance agent, its Connect section shows the agent instead.

fadebox instance connect​

fadebox instance connect pr-42

prints the gateway's state, a row per service, who may connect, what the Tailscale client on your machine says, and the instance agent's state:

Native access to instance 'pr-42': connected — the gateway routes the instance's subnet.

SERVICE ADDRESS PORTS
keycloak 10.224.0.2 8080/tcp
postgres 10.224.0.3 5432/tcp

Anyone the tailnet's policy grants 10.224.0.0/12 can route to this instance; its members and gate do not apply on the tailnet.

This machine: on a tailnet, but not accepting subnet routes — run `tailscale set --accept-routes`.

Agent: connected.
  • The state is one of those the drawer shows, each with what it means for you. Right after a deploy it is checking — the gateway's node has not been read since the instance was deployed; ask again in a few minutes. After a change to the tailnet settings it is checking until the read that change starts has answered. When the last read failed, a line under it says so and how old the state shown is.
  • The node's name gets a line of its own when it is not the instance's namespace: a hostname is one DNS label of at most 63 bytes, so a longer namespace is cut short and ends in a hash.
  • This machine's line comes from the tailscale on your PATH, asked tailscale status --json and, when it is running, tailscale debug prefs — reads only; nothing is changed. It says whether the client is on a tailnet and accepts subnet routes, that whether it accepts them could not be read, that it runs in userspace-networking mode — without the network device that routes for other programs, so your tools reach nothing through it — that Tailscale is not running, or that the client did not answer. Without a tailscale on the PATH there is no such line, and --no-local leaves it out.
  • The last line is the instance agent's: Agent: connected, offline, or disabled on this installation (see the badges). It is absent when the status names no agent.
  • -o json prints the status's access as the server sends it. An instance without a gateway has one too while it runs an agent, carrying the agent and the services it tunnels to; access is null only when there is neither a gateway nor an agent to report. A script that asks whether the instance has native access over the tailnet tests .gateway, not access itself.
  • An instance without a gateway gets one sentence saying why, as far as its status can tell, then the agent's line.

Connecting​

Every service answers on its own port, the one it listens on inside the instance:

psql "postgresql://app@10.224.0.3:5432/app"

In DBeaver or any JDBC tool, the host is 10.224.0.3 and the port 5432 (jdbc:postgresql://10.224.0.3:5432/app). Two instances are two subnets, so you can be connected to pr-42 and pr-43 at once, whichever runtimes they run on.

Keycloak answers at http://10.224.0.2:8080, so its admin CLI and its endpoints are reachable from a script on your laptop:

kcadm.sh config credentials --server http://10.224.0.2:8080 --realm master --user admin
note

A service that builds absolute URLs from a configured hostname — Keycloak with KC_HOSTNAME set to its ingress URL, for one — sends a browser's redirects and its token issuer there, not to the address you used. A service that announces its own address to clients — Kafka's advertised.listeners, a Redis Cluster's announce address — has to announce the fixed one: a template puts it there with {{service.<name>.address}}, and {{instance.subnet}} gives the instance's subnet (see values only the instance knows). Both fail the deploy of an instance that has no subnet, and the error says what is missing.

What the service sees​

The gateway does not forward packets; it relays each connection, opening its own to the service. So the service sees the gateway's address as the client — the same address for every developer, one from the instance's upper half. TCP, UDP and ICMP cross, which covers every developer tool.

  • A pg_hba.conf rule per developer, Redis protected-mode with bind rules, or a rate limit by source address treats every developer as the same client.
  • A rule that allows by source address should allow the instance's subnet — a template writes it with {{instance.subnet}} — never a single address.
  • The service's logs name the gateway's address, not yours.

When it does not connect​

  • No Connect section, or one sentence from the CLI. The instance runs no gateway: it is stopped (a stop deletes the node) or not deployed, its environment has native access off, or it was deployed before native access was turned on. Deploy the instance with native access on.
  • Offline. The gateway is not on the tailnet: a deploy is replacing the node, or the gateway's container is down or cannot reach the control server. Deploy the instance; nothing reaches it until its gateway is back.
  • Waiting for route approval. The gateway is on the tailnet, but the subnet it advertises has not been approved. Nothing to fix on your laptop — tell the tailnet's administrator.
  • Not configured. The installation's tailnet settings were removed while the instance's gateway still runs, so Fadebox can no longer say whether the route works. Tell your Fadebox administrator: the settings are under Administration → System. Until they are back, a deploy of the instance fails while its environment has native access on; once they are, the state reads checking until Fadebox has read the node under them.
  • Linux without --accept-routes. The route exists but your laptop does not use it; run tailscale set --accept-routes. fadebox instance connect says so on its last line.
  • Another VPN on the laptop. A full-tunnel VPN that claims 10.0.0.0/8 wins the route to the default block, and the instances are unreachable while it is up. Disconnect it, or have it leave the address block out.

Without a tailnet: the instance agent​

The tailnet is one way in. The other needs no tailnet and no Tailscale client: the instance agent, a small container Fadebox adds to every instance, and fadebox connect, which tunnels the instance's services to localhost on your machine through Fadebox. Fadebox decides who may open each connection by its own roles and the instance's gate, not by a tailnet policy, and records every one in the audit log.

Tailnet gatewayInstance agent
On your laptopthe Tailscale clientthe fadebox CLI
Switched onper environmentfor the whole installation, on by default
Where a service answersits fixed subnet address, laptop-widelocalhost:<port>, while connect runs
Who gets inwhoever the tailnet's policy grants the address blockper connection, by the project role and the instance's gate
Audited in Fadeboxnoevery connection opened or refused
ProtocolsTCP, UDP, ICMPTCP

An instance can have both. fadebox connect then uses the tailnet when your machine is on it and the agent otherwise.

How the agent works​

Every deploy adds one container, <namespace>-fadebox-agent, on the instance's own network. It publishes nothing and needs no capability, Docker socket or volume. It dials Fadebox back and keeps that connection open. When you run fadebox connect, each TCP connection your tool makes becomes a request to Fadebox. Fadebox checks it and asks the agent to dial the service on the instance network, then joins the two connections.

Fadebox asks only for one of the instance's own services, on a port it exposes, and the agent checks again before it dials. It looks the service's name up without the host's DNS search domains, and dials only an address on the instance's own networks — never a loopback or link-local one. A name the instance does not run never reaches another host, even where Docker's DNS would have forwarded it upstream.

Setting it up​

The Instance agent card under Administration → System shows whether the agents run and, while they are switched on but do not, why not; it also shows the URL they dial, the architectures this build carries agents for, and the image a deploy runs. It only reads: the agent is configured with the variables below.

  • It is on by default. From the first deploy after the upgrade, every instance gets its agent. To switch it off, set FADEBOX_INSTANCE_AGENT_ENABLED=false on the app container; the next deploy of each instance then removes its agent.
  • The image comes with Fadebox, from no registry. The Fadebox image carries the agent's static Linux binaries for amd64 and arm64, under /deployments/agent/. Before the first deploy that runs an agent on a Docker host, Fadebox builds the image fadebox-agent:<its version>-<stamp> from the binary for that host's architecture and loads it there; the stamp names the binaries, CA certificates and notices inside, so two installations sharing a host share the image only when they carry the same content. Later deploys find it and load nothing. So an installation without registry access runs agents too, and an agent is always the version of the server it dials. An image reclaim may remove an unused agent image, including one an older version left behind, which the image list shows as unused rather than foreign; the next deploy loads it again.
  • The agent trusts what Fadebox trusts. The image carries the Fadebox container's own CA certificates, so a private CA you added to the Fadebox image for an https public URL is trusted by the agents as well. FADEBOX_AGENT_CA_BUNDLE names another bundle.
  • amd64 and arm64 hosts only. A deploy onto a Docker host of another architecture fails with agent_architecture_unsupported, and the instance ends in ERROR with the message. Switch the agent off, or run an agent image of your own built for that host.
  • An image of your own. FADEBOX_INSTANCE_AGENT_IMAGE names an image to run instead of the one Fadebox builds, pulled like any other image, for a custom build of the agent.
  • A Fadebox built from source carries no agent binaries. It runs no agents unless FADEBOX_INSTANCE_AGENT_IMAGE names one, and says so once at startup.
  • FADEBOX_PUBLIC_URL must be set, and reachable from every runtime host. It is the address the agent dials, so a remote runtime's host and an elastic runtime's machines need a route to it. While it is unset, no deploy adds an agent, and Fadebox warns once at startup. A value that is not an absolute http(s) URL with a host, or that carries user info (user@), a query or a fragment, refuses every deploy with 409 agent_no_public_url. A host may contain underscores, as a container name on a shared Docker network does (http://fadebox_app:8080); a port, when given, is one from 1 to 65535. A loopback host — localhost, 127.0.0.1, ::1, 0.0.0.0 — is not refused but warned of once at startup: inside an instance's container it is that container, so no agent would ever reach Fadebox.
  • Your reverse proxy must pass WebSocket upgrades on /api/agent/control, /api/agent/streams/ and the instances' …/connect/ paths under /api/projects/, and keep an idle WebSocket open for at least a minute. Fadebox pings every one of them every 30 seconds — the agent's control connection and both sides of each open fadebox connect connection — so an idle database session stays open through a proxy that closes idle connections after 60 seconds or more. Traefik and Caddy pass upgrades as they are, and so does HAProxy in mode http, whose timeout tunnel has to stay above 30 seconds. Nginx needs the upgrade headers forwarded; its default proxy_read_timeout of 60 seconds is enough, and one you set has to stay above 30 seconds.
  • Limits. One user holds at most 32 open connections, one instance at most 256, and the agent has 10 seconds to open each. The configuration reference has the variables. Each agent learns the ping interval, the cap on its connections and the time it has to open one from Fadebox when it is deployed, so a change to them reaches an instance's agent at its next deploy.

The agent is not one of the instance's services. A stopped or missing agent never makes the instance unhealthy, and the instance's status does not list it among the containers. Its log is read under the key fadebox-agent, by the project's maintainers and global admins only, like the gateway's. Every deploy gives the agent a fresh token and cuts the previous one off once the old agent's container is gone, so a deploy that fails before then leaves the running agent connected; a stop, or a deploy that runs no agent, revokes it. The token is never shown. An environment service whose name ends in fadebox-agent is refused, as the gateway's suffix is.

Who may connect​

The tunnel follows the instance's access level, with one floor: nobody gets in without a role in the project.

InstanceWho may connect
Gate open or membersthe project's viewers and up
Gate creatorthe instance's creator, the project's editors and up
Personal, any gatethe instance's creator, the project's editors and up
Anya global admin

So an open instance is not open to the internet through the tunnel. Its URLs may be public, but its database is not. Fadebox decides as each connection opens: removing someone from the project, making the instance personal or raising its gate refuses their next connection, while one already open runs until it closes.

Each connection writes instance.connect_opened to the audit log, with the user, the service, the port and the stream. Each refusal writes instance.connect_refused with the reason: not_member, personal, gate, unknown_target, agent_offline, agent_refused and the others below. If Fadebox cannot write the instance.connect_opened event, it does not open the connection.

Connecting with fadebox connect​

connect is a command of the fadebox CLI, which your Fadebox server hands out for Linux, macOS and Windows, on amd64 and arm64. Once you are logged in there is nothing more to set up. Then, for instance pr-42 of environment staging in project shop:

fadebox connect pr-42 -p shop -e staging
keycloak 8080 → localhost:8080
postgres 5432 → localhost:5432
Press Ctrl-C to stop

And in another terminal, your own tools on localhost:

psql "postgresql://app@localhost:5432/app"

In DBeaver the host is localhost and the port 5432. connect runs in the foreground until Ctrl-C. Each connection your tool opens is one connection through the agent: a psql session is one, a browser tab a handful. When your tool stops sending but keeps reading (nc -q, an HTTP/1.0 client), connect passes that on, and the agent closes its side of the service's connection for writing, so a service that answers and then closes ends the connection at once. One that keeps it open gets up to 30 seconds after its last byte, so the reply arrives whole; then the connection closes. If the service closes first, everything it sent reaches your tool before the connection ends.

  • Which ports. With no service named, connect opens every TCP port the instance's services expose: what each image declares with EXPOSE, and the template's ports: and expose: entries — a sidecar's included, which has no address on the tailnet. A port only a tunnel needs belongs under expose:, which publishes no host port; a ports: entry does, and a published port cannot share a runtime with a sign-in-gated instance. Name services to narrow it: fadebox connect pr-42 postgres, or a single port, postgres:5432.
  • Stable local ports. Each service port listens on localhost — 127.0.0.1, and ::1 where your machine has an IPv6 loopback — at its own number when that number is free on both. When it is taken, by a PostgreSQL on your laptop or by pr-43's postgres, connect picks a free port. It remembers that port for the server, project, environment, instance, service and port in ports.yaml beside the context file, so the next connect gets the same one and a saved DBeaver connection keeps working. A remembered port that is busy for one run stays remembered for the next. Two connects saving ports.yaml at the same moment keep the last one's ports, and the other's are picked again next time.
  • --port sets the local port yourself: --port postgres:5432=15432 for one port of a service, or --port postgres=15432 for a service with a single port (or a single one left once its service:port overrides are taken). Repeat it for more. Your ports are bound before any port connect picks, so a picked port never takes one you asked for. A service=N for a service with several ports, or one local port given twice, is a usage error (exit 2).
  • Tailnet mode or tunnel mode. When the instance's gateway is connected, every service you named has a fixed address (with none named, the services that have one; a sidecar has none and only the tunnel reaches it), and your Tailscale client routes for other programs (not in userspace-networking mode), accepts subnet routes and sees the gateway — an online peer named as the gateway's node that routes the instance's own /24 — connect prints the services' fixed addresses and exits, because your tools reach those directly. A router of a wider block, such as an office network's, or a node of the same name without that route is not the gateway. A client on another tailnet does not count: connect prints Local Tailscale is on another tailnet; tunnelling through the agent. on stderr and tunnels. --tunnel goes through the agent anyway, and so does any connect given a --port.
  • -o json prints the mapping once, as {instance, mode, mappings}, with mode set to tunnel or tailnet and mappings keyed by service, then by port: .mappings.postgres."5432" is where postgres's port 5432 answers, localhost:15432 or 10.224.0.3:5432.

Agent badges​

The instance drawer's Connect section shows the agent beside the tailnet's state, and fadebox instance connect prints it as its last line:

  • Agent: connected. The agent is connected to Fadebox, and fadebox connect works. On an instance with no gateway, the section lists each service with the ports the agent tunnels to, and the command to run, naming the project and the environment so it does not depend on your CLI's context.
  • Agent: offline. The agent has not reached Fadebox. A deploy may be replacing it, its container may be down or unable to reach the server's public URL, or the instance was deployed before the agent was turned on. A running agent keeps retrying on its own, and a deploy starts a new one.
  • No agent badge. The installation runs no agents: they are switched off, the build carries no agent binary and no image is named, or FADEBOX_PUBLIC_URL is unset. A stopped or undeployed instance has no Connect section at all.

When fadebox connect does not connect​

When a refusal might not apply to the next connection, connect keeps listening and prints the reason once. When retrying cannot help, it exits.

  • The agent is not connected (agent_offline). The agent is down, or has not reached Fadebox since the last deploy. Wait a moment, or deploy the instance. If it stays offline, ask your administrator whether the runtime hosts can reach FADEBOX_PUBLIC_URL. The agent's log (the key fadebox-agent, which a maintainer of the project or an admin can read) starts with the server it dials — msg="agent starting" … server=https://fadebox.example.com — so a wrong FADEBOX_PUBLIC_URL shows there, followed by control connection failed lines saying why.
  • Runs no agent: … redeploy it to connect. The instance was deployed while the agents were switched off, so it has none until its next deploy. Redeploy it.
  • This installation runs no instance agents. The agents are switched off, the build has no agent binary and no image is named, or FADEBOX_PUBLIC_URL is unset. connect says so before it opens a port, and exits — also when the agents are switched off while it listens. An instance that is stopped or runs nothing is refused the same way, before a port opens.
  • The service is not running (unknown_target) for a service connect listed when it started: a deploy is replacing its container. That connection is refused, and connect keeps listening; the next one gets through once the container is back.
  • The instance was stopped or deleted while connect listened. On that same refusal connect reads the instance's status, and when the instance is gone, stopped or stopping, or failed with nothing left running, it says so (Instance 'pr-42' was stopped, was deleted, failed and runs no services), closes its ports and the connections still open, and exits 1. Run it again once the instance is deployed.
  • The agent could not reach the service (agent_refused, with the reason). The service's container is down or not listening on that port. Check the instance's status and the service's log.
  • The agent did not open the stream within 10 seconds (agent_timeout). The agent is connected but slow to dial back. Try again. If it keeps happening, the proxy may be dropping WebSocket upgrades on /api/agent/streams/.
  • Too many open connections (429 connect_limited). You hold 32 already, or the instance carries 256. connect retries with a growing pause. Close a client you no longer need.
  • Not recorded (connect_unaudited). Fadebox opens no connection it cannot audit. Try again shortly.
  • Refused (403). The instance's gate, or its being personal, does not admit you, or you have no role in the project. Ask the instance's creator or a maintainer of the project. connect exits. A 401 means your key is no longer accepted. Log in again.
  • No such service or port (404 unknown_target) for a service:port the instance never listed: the service does not run there, or does not expose that TCP port. connect exits.