# sish: self-hosted SSH tunnels for HTTP, WebSocket and TCP

> sish turns an SSH server into a tunnel router, so users forward ports with commands they already know. It is aimed at teams that want ngrok-style sharing on their own domain, and it is only as good as the SSH and TLS configuration around it.

**antoniomika/sish** — HTTP(S)/WS(S)/TCP Tunnels to localhost using only SSH.

- Repository: https://github.com/antoniomika/sish
- Website: https://ssi.sh
- Stars: 4,740 · Forks: 336
- Language: Go
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/antoniomika-sish

## The problem sish solves for teams that already run SSH

Sharing a local web server usually means installing a vendor client, creating an account, and accepting a hostname you do not control. sish takes the opposite position: the tunnel endpoint is an SSH server you run, and the client is the ssh binary that is already on every developer machine. The README describes it as "open source SSH tunneling for HTTP(S), WS(S), TCP, aliases, and SNI" and says it is for people who "like the simplicity of serveo/ngrok-style sharing but want to use plain SSH and run your own infrastructure".

The audience is therefore narrow and specific. It is the platform or infrastructure team that already terminates SSH, already owns a domain, and wants tunnel URLs to live under that domain instead of a third party's. End users get a URL from a single ssh command. The operator gets the routing, the certificates and the access policy in one process.

It is not a general purpose VPN and it is not a way to expose a service without owning any infrastructure. The managed service at tuns.sh exists for people who want the workflow without running the server, and the README presents it as the quickest way to validate that the workflow fits.

## How sish routes a forwarded port to a public URL

The repository layout makes the architecture legible. There is an sshmuxer package and an httpmuxer package at the top level, and main.go wires them together. The sshmuxer side accepts SSH connections and interprets the forwarding requests inside them; the httpmuxer side takes the resulting routes and serves HTTP and HTTPS traffic against them. The Go module list backs this up: golang.org/x/crypto for the SSH server, gin for HTTP handling, gorilla/websocket for WebSocket upgrades, caddyserver/certmagic for certificate management, and vulcand/oxy for proxy behaviour.

The data flow for an HTTP tunnel is: the user runs ssh with a remote forward, sish receives that forward request over the SSH connection, and it registers a hostname derived from the requested name plus the configured --domain. Incoming HTTP requests for that hostname are then proxied down the existing SSH connection to the local port the user specified. Nothing is installed on the user's machine and no inbound port is opened on it.

TCP works differently. The README shows ssh -R 2222:localhost:22 tuns.sh making localhost:22 available at tuns.sh:2222, so a TCP forward claims a port on the sish host rather than a hostname. Aliases are a third mode: ssh -R mylaptop:22:localhost:22 tuns.sh registers a name that is not publicly routable, and the README shows reaching it with ssh -J tuns.sh mylaptop, which means the alias is only usable through the sish server itself. That is a meaningful design choice: an alias is a private entry point, not a published one, and it depends on the user having SSH access to the server.

## Installing sish with Docker and opening a first tunnel

The README gives a Docker path and a from-source path. For Docker, pull the image, create three directories for certificates, private keys and authorized public keys, and copy your own public key into the pubkeys directory so the server has at least one user it will accept.

```bash
docker pull antoniomika/sish:latest
mkdir -p ~/sish/ssl ~/sish/keys ~/sish/pubkeys
cp ~/.ssh/id_ed25519.pub ~/sish/pubkeys
```

The run command mounts those directories and sets the listening addresses. Note that it uses --net=host, so the ports below are the host's ports, and that --domain must be a domain whose DNS points at this machine and whose certificates are in the mounted ssl directory.

```bash
docker run -itd --name sish \
	-v ~/sish/ssl:/ssl \
	-v ~/sish/keys:/keys \
	-v ~/sish/pubkeys:/pubkeys \
	--net=host antoniomika/sish:latest \
	--ssh-address=:2222 \
	--http-address=:80 \
	--https-address=:443 \
	--https=true \
	--https-certificate-directory=/ssl \
	--authentication-keys-directory=/pubkeys \
	--private-keys-directory=/keys \
	--bind-random-ports=false \
	--domain=example.com
```

With the server up, a user connects on the SSH port and forwards port 80 to their local app. The README's self-hosted example is the following, and the user should end up with a URL under the configured domain once the connection is established.

```bash
ssh -p 2222 -R 80:localhost:8080 example.com
```

For local development without Docker, the README clones the repository and runs the binary directly against a development domain, with make dev as the equivalent shortcut. The testing.ssi.sh hostname is documented as pointing at localhost, so tunnels can be exercised without any DNS work.

```bash
git clone git@github.com:antoniomika/sish.git
cd sish
go run main.go --http-address localhost:3000 --domain testing.ssi.sh
```

## Where sish is the wrong tool

The README lists HTTP(S), WS(S), TCP, aliases and SNI. UDP is not in that list, so anything that needs a UDP forward is out of scope, and the related searches around free UDP tunnels are not something this project answers. If your workload is a game server or a QUIC-only service, sish will not carry it.

The second constraint is operational rather than technical. sish is a multi-tenant SSH endpoint that binds ports and hostnames on demand. The README mentions "restrictive binding policies for safer multi-tenant setups", which is an acknowledgement that the default posture needs tuning before strangers can connect. Running it with open authentication and random port binding on a host with a public IP is a different risk profile from running it for a known team.

The third is that the documentation in the repository is thin relative to the surface area. The README points to docs.ssi.sh for getting started, forwarding types and the CLI reference, and there is a config.example.yml in the repository root, but the README itself does not document rollback, upgrade procedures or what happens to active tunnels during a restart. An operator who needs those guarantees will have to derive them from the source or test them directly.

## How sish differs from ngrok and Cloudflare Tunnel

The practical difference from ngrok is where the trust boundary sits. ngrok runs the tunnel endpoint, so the hostname, the TLS certificate and the request metadata belong to ngrok, and the client is ngrok's own binary. With sish, the endpoint is yours: the certificate directory, the authorized keys directory and the domain are all arguments you pass to your own process. That is more work up front and less work later if you already have certificate management and SSH key distribution in place.

Cloudflare Tunnel has a different shape again. It is built around outbound connections to Cloudflare's edge and an account relationship with Cloudflare, which means the public hostname and the TLS termination sit with Cloudflare. sish terminates TLS itself, using certmagic, and routes by hostname or by SNI depending on the mode. If your constraint is "no third party in the request path", sish is the closer fit; if your constraint is "no servers to run", it is not.

Serveo is the closest in user experience, since it is also plain SSH, but it is a shared public service rather than software you host. The README positions sish explicitly against that pattern: same simplicity, your infrastructure.

## Licence, releases and what maintenance costs

sish is MIT licensed, which permits commercial use and modification with the usual requirement to keep the copyright and permission notice. That is a permissive licence, and the practical implication for an operator is that there is no copyleft obligation on the rest of your stack. This is a description of the licence text, not legal advice; if you are redistributing sish inside a product, read the LICENSE file in the repository.

The release cadence visible in the repository is regular: v2.23.0 in June 2026, v2.22.1 in March 2026, v2.21.1 in February 2026, and the last push to the default branch was on 2026-06-25. The project is not archived. Upgrades are a container image swap or a rebuild from source, since the Dockerfile produces a static binary in a scratch image with the deploy directory, templates and certificates copied in. The cost that matters is not the upgrade itself but the configuration surface: the run command in the README already carries eleven flags, and config.example.yml exists because that list grows. Treat the flag set as the thing you have to track between releases, and pin a version rather than following latest if you are running this for other people.

One detail worth knowing for operators: the Dockerfile builds with CGO disabled and copies ca-certificates into a scratch image, so the runtime image has no shell. Debugging inside the container is not an option; you debug from the host and the logs.

## Conclusion

Adopt sish if you already run SSH infrastructure and want tunnel URLs under your own domain: the users need no client beyond ssh, and the Docker command in the README is enough to get a first tunnel. Do not adopt it if you need UDP forwarding, since the README lists only HTTP(S), WS(S) and TCP, or if you cannot supply TLS certificates and a domain for it to terminate on. Before rolling it out, verify that the authentication keys directory contains the public keys you intend to accept, that the certificate directory holds certificates matching the domain you pass to --domain, and that --bind-random-ports is set the way your port policy requires.

## FAQ

### Can you explain how SSH tunnels work?

In sish the client opens a normal SSH connection and asks the server to forward a remote port back to a local one, for example ssh -R 80:localhost:8080 example.com. sish receives that forwarding request and turns it into a public route, either a hostname derived from the requested name and the configured domain, or a TCP port on the sish host.

### Are SSH tunnels bidirectional?

The README only documents remote forwarding, where the traffic arrives at the sish server and is carried down the SSH connection to the user's local port. Nothing in the README describes local forwarding or a second direction of initiation, so treat sish as a one-way inbound path to your local service.

### How do I create an SSH tunnel with sish?

Connect to the sish SSH port with a remote forward. The README's managed service example is ssh -R 80:localhost:8080 tuns.sh, and for a self-hosted instance it is ssh -p 2222 -R 80:localhost:8080 example.com.

## Sources

- [antoniomika/sish on GitHub](https://github.com/antoniomika/sish)
- [License: MIT](https://github.com/antoniomika/sish/blob/main/LICENSE)
- [Project website](https://ssi.sh)
- [README](https://github.com/antoniomika/sish/blob/main/README.md)
- [Releases](https://github.com/antoniomika/sish/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/antoniomika-sish
