-
Notifications
You must be signed in to change notification settings - Fork 0
Remote servers
On your own network the handset dials 192.168.x.x:22 and there is nothing to
set up. For anything else you need a bridge — the handset cannot run a VPN,
and many networks have no raw TCP entrance.
The bridge speaks HTTP on one side and TCP on the other. Put it on your VPN and it reaches everything there. The phone never joins; the bridge does.
handset ──ws://──▶ your proxy ──LAN──▶ wsbridge ──tcp:22──▶ servers
(VPN member)
Not the machine facing the internet — if that box is also a VPN member, compromising it hands over a VPN identity.
mkdir -p /opt/wsbridge
curl -fsSL -o /opt/wsbridge/wsbridge.py \
https://raw.githubusercontent.com/cobanov/berryssh/main/tools/wsbridge.pyThis is the layer that bounds the damage, and the one people skip.
Tailscale — tag it, and grant that tag only port 22 to chosen machines:
{
"tagOwners": { "tag:ssh-bridge": ["autogroup:admin"],
"tag:phone-reachable": ["autogroup:admin"] },
"grants": [
{ "src": ["tag:ssh-bridge"], "dst": ["tag:phone-reachable"], "ip": ["tcp:22"] }
]
}tailscale up --auth-key=tskey-... --advertise-tags=tag:ssh-bridgeWireGuard or OpenVPN: outbound firewall rules on the bridge host instead. No VPN: skip, and rely on the remaining layers.
head -c 24 /dev/urandom | base64 # the key; 16 chars minimum/opt/wsbridge/config.json, then chmod 600:
{
"listen": 8022,
"bind": "127.0.0.1",
"psk": "the string above",
"targets": {
"web": ["100.64.0.3", 22],
"home": ["192.168.1.50", 22]
}
}Names are what the handset shows; addresses are never sent to it. Nothing
outside targets is ever dialled.
# /etc/systemd/system/wsbridge.service
[Unit]
After=network-online.target
[Service]
ExecStart=/usr/bin/python3 /opt/wsbridge/wsbridge.py /opt/wsbridge/config.json
Restart=always
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload && systemctl enable --now wsbridgeCaddy needs nothing extra:
ssh.example.com:80 {
reverse_proxy 192.168.1.10:8022
}
nginx needs the upgrade headers, and the timeout or an idle session dies every minute:
location / {
proxy_pass http://192.168.1.10:8022;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
}The hostname must not redirect to HTTPS.
Host ssh.example.com, Port 80, WebSocket path /, Bridge
key the string from step 3. Then open Bridge target — it asks the bridge
and lists the names. No address is ever typed into the phone.
You are putting a path to sshd on the internet. Three layers, each holding if
the others fail: the VPN policy bounds what the bridge can reach at all;
the bridge key means nothing is disclosed, not even the list of machines,
before it is proved; SSH decides who logs in and whether the server is
real.
The outer layer is plaintext — the device has no TLS 1.2 stack and its trust store predates every current CA. SSH already provides confidentiality, integrity and both identities, so the bridge carries ciphertext it cannot read and a tampered bridge is refused by the handset. What leaks is metadata: that you connected, and which name you asked for.
(This is also why Tailscale Funnel cannot be used — TLS only, and Let's Encrypt roots the handset does not have.)