Client: Terraform (scaleway provider, stack examples/stacks/scaleway) · Branch: feat/503-what-a-stack-must-prove · Runtime: --vm incus-ovn, Incus 7.x · Measured: 2026-08-27
The published sentence the measurement contradicts
docs/limits.md, section "The firewall enforces, within stated bounds", just after #337's table:
A server with a private network keeps the full claim: its NICs are on managed networks, the group attaches to every one of them, and a flexible IP, routed through the filtered NIC, stays covered.
The web servers of examples/stacks/scaleway are exactly that case: a public IP and a private NIC. Their interfaces are not all on managed networks.
The witness
$ incus query /1.0/instances/feint-scw-936e816e-… | jq -c '.expanded_devices | with_entries(select(.value.type=="nic"))'
{"eth0":{"ipv4.address":"203.0.113.3","ipv4.host_address":"169.254.0.1","nictype":"routed","type":"nic"},
"eth1":{"ipv4.address":"10.30.1.10","network":"fnt-0fa112b2861","security.acls":"scw-ff5a3e3cbd3","type":"nic"}}
eth0 carries the published address and no security.acls key. That machine's platform-web group was:
ingress:
- action: allow, source: 0.0.0.0/0, protocol: tcp, destination_port: "443"
- action: allow, source: 10.40.1.0/24, protocol: tcp, destination_port: "22"
No rule names port 80. From the station, with a service listening on 80 inside:
platform-web-1 203.0.113.2:80 OPEN
platform-web-0 203.0.113.3:80 OPEN
platform-bastion 203.0.113.4:80 closed # nothing listens: the negative control
The positive control is in the same pass: the bastion, the same shape of machine, answers on 22 and refuses 80 because nothing listens there. The probe therefore tells the two apart, and 80 on the web machines is a completed connection.
What happens, and why the order matters
The server is created with its ip_id. At boot it carries only the public address, so the driver gives it a routed NIC (#202), which accepts no security option at all (#337). The private NIC arrives afterwards, through scaleway_instance_private_nic, and becomes eth1 with the rule set. The public address stays on eth0.
That is the difference from the path tools/conformance/scaleway/network.sh measures, which passes: there the machine has its private NIC first, then the flexible IP is attached by a PATCH, so the address is routed through the filtered NIC (ipv4.routes) and the group covers it. Both measurements are true; they are about two creation orders.
Why this is not simply #337
#337 is closed, and its answer is a declared capability: capabilities.firewall_public_only: false, whose text says "a machine that joins no emulated network". A consumer reading both fields concludes that the uncovered case is the public-only machine. Here the machine has a private network, capabilities.firewall is true, firewall_public_only is false, and the public half of this machine is covered by nothing. The vocabulary does not describe this shape: it is the same family as #481, where the missing word was enforced.balancing.
What the section should say, and it is corrected in the same batch
docs/limits.md is rewritten with this issue rather than left standing while the issue waits. The paragraph now states the bound as a function of the order, with both rows measured:
| order |
what the machine carries |
the public address |
| private NIC first, address attached afterwards |
one managed NIC, the address on its ipv4.routes |
covered (fifteen runs both ways, already in the page) |
| address at creation, private NIC afterwards |
eth0 nictype=routed and eth1 on a managed network |
not covered: eth0 accepts no security option (#337) |
and adds the sentence that was missing: capabilities.firewall_public_only: false is broader than the case it was written for. It is not only the server whose only interface is routed, it is any address living on a routed NIC.
Consequence for the gates
tools/conformance/functional.sh (#503) therefore cannot assert the negative half of the firewall on the public path: it skips it naming capabilities.firewall_public_only, and measures the open/closed pair on the private cross-subnet path, where the receiver's policy holds. The public path there only proves that the published address is reachable.
What is not measured
- What the real cloud does: a Scaleway group obviously applies to the public address, but no recording under
corpus/ covers a closed port on a flexible IP.
- Whether the driver could move the public address onto the managed NIC when that one arrives afterwards, rather than leaving it on
eth0. Nothing was tried.
- The Outscale and Exoscale packs on the same shape. Only the Scaleway path was observed.
The test that fails without the fix
A machine carrying a routed NIC and a managed one, on a pack that declares enforced.firewall, turns red a control requiring that every interface carrying a published address be covered by a rule set, naming the interface and the address. That control does not exist today: tools/conformance/scaleway/network.sh demands that no interface escape the group, but on a machine whose flexible IP was attached afterwards.
Client: Terraform (scaleway provider, stack
examples/stacks/scaleway) · Branch:feat/503-what-a-stack-must-prove· Runtime:--vm incus-ovn, Incus 7.x · Measured: 2026-08-27The published sentence the measurement contradicts
docs/limits.md, section "The firewall enforces, within stated bounds", just after #337's table:The web servers of
examples/stacks/scalewayare exactly that case: a public IP and a private NIC. Their interfaces are not all on managed networks.The witness
eth0carries the published address and nosecurity.aclskey. That machine'splatform-webgroup was:No rule names port 80. From the station, with a service listening on 80 inside:
The positive control is in the same pass: the bastion, the same shape of machine, answers on 22 and refuses 80 because nothing listens there. The probe therefore tells the two apart, and 80 on the web machines is a completed connection.
What happens, and why the order matters
The server is created with its
ip_id. At boot it carries only the public address, so the driver gives it aroutedNIC (#202), which accepts no security option at all (#337). The private NIC arrives afterwards, throughscaleway_instance_private_nic, and becomeseth1with the rule set. The public address stays oneth0.That is the difference from the path
tools/conformance/scaleway/network.shmeasures, which passes: there the machine has its private NIC first, then the flexible IP is attached by aPATCH, so the address is routed through the filtered NIC (ipv4.routes) and the group covers it. Both measurements are true; they are about two creation orders.Why this is not simply #337
#337 is closed, and its answer is a declared capability:
capabilities.firewall_public_only: false, whose text says "a machine that joins no emulated network". A consumer reading both fields concludes that the uncovered case is the public-only machine. Here the machine has a private network,capabilities.firewallistrue,firewall_public_onlyisfalse, and the public half of this machine is covered by nothing. The vocabulary does not describe this shape: it is the same family as #481, where the missing word wasenforced.balancing.What the section should say, and it is corrected in the same batch
docs/limits.mdis rewritten with this issue rather than left standing while the issue waits. The paragraph now states the bound as a function of the order, with both rows measured:ipv4.routeseth0nictype=routedandeth1on a managed networketh0accepts no security option (#337)and adds the sentence that was missing:
capabilities.firewall_public_only: falseis broader than the case it was written for. It is not only the server whose only interface is routed, it is any address living on a routed NIC.Consequence for the gates
tools/conformance/functional.sh(#503) therefore cannot assert the negative half of the firewall on the public path: it skips it namingcapabilities.firewall_public_only, and measures the open/closed pair on the private cross-subnet path, where the receiver's policy holds. The public path there only proves that the published address is reachable.What is not measured
corpus/covers a closed port on a flexible IP.eth0. Nothing was tried.The test that fails without the fix
A machine carrying a
routedNIC and a managed one, on a pack that declaresenforced.firewall, turns red a control requiring that every interface carrying a published address be covered by a rule set, naming the interface and the address. That control does not exist today:tools/conformance/scaleway/network.shdemands that no interface escape the group, but on a machine whose flexible IP was attached afterwards.