mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
Both compose files carry a build: key beside image:, so compose treats the named image as a build target. Under --skip-build the suites ran a plain `docker compose up -d`, and when the per-run image the harness named was missing, compose silently built it from whatever the build context held and the suite passed against binaries it was never given. With --skip-build the suites now start with `--no-build --pull never`, so a missing image is an error instead of a rebuild or a registry pull. Hand runs without the flag still build as before. The firewall README says that --skip-build now requires the image to exist.
125 lines
4.3 KiB
Markdown
125 lines
4.3 KiB
Markdown
# Firewall Baseline Test
|
|
|
|
End-to-end exercise of the production fips0 nftables baseline at
|
|
`packaging/common/fips.nft`. Closes the v0.3.0 audit gap that the
|
|
default-deny + conntrack + drop-in semantics had no integration coverage.
|
|
|
|
## What this exercises
|
|
|
|
The `fips.nft` baseline polices ONLY the fips0 mesh interface and
|
|
implements default-deny inbound. This suite asserts the four behaviors
|
|
documented in the file's header are actually true on a live mesh:
|
|
|
|
- **(a)** Unallowed inbound on fips0 is **dropped**
|
|
- **(b)** Outbound-initiated flows get their reply via the
|
|
`ct state established,related accept` rule
|
|
- **(c)** ICMPv6 echo-request is **accepted** (ping6 reachability)
|
|
- **(d)** A drop-in `.nft` file under `/etc/fips/fips.d/` adds an
|
|
allowlisted port and that port is **accepted**
|
|
|
|
A drop-counter check after case (a) confirms the connection was
|
|
actively DROP'd by the fips chain (not silently unrouted).
|
|
|
|
## Topology
|
|
|
|
Two FIPS nodes peered over UDP on a Docker bridge network:
|
|
|
|
| Container | Hostname | Firewall |
|
|
|-------------------------|----------|----------|
|
|
| `fips-fw-container-a` | `host-a` | none (probe) |
|
|
| `fips-fw-container-b` | `host-b` | `fips.nft` + drop-in |
|
|
|
|
The bridge network requests no subnet, so docker assigns one from its own
|
|
address pool and two concurrent runs never contend for a fixed range. No
|
|
node's IPv4 address is therefore known before startup, and the generated peer
|
|
stanzas address each other by docker hostname, resolved through the
|
|
container's dnsmasq to docker's embedded DNS. The firewall assertions
|
|
themselves are unaffected: they run over the fips0 overlay, whose addresses
|
|
are derived from the node npubs.
|
|
|
|
`node-b` mounts the production `packaging/common/fips.nft` read-only at
|
|
`/etc/fips/fips.nft`, plus a drop-in at `/etc/fips/fips.d/services.nft`
|
|
containing `tcp dport 22 accept`. `node-a` is unfirewalled and serves
|
|
as the probe origin.
|
|
|
|
Both containers run the unified test image's `default` mode, which
|
|
starts dnsmasq + sshd (port 22) + iperf3 + python http.server on
|
|
port 8000 + the FIPS daemon.
|
|
|
|
## fips-firewall.service activation
|
|
|
|
The production unit's ExecStart is:
|
|
|
|
```text
|
|
ExecStart=/usr/sbin/nft -f /etc/fips/fips.nft
|
|
```
|
|
|
|
The unified test image does not run systemd, so `test.sh` invokes the
|
|
same `nft -f` command directly inside `node-b` after fips0 is up and
|
|
peering has converged. The deb-install harness covers the systemd
|
|
unit-enablement path under real systemd separately.
|
|
|
|
## Run
|
|
|
|
Build the Linux binaries and test image:
|
|
|
|
```bash
|
|
./testing/scripts/build.sh --no-docker
|
|
```
|
|
|
|
Run the suite:
|
|
|
|
```bash
|
|
./testing/firewall/test.sh
|
|
```
|
|
|
|
`test.sh` regenerates fixtures automatically before starting Docker.
|
|
Use `--skip-build` to reuse the existing release binaries and the
|
|
existing test image: the suite then neither builds nor pulls the image
|
|
named by `FIPS_TEST_IMAGE` (default `fips-test:latest`), so that image
|
|
must already exist. Use `--keep-up` to leave the containers running for
|
|
inspection.
|
|
|
|
## Expected output shape
|
|
|
|
```text
|
|
=== Generating firewall fixtures
|
|
=== Starting firewall harness
|
|
=== Waiting for fips0 on both nodes
|
|
=== Waiting for peer convergence
|
|
=== Resolving fips0 addresses
|
|
node-a: fd97:...
|
|
node-b: fd97:...
|
|
=== Activating fips-firewall on fips-fw-container-b
|
|
PASS: fips-fw-container-b: fips.nft baseline + drop-in loaded
|
|
=== Case (c): ICMPv6 echo-request to firewalled node
|
|
PASS: (c) ICMPv6 ping node-a → node-b accepted
|
|
=== Case (a): unallowed inbound TCP/8000 from node-a → node-b
|
|
PASS: (a) inbound TCP/8000 dropped (curl rc=28, timed out as expected)
|
|
=== Case (b): node-b initiates outbound TCP, expects reply via conntrack
|
|
PASS: (b) outbound from node-b got HTTP 200 via conntrack reply path
|
|
=== Case (d): drop-in allowlisted TCP/22 from node-a → node-b
|
|
PASS: (d) drop-in allowlisted TCP/22 reachable
|
|
=== Drop counter incremented (case a should have ticked it)
|
|
PASS: drop counter = N (case a was actually dropped, not just unrouted)
|
|
=== Firewall integration test passed
|
|
```
|
|
|
|
## Inspect the loaded ruleset
|
|
|
|
```bash
|
|
docker exec fips-fw-container-b nft list table inet fips
|
|
```
|
|
|
|
## Stop and clean up
|
|
|
|
```bash
|
|
docker compose -f testing/firewall/docker-compose.yml down
|
|
```
|
|
|
|
## Generated fixture location
|
|
|
|
`testing/firewall/generated-configs/` (gitignored), or
|
|
`generated-configs<suffix>/` when `FIPS_CI_NAME_SUFFIX` is set, which is how
|
|
concurrent runs keep their fixtures apart.
|