Skip to content

cloudfall audit reports false drift once a PostgreSQL service declares remoteAccess #143

Closed

Description

@shipmill-romamo

Found while building #123 (PR #141, spec S-006 remoteAccess).

The problem

cloudfall audit will report false drift on a server whose PostgreSQL service declares remoteAccess:

  • sdk/src/cloudfall/audit.py compares the host's nftables rules without their source match, so the new ip saddr <cidr> tcp dport 5432 accept rule reads as an unexpected open tcp/5432 and the firewall check reports a mismatch
  • its listener check flags PostgreSQL listening on listenAddress as an exposed port, though the firewall only lets the declared sources in

Reproduce

Once #141 lands: declare remoteAccess with listenAddress: 10.0.0.2 and allowedSources: [10.0.0.0/24] on a PostgreSQL service, converge baseline and services, then run cloudfall audit. Expected: no finding for port 5432. Actual (from reading the code, not run live): a firewall mismatch and an exposed-listener finding.

Proposed fix

Teach audit.py the rendered allowedInbound entries' sources: a rule matches when its port, protocol and source CIDRs match, and a listener on a port open only to declared sources is not "exposed". Servers without remoteAccess audit exactly as today.

Related: #81, #123, docs/specs/006-standalone-postgresql.md.

Activity

  1. shipmill-romamo commented on Oct 27, 2026

    @shipmill-romamo
    ContributorAuthor

    Triage: implement, for the next release candidate.

    Confirmed on main (2828e49) by reading the code, as the report says:

    • firewall.allowedInbound in sdk/src/cloudfall/audit.py builds its expected list from firewall_rules_for_server, which knows only the server type's declared rules. The remoteAccess rules are added later, in the engine's _remote_access_rules (engine/src/cloudfall_engine/ansible_inventory.py), so the expected list lacks tcp/5432 while the host has it: a mismatch on every server with remoteAccess
    • _accepted_inbound_endpoint drops the ip saddr match, so even with the rule expected, a rule open to every address would read the same as one narrowed to the declared sources
    • services.bind[<id>] counts any non-loopback listener on the port as exposed, so PostgreSQL on listenAddress fails it

    The plan:

    • Derive the source-restricted entries in the SDK (one function both audit.py and the engine's inventory use, so they can't drift) and add them to the expected firewall rules
    • Read the ip saddr / ip6 saddr match of each accepted rule; a restricted rule compares as tcp/5432 from 10.0.0.0/24, one entry per source as nftables holds them. Rules without sources keep today's tcp/22 form, so servers without remoteAccess audit exactly as today
    • services.bind accepts a listener on the service's remoteAccess.listenAddress when the declared sources narrow the port; any other non-loopback listener is still exposed
    • Tests: a snapshot with the rendered remoteAccess rule audits clean; the same port open to every address, a wrong source, and a listener on an undeclared address each still fail

    I'll link the PR here.

  2. shipmill-romamo commented on Oct 27, 2026

    @shipmill-romamo
    ContributorAuthor

    @romamo Decided without waiting: cloudfall audit shows a source-restricted firewall rule as tcp/5432 from 10.0.0.0/24 in firewall.allowedInbound, one entry per source, and keeps tcp/22 for rules without sources.

    Why: the check has to tell a rule narrowed to the declared sources from one open to every address, or the audit would pass a wide-open PostgreSQL port; the new form appears only on servers with remoteAccess, so every existing audit reads as before.
    Not taken: keep the protocol/port form and compare sources only internally, which keeps the output shorter but hides which source drifted.

    Reply with a comment starting with veto before 2026-10-28 11:15 UTC to reverse it, adding what you want instead if you like. A later veto still counts and gets a follow-up.

  3. shipmill-romamo commented on Oct 27, 2026

    @shipmill-romamo
    ContributorAuthor

    The fix is in #148: firewall.allowedInbound now expects one tcp/5432 from <cidr> entry per declared source and reads the ip saddr/ip6 saddr match of each nftables rule, and services.bind requires PostgreSQL to listen on both 127.0.0.1 and the declared listenAddress. The engine and the audit derive the source rules from one SDK function, remote_access_rules_for_server. Servers without remoteAccess audit exactly as before, and a test pins that. The behaviour change for remoteAccess servers: a PostgreSQL that failed to bind listenAddress now reads as drift, and so does any source rule nftables holds in a form the audit doesn't parse (a set, a negation). Unverified: the nft JSON shapes are taken from libnftables' documented format, not captured on a live host; the S-006-9 proving run in #145 can confirm them. This issue closes when #148 merges.

  4. shipmill-romamo commented on Oct 27, 2026

    @shipmill-romamo
    ContributorAuthor

    Released in v0.8.0rc7 (pre-release). uv tool install --prerelease allow cloudfall==0.8.0rc7
    A plain install gets it with the next stable release.
    If you filed this: react 👍 on this comment once it works for you, or reply if it doesn't.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdecidedAn agent decided at notify; the maintainer may veto

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions