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:

  1. Given a request for TCP ports 1935 and 1936, When the gate renders, Then it has the rule tcp dport { 1935, 1936 } accept.
  2. 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.
  3. 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:

  1. Given no adminSources, When the gate renders, Then the adminTCPPorts are published to everyone.
  2. Given adminSources, When the gate renders, Then the gate accepts those sources with ip saddr or ip6 saddr rules, and the adminTCPPorts are not published.
  3. Given adminSources and 22 in adminTCPPorts, When services.openssh.enable is 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:

  1. Given served ports that are not published, When no source and no trusted interface is set, Then the evaluation fails with an assertion.
  2. Given a source that contains :, When the gate renders, Then the source appears in an ip6 saddr rule.
  3. Given trustedInterfaces = [ "incusbr0" ], When the gate renders, Then the rule is iifname { "incusbr0", "lo" } accept.
  4. Given an IPv4 and an IPv6 address with a prefix length in fediversity.network.allocated, and the addresses of a peer in fediversity.network.peers, When the gate renders, Then the ip saddr rule holds the IPv4 host addresses after internalSources, the ip6 saddr rule 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:

  1. Given a node that the uplink delivers a public address to, When the policy evaluates, Then adminTCPPorts is empty.
  2. Given a node that answers ACME challenges and is a DNS node that something outside points at, When the policy evaluates, Then publicTCPPorts contains 80 and 53, and publicUDPPorts contains 53.
  3. Given the incus node, When the policy evaluates, Then adminTCPPorts is [ 22 8443 ] and trustedInterfaces is [ "incusbr0" ].

Edge Cases

  • The renderer does nothing unless both networking.firewall.enable and networking.nftables.enable are true.
  • A source that is empty or contains a space fails an assertion.
  • The gate accepts DHCP (udp dport 68), DHCPv6 on fe80::/64 (udp dport 546) and established traffic before the source rules.
  • The gate leaves ICMP and ICMPv6 to nixos-fw.
  • checkRuleset runs nft check at build time, so a malformed CIDR fails the build.

Requirements (mandatory)

Functional Requirements

  • FR-001: The request MUST accept publicTCPPorts and publicUDPPorts (default [ ]).
  • FR-002: The policy MUST accept internalSources, adminSources, adminTCPPorts (default [ 22 ]), publicTCPPorts, publicUDPPorts and trustedInterfaces.
  • FR-003: apply MUST concatenate the policy ports with the ports of all requests.
  • FR-004: The renderer MUST set networking.firewall.allowedTCPPorts to the published ports plus adminTCPPorts, and allowedUDPPorts to the public UDP ports.
  • FR-005: The renderer MUST publish adminTCPPorts only when adminSources is empty.
  • FR-006: The renderer MUST add table fediversity-fw (family inet) with chain input-gate at priority filter - 10, policy accept, that ends in drop.
  • FR-007: The chain MUST accept trusted interfaces, internalSources, the host addresses in fediversity.network.allocated and fediversity.network.peers, and adminSources before the published ports.
  • FR-008: The renderer MUST fail evaluation for a malformed source, for adminSources with port 22 but no sshd, 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, which apply sets 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 internalSources or adminSources, or a host address in fediversity.network.allocated or fediversity.network.peers.
  • input-gate: the nftables chain in table fediversity-fw.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: nix-unit-resources-firewall passes (29 tests).
  • SC-002: nix-unit-setups-public-uplink passes.
  • SC-003: nix-unit-setups-firewall-policy passes.

Assumptions

  • nixos-fw runs at priority filter and drops what no rule allows. The gate runs before it.
  • The roster addresses in internalSources are the real addresses of the deployment.

Known Deviations

None.