Two firewalls, and the one that surprises people
Most cloud providers hand you a bare instance and leave filtering entirely to you. Vultr does not: its images come with an OS-level firewall already enabled, and which one depends on the distribution. Rocky and AlmaLinux images use firewalld. Ubuntu and Debian images use ufw, nftables or iptables. FreeBSD uses IPFW or IP Filter, OpenBSD uses pf.
The practical consequence is a support question people ask constantly without realising the cause: an application is listening, the Vultr Firewall Group allows the port, and connections still time out. The OS firewall on the box is dropping them, and nobody configured it, so nobody thinks to look.
The reverse also catches people. Loosening the OS firewall to debug something does not open the port if a Firewall Group is attached and does not allow it. Two layers, both silent, and the error looks identical from the client.
Find out what is actually filtering, in thirty seconds
Before changing anything, establish which layer is in play. Run these on the instance.
- systemctl is-active firewalld ufw nftables 2>/dev/null tells you which OS firewall is running. On a Vultr image the answer is usually not "none".
- For firewalld: firewall-cmd --list-all shows the active zone, its allowed services and ports.
- For ufw: ufw status verbose. For nftables: nft list ruleset. For iptables: iptables -L -n -v --line-numbers.
- ss -ltnp shows what is actually listening and on which address. A service bound to 127.0.0.1 is unreachable regardless of any firewall, which is a third cause of the same symptom.
- Then check the Vultr Console for the Firewall Group attached to the instance. If the OS firewall allows the port and the connection still fails, that is where to look.
Using Vultr Firewall Groups well
A Firewall Group is a reusable set of IPv4 and IPv6 rules that you attach to instances, rather than a per-instance configuration. That is the feature worth exploiting: define one group per role, a web group, a database group, a bastion group, and attach it to every instance of that role. Changing the rule once changes it everywhere, which is how you avoid the drift where five servers that should be identical each have slightly different rules.
Manage them through the Console, the API, the CLI or Terraform. If you are building instances repeatedly, define the group in Terraform alongside the instance so a rebuilt server cannot come up with no group attached, which is the most common way an instance ends up more exposed than intended.
Keep the OS firewall on as well. Two layers is not redundancy for its own sake here: a Firewall Group can be detached, and an instance rebuilt from a snapshot may not come back with the same one, and in either case the OS firewall is the only thing left.
What to harden on the instance itself
Vultr images are clean rather than hardened, which is the right choice for a provider. This is the list that catches the real problems.
- SSH: set PasswordAuthentication no and PermitRootLogin prohibit-password, after confirming your key works in a session you keep open. If you deployed with a password rather than an SSH key, this is the first thing to change.
- Check the effective SSH config with sshd -T rather than reading sshd_config, because a drop-in in /etc/ssh/sshd_config.d/ placed there by the image can override the file you edited.
- Enable unattended security upgrades, or run a patching process you actually follow.
- Bind internal services to 127.0.0.1 or to the VPC address, never 0.0.0.0, and verify with ss -ltnp.
- Confirm the clock is synchronised: timedatectl show should report NTP=yes and NTPSynchronized=yes. Drifting clocks make logs useless as evidence after an incident.
- If you use a VPC for instance-to-instance traffic, still require authentication on internal services. A private network is a network, not a trust boundary.
What neither firewall can do
A firewall decides what may reach a port. It has no opinion about what happens next, and most real incidents happen next.
Neither the Vultr Firewall Group nor the OS firewall notices a successful SSH login with a stolen key, a new setuid binary, a webshell written into a web root through an application vulnerability, a package carrying a known CVE, a change to sudoers, or a process making outbound connections to somewhere it has never contacted. All of that passes through correctly configured firewalls, because it is either not network traffic at all or it is traffic you deliberately allowed.
SecAI covers that layer on a Vultr instance: authentication and privilege changes, file integrity, package vulnerabilities, webshells, rootkit indicators and outbound anomalies, with attacking sources blocked within seconds and fixes that verify themselves and roll back if they break a service. One install command, and it does not care which provider the server is on.