Hardening a Forge-Provisioned Server
One of my shared Hetzner boxes runs several production Laravel apps, provisioned through Forge like most of my servers. Forge gets you a long way — firewall, SSH keys, PHP-FPM pools, Nginx vhosts — but a few defaults on the underlying Ubuntu 24.04 image are looser than they need to be. None of these are exotic; they're the kind of thing you'd catch in any general server-hardening checklist. I wrote a script to close them and ran it against that box.
PostgreSQL listens on every interface
Default provisioning ships `listen_addresses = '*'` in postgresql.conf, even though every app on the box connects over localhost. pg_hba.conf compounds it with a blanket `host all all 0.0.0.0/0 md5` rule — anyone who reaches port 5432 can attempt password auth against any database on the server. The loopback-only rules Forge also ships are the only ones actually needed.
sed -i "s/^listen_addresses = .*/listen_addresses = 'localhost'/" "$PG_CONF"
sed -i "/^host[[:space:]]\+all[[:space:]]\+all[[:space:]]\+0\.0\.0\.0\/0[[:space:]]\+md5/d" "$PG_HBA"
systemctl restart postgresql
Memcached binds to 0.0.0.0 too
Same story, different service: the IPv6 line in memcached.conf is correctly loopback-only (`-l ::1`), but the IPv4 line ships as `-l 0.0.0.0`. Memcached has no built-in auth, so the firewall is the only thing standing between "internal cache" and "anyone on the internet with read/write access to it" — worth not depending on that being the sole layer.
sed -i 's/^-l 0\.0\.0\.0/-l 127.0.0.1/' /etc/memcached.conf
systemctl restart memcached
SSH ships X11 forwarding enabled
Unused on a headless server, on by default, zero functional cost to disable. A small drop-in in sshd_config.d keeps it out of the main config file Forge manages.
cat > /etc/ssh/sshd_config.d/50-harden.conf << 'EOF'
X11Forwarding no
EOF
sshd -t && systemctl reload ssh.service
One sysctl tweak
This host isn't a router, so it shouldn't be sending ICMP redirects.
cat > /etc/sysctl.d/60-harden-send-redirects.conf << 'EOF'
net.ipv4.conf.all.send_redirects = 0
EOF
sysctl -p /etc/sysctl.d/60-harden-send-redirects.conf
What I deliberately left alone
Hardening scripts have a habit of turning into checklists nobody reads, so I only wanted things that don't cost anything to change. Two defaults I left untouched, on purpose:
PermitRootLogin prohibit-password— Forge's own worker key (worker@forge.laravel.com) is trusted for root on provisioned servers, and it's what powers panel features that fall outside the site-user's sudoers whitelist: SSL/Let's Encrypt, the Firewall tab, Daemons, database management, PHP version installs, Recipes. SettingPermitRootLogin nobreaks all of that. Password auth for root was already disabled globally, so this isn't a real exposure either way — just a Forge-integration dependency that isn't obvious until you go looking for it.- Redis without
requirepass— it's already loopback-only and firewalled by default. Adding a shared password wouldn't add real isolation on a single-OS-user box, since every app would just end up storing the same secret in its.envanyway.
The full script
Idempotent, safe to re-run, and skips gracefully on servers that don't run a given service. Run as root, or via sudo, right after provisioning.
#!/usr/bin/env bash
# forge-hardening.sh
#
# A handful of hardening gaps found on a default Forge-provisioned Ubuntu
# 24.04 server (single-site-user, PostgreSQL + Memcached + Redis stack).
# None of these should affect Forge's own management of the box or any
# deployed site — every change was verified against a live server with
# several production Laravel apps before/after.
#
# Idempotent: safe to re-run. Run as root (or via sudo).
set -euo pipefail
echo "==> 1/4: PostgreSQL — restrict to localhost"
PG_CONF=$(find /etc/postgresql -maxdepth 2 -name postgresql.conf 2>/dev/null | sort -V | tail -1)
PG_HBA=$(find /etc/postgresql -maxdepth 2 -name pg_hba.conf 2>/dev/null | sort -V | tail -1)
if [[ -n "${PG_CONF:-}" && -n "${PG_HBA:-}" ]]; then
cp -n "$PG_CONF" "${PG_CONF}.bak-$(date +%Y%m%d)"
cp -n "$PG_HBA" "${PG_HBA}.bak-$(date +%Y%m%d)"
sed -i "s/^listen_addresses = .*/listen_addresses = 'localhost'/" "$PG_CONF"
sed -i "/^host[[:space:]]\+all[[:space:]]\+all[[:space:]]\+0\.0\.0\.0\/0[[:space:]]\+md5/d" "$PG_HBA"
systemctl restart postgresql
else
echo " postgresql not installed on this server, skipping"
fi
echo "==> 2/4: Memcached — bind to loopback only"
if [[ -f /etc/memcached.conf ]]; then
cp -n /etc/memcached.conf /etc/memcached.conf.bak-"$(date +%Y%m%d)"
sed -i 's/^-l 0\.0\.0\.0/-l 127.0.0.1/' /etc/memcached.conf
systemctl restart memcached
else
echo " memcached not installed on this server, skipping"
fi
echo "==> 3/4: SSH — disable unused X11 forwarding"
mkdir -p /etc/ssh/sshd_config.d
cat > /etc/ssh/sshd_config.d/50-harden.conf << 'EOF'
X11Forwarding no
EOF
sshd -t
systemctl reload ssh.service 2>/dev/null || systemctl reload sshd.service
echo "==> 4/4: sysctl — disable ICMP send_redirects"
cat > /etc/sysctl.d/60-harden-send-redirects.conf << 'EOF'
net.ipv4.conf.all.send_redirects = 0
EOF
sysctl -p /etc/sysctl.d/60-harden-send-redirects.conf
echo "==> Done."
None of this is groundbreaking — it's the same handful of items you'd find on any general Ubuntu hardening checklist. But it's exactly the kind of thing that's easy to forget between spinning up a new server and deploying the first site to it, which is why it's now a script instead of a mental checklist. If you run Forge, it'd make a solid addition to the default provisioning script.