Securing the system
Provider trust boundary
Not actionable from here, but worth stating explicitly rather than leaving implicit: physical security of the datacenter, and isolation between tenants sharing the same hypervisor, are Infomaniak's responsibility, not something this server's own configuration can touch. Knowing where self-hosting's actual control ends is part of a complete picture, even when there's nothing left to configure at this layer.
Firewalls
OpenStack security layer
The security group (security.tofu) controls what can reach the box at all — everything closed by default except:
- 22 (SSH) — administrative access to the box itself.
- 80 (HTTP) — Let's Encrypt's ACME HTTP-01 challenge needs it, and it's where a plain HTTP request lands before being redirected to HTTPS.
- 443 (HTTPS, TCP and UDP) — every deployed service's actual public traffic, routed through the reverse proxy; UDP alongside TCP for HTTP/3 (QUIC).
Mostly already set correctly — worth actually confirming that again periodically rather than assuming it stays that way unreviewed.
Host firewall
A host-level firewall (something like ufw or nftables, running on the box itself) filters traffic the same way the security group already does — allow or deny by port. Not doing this: since it'd cover the exact same 22/80/443 the security group already blocks, it would just be the identical rule enforced twice rather than catching anything different. As such using these generates more maintenance burden than actual safety, especially because the Docker network model bridges it entirely.
Docker bridge network
Docker uses its own bridge network for containers, inserting NAT rules straight into the kernel's FORWARD chain — not INPUT, which is what a host firewall manages — so a host firewall never sees this traffic at all. DOCKER-USER is Docker's own reserved chain for adding rules here, never touched by Docker itself.
This bypass only applies to a host firewall, though: the OpenStack security group is enforced entirely outside the VM, before traffic even reaches it, so nothing Docker does inside the guest can affect it either way. Limiting what the DOCKER-USER can do can only conflict with what's running in Coolify, which changes frequently.
SSH maintenance
Against brute force / guessing specifically — see Framework → Credentials for the reasoning.
Naming
A key's comment field is cosmetic — safe to edit in place without touching the key material or breaking any existing connection. Worth doing anyway: an unnamed key becomes hard to recognize, and a key nobody can identify tends to get over-scoped, since revoking or narrowing it requires first knowing what it's actually for. See Security → Security architecture → SSH credentials for the concrete case.
- Rename it in
authorized_keys, everywhere it's authorized — a key can be authorized against more than one account, and each line needs its own edit:sudo sed -i 's/<old-comment>/<new-comment>/' /root/.ssh/authorized_keyssudo sed -i 's/<old-comment>/<new-comment>/' /home/debian/.ssh/authorized_keys - Confirm:
sudo cat /root/.ssh/authorized_keyssudo cat /home/debian/.ssh/authorized_keys
- Rename the matching entry in Proton Pass too, so the two stay in sync.
SSH hardening
PasswordAuthentication no— key-only login, removing password guessing as an attack vector entirely rather than just making it harder.PermitRootLogin prohibit-password— root SSH stays reachable, but key-only, never by password. Closing it off entirely (no) was the original plan, moving Coolify's own connection to a dedicated non-root account first — but that migration was tried and reverted after it broke real deploys (a structural gap in Coolify's own deploy pipeline, not something fixable on this end), so root SSH stays in use and gets hardened instead of removed.MaxAuthTries 3— caps how many guesses a single connection gets before it's dropped.AllowUsers debian root— narrows the login surface to exactly the accounts actually meant to connect:debianfor a human,rootfor Coolify's own server-management connection. See Security → Security architecture → SSH credentials for which keys that actually governs.
- Check what's actually in effect right now, before changing anything —
sshd_config.d/drop-ins can override the main file, so the file's contents alone don't guarantee what's live:sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin|maxauthtries|allowusers' - Edit
/etc/ssh/sshd_config:PasswordAuthentication noPermitRootLogin prohibit-passwordMaxAuthTries 3AllowUsers debian root - Restart to apply:
sudo systemctl restart sshd
- Confirm it actually landed, same command as step 1:
sudo sshd -T | grep -E 'passwordauthentication|permitrootlogin|maxauthtries|allowusers'
Rate limiting
MaxAuthTries only caps guesses within one connection — nothing above stops the same source from just reconnecting and trying again. fail2ban closes that gap: it watches sshd's own logs and bans a source IP at the firewall level after too many failures across a time window, not just within a single session.
- Install it:
sudo apt install fail2ban
- Enable the SSH jail in
/etc/fail2ban/jail.local, notjail.conf— local overrides survive package updates, the shipped config doesn't:[sshd]enabled = truemaxretry = 3findtime = 10mbantime = 1h - Restart to apply:
sudo systemctl restart fail2ban
- Confirm the jail is actually active:
sudo fail2ban-client status sshd
Updating
apt update && apt upgrade — see Coolify → FAQ for the actual commands. Not yet decided whether this stays a manual, reviewed cadence or moves to unattended-upgrades: manual review costs attention but catches anything an automated run might apply blindly; automatic keeps security patches from lingering unapplied between checks.