Back
Build a self-hosted localhost tunnel with OpenSSH and nginx
SiTech AI Team2 min read

Build a self-hosted localhost tunnel with OpenSSH and nginx

A self-hosted setup combines OpenSSH remote forwarding with nginx to expose a localhost preview, add wildcard TLS, and protect shared URLs with expiring hash-based credentials.

Forwarding a local preview

A blog preview running on localhost:8080 can be shared through a self-hosted tunnel without a commercial service. The command ssh -N -R 0:localhost:8080 web02.luffy.cx asks OpenSSH to forward a remote port to the local service. A remote port of 0 makes the server allocate a free port, which is 41535 in the example.

On the remote server, nginx terminates HTTPS connections and proxies requests to the allocated loopback port. A server-name expression captures the port from the tunnel hostname, allowing nginx to pass traffic to the correct local forward. The setup also requires a wildcard DNS record for *.ssh.luffy.cx and a wildcard certificate from Let's Encrypt.

Securing shared URLs

Without additional protection, the allocated port is the only secret keeping the content private. The configuration improves access control with ngx_http_secure_link_module, which calculates a hash from request values and a secret. Because the base64-encoded hash cannot be placed in a case-insensitive domain name, it is sent as part of the HTTP Basic Authentication username together with an expiration timestamp.

Nginx reads the username from the $remote_user variable. A map expression separates the hash and expiration timestamp, while the secure link expression combines the timestamp, captured port, and configured secret for verification. A missing or incorrect hash produces a 401 response with a WWW-Authenticate header. An expired link produces a 410 response. Before proxying a valid request, nginx removes the Authorization header and is configured to support WebSocket connections.

Generating tunnel links

The link hash is generated by passing the expiration time, port, and secret through OpenSSL's MD5 and base64 operations, then converting the result to a URL-safe token. The helper script uses a lifetime of 86400 seconds. Because OpenSSH does not expose its allocated port through an environment variable, the script walks the parent process tree to find sshd-session processes and their listening ports.

After obtaining the ports, the script generates a protected URL for each one and keeps the session open. An SSH configuration entry named http-over-ssh runs the helper on the remote server. The resulting setup relies only on OpenSSH and nginx already running there, while a separate NixOS configuration is available for managing the helper on NixOS.

SSiTech

SiTech — AI-powered web development

We build fast, modern websites and bring AI into real business workflows. Have a project or a question? We'd love to help.