Feature Specification: Resource firewall
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/resources/firewall/default.nix, core/resources/firewall/nixos.nix, core/resources/firewall/test.nix, core/effects/env-config.nix, core/setups/hosts-common.nix, core/setups/wiring/public-uplink.nix, the "web".firewall and "ingest".firewall requests in core/components/), pull requests #1238, #1241, #1247 and #1254.
User Scenarios & Testing (mandatory)
The firewall resource decides which ports of a node the internet reaches.
It wraps no contract. Its apply returns a NixOS module that sets
networking.firewall and adds an nftables gate chain. The shared resource
rules are in the resource family spec.
User Story 1 - Publish the public ports of an application (Priority: P1)
A component serves a public protocol, for example HTTP or RTMP. It requests
"web".firewall.publicTCPPorts or "ingest".firewall.publicTCPPorts. The
policy adds the requested ports to its own publicTCPPorts and
publicUDPPorts. The gate accepts these ports from every source.
Why this priority: an application that the internet cannot reach does not serve its users.
Independent Test: nix-unit-resources-firewall, tests
test-requests-are-published, test-udp-requests-are-published and
test-policy-publishes-alongside-requests.
Acceptance Scenarios:
- Given a request for TCP ports
1935and1936, When the gate renders, Then it has the ruletcp dport { 1935, 1936 } accept. - Given a request for a port, When the module evaluates, Then the port is published, but the request does not make any service listen on it.
- Given policy ports and request ports, When the gate renders, Then both sets are published.
User Story 2 - Restrict the administrative surface (Priority: P1)
A hosting provider wants SSH reachable only from its management addresses.
It states ADMIN_SOURCES (comma-separated, read in
core/effects/env-config.nix). The setup puts the list into the policy
adminSources. The gate accepts any port from these sources. The
adminTCPPorts (default [ 22 ]) stay served but are not published.
Why this priority: an open SSH port on every node is a large attack surface.
Independent Test: nix-unit-resources-firewall, tests
test-stated-admin-sources-gate-ssh, test-unstated-admin-sources-publish-ssh,
test-policy-admin-sources-gate-ssh and
test-restricted-admin-ports-are-still-served.
Acceptance Scenarios:
- Given no
adminSources, When the gate renders, Then theadminTCPPortsare published to everyone. - Given
adminSources, When the gate renders, Then the gate accepts those sources withip saddrorip6 saddrrules, and theadminTCPPortsare not published. - Given
adminSourcesand22inadminTCPPorts, Whenservices.openssh.enableis false, Then the evaluation fails with an assertion.
User Story 3 - Keep internal services reachable by peers only (Priority: P2)
Nodes of a deployment talk to each other on ports they do not publish. The
setup sets internalSources to the incus bridge subnet and the real
addresses of the roster. The addresses that NetBox allocates to the node
(fediversity.network.allocated) and to the other nodes of its deployment
(fediversity.network.peers) are not in the plan, and the gate adds them to
the sources. Each source that is an address goes into the gate as a host
address, without its prefix length, because nft makes a source with a prefix
length into its full range. The gate accepts all traffic from these sources and from
trustedInterfaces. It drops other traffic to unpublished ports.
Why this priority: internal services such as databases must not reach the internet, and peers must still reach them.
Independent Test: nix-unit-resources-firewall, tests
test-internal-ports-without-sources-fail,
test-internal-ports-with-sources-pass, test-v4-sources-render-as-ip-saddr,
test-v6-sources-render-as-ip6-saddr,
test-trusted-interfaces-include-loopback and
test-allocated-addresses-reach-the-gate.
Acceptance Scenarios:
- Given served ports that are not published, When no source and no trusted interface is set, Then the evaluation fails with an assertion.
- Given a source that contains
:, When the gate renders, Then the source appears in anip6 saddrrule. - Given
trustedInterfaces = [ "incusbr0" ], When the gate renders, Then the rule isiifname { "incusbr0", "lo" } accept. - Given an IPv4 and an IPv6 address with a prefix length in
fediversity.network.allocated, and the addresses of a peer infediversity.network.peers, When the gate renders, Then theip saddrrule holds the IPv4 host addresses afterinternalSources, theip6 saddrrule holds the IPv6 host addresses, and no rule holds the range of an allocated address.
User Story 4 - Open the setup-specific ports (Priority: P3)
The setup adds ports that no component requests. A node that answers ACME
HTTP-01 challenges, or the SMTP provider with smtpAcme, publishes TCP port
80. A DNS node that something outside points at publishes TCP and UDP port
53. The incus node trusts incusbr0 and adds 8443 to adminTCPPorts.
A node that the public uplink delivers to gets adminTCPPorts = [ ].
core/setups/wiring/firewall-policy.nix gives each addition as a separate
module definition of the policy, so the port lists of the additions that apply
to one node concatenate.
Why this priority: these ports follow from the deployment, not from the application.
Independent Test: nix-unit-setups-public-uplink, test
test-delivered-node-publishes-no-admin-surface, and
nix-unit-setups-firewall-policy.
Acceptance Scenarios:
- Given a node that the uplink delivers a public address to, When the policy evaluates, Then
adminTCPPortsis empty. - Given a node that answers ACME challenges and is a DNS node that something outside points at, When the policy evaluates, Then
publicTCPPortscontains80and53, andpublicUDPPortscontains53. - Given the
incusnode, When the policy evaluates, ThenadminTCPPortsis[ 22 8443 ]andtrustedInterfacesis[ "incusbr0" ].
Edge Cases
- The renderer does nothing unless both
networking.firewall.enableandnetworking.nftables.enableare true. - A source that is empty or contains a space fails an assertion.
- The gate accepts DHCP (
udp dport 68), DHCPv6 onfe80::/64(udp dport 546) and established traffic before the source rules. - The gate leaves ICMP and ICMPv6 to
nixos-fw. checkRulesetrunsnft checkat build time, so a malformed CIDR fails the build.
Requirements (mandatory)
Functional Requirements
- FR-001: The request MUST accept
publicTCPPortsandpublicUDPPorts(default[ ]). - FR-002: The policy MUST accept
internalSources,adminSources,adminTCPPorts(default[ 22 ]),publicTCPPorts,publicUDPPortsandtrustedInterfaces. - FR-003:
applyMUST concatenate the policy ports with the ports of all requests. - FR-004: The renderer MUST set
networking.firewall.allowedTCPPortsto the published ports plusadminTCPPorts, andallowedUDPPortsto the public UDP ports. - FR-005: The renderer MUST publish
adminTCPPortsonly whenadminSourcesis empty. - FR-006: The renderer MUST add table
fediversity-fw(familyinet) with chaininput-gateatpriority filter - 10,policy accept, that ends indrop. - FR-007: The chain MUST accept trusted interfaces,
internalSources, the host addresses infediversity.network.allocatedandfediversity.network.peers, andadminSourcesbefore the published ports. - FR-008: The renderer MUST fail evaluation for a malformed source, for
adminSourceswith port22but nosshd, and for unpublished served ports without any source or trusted interface. - FR-009: The renderer MUST read its settings from the NixOS option
fediversity.firewall, whichapplysets once for each machine. When a node holds more than one machine, the renderer MUST use the union of their settings. - FR-010: The setup MUST give each environment addition to the policy as a separate module definition, so that the port lists of all additions that apply to one node concatenate.
Key Entities
- Published port: a port that the gate accepts from every source.
- Gate source: an address or CIDR in
internalSourcesoradminSources, or a host address infediversity.network.allocatedorfediversity.network.peers. input-gate: the nftables chain in tablefediversity-fw.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
nix-unit-resources-firewallpasses (29 tests). - SC-002:
nix-unit-setups-public-uplinkpasses. - SC-003:
nix-unit-setups-firewall-policypasses.
Assumptions
nixos-fwruns atpriority filterand drops what no rule allows. The gate runs before it.- The roster addresses in
internalSourcesare the real addresses of the deployment.
Known Deviations
None.