Securing your sh0 server
The same advice wherever your server is hosted: no provider-specific step, only what runs on the machine itself.
Overview
sh0 secures what it serves (HTTPS, encrypted secrets, roles), but the server under it is yours. Most compromised servers are not broken through the application: they are entered over SSH with a guessed password, or through a port nobody meant to expose. This guide closes those doors, in the order that never locks you out. sh0 checks the same points for you, live, on the Server security page of the dashboard and with sudo sh0 doctor.
Before you start
Keep your current SSH session open until the end, and test every change from a NEW session. From your computer, check that your key opens a session without a password:
ssh -o PasswordAuthentication=no root@SERVER_IPIf that session asks for a password, you have no key on the server yet. Install one, then test again:
ssh-copy-id root@SERVER_IPSSH: keys only
A server that accepts SSH passwords can be guessed, and root is the first account tried. Once your key works, refuse passwords:
cat > /etc/ssh/sshd_config.d/00-sh0-hardening.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOFValidate, then reload. Never restart sshd on a file that sshd -t rejects; a reload keeps the open sessions alive. The service is named ssh on Debian and Ubuntu, sshd elsewhere.
sshd -t && systemctl reload sshCheck the effective value for root from an outside address, then open a new session with your key before closing the current one:
sshd -T -C user=root,host=check.invalid,addr=203.0.113.1 | grep -Ei '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin) 'Firewall, and the Docker trap
Allow SSH, HTTP, HTTPS and the panel port, then enable ufw. If SSH listens on a port other than 22, allow that port first.
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443
ufw allow 9000/tcp
ufw --force enablePort 9000 stays open as long as the *.sh0.app addresses reach your server directly. Close it (sh0 serve --bind 127.0.0.1) only once the panel is served on its own domain over HTTPS.
cat > /usr/local/sbin/sh0-docker-user <<'EOF'
#!/bin/sh
# Docker-published ports: from the public interface, only replies get through.
IF=$(ip -o route get 1.1.1.1 | sed -n 's/.* dev \([^ ]*\).*/\1/p')
[ -n "$IF" ] || exit 1
iptables -C DOCKER-USER -i "$IF" -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN 2>/dev/null ||
iptables -I DOCKER-USER 1 -i "$IF" -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
iptables -C DOCKER-USER -i "$IF" -j DROP 2>/dev/null ||
iptables -I DOCKER-USER 2 -i "$IF" -j DROP
EOF
chmod 755 /usr/local/sbin/sh0-docker-user
cat > /etc/systemd/system/sh0-docker-user.service <<'EOF'
[Unit]
Description=sh0: filter Docker-published ports (DOCKER-USER)
After=docker.service
Requires=docker.service
PartOf=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/sh0-docker-user
[Install]
WantedBy=docker.service
EOF
systemctl daemon-reload
systemctl enable --now sh0-docker-user
iptables -S DOCKER-USERTo keep one published port reachable, add an exception before the DROP rule in /usr/local/sbin/sh0-docker-user:
iptables -I DOCKER-USER 2 -i "$IF" -p tcp -m conntrack --ctorigdstport 5432 -j RETURNIf your provider offers a network firewall in front of the server, whichever provider it is, use it as well: allow 22, 80, 443 (and 9000 while you rely on *.sh0.app), deny the rest. It filters before the traffic reaches the machine, Docker included.
fail2ban
With keys only, guessing attempts are harmless, but they fill the logs. fail2ban bans the addresses that keep trying:
apt-get install -y fail2ban python3-systemd
cat > /etc/fail2ban/jail.d/sh0-sshd.local <<'EOF'
[sshd]
enabled = true
backend = systemd
journalmatch = _COMM=sshd + _COMM=sshd-session
maxretry = 5
findtime = 10m
bantime = 1h
EOF
systemctl enable fail2ban
systemctl restart fail2ban
fail2ban-client status sshdUpdates and reboots
Install security updates, and let the system install them automatically:
apt-get update && apt-get upgrade
apt-get install -y unattended-upgrades && dpkg-reconfigure -plow unattended-upgradesWhen the kernel or a core library is updated, the fix only applies after a reboot (/var/run/reboot-required exists). Plan one when traffic is low: sh0 starts again with the system.
The panel over HTTPS
Without a domain of its own, the panel is served over plain HTTP and the owner password crosses the network unencrypted. Point a DNS A record at the server, then:
sh0 panel-domain set panel.example.comLet the installer do it
On a new server, the installer applies the SSH, firewall and fail2ban steps above when asked to. It is off by default; in a terminal, the installer also asks the question (no answer within 30 seconds means no).
curl -fsSL https://get.sh0.dev | bash -s -- --hardenCheck the result
The Server security page of the dashboard (owner and admin) shows these checks live, with the fix under each warning; a dot in the navigation signals an active warning. From a terminal on the server:
sudo sh0 doctor