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 stocktailscale/tailscaleimage 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
/24of 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 intopr-42reachespr-42and 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:
| Scope | Why |
|---|---|
auth_keys | Minting each gateway's key. |
devices:core | Finding and deleting gateway nodes; Verify reads the devices. |
policy_file:read | Verify 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": ["*"] } ]
tagOwnersdeclares the tag. Verify fails without it.autoApprovers.routesapproves 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:developersstands for a group of your own — whichever of the tailnet's users may reach instances, defined undergroupsin 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:adminin 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 unlesstagOwnersdeclares 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-serveralone, since Headscale refuses a node that also sends--advertise-tagsbeside 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 userdev. 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; otherwisetag:, a letter, then letters, digits and dashes. - Address block — the IPv4 block each instance's
/24is carved from. Empty means10.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 says | Outcome |
|---|---|
The tag is not declared under tagOwners | Refused: add it to the policy. |
| Routes in the block are not auto-approved for the tag | Warning: add the autoApprovers.routes entry, or every gateway waits for approval. |
| Headscale runs without a policy | Warning: give it one with the entries above. |
| Headscale older than 0.28.0, or 0.30.0 and later | Refused, naming the version. |
| The control URL is plain HTTP | Warning: the API key crosses the network unencrypted. |
The OAuth client lacks auth_keys or devices:core | Refused. |
| The policy file could not be read | Warning: the client lacks policy_file:read, so the block was not checked. |
| Nodes carrying the tag are not gateways of this installation | Warning: another installation may share the tag — give each its own. |
| Such nodes advertise routes inside the block | Warning: give each installation its own address block. |
| The tagged nodes could not be listed | Warning: whether another installation shares the tag was not checked. |
| A stored secret was left out while a plain field changed | Refused, 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/12or192.168.0.0/16— which also keeps it clear of100.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/24of it: a/12holds 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/12to10.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.
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
fadeboxexactly; - an environment service whose name ends in
fadebox-tailnetorfadebox-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-tailnetor-fadebox-agent— a servicetailnetin 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.routesentry 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 keyfadebox-tailnet— by amaintainerof the project or a globaladmin. The log export carries it under the same key beside the services. Anyone else gets403for 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:
| Service | Address |
|---|---|
keycloak | 10.224.0.2 |
postgres | 10.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 copiesaddress:portfor 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
tailscaleon yourPATH, askedtailscale status --jsonand, 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 atailscaleon thePATHthere is no such line, and--no-localleaves 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 jsonprints the status'saccessas 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;accessisnullonly 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, notaccessitself.- 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
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.confrule per developer, Redisprotected-modewith 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; runtailscale set --accept-routes.fadebox instance connectsays so on its last line. - Another VPN on the laptop. A full-tunnel VPN that claims
10.0.0.0/8wins 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 gateway | Instance agent | |
|---|---|---|
| On your laptop | the Tailscale client | the fadebox CLI |
| Switched on | per environment | for the whole installation, on by default |
| Where a service answers | its fixed subnet address, laptop-wide | localhost:<port>, while connect runs |
| Who gets in | whoever the tailnet's policy grants the address block | per connection, by the project role and the instance's gate |
| Audited in Fadebox | no | every connection opened or refused |
| Protocols | TCP, UDP, ICMP | TCP |
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=falseon 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 imagefadebox-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
httpspublic URL is trusted by the agents as well.FADEBOX_AGENT_CA_BUNDLEnames 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 inERRORwith 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_IMAGEnames 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_IMAGEnames one, and says so once at startup. FADEBOX_PUBLIC_URLmust 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 absolutehttp(s)URL with a host, or that carries user info (user@), a query or a fragment, refuses every deploy with409 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 openfadebox connectconnection — 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 inmode http, whosetimeout tunnelhas to stay above 30 seconds. Nginx needs the upgrade headers forwarded; its defaultproxy_read_timeoutof 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.
| Instance | Who may connect |
|---|---|
| Gate open or members | the project's viewers and up |
| Gate creator | the instance's creator, the project's editors and up |
| Personal, any gate | the instance's creator, the project's editors and up |
| Any | a 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,
connectopens every TCP port the instance's services expose: what each image declares withEXPOSE, and the template'sports:andexpose:entries — a sidecar's included, which has no address on the tailnet. A port only a tunnel needs belongs underexpose:, which publishes no host port; aports: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::1where 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 bypr-43'spostgres,connectpicks a free port. It remembers that port for the server, project, environment, instance, service and port inports.yamlbeside the context file, so the nextconnectgets the same one and a saved DBeaver connection keeps working. A remembered port that is busy for one run stays remembered for the next. Twoconnects savingports.yamlat the same moment keep the last one's ports, and the other's are picked again next time. --portsets the local port yourself:--port postgres:5432=15432for one port of a service, or--port postgres=15432for a service with a single port (or a single one left once itsservice:portoverrides are taken). Repeat it for more. Your ports are bound before any portconnectpicks, so a picked port never takes one you asked for. Aservice=Nfor a service with several ports, or one local port given twice, is a usage error (exit2).- 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—connectprints 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:connectprints Local Tailscale is on another tailnet; tunnelling through the agent. on stderr and tunnels.--tunnelgoes through the agent anyway, and so does anyconnectgiven a--port. -o jsonprints the mapping once, as{instance, mode, mappings}, withmodeset totunnelortailnetandmappingskeyed by service, then by port:.mappings.postgres."5432"is wherepostgres's port 5432 answers,localhost:15432or10.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 connectworks. 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_URLis 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 reachFADEBOX_PUBLIC_URL. The agent's log (the keyfadebox-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 wrongFADEBOX_PUBLIC_URLshows there, followed bycontrol connection failedlines 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_URLis unset.connectsays 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 serviceconnectlisted when it started: a deploy is replacing its container. That connection is refused, andconnectkeeps listening; the next one gets through once the container is back. - The instance was stopped or deleted while
connectlistened. On that same refusalconnectreads 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 exits1. 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.connectretries 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.connectexits. A401means your key is no longer accepted. Log in again. - No such service or port (
404 unknown_target) for aservice:portthe instance never listed: the service does not run there, or does not expose that TCP port.connectexits.