The pen (test) is mightier than the sword
The NGI0 Entrust Fund, of which Liminix is a beneficiary, also offers technical services and expertise, and one of the services on offer is a security audit.
Between October 7, 2024 and November 7, 2024, Radically Open Security B.V. carried out a penetration test for Liminix, and they’ve also kindly agreed to remove the “Confidential” label from the report so I can share it.
The “Findings” were two cases of path traversal vulns, both caused by string concatenation. One was in the TFTP service, the other in watch-ssh-keys.fnl, and both were fixed back in November.
The “Non-findings”, though, included a review of the default firewall rules and some very useful advice on how to tighten them up. This has led to some extended comments in the code :-) and also some improvements in how it actually works.
Zones in firewall rules
Technically not a RoS finding at all, but I realised while looking at
the firewall rules that the network interface names I’d originally
hardcoded as int
and ppp0
, intending to go back and fix them
later, were still hardcoded as int
and ppp0
.
For the particular config in modules/profiles/gateway.nix
that’s not so bad as
they are most of the time the correct network interface names, but
even in that context not always. Sometimes when PPP wedges
itself in weird ways and the service restarts you end up talking to ppp1
instead
of ppp0
. Or even ppp27
if that kind of thing goes on for a
while. And for other more complicated configs with pluggable devices,
this simply wasn’t going to help at all.
What we’ve done is to extend the firewall module with a zones
parameter, which maps zone names to interface services
services.firewall = svc.firewall.build {
zones = {
wan = [ services.pppoe ];
lan = [ services.lan4 ];
};
};
For each zone we create an nftables set with the zone name, allowing us to write the firewall rules referencing sets instead of hardcoding interfaces
- (accept "oifname \"int\" iifname \"ppp0\" meta l4proto udp ct state established,related")
- (accept "iifname \"int\" oifname \"ppp0\" meta l4proto udp")
+ (accept "oifname @lan iifname @wan meta l4proto udp ct state established,related")
+ (accept "iifname @lan oifname @wan meta l4proto udp")
and then we use the secrets subscriber service (which turns out to be good for more than just secrets, and so could use a better name) to watch for interfaces appearing/disappearing and add them to the appropriate set.
DNS rule tightening
We no longer allow incoming packets on port 53 unless part of an established connection. This rule was added to enable DNS replies but it was never needed as conntrack already takes care of those.
ICMP rate limiting
ICMP is important to the correct functioning of a network (and in IPv6, even more so) so one does not simply turn it off. But that doesn’t mean we have to leave ourselves open to being ping flooded.
In theory it should have been easy to add rate limits for ICMP, but in practice, to calculate the cap at 5% of available bandwidth you need to know what the available bandwidth is - and the only person who knows that is the site administrator. We can’t identify the speed limit of the upstream just by looking at whether it’s DSL or fibre or ethernet or carrier pigeon, nor can we query the limit except by testing it, which would be somewhat invasive.
What we’ve come up with, which may with hindsight now I try to
explain it be over-engineered, is a new concept of “service
properties”, which can be added to any longrun service. Properties are
similar to outputs except that they’re static: a service property is
set at configuration time and exists whether the service is up or
down. So we now have a bandwidth
property on interface services, and
the firewall service checks it whenever it gets notified of an
interface change and uses it to write a new rule:
# modules/ppp/common.nix
service = longrun {
inherit name;
run = ''
# ...
notification-fd = 10;
properties.bandwidth = bandwidth;
What’s next?
Liminix 1.0 is next! I’ve been dogfooding Liminix for a year or more, and it’s been reliable for all but the first three weeks of that, so it’s time to say “good enough that you can use it” - and also, just as importantly, “we’ll tell you first if we’re going to radically break things”.
On my pre-release task list:
-
update the device documentation so that the reader knows which devices are usable and which are experimental
-
impose a consistent look and feel across the web site, so that the docs match the rest of it
-
make another video
Not on my pre-release task list, despite that I am sorely tempted to: completely redesigning the firewall config. I will resist. That can be in 1.1.