DEV Community

Recca Tsai
Recca Tsai

Posted on • Originally published at recca0120.github.io

sslh: Run HTTPS and SSH on Port 443 at the Same Time

Originally published at recca0120.github.io

The corporate or university network only allows ports 80 and 443 out. SSH on 22 is blocked.
Getting back to your home server means VPN or a web terminal, both annoying.
sslh lets your server accept both HTTPS and SSH on 443. The firewall sees 443. SSH is hidden inside it.

How It Works

sslh sits in front of port 443. When a connection arrives, it reads the first packet, identifies the protocol, and forwards to the right backend.

Incoming connection → 443
                          ↓
                       sslh (inspects first packet)
                          ├── SSH packet  → local :22
                          └── TLS packet  → local nginx:443
Enter fullscreen mode Exit fullscreen mode

From the SSH client's perspective, it connected to 443 and got SSH. From nginx's perspective, it's still listening on its own port. sslh sits between them.

Supported protocols: SSH, TLS/HTTPS, HTTP, OpenVPN, SOCKS5, tinc, XMPP, and custom regex patterns.

Installation

# Ubuntu / Debian
sudo apt install sslh

# Arch
sudo pacman -S sslh

# macOS
brew install sslh

# Docker
docker pull ghcr.io/yrutschle/sslh:latest
Enter fullscreen mode Exit fullscreen mode

Quick Start: Command Line

Test it first before setting up a service:

# Port 443 accepts SSH (→ local 22) and HTTPS (→ local nginx 8443)
sudo sslh --listen=0.0.0.0:443 \
          --ssh=127.0.0.1:22 \
          --tls=127.0.0.1:8443
Enter fullscreen mode Exit fullscreen mode

Before running this, move nginx off 443 to 8443 (sslh now owns 443):

# /etc/nginx/sites-available/default
server {
    listen 8443 ssl;  # moved from 443
    # everything else stays the same
}
Enter fullscreen mode Exit fullscreen mode

Config File

For production, use a config file at /etc/sslh.cfg:

# /etc/sslh.cfg

verbose: 0;
foreground: false;
inetd: false;
numeric: false;
transparent: false;

# sslh listens here
listen:
(
    { host: "0.0.0.0"; port: "443"; }
);

# Protocol routing
protocols:
(
    { name: "ssh";     host: "127.0.0.1"; port: "22";   log_level: 0; },
    { name: "tls";     host: "127.0.0.1"; port: "8443"; log_level: 0; },
    # catch-all: anything unrecognized goes to nginx
    { name: "anyprot"; host: "127.0.0.1"; port: "8443"; log_level: 0; }
);
Enter fullscreen mode Exit fullscreen mode

Enable and start:

sudo systemctl enable sslh
sudo systemctl start sslh
sudo systemctl status sslh
Enter fullscreen mode Exit fullscreen mode

Client Connection

SSH to port 443:

# Explicit port
ssh -p 443 user@yourserver.com

# Or set it permanently in ~/.ssh/config
Host myserver
    HostName yourserver.com
    Port 443
    User yourname
Enter fullscreen mode Exit fullscreen mode

After that, ssh myserver works without -p 443. HTTPS needs no changes — browsers connect to https://yourserver.com as usual.

Docker Compose

If you run services in Docker, sslh fits naturally as a container in front of nginx:

# docker-compose.yml
services:
  sslh:
    image: ghcr.io/yrutschle/sslh:latest
    ports:
      - "443:443"
    command: >
      --foreground
      --listen=0.0.0.0:443
      --tls=nginx:443
      --ssh=sshd:22
    depends_on:
      - nginx
      - sshd
    cap_add:
      - NET_RAW
      - NET_BIND_SERVICE
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    # nginx doesn't expose 443 externally — sslh handles that
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf

  sshd:
    image: linuxserver/openssh-server
    environment:
      - PUBLIC_KEY_FILE=/keys/authorized_keys
    volumes:
      - ./keys:/keys
Enter fullscreen mode Exit fullscreen mode

Neither nginx nor sshd need to expose ports externally. sslh multiplexes everything.

Adding OpenVPN

If OpenVPN is also blocked, sslh handles that too:

protocols:
(
    { name: "ssh";     host: "127.0.0.1"; port: "22";   },
    { name: "openvpn"; host: "127.0.0.1"; port: "1194"; },
    { name: "tls";     host: "127.0.0.1"; port: "8443"; },
    { name: "anyprot"; host: "127.0.0.1"; port: "8443"; }
);
Enter fullscreen mode Exit fullscreen mode

Set your OpenVPN client's remote to yourserver.com 443. sslh identifies the OpenVPN handshake and forwards to 1194.

sslh vs Other Approaches

Approach Pros Cons
sslh Lightweight, multi-protocol, transparent to clients Needs root, backend loses real client IP (needs transparent mode)
}}">reverse_ssh Target machine doesn't need a public IP Requires a relay server
HAProxy More powerful, PROXY protocol support More complex config
Nginx stream Simple, native TLS SNI routing Can't demux SSH from TLS

If the goal is "SSH through a firewall," sslh is the most direct solution. If the target machine has no public IP at all, }}">reverse_ssh is a different approach worth knowing.

Things to Know

Backend loses real client IP: By default sslh NATs connections, so backend services see 127.0.0.1 instead of the real client IP. If you need real IPs for fail2ban or access logs, enable transparent proxy mode — it requires extra iptables rules.

Move nginx off 443 first: sslh needs to own 443. Whatever was listening there before needs to move to another port.

CVE history: sslh has had a few CVEs over the years. Keep it updated; not a reason to avoid it.

Summary

Multiple services, one port. sslh routes by inspecting the first packet. The most common setup is SSH on 443, getting through firewalls that only allow HTTPS. Once configured, SSH clients just change port from 22 to 443 — everything else stays the same.

References

Top comments (0)