First, the constraint
Upsun is a cloud platform. Applications run on shared infrastructure in a public cloud region, which is what makes it affordable. Dedicated and on-premise setups exist, and they solve a different problem, but they carry the price of not sharing anything with anybody. For most teams that price is out of proportion to the actual requirement. The requirement is almost never “run this on hardware nobody else touches”. It’s “make sure no unauthorized request reaches this application”. Those are different problems, and the second one is solved by controlling reachability, not by owning the machine.Family one: a public route with a locked door
The common case keeps the route. A CDN needs an origin to pull from, a partner integration can’t join your VPN, and somebody has to open the admin panel in a browser. The application stays addressable, and every request has to prove something before the router passes it through.Mutual TLS
mTLS turns the usual TLS handshake around. The server proves its identity to the client as always, and the client also presents a certificate that the server validates against a certificate authority you decide on:.upsun/config.yaml
X-Client-Verify and X-Client-Cert headers, so the application can log which client it was talking to or authorize per client on top.
The reason to reach for this over a shared secret is rotation. Plenty of organizations require credentials to be rotated on a schedule, and a shared secret in a header means coordinating both ends every time. With mTLS the rotating part is the client certificate, which the client rotates on its own, while the server side holds a CA certificate that contains a public key and no secret. Root CA certificates are commonly issued with lifetimes of 10 to 20 years, and public trust store roots sit at the top of that range. The part that rotates stays on the client side, and the route configuration goes untouched for years.
A shared secret in a header
The simplest version of the locked door is a value the client has to send. Upsun’s HTTP access control implements this as HTTP basic authentication for a whole environment:Authorization header is removed before the request reaches the application, because Upsun consumes those credentials itself. Application-level tokens belong in a header of your own, checked in application code.
That variant, a header the application requires and every other request rejected, is what most CDN setups use. Fastly or Cloudflare adds a secret header at the edge, the origin refuses anything without it, and the origin address stops being interesting to anyone who discovers it.
It works well and it has the rotation problem described above. Treat it as the pragmatic option, not the strong one, and prefer mTLS wherever the client can be made to hold a certificate.
An IP allowlist
If the caller has a static, dedicated address, allow that address and deny the rest. HTTP access control does this too:Family two: no public route at all
The stronger option, where the clients allow it, is an application the router never sends anything to. Nothing listens on the internet, so there’s no endpoint to scan and no login page to brute-force. Access happens over a channel you set up yourself. Start by making sure there’s no route. This is the step people get wrong, because omitting theroutes key entirely gives you the default route, which sends all traffic to your app. To have no public entry point, define the key with nothing pointing at the application:
.upsun/config.yaml
A VPN
Put the container on a private network and reach it from devices already on that network. Tailscale on Upsun covers the setup, including the part that trips people up: Upsun containers can’t create atun device, so Tailscale runs in userspace networking mode. That post uses the outbound direction, where the application reaches endpoints inside the tailnet. Exposing the application to the tailnet is the mirror image of it, with tailscale serve publishing the local port to the network instead.
The container dials out to join the tailnet. Nothing has to reach in for the connection to exist.
This is the option to reach for when the audience is people rather than machines. Access follows the identity and device policy you already run in the VPN, and revoking someone there revokes their access to the application at the same time.
A tunnel
A VPN gives every device on the network a path to the application. A tunnel gives one process a path, which is less to keep track of. Chisel carries TCP over HTTP, and the post above uses it to connect services across Upsun projects. Running over HTTP is what makes it convenient: the traffic looks like ordinary web traffic and travels wherever web traffic is allowed. For a human at a terminal, the Upsun CLI already does this.upsun tunnel:open opens SSH tunnels to the services in an environment and prints local addresses to connect to, and upsun ssh -L forwards a local port to the application itself. Nothing to configure and nothing to deploy, which makes it the right answer for occasional access by someone who already has SSH access to the project.
The same commands work unattended. Add a dedicated account to the project with only the access it needs, give it an API token, and a backup job or a reporting script opens the tunnel, does its work, and closes it. The token is a credential on a rotation schedule, which is the cost of this option, and the account is one you can revoke on its own without touching anyone else’s access.
The tradeoff across this family is the same one. Removing the public route also removes the browser as a client. Anyone who needs the application has to be on the network or hold the tunnel open first, and that’s a real cost for a tool used daily by non-technical colleagues.