Skip to content

A server created with a public IP keeps an unfiltered routed NIC beside its private one: the group covers one and not the other, and docs/limits.md claims the opposite #548

Description

@stephrobert

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugAn official client noticed the difference

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions