<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/self-hosted-http-tunnels-with-ssh-and-nginx-qeu3pqxry" -->

---
title: Self-hosted HTTP tunnels with SSH and nginx | daily.dev
description: A self-hosted alternative to ngrok and Cloudflare Quick Tunnels uses only OpenSSH remote forwarding and nginx to expose a local service via a shareable HTTPS...
canonical: https://daily.dev/posts/self-hosted-http-tunnels-with-ssh-and-nginx-qeu3pqxry
copyright: © 2026 Daily Dev Ltd.
terms: https://daily.dev/tos/
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: Self-hosted HTTP tunnels with SSH and nginx | daily.dev
og:description: A self-hosted alternative to ngrok and Cloudflare Quick Tunnels uses only OpenSSH remote forwarding and nginx to expose a local service via a shareable HTTPS...
og:url: https://daily.dev/posts/self-hosted-http-tunnels-with-ssh-and-nginx-qeu3pqxry
og:image: https://api.daily.dev/og/posts/qEu3PQxrY.png
og:image:alt: Self-hosted HTTP tunnels with SSH and nginx
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-hosted HTTP tunnels with SSH and nginx

**[Lobsters](https://daily.dev/sources/lobsters)** · 5 min read · 1 upvotes · 0 comments

## Summary

A self-hosted alternative to ngrok and Cloudflare Quick Tunnels uses only OpenSSH remote forwarding and nginx to expose a local service via a shareable HTTPS URL. OpenSSH's -R 0 flag allocates an ephemeral remote port, nginx proxies requests to that port based on a server_name regex, and nginx's secure_link module generates a time-limited token embedded as an HTTP basic-auth username (hash plus expiration timestamp) to prevent URL guessing. A shell helper script automates finding the allocated port via sshd-session process inspection and prints ready-to-share URLs, with a companion NixOS module provided.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://vincent.bernat.ch/en/blog/2026-http-over-ssh>

## Questions this post answers

### How can I create a shareable HTTPS URL for a localhost service without installing ngrok or another tunneling client?

Use OpenSSH remote port forwarding combined with nginx. Running ssh -N -R 0:localhost:8080 host allocates a free remote port; nginx then proxies a subdomain like p41535.ssh.luffy.cx to that port via a server_name regex capturing the port number. No extra client or server software beyond OpenSSH and nginx is needed.

_See the full nginx and ssh config on daily.dev for setting up your own tunnel without third-party services._

### How do I add an expiring access token to an nginx reverse proxy URL using the secure_link module?

Use ngx_http_secure_link_module's secure_link and secure_link_md5 directives, hashing an expiration timestamp plus the port plus a secret string with MD5, base64-encoding it, and making it URL-safe by swapping +/ for -_ and stripping padding. Embed the hash and expiration as an HTTP basic-auth username so nginx reads it from $remote_user; expired or invalid tokens return 410 or 401 respectively.

_Developers securing self-hosted proxies can dig into secure_link configuration examples on daily.dev._

### How can I find the ephemeral remote port that OpenSSH allocated with ssh -R 0:localhost:PORT since it isn't in any environment variable?

Walk the process tree from the current shell up to find the ancestor sshd-session process ID, then run ss --listening --numeric --tcp --processes to list ports owned by that process. Filtering ss output by the sshd-session PIDs reveals which ephemeral port OpenSSH bound for the remote forward, since no environment variable exposes it directly.

_Anyone scripting around SSH remote forwarding can check daily.dev for similar process-inspection tricks._

## Community take

How the wider developer community reacted, aggregated from 2 discussions and 72 comments across lobsters, hackernews (as of 2026-10-05).

**TL;DR:** Developers appreciate the minimalist, tool-composing approach to self-hosted tunnels, but discussion quickly turns to security caveats (token/port reuse, port-scanning risk) and a flood of alternative tools people prefer instead.

**Sentiment:** 40% positive · 35% mixed · 25% skeptical

**The case for**

- Praised for reusing existing, well-understood tools (SSH, nginx) rather than inventing new daemons or software.
- Several found it a clever, creative way to add HTTPS and access control on top of plain SSH remote forwarding.
- One commenter liked that it gives terminal-visible access logs when tied to the SSH session.

**The pushback**

- If a remote port is reused, an old session's auth token can remain valid for the new tunnel, which nginx can't detect.
- The author's own nginx config was flagged as letting attackers probe and connect to any arbitrary localhost port before mitigations were added.
- Several felt the scheme was over-complicated compared to simpler alternatives like picking a high port directly or using purpose-built tools (sish, tuns.sh).
- Self-hosting without a reverse proxy/service in front exposes endpoints with minimal built-in defensive tooling.

**By community**

- lobsters (positive): Commenters like the off-the-shelf-tools philosophy and dig into the token-reuse edge case constructively with the author.
- hackernews (mixed): Interest in the minimalist approach is mixed with pointed security critiques and a long tangent promoting competing tunneling tools.

**Hottest debate:** Whether the nginx/token scheme is a clever minimal solution or an over-engineered approach with real footguns (port/token reuse, port enumeration) compared to just using a dedicated tunneling tool.

**Open questions**

- What is the actual threat model for leaked URLs, tunnel enumeration, forwarded credentials, and abandoned sessions?
- Could the port-reuse token validity issue be fixed without adding another daemon, e.g. via a unique socket or tying nginx to the SSH session as a forced command?

**Highlights**

> This is wildly over-complicated and also has a bunch of footguns and security risks. For example, that very first NGINX section allows an attacker to direct their incoming traffic to any arbitrary listening port on localhost. They can even write a simple for loop in bash that would use curl to test all of the different ports. This also bypasses any firewall rules that you might have blocking traffic from the outside world. be very careful following the instructions in this article. Read the man page for SSH, and especially the remote forward section. https://man7.org/linux/man-pages/man1/ssh.1.html
> — [jamiesonbecker on hackernews · 1 comments](https://news.ycombinator.com/item?id=49960432)

> I like "solve problems with off-the-shelf tools". The nginx auth logic is a bit fiddly for me to follow, but it looks like the generated URL includes a token (with a ye olde MAC) that lasts for 24h and authenticates the port? If a port is reused for a subsequent tunnel, what stops the token from the old tunnel from being valid for the new tunnel?
> — [tuxes on lobsters · 2 points, 1 comments](https://lobste.rs/s/jba52r/self_hosted_http_tunnels_with_ssh_nginx#c_teqwwp)

> I did notice that you tried to mitigate the risk, but why not just pick a high port rather than this complex redirection? Your scheme is smart and creative but unnecessarily risky. SSH can do remote listens to the world by itself, no second nginx needed, but sish or ngrok might be a better solution for the general case. Anytime you punch holes, you're taking a risk, so make it as narrow as you can.
> — [jamiesonbecker on hackernews · 1 comments](https://news.ycombinator.com/item?id=49960764)

> A number of years ago, I created a fully open source (MIT) project called sish [0] that does just this. sish is a SSH server written specifically for tunneling. You get all of the benefits of SSH, but also automatic TLS, a web console of requests a tunnel has received, and various other features. You can also tunnel more than just HTTP(S). You can tunnel websockets, TCP connections, and even have internal alias connections for using ProxyJump within the tunnel. All stateless and all protected with SSH auth. If you’re not interested in self hosting, there’s a hosted version at tuns.sh [1] that has multi region support and a few other cool features (including UDP tunneling) as part of the pico.sh [2] membership ($2/mo). Happy to answer any questions in this space! [0] https://github.com/antoniomika/sish [1] https://tuns.sh [2] https://pico.sh
> — [antoniomika on hackernews · 2 comments](https://news.ycombinator.com/item?id=49960540)

> Yes, it's still valid. nginx has no way to know this is a different SSH session. Sometimes, instead of 0, I specify the port explicitly to reuse a previous one. We could forward from an Unix socket instead, but the user has to provide a "unique" one. OpenSSH does not seem to support abstract Unix socket, so you also have to specify the full path.
> — [vbernat on lobsters · 2 points, 1 comments](https://lobste.rs/s/jba52r/self_hosted_http_tunnels_with_ssh_nginx#c_nqgluy)

**Source threads**

- [lobsters](https://lobste.rs/s/jba52r/self_hosted_http_tunnels_with_ssh_nginx) · 16 points · 8 comments
- [hackernews](https://news.ycombinator.com/item?id=49958569) · 147 points · 64 comments

## Similar posts on daily.dev

- [Cloudflare Tunnel: Self-Host Behind CGNAT and Hide Your IP](https://daily.dev/posts/cloudflare-tunnel-self-host-behind-cgnat-and-hide-your-ip-oym3uguge) · Milan Jovanović · 46 upvotes · 2 comments
- [Cloudflare Tunnels with Docker Compose: Free ngrok Alternative](https://daily.dev/posts/cloudflare-tunnels-with-docker-compose-free-ngrok-alternative-chygejkfv) · SitePoint · 1 upvotes · 0 comments
- [Is Cloudflare Tunnel Secure for Self-Hosting?](https://daily.dev/posts/is-cloudflare-tunnel-secure-for-self-hosting--btpc3mo3w) · Noted · 0 upvotes · 0 comments
- [How to Expose Your Rails Localhost Securely Using Cloudflare Tunnel](https://daily.dev/posts/how-to-expose-your-rails-localhost-securely-using-cloudflare-tunnel-5icndqzje) · RUBYLAND · 0 upvotes · 0 comments
- [GitHub - m1dnight/ferri: A self-hostable HTTP tunnel. Expose your local dev server to the internet with a single command.](https://daily.dev/posts/github---m1dnight-ferri-a-self-hostable-http-tunnel-expose-your-local-dev-server-to-the-internet-w-iemb1h8yg) · ElixirStatus · 26 upvotes · 1 comments

---

Tags: [#self-hosting](https://daily.dev/tags/self-hosting), [#ssh](https://daily.dev/tags/ssh), [#nginx](https://daily.dev/tags/nginx), [#nixos](https://daily.dev/tags/nixos)

[View this post on daily.dev](https://daily.dev/posts/self-hosted-http-tunnels-with-ssh-and-nginx-qeu3pqxry)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"Self-hosted HTTP tunnels with SSH and nginx","url":"https://daily.dev/posts/self-hosted-http-tunnels-with-ssh-and-nginx-qeu3pqxry","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/self-hosted-http-tunnels-with-ssh-and-nginx-qeu3pqxry"},"datePublished":"2026-10-05T11:05:04.574Z","dateModified":"2026-10-05T11:05:43.284Z","description":"A self-hosted alternative to ngrok and Cloudflare Quick Tunnels uses only OpenSSH remote forwarding and nginx to expose a local service via a shareable HTTPS...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/ac33f30b92f685fab1353925b7779b7a?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/ac33f30b92f685fab1353925b7779b7a?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"Lobsters","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"Lobsters","logo":"https://media.daily.dev/image/upload/s--tl8v_Fku--/f_auto,t_logo/v1698841318/logos/lobste.jpg","url":"https://daily.dev/sources/lobsters"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/self-hosted-http-tunnels-with-ssh-and-nginx-qeu3pqxry","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":1},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"self-hosting,ssh,nginx,nixos","timeRequired":"PT5M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"Lobsters","item":"https://daily.dev/sources/lobsters"},{"@type":"ListItem","position":3,"name":"Self-hosted HTTP tunnels with SSH and nginx"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/self-hosted-http-tunnels-with-ssh-and-nginx-qeu3pqxry#faq","mainEntity":[{"@type":"Question","name":"How can I create a shareable HTTPS URL for a localhost service without installing ngrok or another tunneling client?","acceptedAnswer":{"@type":"Answer","text":"Use OpenSSH remote port forwarding combined with nginx. Running ssh -N -R 0:localhost:8080 host allocates a free remote port; nginx then proxies a subdomain like p41535.ssh.luffy.cx to that port via a server_name regex capturing the port number. No extra client or server software beyond OpenSSH and nginx is needed. See the full nginx and ssh config on daily.dev for setting up your own tunnel without third-party services."}},{"@type":"Question","name":"How do I add an expiring access token to an nginx reverse proxy URL using the secure_link module?","acceptedAnswer":{"@type":"Answer","text":"Use ngx_http_secure_link_module's secure_link and secure_link_md5 directives, hashing an expiration timestamp plus the port plus a secret string with MD5, base64-encoding it, and making it URL-safe by swapping +/ for -_ and stripping padding. Embed the hash and expiration as an HTTP basic-auth username so nginx reads it from $remote_user; expired or invalid tokens return 410 or 401 respectively. Developers securing self-hosted proxies can dig into secure_link configuration examples on daily.dev."}},{"@type":"Question","name":"How can I find the ephemeral remote port that OpenSSH allocated with ssh -R 0:localhost:PORT since it isn't in any environment variable?","acceptedAnswer":{"@type":"Answer","text":"Walk the process tree from the current shell up to find the ancestor sshd-session process ID, then run ss --listening --numeric --tcp --processes to list ports owned by that process. Filtering ss output by the sshd-session PIDs reveals which ephemeral port OpenSSH bound for the remote forward, since no environment variable exposes it directly. Anyone scripting around SSH remote forwarding can check daily.dev for similar process-inspection tricks."}}]}
```

