Run and test
Tunnels and security
Keep internal services off the internet and reach them as localhost through SSH.
Reviewed 2026-10-04
Chalupa reduces the public attack surface in two ways: it rewrites Compose ports to listen on loopback, and it uses an outbound telemetry flow. Only SSH accepts inbound traffic on the server. An opt-in share adds an outbound agent that publishes one service at a public hostname; nothing is shared unless you declare and create it.
How ports are rewritten
This Compose mapping:
services:
api:
ports:
- "8080:8080"
becomes this mapping on the server:
services:
api:
ports:
- "127.0.0.1:8080:8080"
The process does not invent a second port list. chalupa tunnel consumes
outputs derived from the same Compose file.
Open the tunnel
Keep the absolute config path:
export DEMO_CONFIG="/path/to/project/chalupa.yml"
chalupa tunnel --config "$DEMO_CONFIG"
Conceptually, SSH creates one rule per port:
ssh -N -L 8080:localhost:8080 root@192.0.2.44
192.0.2.44 belongs to an RFC 5737 documentation range. The task reads the
real address from Pulumi output; do not copy it into public documentation.
For an interactive shell on the server, use the same boundary instead of a hand-written SSH command:
chalupa ssh --config "$DEMO_CONFIG"
It resolves the address from the same Pulumi output, validates an optional dedicated identity, and never forwards your SSH agent. Chalupa deliberately publishes no per-environment hostname: real addresses stay out of control-plane metadata, and the control plane cannot proxy an SSH session.
While SSH remains active:
http://localhost:8080reaches the remote API;- a local client can use
localhostfor the database; - suites do not need to know the server IP.
Supervised tunnels
chalupa tunnel with no verb is one-shot: it opens every forward and ends with
your terminal. For long sessions, such as overnight replays or headed browser
runs, a supervised tunnel keeps the forwards alive instead. It opens one SSH
transport per port, and when the set drops (laptop sleep, a network change) it
reconnects every port together after a 1, 2, 5 and 10 second backoff and prints
a one-line notice. A tunnel that never stays up for a minute within 15 minutes
is reported honestly, with "is the server still up?", instead of retrying
forever.
| Command | Effect |
|---|---|
chalupa tunnel keep |
Starts the supervisor in the background and leaves it running when the command exits. |
chalupa tunnel stop |
Stops it. |
chalupa tunnel status |
Reports it: supervised, stale or none, with the pid, ports and uptime. |
chalupa tunnel watch |
Runs the supervisor in the foreground. |
None of them needs a terminal. keep, stop and status take --json for a
script. chalupa status shows the same state, and chalupa exec and
chalupa launch reuse a live supervised tunnel instead of refusing the busy
ports. The state file is ~/.config/chalupa/tunnels/<environment>.json. It is
local to your machine, so the hosted console never sees it. The operator UI
shows the same state on its Home screen.
Lazy transports
The server counts any open SSH connection as activity, so a supervised tunnel
that is merely left running keeps the box from ever going idle. With
tunnel.local: leased the supervisor is lazy by default: it listens on each
leased port itself and opens that service's SSH connection when something first
connects, then closes it tunnel.idleGraceMinutes (default 5) after the last
connection ends. A tunnel at rest holds no connection to the server, and
chalupa urls shows each service as idle until it is used. The first
connection after a quiet period pays about a second for the SSH handshake.
tunnel.transport: eager keeps the one-SSH-per-port behaviour. The first
readiness probe of chalupa wait opens the transports it needs.
Extra ports
Services started outside Compose, such as an app overlay started over SSH or a
host service, are invisible to port inference. List their loopback ports in
tunnel.extraPorts and they ride the same supervised forwards
(127.0.0.1 on both ends; nothing opens on the server):
tunnel:
extraPorts: [8080, 9005, 9012]
One to 16 distinct ports. ssh.localPorts can remap them, a
readiness probe addresses one as
remotePort, and a port Compose already publishes is not forwarded twice.
Ports reach the tunnel through the stack's recorded outputs, so adding
extraPorts to a running environment takes effect on the next chalupa up.
Managed GPU hosts forward only the model endpoint and refuse the block.
When a tunnel should not stay open
The supervised tunnel binds every forwarded Compose port on your machine (27017, 5432 and so on). A caller that opens its own forward to one of those ports collides with it. Two settings decide when the supervisor is left running:
onReuse(keepornone, defaultkeep) is for a warmchalupa upthat reuses a live server (session.reuse).keepre-opens the tunnel so the next flow finds its ports;noneleaves it closed, andchalupa tunnel keepopens it when you want one.afterUp(keepornone, defaultkeep) is the same choice for the end of a coldchalupa up --wait. The readiness probes run from your machine, so the wait opens the tunnel and, by default, leaves it running for the next command. Withnoneit closes the tunnel as soon as readiness is decided, passed or not.
tunnel:
onReuse: none
afterUp: none
The two are independent. afterUp does not affect up without --wait (it
opens no tunnel) or a reuse, and onReuse needs no extraPorts. The field
reference is tunnel.
What is exposed
The expected result is:
| Surface | Exposure |
|---|---|
| SSH | public server interface |
| Service ports | remote 127.0.0.1 |
| Heartbeats | outbound HTTPS from the agent |
| Shares (opt-in, off by default) | an outbound agent on the server dials the share provider; no inbound port opens. The service is reachable at https://<service>--<environment>.box.chalupa.run, behind console sign-in by default. auth: none makes it public to anyone with the link and needs the exact share <environment> <service> publicly phrase. A share ends at its expiry, when you revoke it, and when the server is sunk or reaped. See Shares. |
| Local Pulumi state | remains on the operator's machine |
Review the preview to verify every bind. A provider firewall adds another layer of defense; it does not replace this review.
SSH keys
Use managed keys and limit who can start tunnels. Do not permanently disable host verification. The first connection can explicitly accept a new key; investigate any later change.
A dedicated Chalupa identity avoids reusing a key from another provider or purpose. Add its public half to the DigitalOcean account and keep the private half only on the operator machine. Then select it explicitly:
export SSH_IDENTITY="$HOME/.ssh/id_ed25519_chalupa"
chalupa tunnel --config "$DEMO_CONFIG"
SSH_IDENTITY is a Taskfile input read from the environment, so the CLI
forwards it without needing a flag. It must expand to an absolute path.
Chalupa accepts only an
operator-owned, unencrypted regular private-key file recognized by the system
OpenSSH implementation with mode 0400 or 0600, rejects symlinks, and
invokes OpenSSH with IdentitiesOnly=yes. Supply the same input to up,
agent-bootstrap, and down so bootstrap and final telemetry/log flushes use
the same identity. Leaving it unset preserves normal OpenSSH agent and
default-key behavior.
Deleting a public key from the DigitalOcean account does not remove it from
an already-created droplet's authorized_keys. For ephemeral environments,
rotate by registering the replacement public key, recreating compute with the
replacement selected in ssh.keys, verifying access, and only then removing
the old account key. Never store a private key in DigitalOcean, Chalupa
configuration, tvault, Vercel, or Git.
The remote account and keys are outside the Chalupa console's scope. The console shows commands, but it does not store private keys.
Secrets
Treat these values as secrets:
- provider API token;
- ingest HMAC key;
- Chalupa administrator token;
- service-specific secrets.
They must not appear in:
chalupa.yml;- URLs or query parameters;
- suite-run payloads;
- logs or screenshots;
- public metadata;
- commits.
The agent receives only the credential it needs to sign ingest requests. The control plane uses a separate read-only credential for cost estimates. The current version does not give the server a deletion credential.
Signed signals
The agent signs the exact body before sending it. The control plane verifies the signature and time window before it persists the observation.
HTTPS protects the transport; HMAC authenticates the sender and detects changes. Rotate the key like any other fleet secret. The read API never returns it.
The console is not an execution panel
The console UI does not include buttons to launch or sink resources. Operator
actions remain in the terminal, behind the chalupa CLI and the Taskfile it
delegates to, where the context and typed confirmation are visible. A separate
trusted worker may enforce an expired idleShutdownMinutes policy with its
dedicated write credential; it rechecks the provider identity and Chalupa tag,
never runs in the browser, and cannot target the protected data stack.