1

I'd like to use nftables to set up a firewall in the following way:

  1. a basic table with a default-deny policy
  2. custom tables (which I'd add per service running on a machine) which allow only relevant ports

Here is an example of what I'd ideally like:

 # base table, set on every machine, used to default-deny all incoming traffic table inet basic delete table inet basic table inet basic { chain input { # see example below about the priority type filter hook input priority 0 policy drop } } # table for rules required by HTTP servers, created only when needed on HTTP servers table inet http_server delete table inet http_server table inet http_server { chain input { # see example below about the priority type filter hook input priority 1 policy drop # allow http and https traffic tcp dport 80 accept tcp dport 443 accept } } 

Note that the reason I'm using separate tables rather than appending rules to chains is for the isolation and atomicity tables provide: a new table doesn't have to be aware of other tables, and can be deleted in one go. This gives me a nice basis for building a declarative firewall, rather than needing to imperatively add/delete rules in specific chains (I'd always delete and recreate a whole table defined in one file when I change something).

Unfortunately however, it appears that what I'm trying to achieve is impossible with nftable's design. If I understand correctly, chains in separate tables will be evaluated in order of their hook priority, with an accept rule terminating the current chain, but a drop terminating the packet evaluation globally. Basically, for a packet to be accepted, I need an accept in every chain, but one drop in any chain, including default policies is enough to stop it.

Regardless of the priority of tables, it appears that a drop verdict will always be reached, even for packets that I'd actually want to accept in specific configuration. E.g. assuming a configuration as specified above, and a packet arriving on port 80 (which I'd want to accept), this is what happens:

  • if priority table basic < priority table http_server:

    table basic -> no match -> default drop -> *end evaluation, verdict: drop* 
  • if priority table http_server < priority table basic:

    table http -> match -> accept -> next table basic -> no match -> default drop -> *end evaluation, verdict: drop* 

Hence, it appears I cannot have a separate table with a chain with a default deny policy, and other tables which chains which selectively accept packets. So I am left with either a default-accept firewall, or I need to manage all rules imperatively within one hook, just like iptables.

Can anyone confirm my assumption or show me a declarative alternative?

I'm also curious as to why nftables was designed in a way that an accept in a lower priority chain does not override default policies in a higher priority chain. I can understand why you'd want a drop in any chain to prevent accepts, but not why that should also happen with the default policy.

Thanks

1
  • I'm also curious as to why nftables was designed in a way Because that's what would happen when you have layers of filters in reality? For something to get through them, it needs to be able to get through every layer, and as long as one layer blocks it, it goes no further. Commented Jun 26 at 9:16

1 Answer 1

0

nftables drop short-circuits right where it matches, while allow must still traverse later chains of the same type. Note this makes the safer action of stopping traffic easier. I do not have a citation to exactly why this design, review netfilter mailing lists to find discussion.

Chain priority is the order in which they are processed, low to high. Your default drop is a lower number, matching almost anything it will prevent anything from reaching table http_server.

If you wish to continue what I will call the "many filter input chains with a default" approach, priority must be set so that the deny is last. Default deny would not make sense for the inet_service chains, because there are multiple of them and there might be an allow later. You put the default drop in a different chain.

Multiple firewalls exist based on nftables, that generate allows and other policy based on a set of allowed services. firewalld, ufw, proxmox-firewall, others. Hardly any bother to generate separate chains for each inet service. Easier to order and read when the chains are a list of everything at that specific hook, I suppose.

Many tutorials exist about allowing ports in whatever high level firewall. Let's instead take a simple nftables rule set and consider how to make it friendlier to drop in file modification. Fedora has an example in the nftable package, main.nft

# a common table for both IPv4 and IPv6 table inet nftables_svc { # protocols to allow set allowed_protocols { type inet_proto elements = { icmp, icmpv6 } } # interfaces to accept any traffic on set allowed_interfaces { type ifname elements = { "lo" } } # services to allow set allowed_tcp_dports { type inet_service elements = { ssh, 9090 } } # this chain gathers all accept conditions chain allow { ct state established,related accept meta l4proto @allowed_protocols accept iifname @allowed_interfaces accept tcp dport @allowed_tcp_dports accept } # base-chain for traffic to this host chain INPUT { type filter hook input priority filter + 20 policy accept jump allow reject with icmpx type port-unreachable } } 

Note the single chain for type filter hook input, plus a non-base chain to contain allow rules. Default deny is achieved with a reject at the end. Also covers a couple additional essentials like allowing icmp.

set allowed_tcp_dports could be split into its own file, and replaced with let's call it include "/etc/nftables/inet_service.nft" Containing just the set it would be relatively straightforward for automation to drop in

 # services to allow set allowed_tcp_dports { type inet_service elements = { http, https } } 

nftable sets are convenient named data structures, however writing out a config file this way the automation would need to combine all the inet services. Annoying if you wish to make things modular and not know about each other, but doable.

Perhaps something like a key value store to hold the port numbers per host, adding each service one at a time, and generate a nftable config from that on command. Congratulations, you have reinvented a higher level firewall.

1
  • I think you didn't get the idea / realize how it actually works -- for a packet to be allowed, it cannot be "caught" by any drop rule / chain policy, so priority isn't really relevant / the issue here. (Priority is just about how the chains are "stacked" / "overlayed", but for a packet to be allowed, it needs to "get through" / be accepted by every chain.) Commented Jul 2 at 1:45

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.