{"slug": "show-hn-self-hosted-tunnels-local-directory-port-your-domain-https-passkeys", "title": "Show HN: Self-hosted tunnels: local directory/port, your domain, HTTPS, passkeys", "summary": "Developer Markus Schappim released mygrok, a self-hosted tunnel tool written in about 9,500 lines of Go with eight dependencies, that lets users forward local ports or directories to public HTTPS URLs on their own domains without accounts, session limits, or third-party infrastructure. The tool includes two binaries, mygrokd for the server and mygrok for clients, and supports stable subdomains, automatic TLS, WebAuthn passkeys, IP rules, and quick recovery from network blips, with installation via Homebrew, Go, or a server install script.", "body_md": "**Your own ngrok.** Forward a local port to a public HTTPS URL on a domain you own —\non a server you own, for the price of the smallest VPS your provider sells.\n\nTwo Go binaries, no daemon zoo, no account, no session limits, no \"your\ntunnel expired\" page. `mygrokd`\n\nruns on a box with wildcard DNS pointed at\nit; `mygrok`\n\nruns wherever you want to expose something.\n\n```\nmygrok http 3000 --subdomain=jarvis     # → https://jarvis.your-domain.com\nmygrok serve gallery ./photos           # static folder, public URL, one command\nmygrok mcp 8790 --subdomain=tools       # local MCP server → claude.ai connector\n```\n\n[Why](#why)·[Install](#install)·[60 seconds to a public URL](#60-seconds-to-a-public-url)[What it looks like](#what-it-looks-like)— the web UI[Locking things down](#locking-things-down)— basic auth, IP rules, passkeys[Feature tour](#feature-tour)·[CLI at a glance](#cli-at-a-glance)**Docs:**[CLI reference](/schappim/mygrok/blob/main/docs/cli.md)·[Config file](/schappim/mygrok/blob/main/docs/config.md)·[Access control](/schappim/mygrok/blob/main/docs/access-control.md)·[Run your own server](/schappim/mygrok/blob/main/docs/server.md)·[MCP connectors](/schappim/mygrok/blob/main/docs/mcp.md)·[Architecture & protocol](/schappim/mygrok/blob/main/docs/architecture.md)[Limitations](#limitations)·[Contributing](/schappim/mygrok/blob/main/CONTRIBUTING.md)·[Security](/schappim/mygrok/blob/main/SECURITY.md)\n\nTunnel services are great until you're paying monthly for a random subdomain that changes every restart, or your webhook testing hits a session limit mid-debug, or you'd rather not route your unreleased product through someone else's infrastructure.\n\nmygrok is the boring alternative: **you own every piece.** The server, the\ndomain, the certificate, the auth token, the binary. Nothing phones home.\nThere's no account to create and no free tier to age out of. The whole thing\nis about 9,500 lines of Go with eight dependencies.\n\nWhat you get that a plain SSH reverse tunnel doesn't give you:\n\n**Stable, memorable URLs.**`jarvis.your-domain.com`\n\n, every time.**Real TLS**, issued and renewed automatically. No self-signed warnings.** Survives reboots**— one command installs a launchd agent or systemd unit.** Access control that isn't just a password**— IP rules and WebAuthn passkeys, both managed from a web UI or the CLI.** Heals from network blips in under a second**instead of sitting in backoff waiting for a dead session to time out.\n\n**Homebrew** (macOS and Linux):\n\n```\nbrew install schappim/mygrok/mygrok\n```\n\n**From your own server** — this is the nice one. Binaries served by your\n`mygrokd`\n\nare stamped with its address at build time, so they arrive already\nknowing where to connect:\n\n```\ncurl -sSL https://tunnel.your-domain.com/install | bash\n```\n\nSet `MYGROK_INSTALL_DIR=$HOME/bin`\n\nif you'd rather not use sudo.\n\n**With Go:**\n\n```\ngo install github.com/schappim/mygrok/cmd/mygrok@latest\n```\n\nHomebrew and `go install`\n\nbuilds are deliberately generic — they carry no\ndefault server, so point them at yours with `MYGROK_SERVER`\n\nor\n`~/.mygrok/config.toml`\n\n. See [Config file](/schappim/mygrok/blob/main/docs/config.md).\n\nOne command on a fresh Debian/Ubuntu box — DigitalOcean, Hetzner, EC2, Vultr, a Pi under your desk:\n\n```\ncurl -sSL https://raw.githubusercontent.com/schappim/mygrok/main/deploy/install-server.sh \\\n  | sudo bash -s -- --domain example.com --email you@example.com\n```\n\nIt creates a service user, installs the binary, generates an auth token,\nwrites a hardened systemd unit, opens the firewall, and prints the two DNS\nrecords to add. Full walkthrough for each cloud, plus DNS and certificate\noptions, in ** docs/server.md**.\n\n```\nexport MYGROK_AUTHTOKEN=\"<the token your server printed>\"\n\nmygrok http 3000 --subdomain=jarvis\nmygrok tunnel active\n\n  Forwarding   https://jarvis.example.com               -> 127.0.0.1:3000\n               http://jarvis.example.com                -> 127.0.0.1:3000\n\n  HTTP Requests\n  -------------\n\n09:14:02 203.0.113.9     GET    200 OK                      18ms  /\n09:14:03 203.0.113.9     POST   201 Created                 31ms  /api/orders\n```\n\nMake it permanent:\n\n```\nmygrok service install 3000 --subdomain=jarvis\n```\n\nThat's a launchd agent on macOS, a systemd unit on Linux. Starts at login/boot, restarts on crash, and — on Linux — keeps your auth token in a root-only environment file rather than in the world-readable unit.\n\nDon't have a server running but do have a folder?\n\n```\nmygrok serve                    # cwd at a random subdomain\nmygrok serve gallery ./photos   # ./photos at gallery.example.com\nmygrok serve ./photos           # no tunnel, just http://127.0.0.1:8080\n```\n\nEvery server has a management host at `tunnel.<your-domain>`\n\n— the landing\npage, the installer, the client downloads, and the admin UI. No separate\ndashboard to deploy, no extra port.\n\nEvery tunnel, live or merely configured, with its rules at a glance. Rules apply the moment a client registers, so you can lock a subdomain down before anyone has ever tunnelled it.\n\nPer-tunnel allow and block lists, single IPs or CIDR, each with a note so you remember why in six months:\n\nIssue a one-shot link, send it to a person, they register a passkey with Touch ID or their phone. Then grant them the specific tunnels they should reach. No shared password, no accounts to create, revocable per device.\n\nHit a subdomain with no client connected and you get this, rather than a browser error page — with the exact command to bring it back up.\n\nA tunnel is public by default — anyone with the URL reaches your app. Three mechanisms change that, and they stack.\n\n**A shared password**, enforced by the client before your app sees anything:\n\n```\nmygrok http 3000 --subdomain=jarvis --basic-auth=alice:s3cret\n```\n\n**IP rules**, enforced at the server edge before the request even crosses the\ntunnel:\n\n```\nmygrok admin allow jarvis 203.0.113.7 --note=office\nmygrok admin block global 198.51.100.0/24 --note=scanner\nmygrok admin rules --json | jq '.tunnels_configured'\n```\n\n**Passkeys**, for actual per-person access:\n\n```\nmygrok admin invite alice        # → send her the printed URL\nmygrok admin grant jarvis alice  # she can now reach jarvis, nothing else\nmygrok admin revoke jarvis alice\n```\n\nLocked tunnels bounce unauthenticated visitors to a WebAuthn login and issue\na 24-hour session cookie scoped to your whole zone. Users add extra devices\nthemselves at `/account`\n\nwithout bothering you.\n\nFull semantics — evaluation order, storage format, the safety net that stops\nyou locking yourself out — in ** docs/access-control.md**.\n\nPublic URLs |\n`https://<subdomain>.<your-domain>` , stable across restarts |\nTLS |\nLet's Encrypt, automatic. A wildcard via DNS-01 if you configure a provider, otherwise per-hostname on demand — no cloud credentials needed either way |\nDNS providers |\nRoute 53, Cloudflare, DigitalOcean, or none |\nPersistence |\n`mygrok service install` → launchd agent or systemd unit |\nStatic folders |\n`mygrok serve [subdomain] [dir]` — `python -m http.server` plus a public URL |\nMCP connectors |\n`mygrok mcp <port>` exposes a local MCP server to claude.ai, gated by an unguessable capability URL.\n|\nCustom hostnames |\n`--hostname=app.example.com` — CNAME your own domain at a tunnel, certificate issued on demand |\nBasic auth |\n`--basic-auth=user:pass` , constant-time, credentials never leave your machine |\nIP allow/block |\nPer-tunnel and global, CIDR supported, web UI + JSON API |\nPasskeys |\nMulti-user WebAuthn with per-tunnel grants and self-serve device enrolment |\nLAN-direct |\nSame-NAT visitors get redirected straight to your machine instead of round-tripping through the server. Real certificate, no warnings.\n|\n\n**Self-update**`mygrok update`\n\npulls the matching build from your server**Real client IPs**`X-Forwarded-For`\n\n/`X-Real-IP`\n\nare stripped and replaced with the true source, so your app's IP logic works**WebSockets****Fast reconnect****Offline page****Claude skill**[teaches Claude Code to drive all of this](/schappim/mygrok/blob/main/skills/mygrok)`skills/mygrok/`\n\n```\nmygrok http  <port> [--subdomain=<name>] [--hostname=<host>] [--basic-auth=<u>:<p>] [--lan=auto]\nmygrok mcp   <port> [--subdomain=<name>] [--secret=<token>] [--no-strip] [--path=/mcp]\nmygrok serve [<subdomain>] [<dir>] [--port=<n>] [--index=<file>]\nmygrok service install <port> --subdomain=<name> [--name=<svc>] [--mcp]\nmygrok service uninstall|list|status|logs [<name>]\nmygrok admin [login|tunnels|rules|allow|block|invite|grant|revoke|users|passkeys]\nmygrok update\nmygrok version\n```\n\nShared by every command that talks to a server: `--server=<host:port>`\n\n,\n`--auth=<token>`\n\n(not `update`\n\n, which only fetches a public binary), and\n`--config=<path>`\n\n. Precedence is **CLI flag → environment\n→ config file → built-in default**, so a `.mygrok.toml`\n\nin your project can\nsupply the port and subdomain and you just run `mygrok`\n\n.\n\n`mygrok admin help`\n\nprints a complete machine-readable reference — every\nsubcommand, flag, JSON schema, and exit code. It's written to be pasted into\nan LLM prompt.\n\nFull reference with every flag: ** docs/cli.md**.\n\nStated plainly, because finding out later is worse:\n\n**HTTP/1.1 only.** WebSockets work (they're still HTTP/1.1). HTTP/2 and gRPC do not.**No raw TCP tunnels.** Routing is by`Host`\n\nheader, so there's no equivalent of`ngrok tcp 22`\n\n.**One shared auth token.** Everyone holding it can register tunnels. Per-client identity and revocation don't exist; passkeys gate*visitors*, not tunnel operators.**Single node.** No HA. If the box goes down, tunnels drop until it's back.**The control plane is unencrypted.** The client↔server connection on`:7000`\n\nis plain TCP: the shared token and all tunnelled traffic are visible to anyone on the path. Encrypting it is the top roadmap item. See[SECURITY.md](/schappim/mygrok/blob/main/SECURITY.md).**No traffic inspector.** No local request-replay UI like ngrok's`:4040`\n\n.**First-come subdomains.** A name belongs to whoever claims it, until they disconnect. No permanent reservations.**Custom hostnames don't combine with the passkey gate or LAN-direct**— both depend on cookies and certificates scoped to your wildcard zone, and a CNAME is a different origin.\n\nMore detail, plus the security posture behind each, in [SECURITY.md](/schappim/mygrok/blob/main/SECURITY.md).\n\n```\ngit clone https://github.com/schappim/mygrok.git\ncd mygrok\ngo build ./...\ngo test ./...\n```\n\n`./build.sh`\n\ndoes the full cycle — cross-compiles all four client variants,\nembeds them in the server so `/install`\n\nand `/dl`\n\nwork, builds the server,\ninstalls the client locally, and ships it over SSH:\n\n```\nMYGROK_HOST=tunnel.example.com ./build.sh\n./build.sh --no-deploy          # build only\n./build.sh --skip-clients       # only mygrokd changed\n```\n\nSetting `MYGROK_HOST`\n\nalso stamps that address into the clients, which is why\nbinaries downloaded from your server don't need a config file. See\n[CONTRIBUTING.md](/schappim/mygrok/blob/main/CONTRIBUTING.md) for the local development loop — you can\nrun the whole stack against `*.localhost`\n\nwith no server and no domain.\n\nMIT — see [LICENSE](/schappim/mygrok/blob/main/LICENSE).\n\nBuilt by [Marcus Schappi](https://github.com/schappim). More open source at\n[littlebirdelectronics.com.au/open-source](https://littlebirdelectronics.com.au/open-source).", "url": "https://wpnews.pro/news/show-hn-self-hosted-tunnels-local-directory-port-your-domain-https-passkeys", "canonical_source": "https://github.com/schappim/mygrok", "published_at": "2026-08-24 22:15:32+00:00", "updated_at": "2026-08-24 22:43:16.080346+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Markus Schappim", "mygrok", "Go", "Homebrew"], "alternates": {"html": "https://wpnews.pro/news/show-hn-self-hosted-tunnels-local-directory-port-your-domain-https-passkeys", "markdown": "https://wpnews.pro/news/show-hn-self-hosted-tunnels-local-directory-port-your-domain-https-passkeys.md", "text": "https://wpnews.pro/news/show-hn-self-hosted-tunnels-local-directory-port-your-domain-https-passkeys.txt", "jsonld": "https://wpnews.pro/news/show-hn-self-hosted-tunnels-local-directory-port-your-domain-https-passkeys.jsonld"}}