sish: Ngrok self-hosted alternative

sish: Ngrok self-hosted alternative

Introduction of sish project as a self-hosted alternative to ngrok for secure tunneling with TLS support for HTTPS, WSS, and TCP using custom domain.

sish: Ngrok self-hosted alternative

I am not affiliated with the sish project in any way. I just find it to be a great solution for secure tunneling and wanted to share it with you. I have been using sish for a while now, and I am very happy with it. It is a great alternative to ngrok, offering full control over your data and traffic.

sish Introduction

sish is an open-source project created by Antonio Mika, that allows you to create secure tunnels to your local services, similar to ngrok. There is no need to install additional client software on developer machines, as it uses the standard SSH protocol to create tunnels

If you want to expose their local services to the internet, they usually use ngrok, since it is the first result on search engines. However, ngrok is a proprietary solution. You have to trust a third party with your data and traffic. Additionally, the free tier has limitations that may not suit all use cases.
The most problematic aspect of ngrok is that all traffic is routed through their servers, with TLS termination handled on their infrastructure, which poses the risk of unencrypted traffic between ngrok servers and your local services. This can be a significant security concern, especially if you are handling sensitive data.

There are various self-hosted alternatives, but many require installing additional client software on developer machines, which can be a barrier to adoption, or are not easily self-hosted. sish stands out as it uses the standard SSH protocol, which is already available on most systems, making it easy to use without additional software.

If you accept the risk of exposing your server to the internet and can tolerate time-to-time downtime (due to maintenance), you can have a fully functional tunneling solution at minimal cost and maximum control over the data. Usually, you can use the cheapest VM on any cloud provider to host sish.

For a more detailed introduction, check the official documentation: https://docs.ssi.sh/how-it-works

Usage of sish

I will demonstrate here the basic usage of sish to create a tunnel to a local service, so you can see how it works in practice and if it fits your use case. More advanced features and use cases can be found in the official cheatsheet: https://docs.ssi.sh/cheatsheet

This usage demonstration will already use the server that was created as part of the tutorial.

To create a tunnel, you run the following SSH command:

1
2
3
4
5
6
7
8
ssh -R usage-demo:80:localhost:9090 dt
    ^       ^      ^      ^     ^   ^
    |       |      |      |     |   +-- SSH alias of the `sish` server, defined in `~/.ssh/config`
    |       |      |      |     +------ Port on the local machine where the service is running
    |       |      |      +------------ Your local machine. `localhost` is fine in all cases
    |       |      +------------------- Port of the HTTP service on the `sish` server
    |       +-------------------------- Subdomain to be assigned to the tunnel. It will be accessible at `https://usage-demo.dt.smtl.cz`
    +---------------------------------- Reverse shell connection to the `sish` server

Output of this command will look like this:

1
2
3
4
5
Press Ctrl-C to close the session.

Starting SSH Forwarding service for http:80. Forwarded connections can be accessed via the following methods:
HTTP: http://usage-demo.dt.smtl.cz
HTTPS: https://usage-demo.dt.smtl.cz

Head to the provided URL in your browser, and you should see the service running on your local machine, but accessible through the sish tunnel and secured with a certificate. usage demo website

The subdomain does not need to be provided. If you do not specify it, sish will generate a random one for you:

1
2
3
4
5
6
7
$ ssh -R 80:localhost:9090 dt
Press Ctrl-C to close the session.

The subdomain localhost.dt.smtl.cz is unavailable. Assigning a random subdomain.
Starting SSH Forwarding service for http:80. Forwarded connections can be accessed via the following methods:
HTTP: http://k9x.dt.smtl.cz
HTTPS: https://k9x.dt.smtl.cz

To simplify the usage to have ngrok-like experience, you can create a custom alias in your zsh/bash rc file:

1
2
3
4
5
6
7
8
sish() {
    port="${1:-80}"
    domain="${2:-${HOST}}"
    domain="${domain//./-}:" # Replace dot with dash and add colon at the end
    
    echo "Opening local tunnel on port ${port}"
    ssh -R "${domain}80:localhost:${port}" dt
}

This simple function will make the tunnel creation as simple as this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ sish 9090
Opening local tunnel on port 9090
Press Ctrl-C to close the session.

Starting SSH Forwarding service for http:80. Forwarded connections can be accessed via the following methods:
HTTP: http://mctrouba.dt.smtl.cz
HTTPS: https://mctrouba.dt.smtl.cz

Connection to localhost closed by remote host.
Connection to localhost closed.
$ sish 9090 my-awesome-service
Opening local tunnel on port 9090
Press Ctrl-C to close the session.

Starting SSH Forwarding service for http:80. Forwarded connections can be accessed via the following methods:
HTTP: http://my-awesome-service.dt.smtl.cz
HTTPS: https://my-awesome-service.dt.smtl.cz

To keep your muscle memory in place, you can name the function ngrok instead of sish, so you can use the same command as before, but it will use your self-hosted sish server instead of ngrok.

Setup

I will use the cheapest VM on Azure with manual wildcard certificate from Let’s Encrypt for this demo. You can use any cloud provider or your own hardware to host sish with automatic certificate renewal (using https://github.com/adferrand/dnsrobocert, for example).

Whenever you see dt.smtl.cz, replace it with your own domain.

Step 1: Prepare the VM

The Internet is already full of tutorials on how to create a new VM and install Docker on it, so I do not see the point of going into detail. Follow your best practices or an internet tutorial to create a new Ubuntu VM with Docker and Docker Compose installed.

When you have Ubuntu and Docker ready, allow the following ports in the VM firewall:

  • 22 - Usual SSH port for server management
    • Should be limited to your management IPs only
  • 2222 - sish SSH port for tunnel creation
    • Should be limited to your developers/VPN IPs only
  • 80 - HTTP port for HTTP traffic forwarding
    • Should be publicly available
  • 443 - HTTPS port for HTTPS traffic forwarding
    • Should be publicly available

Step 2: Domain configuration

The DNS configuration process will vary depending on your DNS provider. You need to create two DNS records:

  1. Record of type A
    • Name: dt.smtl.cz
    • Value: <Public_IP_Of_VM>
  2. Record of type CNAME
    • Name: *.dt.smtl.cz
    • Value: dt.smtl.cz
DNS records configuration DNS records configuration

Step 3: Request a wildcard certificate

I use WEDOS for my DNS configuration, so I will use DNS-01 challenge to request the certificate. Consult your DNS provider documentation on DNS-01 support.

To create a simple certificate, you can use lego client in a Docker container. Make sure to replace the environment variables with your own values. All lego providers can be found on their official website: https://go-acme.github.io/lego/dns/index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
docker run --rm -ti -v "${PWD}:/data" -w /data \
  -e WEDOS_USERNAME="<EMAIL>" \
  -e WEDOS_WAPI_PASSWORD="<WAPI_PASSWORD>" \
  goacme/lego \
    --accept-tos \
    --email='Your mail' \
    --dns="wedos" \
    --domains='*.dt.smtl.cz' \
    --domains='dt.smtl.cz' \
    run

It should look like this: lego output

Step 4: Create required configurations

As soon as the certificate is ready, we can move to the next step and create the required configuration files for sish.

Connect to the VM and create the following directory structure:

You do not need to follow the directory structure I use, but make sure to update the paths in the configuration files accordingly.

mkdir -p /srv/sish/{certs,keys,pubkeys,ssh-keys}

1
2
3
4
5
6
7
8
/srv/
└── sish/
    ├── certs/
    ├── keys/
    ├── pubkeys/
    ├── ssh-keys/
    ├── compose.yml
    └── config.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  sish:
    image: antoniomika/sish:latest
    container_name: sish
    restart: unless-stopped
    volumes:
      - ./pubkeys:/pubkeys
      - ./keys:/keys
      - ./certs:/ssl
      - ./config.yml:/app/config.yml
    ports:
      - "2222:2222"
      - "80:80"
      - "443:443"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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
bind-random-subdomains: false  # Configuring this to false will allow users to specify custom domain
domain: dt.smtl.cz
authentication: true

append-user-to-subdomain: false # Configuring this to false will allow users to specify custom domain
append-user-to-subdomain-separator: "-"

Copy the certificate and private key to the certs directory.

1
cp '.lego/certificates/'*'dt.smtl.cz.'{crt,key} './certs/'

Add the public keys of all users who will have access to create tunnels to the pubkeys directory. Each key should be in a separate file with a .pub extension.

Step 5: Run the stack and verify

While inside the /srv/sish directory, run docker compose up -d to start the sish server. Check the logs with docker logs -f sish to verify that the server is running without errors.

docker compose logs -f docker compose logs -f

Just to make sure everything is working, you can run:

1
2
3
4
$ nc -zv localhost 80; nc -zv localhost 443; nc -zv localhost 2222
Connection to localhost (127.0.0.1) 80 port [tcp/http] succeeded!
Connection to localhost (127.0.0.1) 443 port [tcp/https] succeeded!
Connection to localhost (127.0.0.1) 2222 port [tcp/*] succeeded!

The same test can be done from your local machine to verify that the ports are open and accessible from your location:

1
2
3
4
$ nc -zv dt.smtl.cz 80; nc -zv dt.smtl.cz 443; nc -zv dt.smtl.cz 2222
Connection to dt.smtl.cz port 80 [tcp/http] succeeded!
Connection to dt.smtl.cz port 443 [tcp/https] succeeded!
Connection to dt.smtl.cz port 2222 [tcp/rockwell-csp2] succeeded!

Step 6: Test the forwarding

To test the forwarding, we will create a simple tunnel for whoami service running locally. This service will return the request details, so we can verify that traffic is properly forwarded through the tunnel.

  1. Start whoami container:
    • docker run -d -p 9090:80 --name iamfoo traefik/whoami
  2. User browser or curl to visit http://localhost:9090 and verify that you see the whoami page.
    • whoami page
  3. Open the sish tunnel:
    • ssh -R whoami-test-jc:80:localhost:9090 -p 2222 dt.smtl.cz
    • You should get a similar output:
    • sish tunnel output
  4. Visit the URL https://whoami-test-jc.dt.smtl.cz in your browser
    • You should see the same whoami page, but this time the traffic is forwarded through the sish tunnel and secured with a certificate.
    • whoami page through sish tunnel

If you face some issues, check the sish logs with docker logs -f sish to see if there are any errors or warnings.

Wrap up

You successfully configured a self-hosted alternative to ngrok using sish. You can now create tunnels to your local services and access them securely over the internet. Remember to monitor the server and keep it up to date to ensure security and stability.

Consult the official configuration example file for all available configuration options, and adjust it to your needs.

✨ Bonus: Authentication using OS users (smallstep compatible)

Securing sish could be achieved by various methods like manually generated SSH keys, Smallstep SSH certificates, or VPN.

If you have an existing authentication system used for connecting to Ubuntu VMs, you can leverage it for sish authentication. This way, you don’t need to manage separate keys for sish in the local directory (/pubkeys).

To leverage OS level auth, do the following:

  1. Use configuration authentication: false in the sish configuration file
  2. Edit docker compose.yml to expose the SSH port of sish service only on localhost:
  • 1
    2
    
      ports:
          - "127.0.0.1:2222:2222"

This will expose the internal SSH port of sish only on localhost, so it is not accessible externally.
To use sish in this setup, you need to use the Docker host system as a ProxyJump for the SSH connection.

Previous example command for creating the tunnel, that is adjusted for this setup, would look like this:

1
2
3
ssh -J ansible@dt.smtl.cz -R whoami-test-jc:80:localhost:9090 -p 2222 localhost
# Before:
# ssh -R whoami-test-jc:80:localhost:9090 -p 2222 dt.smtl.cz

When you use this command, the SSH client first connects to the Docker host system (dt.smtl.cz), using the OS user ansible, then connects to the sish service running on localhost via port 2222. This way, you can leverage your existing authentication system based on OS users without the need to manage separate keys for sish.

SSH connection with jump host SSH connection with jump host

To make it easier to use, you can create an SSH config file with the following content:

1
2
3
4
5
6
7
8
Host dt-jump
    HostName dt.smtl.cz
    User ansible
Host dt
    ProxyJump dt-jump
    HostName localhost
    User CouldBeAnything # Will help to identify the connection in sish logs
    Port 2222

The final command for creating the tunnel would look like this:

ssh -R whoami-test-jc:80:localhost:9090 dt

Connection with SSH config entry Connection with SSH config entry

✨ Bonus 2: Setup automatic certificate renewal

Having a manually generated certificate is ideal only for testing purposes. For production use, you should set up automatic certificate renewal to ensure your certificate remains valid and you don’t have to worry about it expiring.

The goal is to have a cron job that runs the certificate renewal command periodically and updates the certificate files in the certs directory.
Since we need to restart the sish container after the certificate is renewed, we need to have lego accessible on the Docker host and create a simple script that handles the renewal and restarts the container.

First, install lego on the Docker host systems.

For the demo, I prefer getting the binary from the Docker image, but you should follow your internal best practices for installing (and keeping up to date) software on your servers.

docker run --rm -ti -v "${PWD}:/data" -w /data --entrypoint sh goacme/lego -c 'cp /lego /data/lego'

This command copied the lego binary to the current directory, so you can move it to a more suitable location.

mv ./lego /usr/local/bin/lego

Create a script that will handle the certificate renewal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh

cd "/srv/sish" || exit 10

# Follow your best-practices for loading a secret values (ideally from some keyvault)
export WEDOS_USERNAME="..."
export WEDOS_WAPI_PASSWORD="..."
export LEGO_EMAIL="..."
DOMAIN="..."
PROVIDER="wedos"
RESOLVER="ns.${PROVIDER}.com:53"

# Renew the cert and restart the container using docker client when renewed
lego \
    --accept-tos \
    --dns="${PROVIDER}" \
    --domains="*.${DOMAIN}" \
    --domains="${DOMAIN}" \
    --dns.resolvers="${RESOLVER}" \
    renew \
    --renew-hook='./hooks/copy-restart.sh'

Create the hook script that will copy the renewed certificate to the sish certs directory and restart the container:

1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

cd '/srv/sish' || exit 10

# You can adjust it by adopting the environment variables as specified here: https://go-acme.github.io/lego/usage/cli/obtain-a-certificate/index.html#running-a-script-afterward 
cp '.lego/certificates/'*'.cz.'{crt,key} './certs/'
docker compose restart sish

echo 'Certificate renewal successful, sish container restarted'

Make both scripts executable: chmod +x /srv/sish/hooks/*.sh

Finally, create a cron job that will run the renewal script periodically.

Always pick a random hour and minute to run the renewal to avoid overloading of the ACME servers. See https://go-acme.github.io/lego/usage/cli/renew-a-certificate/index.html#automatic-renewal for more information.

29 2 * * * /srv/sish/hooks/renew-cert.sh >> /srv/sish/renew.log 2>&1

This post is licensed under CC BY 4.0 by the author.