2

So I have a quick and dirty firewall that I plan to use on my vps using nftables. Here's the initial rule

table inet filter {
    chain input {
        type filter hook input priority 0;

        # Allow connections from localhost
        iif "lo" accept

        # Allow connections on port 5432 (PostgreSQL) from localhost
        tcp dport 5432 iif "lo" accept

        # Allow ping
        icmp type { echo-request, echo-reply } limit rate 4/second accept
        icmpv6 type { echo-request, echo-reply } limit rate 4/second accept

        # Allow specific ports from remote hosts via TCP
        tcp dport { 53, 80, 22,443, 1194, 8080 } accept

        # Allow specific ports from remote hosts via UDP
        udp dport { 53, 1194 } accept
        # Drop all other incoming traffic
        drop
    }
}

But If I apply this, I can't ping from my vps instance. The ping command (e.g. ping yahoo.com )just hang there. However if I remove the drop , I can ping to public but that would defeat the purpose of my firewall wouldn't it? But please cmiiw as I'm noob for this. Anyone can help? Thanks.

2 Answers 2

2

Reasons it doesn't work

Reason 1: IPv6 NDP is dropped

The current ruleset prevents IPv6 to work properly on Ethernet, because it drops ICMPv6 traffic related to NDP which is the equivalent of ARP for IPv6. So it kills IPv6 connectivity for most setups in the next seconds or minutes (can be seen with ip -6 monitor neigh with the router entry going REACHABLE->STALE->PROBE->FAILED instead of REACHABLE->STALE->PROBE->REACHABLE) , just as when ARP is firewalled then IPv4 on Ethernet stops working.

While it should not affect that drastically IPv4, it creates conditions where PMTUD blackhole can appear for the same reasons: not allowing related ICMP errors to be accepted.

When trying to ping yahoo.com, if the attempt is done through IPv6 it will then fail.

Let's now consider there is no IPv6 used on this system (but the following paragraph still applies for IPv6). There's a more important reason it doesn't work.

Reason 2: return traffic is dropped

Again when trying to ping yahoo.com, a DNS query is made toward one or multiple other DNS servers to resolve the address. The reply is made from remote port 53 to a local random port. There is no rule allowing such reply traffic: it's dropped, and thus DNS fails. The same happens for most attempts when the system is a client using UDP or TCP, rather than a server replying to remote client's requests to ports explicitly allowed on the VPS.

A ping to an IPv4 address should still succeed, since the ruleset accepts such ping replies explicitly.

Fixing it

Reply traffic requires a stateful firewall

This can't be fixed with a stateless firewall without voiding the security the firewall is supposed to provide. One can't just add: udp sport 53 accept + tcp sport { 53, 443 } accept to allow DNS replies and HTTPS replies to work, because then any remote attack can use such source ports to access any ports and thus services on the VPS system.

Just use Netfilter's stateful facility conntrack to automatically allow return traffic and only return traffic, as well as related traffic, which by default will be the possible ICMP errors received (thus resuming proper handling of PMTUD and preventing timeouts for remote rejected UDP services, and of course any transient Internet error). Add this rule first:

ct state established,related accept

Then there's no need to allow incoming ICMP echo-reply anymore: it's handled above.

IPv6 relies on NDP and thus on ICMPv6 to work properly

Before it's figured out exactly what ICMPv6 types should be allowed to keep proper connectivity from NDP documentation (RFC 4861: Neighbor Discovery for IP version 6 (IPv6)), one can allow all of them with:

meta l4proto ipv6-icmp accept

That's the syntax to use when the match icmpv6 is not also used.

If you insist on filtering them, these should be enough for an end-point system to get connectivity. Some of these ICMPv6 packets might be handled by conntrack in the short-circuiting stateful firewall rule above, but most won't, either because they aren't replies or because multicast is involved (and can't be tracked properly):

icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept

Then more are needed if multicast services are used or provided, potentially some or all of these (they are not part of NDP but MLD, the equivalent of IPv4's IGMP):

# nft describe icmpv6 type | grep mld
    mld-listener-query                               130
    mld-listener-report                              131
    mld-listener-done                                132
    mld-listener-reduction                           132
    mld2-listener-report                             143

and yet more if there are more than one router in the LAN (nd-redirect), if the server is also a router (nd-router-solicit) including hosting a container or VM through IPv6, etc.

Suggested ruleset

The pings are handled before the generic stateful rule: else once a ping flow starts, it would be short-circuited by the stateful rule and get no chance to be rate-limited. Further reordering, filtering or refactoring for this kind of case could be needed.

table inet filter        # for idempotence
delete table inet filter # for idempotence

table inet filter {
    chain input {
        type filter hook input priority 0; policy accept;

        # Allow incoming ping
        # but still honor limit and drop the excess
        # It could have been rewritten using anonymous chains
        # but I kept it simple.
        # It has to be done as an exception to the stateful rule
        # so before it.

        icmp type echo-request limit rate 4/second accept
        icmp type echo-request drop
        icmpv6 type echo-request limit rate 4/second accept
        icmpv6 type echo-request drop

        # firewall becomes stateful from here:
        # bulk of the traffic will be handled by this single rule
        ct state established,related accept

        # Allow connections from localhost
        iif "lo" accept

        # Allow connections on port 5432 (PostgreSQL) from localhost
        # tcp dport 5432 iif "lo" accept # already allowed by rule above
        
        # Allow specific ports from remote hosts via TCP
        tcp dport { 53, 80, 22,443, 1194, 8080 } accept

        # Allow specific ports from remote hosts via UDP
        udp dport { 53, 1194 } accept

        # minimal ICMPv6 support for an end-node system
        # non-LAN ICMPv6 (as well as IPv4 ICMP) packets used to report
        # errors are handled by the generic stateful rule.
        icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept

        # Drop all other incoming traffic
        drop
    }
}
1
  • Thank you very much for the comprehensive answer. This rule works, can ping to outside.
    – mhd
    Feb 29 at 16:07
0

Try this for the input filter. I think with the drop at the end your dropping all traffic including icmp.

You need to set the default policy to drop instead of drop at the end. type filter hook input priority 0; policy drop;

table inet filter {
    chain input {
        type filter hook input priority 
        0; policy drop;

        # Allow connections from localhost
        iif "lo" accept

        # Allow connections on port 5432 (PostgreSQL) from localhost
        tcp dport 5432 iif "lo" accept

        # Allow ping
        icmp type { echo-request, echo-reply } limit rate 4/second accept
        icmpv6 type { echo-request, echo-reply } limit rate 4/second accept

        # Allow specific ports from remote hosts via TCP
        tcp dport { 53, 80, 22,443, 1194, 8080 } accept

        # Allow specific ports from remote hosts via UDP
        udp dport { 53, 1194 } accept
       
    }
}
1
  • still doesn't work
    – mhd
    Jan 27 at 4:39

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .