An HTTP Tunnel for a Local Site Built with OpenSSH and Nginx
Vincent Bernat showed how to make a local web service accessible through your own server. OpenSSH creates the tunnel, while Nginx handles HTTPS requests and checks access.
On October 5, 2026, Vincent Bernat described a way to make a local web service accessible through your own server without a separate tunneling client. As he explains in Self-hosted HTTP tunnels with SSH and Nginx, he uses OpenSSH and Nginx: SSH connects a remote port to the local service, while Nginx accepts HTTPS requests and routes them into the tunnel. The use case in the article is letting someone view a blog draft that is currently available only at localhost:8080.
How a request reaches the local port
The developer sets up SSH remote port forwarding from the remote server to the local service. In the command ssh -N -R 0:localhost:8080 web02.luffy.cx, the zero tells the server to choose an available remote port. In the article’s example, it assigned port 41535. As long as the SSH connection stays open, requests to that port can be forwarded to the service on the developer’s machine.
On the remote server, Nginx accepts HTTPS requests for subdomains such as p41535.ssh.luffy.cx. It extracts the port number from the hostname and proxies the request to 127.0.0.1:41535. For these addresses, Bernat sets up a wildcard DNS record and a Let’s Encrypt certificate covering the subdomains. The certificate is validated through DNS-01; in his setup, NixOS obtains certificates automatically.
The setup is not tied to a single port assigned in advance: SSH may allocate a different one on the next connection, and the Nginx rule will pick up the new number from the address. The trade-off is that Nginx alone is not enough. You need a server running OpenSSH, configured DNS and HTTPS, and an open SSH connection to the machine running the local service. This is a way to use existing infrastructure, not avoid configuring it.
A link with access control
In the basic configuration, the port number is the only barrier to accessing the page: anyone who finds it can get through the proxy. Bernat adds a check using the Nginx module ngx_http_secure_link_module. The link contains a hash and an expiration time, placed in the username portion before the domain. The client sends that part of the address using HTTP Basic authentication, and Nginx compares the supplied hash with one calculated from the expiration time, port number, and a server-side secret.
If the hash is missing or incorrect, the server responds with a 401 and asks for credentials; once the link expires, it returns a 410. In Bernat’s example, the link is valid for 86400 seconds. Before forwarding the request to the local service, Nginx removes the Authorization header that carried the access credentials. The configuration also supports proxying WebSocket connections.
Putting such an address together by hand is awkward: first you need to find the SSH port that was assigned, then calculate the hash and build the link. So Bernat wrote a helper script for the server. It finds the SSH session’s processes, identifies their associated listening ports, prints the addresses, and keeps the session open. To find the ports, the script runs ss through sudo -n—anyone replicating the setup will need to configure that access. An entry in the SSH configuration runs the script on connection.
What this means when working on a site
The use case demonstrated in the article is showing someone a draft page running on a local machine. The recipient opens the HTTPS address they have been given; the request reaches the server running Nginx and passes through SSH remote port forwarding to the local service. There is no need to deploy the draft separately to a remote web server in this scenario, but the tunnel only works while the SSH session remains open.
The link contains the access credentials. If it is forwarded to someone else, that person can open the page too, until the link expires. The expiration time limits access by time, but it does not, on its own, limit who can use an address they have received.
My take: fewer dependencies, more responsibility on your side
I like that you can trace the path of a request here and see what each component does. SSH handles forwarding; Nginx handles HTTPS and checks the link. If both are already running on the server, there is no need for a dedicated tunneling client. But I would not call the setup simple just because the connection command is short: DNS, the certificate, Nginx rules, and the script’s access to port information are still part of the solution.
Before recommending this approach to a team, I would ask how often they need temporary previews and who will maintain the server configuration. For an occasional look at a draft, the benefit is not obvious to me if everything has to be set up from scratch. For regular use, I would agree in advance on how long links should remain valid and who they can be forwarded to. Otherwise, a convenient command conceals a long list of assumptions that will only become apparent in use.
Sources
Where the news comes from. The text is a retelling in the author’s own words; the facts come from the source, the opinion is the author’s.
- Self-hosted HTTP tunnels with SSH and Nginx vincent.bernat.ch
E-commerce frontend developer
I maintain stores with large catalogs and regularly clean up the aftermath of “quick” plugin installations. Before accepting advice about a new stack, I ask what exactly it will improve and how we’ll measure it.
All posts by the authorRelated articles
Breach of .gh, .sl and .as Registry Infrastructure Enabled Unauthorized TLS Certificates
Attackers compromised the infrastructure behind the .gh, .sl and .as domains, changed DNS records and obtained unauthorized HTTPS certificates, including for Google domains. Google blocked them in Chrome and advises site owners to check CT logs.
Next.js Releases Security Updates for Versions 15.5 and 16.3
Next.js has released versions 16.3.8 and 15.5.27 to fix vulnerabilities involving caching, information disclosure, and image optimization. Teams are advised to update their apps and check which features they use.
Bez Generates a Browser Engine from Web Specifications and Tests
Bez builds a browser engine from web specifications, checking its results against Chromium, Firefox, and WebKit. The project is still at an early stage, and its published figures do not mean the engine is ready.
Discussion 5
Leonas Eidis
One thing worth adding: treat that SSH session as production infrastructure, not a temporary magic pipe. Use a dedicated account and key, restrict what it can forward, and make the tunnel reconnect reliably; otherwise the “share this draft” link becomes a very elegant way to share a dead port. I’d also put an expiry on the access rule in Nginx, since temporary previews have a habit of becoming permanent by accident.
Timo Prins
How would you restrict the forwarded port for that `ssh -N -R 0:localhost:8080` setup without blocking the Nginx proxy—does the dedicated account need a forced command or specific SSH forwarding options?
Janek Sobczak
For a tight allowlist, I’d drop `-R 0` and use a fixed remote port bound to loopback, then put `AllowTcpForwarding remote` and `PermitListen localhost:41535` in an `sshd_config` `Match User` block for that dedicated account. Nginx can still proxy to the loopback listener, but the account can’t pick arbitrary ports; I haven’t tested this exact config, so I’d verify the `PermitListen` syntax against the OpenSSH version in use.
Pēteris Zālītis Post author
The key detail is that Nginx proxies to the server-side port OpenSSH allocated (41535 in the example); it doesn’t need direct access to the developer’s local port. For a dedicated account, I’d allow remote forwarding only to the required listener address/port and disable unrelated SSH features, but the `0` asks sshd to allocate a port dynamically, so a fixed remote port is easier to constrain with `PermitListen`. If you keep `-R 0`, you need to capture the allocated port and update the Nginx access rule accordingly; otherwise the restriction and routing can drift apart.
Emil Ursu
I think “production infrastructure” overstates the case for a short-lived draft preview. A dedicated key and narrowly scoped forwarding make sense, but reliable reconnects can keep an unintended exposure alive just as reliably; I’d rather let the SSH session fail closed and use an explicit expiry on the Nginx access rule.