On an AlmaLinux 9 box running CSF v15, ip6tables -L INPUT -n showed policy ACCEPT and not a single CSF chain — the IPv6 half of the firewall did not exist. The cause is the default IPV6 = "0" in /etc/csf/csf.conf. I had read that as "IPv6 is off". It means CSF never touches ip6tables, which on a host with a global IPv6 address is close to the opposite.
This behaviour is the same on CSF 14.x and on the 15.x line that followed ConfigServer's shutdown in August 2025: IPV6 = "0" has always meant "do not manage ip6tables", never "block IPv6".
What CSF's IPV6 = 0 actually does: ip6tables policy ACCEPT
ip6tables -L INPUT -n
Chain INPUT (policy ACCEPT)
target prot opt source destination
ts-input all -- ::/0 ::/0
Policy ACCEPT, no CSF chains, nothing but the VPN's own chain. Meanwhile the host had a global address and a default route:
ip -6 addr show scope global
ip -6 route show default
inet6 2001:db8:1:1::1/64 scope global
default via fe80::1 dev eth0 proto static metric 100
And these were listening on it:
ss -tlnp6
| Port | Service | Why it was a problem |
|---|---|---|
| 22 | sshd on [::]:22 |
my "SSH only over the VPN" rule was an IPv4 rule |
| 3306 | mariadbd on *:3306 |
bind-address was commented out in mariadb-server.cnf |
| 8181, 8443 | httpd on * |
the backend Apache, which is supposed to sit behind nginx |
[::]:22 is an IPv6-only socket; a bare *:3306 is the dual-stack wildcard, which is worse — one socket answering on every IPv4 and IPv6 address the host has. So the database and the raw backend were reachable from the internet over IPv6, and SSH bypassed the VPN restriction entirely. Logs showed no IPv6 source addresses in /var/log/secure — assuming the timestamps in /var/log are actually correct — but that is luck, not design: low-numbered suffixes like ::1 are the first thing an IPv6 scanner tries on a discovered prefix.
Emptying TCP6_IN does nothing while IPV6 = "0"
The natural reaction is to open csf.conf, see TCP6_IN and UDP6_IN, empty them out, reload, and consider IPv6 closed.
That does nothing while IPV6 = "0". CSF skips the entire IPv6 stack, so the IPv6 port lists are inert — they are configuration for a firewall that is never built. An empty TCP6_IN next to IPV6 = "0" reads like a locked door and is not attached to a wall.
The only honest check is the kernel's own view:
ip6tables -L INPUT -n | head -3
If the policy is ACCEPT and there are no CSF chains, IPv6 is wide open, whatever csf.conf says.
Before you set IPV6 = "1": do not lock yourself out
Turning IPv6 filtering on is a real firewall change. Walk through this first:
- Know how you get back in. If you administer the box over a VPN, confirm the VPN interface is excluded from filtering —
ETH_DEVICE_SKIP = "tailscale0"or your equivalent — so CSF cannot cut your management path. Otherwise make sure your admin IPv6 is incsf.allow, or keep a provider console session open. - Set the IPv6 ingress lists explicitly, in the same edit:
TCP6_IN = ""andUDP6_IN = ""if nothing should reach the host over IPv6. - Check egress, which is easy to forget.
TCP6_OUTshould still allow what the box needs — 53, 80, 443, and 25/587/993 if it sends mail — or updates and outbound SMTP will start failing in ways that look unrelated. - Check what actually serves traffic over IPv6. On my box nginx binds IPv4 only, so no site traffic was affected. If your web server does listen on IPv6, you need 80 and 443 in
TCP6_IN. - Leave the ICMPv6 defaults alone.
IPV6_ICMP_STRICT = "0"andIPV6_SPI = "1"are the CSF defaults and the ones you want. ICMPv6 is not optional the way ICMP is on IPv4 — Neighbour Discovery and Path MTU Discovery ride on it. SettingIPV6_ICMP_STRICT = "1"restricts inbound ICMPv6 and can break both, which surfaces hours later as "IPv6 randomly stopped working" and never looks like a firewall problem.
The fix: set IPV6 = "1" and run csf -r
# in /etc/csf/csf.conf
IPV6 = "1"
csf -r
In WHM this is Plugins → ConfigServer Security & Firewall → Firewall Configuration, and the DirectAdmin, CWP and CyberPanel plugins expose the same IPV6 toggle; editing /etc/csf/csf.conf directly does exactly the same thing.
Verify against the kernel, not the config — and check that CSF really built its chains, because a DROP policy alone does not prove that:
ip6tables -L INPUT -n | head -5
ip6tables -S | grep -c 'ALLOWIN\|DENYIN\|LOGDROPIN'
Chain INPUT (policy DROP)
A non-zero count on the second command is what proves CSF is managing ip6tables. The paths and defaults are the same on AlmaLinux 9, Rocky Linux 9 and RHEL 9, where CSF drives the nft-backed iptables-nft; ip6tables -L still reads correctly, and nft list ruleset shows the same chains if you prefer.
Then confirm egress still works:
curl -6 -s -o /dev/null -w '%{http_code}\n' https://example.com
Before you close the session, open a second SSH connection and confirm it succeeds. Do not disconnect the first one until it does. Rollback is IPV6 = "0" and csf -r.
If you actually want IPv6 gone
Half the people who reach this page do not want CSF to manage IPv6 — they want IPv6 off the host entirely. IPV6 = "0" does not do that and never did. The honest way is the kernel:
# /etc/sysctl.d/99-disable-ipv6.conf
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
sysctl --system
ip -6 addr show scope global # must return nothing
Also bind MariaDB and Apache off the wildcard address
The firewall now covers those services, but a service bound to * is still one firewall mistake away from being public. Bind them where they belong: MariaDB to 127.0.0.1 via bind-address in mariadb-server.cnf, and the backend Apache to loopback or to the single address the reverse proxy uses. Both need a service restart, so do them in a maintenance window — but do them.
The general lesson I took from this: any hardening you do on a dual-stack host is only half done until you have verified it with ip6tables and ss -tlnp6, not with the firewall's configuration file.
The same box had a second CSF blind spot at the same time: lfd had stopped counting SSH brute-force attempts altogether, because OpenSSH 9.8 renamed the process that logs them.