In short: A firewall for web servers allows only TCP 80 and 443 inbound, with HTTP/3 additionally UDP 443, and SSH only from known networks. Everything else is dropped: databases, caches, PHP-FPM and admin interfaces do not belong on the internet. First use ss -tulpn to check what is listening, then build a rule set with the default “drop” and test the result from outside. Restricting outgoing connections additionally limits the consequences of a break-in.
The forgotten doors next to the front door
With PHP2Shell, attention turned to the web server itself, to ports 80 and 443. These must be open, otherwise there is no website. But in inventories after such vulnerabilities, something else often stands out: next to the front door, further doors are open. A database listens on all network interfaces, a PHP-FPM pool on port 9000, a cache service without a password, an admin interface from the setup phase.
Each of these doors is an attack surface of its own with its own vulnerabilities. A firewall that allows only what is necessary closes them with a few rules. This is one of the most effective measures when securing your own server, as the page Securing root servers and vServers yourself also describes.
What should be open, what not?
The principle is an allowlist: everything is closed, and only deliberately released services are reachable. The following overview helps with classification.
| Service | Typical port | Reachable from outside? |
|---|---|---|
| HTTP and HTTPS | TCP 80, TCP 443 | yes |
| HTTP/3 (QUIC) | UDP 443 | only if used |
| SSH | TCP 22 | only from known networks or via VPN |
| Hosting panel | depends on the product | only from known networks |
| MySQL/MariaDB, PostgreSQL | TCP 3306, TCP 5432 | no |
| Redis, Memcached | TCP 6379, TCP 11211 | no |
| PHP-FPM | TCP 9000 or Unix socket | no, preferably socket only |
| Mail server | TCP 25, 465, 587, 993 | only if the server receives or provides mail |
A service that does not have to be reachable from outside should additionally listen only on 127.0.0.1 or a Unix socket. Then it stays unreachable even if the firewall fails. For admin access, IP allowlists or a VPN are suitable; details are in the article Shielding admin areas by IP, VPN or additional protection.
Step 1: find out what is listening
Before you write rules, get an overview. The following commands only read:
# All listening TCP and UDP ports with process
sudo ss -tulpn
# Current nftables rule set
sudo nft list ruleset
# Status of ufw, if used
sudo ufw status verbose
In the output of ss, watch the address before the port. 0.0.0.0 or [::] means: the service listens on all interfaces. 127.0.0.1 means: local only. Note for each service whether it has to be reachable from outside. This list is also a piece of your server inventory.
Firewall for web servers with nftables
nftables is the current packet filter on Linux. The following rule set follows the example in the official nftables wiki and adds a release for SSH only from a management network. The addresses are documentation examples and must be replaced.
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set verwaltung_v4 {
type ipv4_addr
flags interval
elements = { 203.0.113.0/28 }
}
chain input {
type filter hook input priority 0; policy drop;
ct state vmap { established : accept, related : accept, invalid : drop }
iifname lo accept
# IPv6 needs neighbor discovery
icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
icmp type echo-request limit rate 5/second accept
icmpv6 type echo-request limit rate 5/second accept
# Website
tcp dport { 80, 443 } accept
# only for HTTP/3:
# udp dport 443 accept
# SSH only from the management network
ip saddr @verwaltung_v4 tcp dport 22 accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
}
Before loading the rule set, check it with nft -c -f /etc/nftables.conf. The first line flush ruleset deletes all existing rules, including those entered by Docker or Fail2ban. On servers with Docker or other services that set their own rules, adapt the rule set instead of replacing it wholesale. How Fail2ban works together with the firewall is covered under Setting up Fail2ban for web servers.
Anyone who prefers it simpler uses ufw on Ubuntu. The order is important: first the SSH release, then switch on.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow proto tcp from 203.0.113.0/28 to any port 22
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Change firewall rules on remote servers only with a second means of access, such as your provider's console. A wrong SSH rule otherwise locks you out yourself.
Outgoing connections: the underestimated half
Most firewalls allow everything outbound. For a web server that is generous. After a break-in, attackers use outgoing connections to download tools, extract data or receive commands. Web server or PHP processes that establish unexpected connections are therefore considered a warning sign.
Limiting outgoing connections is more laborious because every legitimate connection must be known: package sources for updates, payment services, interfaces, mail sending, time servers. A pragmatic way is first to log outgoing connections, then build an allowlist and only after that block. How to check existing connections is described in Checking a server's outgoing connections; which other traces point to a break-in is under Am I already compromised?.
Finally, test from outside, from another computer and only against your own systems, which ports are actually reachable. Only this test shows whether Docker, provider firewall and server rules together give the desired picture.
Conclusion: few rules, big effect
A web server firewall does not have to be complicated. Web ports open, administration only from known networks, everything else dropped: with these basic rules, databases, caches and forgotten services disappear from the internet. This is one of the measures with the best ratio of effort to benefit.
The firewall does not help against vulnerabilities in the web server or in PHP itself, because exactly these ports must stay open. Its strength lies in keeping the remaining attack surface small and, with a limit on outgoing connections, containing the consequences of a break-in.
Maintenance is decisive. Every new service, every container and every change at the provider can shift the picture. So repeat the check from outside after every major change and at fixed intervals, for example once a quarter.
How does PHP SOS help?
- Take stock: We record all listening services and check from outside what is actually reachable.
- Build the rule set: We create an allowlist for incoming and, where sensible, outgoing connections, coordinated with Docker, Fail2ban and the provider firewall.
- Switch over safely: We implement changes with a second means of access and a fallback plan.
- Document: We record ports, releases and reasons as part of server hardening.
Frequently asked questions
Which ports does a web server need?
For visitors usually only TCP 80 and 443, with HTTP/3 additionally UDP 443. SSH and admin interfaces should be reachable only from known networks or via VPN. Database, cache and PHP-FPM ports do not belong on the internet.
Does a firewall protect against PHP2Shell?
No. PHP2Shell is exploited via the web server, that is, via exactly the ports that have to be open. The firewall does, however, reduce the remaining attack surface, and limiting outgoing connections can contain the consequences of a break-in.
Why does ufw show ports as closed that are still reachable?
A known case is Docker containers: Docker enters published ports directly into the packet filter rules, bypassing ufw. So always check open ports additionally from outside.
Is my hosting provider's firewall enough?
A provider's upstream firewall is a good second layer. It does not replace the firewall on the server, though, because it does not take effect in case of an error in the provider's configuration or for internal connections. It is best to use both with the same basic rules.