Front door & TLS
How a browser reaches the console and a workspace: one entry point, one door behind it, and the TLS you put in front.
Pick a request
- TLS edgetunnel or your proxy · 443tls ends
Your tunnel or TLS proxy ends TLS and forwards to 127.0.0.1:5000.
- Console process:5000owner ok
Session found, and this person owns acme/dev01. Forwarded over loopback with this workspace's own credential.
- Front doorCaddy · 127.0.0.1:8080route
The route file for acme/dev01 matches and checks that credential.
- Workspacews-acme-dev01ttyd :7681
The terminal answers.
handle /ws/acme/dev01/* -> ws-acme-dev01:7681 (ttyd)
Pick a request to see which process answers it and which rule refused it. The organisation acme, the workspaces and the addresses are examples; the rules and messages are the product's own.
What it is
The front door is a Caddy container, run by bin/front-door, that routes /ws/<organisation>/<workspace>/ to the right container: the terminal, the browser IDE or a preview of the developer's app. It sits behind the console process, which is the one entry point on port 5000. The console process checks the session and the owner first, then forwards to the door over loopback.
It is not what people type into their browser, and it does not give your users TLS. Kodela serves plain HTTP unless you put a TLS edge (a Cloudflare Tunnel or a reverse proxy you run) in front of port 5000. There is no automatic certificate.
Ports and binds
| Port | Process | Default bind | Change it |
|---|---|---|---|
443 | your TLS edge | yours | not part of Kodela |
5000 | console process | all addresses; the firewall admits loopback and Docker bridges only | firewall unit |
8080 | front door | 127.0.0.1 | --bind ADDR or FRONT_DOOR_BIND; --port moves the host side |
7681, 8443 | terminal, IDE inside each workspace | the workspace network only | not published to the host |
Workspaces are reached by container name on an internal Docker network, so nothing per workspace is published on the host and two organisations can both have a dev01. The door's port stays on loopback on purpose: a published container port is forwarded past the host firewall, so binding is the only thing that keeps the door from being reached around the console.
Door authentication: --auth
| Mode | Default | What guards /ws/* | Limit |
|---|---|---|---|
workspace | yes | one bcrypt verifier per workspace, written into that workspace's route file by workspace-agent create. The door holds no shared credential. | a door with no route files serves nothing, which looks like a broken mount; status tells them apart |
basic | no | one shared user and password for every workspace (--password or FRONT_DOOR_PASSWORD) | the door cannot tell two workspaces apart; anyone with the password reaches all of them |
access | no | each request is checked for a Cloudflare Access token and for ownership of the workspace, by a validator container | partial: verified against a stand-in issuer only. Only for a topology where Access sits directly in front of the door; with the console process in the path it refuses every terminal. up refuses unless the validator is running and answering. |
Leave it on workspace in the standard install. People are identified by the console process before the door sees them; the door's credential is between two of Kodela's own processes.
TLS options
| Option | Covers | How | Limit |
|---|---|---|---|
| TLS edge in front of port 5000 (recommended) | everything: sign-in, console, API, terminals, IDE, previews | a Cloudflare Tunnel or your own reverse proxy, plus an https base URL | you run the certificate; no ACME in Kodela |
Door internal CA: --tls internal | /ws/* on the door only | Caddy signs its own certificate; every client installs its root | a stopgap for having no domain; the console on 5000 stays plain |
Off: --tls off | nothing | the default for a door that has never had TLS | passwords cross the network in the clear |
An https origin needs two halves, and both must be on. The edge encrypts the connection; the console's public-TLS switch marks the session cookie Secure and sends HSTS. provision-host sets the switch from the scheme of --base-url; the committed serving-tier unit has it on. Without it the startup banner says PLAIN HTTP while everything still appears to work.
HSTS has no server-side undo. A browser that has seen it refuses http:// to that host for the whole max-age. Turn the switch on only when the https origin works.
The door's internal CA
$ cd ~/kodela && export AGENT_STACK_SHARED="$PWD/.agent-stack-data"
$ bin/front-door up --port 8080 --tls internal --tls-host kodela.example.test
$ bin/front-door ca --out kodela-door-root.crt
$ bin/front-door statusInstall the printed root in every client's trust store, including the browser's: clicking through a warning does not cover the websocket the IDE needs. --tls-host is repeatable; the first becomes the default for clients that connect by IP address, which send no server name. The CA lives in a volume that survives down and up; removing it revokes every installed root. A door that already has TLS keeps it across up unless you say --tls off.
Cloudflare Tunnel and Access
A named tunnel is the recommended shape. The connector runs as a container on the door's network; the tunnel's public hostname, and any Cloudflare Access policy in front of it, live in your Cloudflare account.
$ read -rs FRONT_DOOR_TUNNEL_TOKEN && export FRONT_DOOR_TUNNEL_TOKEN
$ bin/front-door tunnel-up
# in Cloudflare: point kodela.example.com at http://203.0.113.10:5000
$ bin/front-door tunnel-downtunnel-up --quick asks Cloudflare for a random, temporary hostname with no account (tunnel-url prints it), but it originates at the door, so it reaches /ws/* and not the console. Use it to test a workspace route, not to run a team.
The connector image is not pinned. The door's Caddy image is pinned by digest; cloudflared is not yet.
How workspaces and previews are routed
| Path | Goes to | Notes |
|---|---|---|
/admin/, /portal/, /api/ | the console process | sessions, ownership and roles are decided here |
/ws/acme/dev01/ | ws-acme-dev01:7681 | the terminal; owner only, and never an auditor |
/ws/acme/dev01/ide/ | ws-acme-dev01:8443 | only for a workspace created with --ide; the prefix is stripped |
/ws/acme/dev01/preview/<port>/ | ws-acme-dev01:<port> | ports 1024 to 65535, never 7681 or 8443; owner-only unless an admin widens the port |
/ws/<anything else> | the door answers 404 | no such workspace, or it has no route on this host |
workspace-agent create writes one route file per workspace into the state tree and reloads the door. The route file's authentication and its handlers are generated together, so a handled path is never an unauthenticated one.
Commands
bin/front-door up- Create or recreate the door.
--port,--bind,--auth workspace|basic|access,--tls internal|off,--tls-host,--no-publish. Refuses unlessAGENT_STACK_SHAREDnames the state tree. bin/front-door reload- Pick up route files written since it started, without dropping websockets.
bin/front-door status- Ports, bind, TLS mode and who decided it, auth mode, and the routes Caddy has loaded compared with those on disk.
bin/front-door ca- The CA root clients must trust, under
--tls internal. bin/front-door tunnel-up/tunnel-url/tunnel-down- Run, read or remove the Cloudflare connector.
bin/front-door down- Remove the door. Workspaces keep running; their pages stop answering until it is back.
Verify it works
$ bin/front-door status
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/ws/acme/dev01/
$ curl -sI https://kodela.example.com/admin/ | grep -i strict-transport-security$ curl -m 5 -s -o /dev/null -w '%{http_code}\n' http://203.0.113.10:5000/
$ curl -m 5 -s -o /dev/null -w '%{http_code}\n' http://203.0.113.10:8080/Good looks like: status shows auth workspace, bind loopback and every route on disk loaded; the door answers 401 to a request with no credential; the https origin sends HSTS; and from the network both direct addresses print 000, a timeout or a refusal, never a page.
Troubleshooting
Every workspace answers 404
The route is on disk but not loaded, often because a reload could not use sudo. bin/front-door status compares loaded and on-disk routes; bin/front-door reload loads them. A 502 instead means loaded but stale: recreate the door with bin/front-door down, then up.
refusing to guess which routes to serve
Neither AGENT_STACK_SHARED nor WORKSPACE_ROUTES is set in this shell. Export the state tree the tier uses and retry.
--tls internal needs a reachable hostname
Pass --tls-host with a name or address clients use; loopback alone serves nobody.
Certificate name mismatch when opening by IP
Clients that connect by IP send no server name and only get the first --tls-host. Put the address first, or use a DNS name.
the validator is not running - refusing to flip the door onto it
--auth access needs the Access validator container up and answering first. In the standard install, stay on workspace.
The banner says PLAIN HTTP on an https origin
The public-TLS switch is off in the running unit. Re-provision with an https --base-url, or restore the committed unit's value and restart.
The IDE loads but never connects
Under the internal CA the root is not trusted by the browser, or your proxy is not passing websocket upgrades.
The console times out from the office network
By design: the firewall drops port 5000 from the network. Use the tunnel or your TLS proxy.
What it does not do yet
- No ACME. Kodela does not obtain or renew certificates.
- Plain HTTP by default. Until a TLS edge is in front, passwords and session cookies cross the network in the clear.
- Internal CA covers the door only. It encrypts
/ws/*, not the console on port 5000. - Access mode is partial. Verified against a stand-in issuer, and not compatible with the standard topology.
- cloudflared is not pinned. The connector image tag moves.
- Preview, IDE download and terminal copy are detectable, not preventable. Data can leave through a preview route a developer opens.
- The shared front-door password is unattributable. Until “require identity” is turned on, a sign-in with it names nobody.
Related
Verified against the main branch, 28 Sep 2026.