← Back to writing

Inbound First: Scoping Enforcement for a First Release

·–

Part 6 of Microsegmentation from First Principles, a series that works through the design of Innerwall, an open-source microsegmentation platform, one problem at a time.

Part 5 ended on scope. Identity, policy, distribution, and credentials are settled: a workload is who its certificate says, what may reach it is rendered from labels it cannot choose, delivered exactly, and enforced fail-static. A first enforcement release could constrain traffic in both directions. Innerwall's constrains one.

The easy reading is a release cut in half to ship sooner. This post argues otherwise. Inbound-only is not half a release. It is the half that pays, chosen over the half that bites, and the other half is reserved in the contract rather than left to be bolted on later.

Every internal flow is someone's inbound

A connection between two managed workloads has two ends, and enforcement could sit at either. Put it at the destination, on every workload, and each such connection meets exactly one enforcement point: the host it is trying to reach. None escapes, because every connection between managed workloads has a managed destination. None is evaluated twice, because no connection has two destinations. Draw the estate's east–west traffic as arrows between workloads; inbound enforcement puts a check at every arrowhead, and every arrow has exactly one.

Enforcing both ends would not make that coverage more complete. It would make every internal connection need two rules that agree, an egress rule at the source and an ingress rule at the destination, either of which can break it. The record puts the cost plainly: "Full bidirectional policy doubles the surface an operator must reason about before anything can be safely enforced."

The design record states what destination enforcement buys against an attacker in one line: "a workload that accepts connections only from its declared dependents is a hard target regardless of its neighbors' egress." Lateral movement is a sequence of inbound connections. A foothold on a web host can try the database, but whether it gets there is decided at the database, by a rule that names the web tier's labels and nothing else. The compromised host's own egress is unconstrained, and for movement between managed workloads that doesn't matter: every workload it can try to reach is guarding its own door.

The choice also closes a loop the series opened in its first post. Part 1 chose the destination as the reporting viewpoint, so that each connection is reported once, with the source address the destination actually sees. The design record states the same rule from the enforcement side: a connection between two managed workloads is "seen once, at its destination." The agent's connection-tracking source keeps only connections whose original destination is one of the host's own addresses, and the enforcement chain hooks that same host's input path and matches that same source address. The flow map, Part 2's would-block evidence, and the rule that finally drops a packet all describe one place. Nothing has to be translated from where the evidence was gathered to where the decision is made.

Between managed workloads

  • Declared dependency

    web-1

    app=web

    egress not consulted

    8443/tcp

    checkout-1

    app=checkout

    • enforced here
    • observed here (Part 1)
    • evidence here (Part 2)

    admitted by checkout-1's rule for app=web

  • Declared dependency

    checkout-1

    app=checkout

    egress not consulted

    5432/tcp

    payments-db-1

    app=payments-db

    • enforced here
    • observed here (Part 1)
    • evidence here (Part 2)

    admitted by payments-db-1's rule for app=checkout

  • Lateral attempt from a compromised web-1

    web-1

    app=web

    egress not consulted

    5432/tcp

    payments-db-1

    app=payments-db

    • enforced here
    • observed here (Part 1)
    • evidence here (Part 2)

    no rule names app=web: dropped at payments-db-1, recorded BLOCKED

3 connections · 3 enforcement points · none evaluated twice

To an unmanaged destination

checkout-1

app=checkout

egress not consulted

443/tcp

203.0.113.7

unmanaged, external

not seen, not enforced

deferred with outbound

Every connection between managed workloads has one arrowhead, and the chain there is the only one that evaluates it. The same host reports it and gathers its simulation evidence. A connection whose arrowhead is not a managed workload has no such point in this version.

Outbound is a different problem, not a smaller one

If inbound covers internal traffic, outbound can look like the remainder: the same mechanism pointed the other way, to ship once the first half settles. The design record gives two reasons it is not.

The directions fail differently. An over-tight inbound rule fails immediately and legibly: a known dependency stops reaching a workload, and the refusal shows up as a blocked flow from a named peer on a named service. An over-tight outbound rule breaks the host's infrastructure dependencies, such as DNS, directory services, NTP, and package mirrors, and in the record's words presents "as mysterious host misbehavior rather than as a blocked flow." Name resolution times out, logins stall, clocks drift, updates fail, and nothing in any of those symptoms says "firewall". The series has spent five parts making enforcement legible before it is switched on. Outbound is the direction in which a mistake is least legible after it is.

Outbound has a structural problem. Every application depends on the same infrastructure: the same resolvers, directory, time sources, and mirrors. Inbound rules have no analogue, because a workload's inbound rules describe its own clients, which are particular to it. Outbound rules written per application must each re-declare the shared plumbing, and the record names the outcome: "either massive duplication or massive holes." Either every scope carries its own copy of the estate's resolvers, and moving one becomes an edit to every policy, or scopes stop carrying them and allow too much.

The recorded answer is a layered model, with shared baseline policies owned by platform operators composed under app-scoped policies owned by application teams, and its composition and precedence semantics are open design work on the public roadmap.

The invariant inbound-only buys

The agent programs the host's firewall while depending on a connection through it. Its policy stream from Part 4 is the only way it hears of a new rule, including the one that would fix a mistake. An agent that installed a rule cutting its own stream would go on enforcing the mistake, fail-static, with no remote way to receive the correction. Recovering it would take a person at the host.

Inbound-only scope rules that out by construction. Part 2 drew the chain: accept loopback, accept established and related traffic, one accept per rule, then the terminal rule. The order is the invariant, and the design record states it in one sentence:

The established-and-related accept comes before any rule and, with inbound-only scope (ADR-0010), is the invariant that makes it impossible for the agent to sever its own control-plane stream: that stream is an outbound connection the agent initiated, its return traffic is established, and nothing after that line can touch it.

Host

Agent

Opened the stream.

Sync
outbound
output path, not hooked

Remote

Control plane

Sends policy down the stream.

Replies return on the input path and enter the agent's chain

Input chain · the agent's table

1iif "lo" acceptloopback
2ct state established,​related accept

The stream's replies exit here, back to the agent, before any rule is consulted. So does every reconnect's: an outbound connection's first reply is already established.

The policy · never reached by the stream

3ip saddr @r1 tcp dport {8443} acceptrule 1
4ip saddr @r2 tcp dport {5432} acceptrule 2 … rule n
5log group 200 dropterminal
No inbound rule, however wrong, can touch it. The established-and-related accept precedes every per-rule accept in the rendered chain, and the agent's table hooks input only, so an empty or over-tight policy cannot cut the stream that would carry its correction.

The agent's packets to the control plane leave on the output path, which its table does not hook at all. The replies come in on the input path, where the second line accepts them before any rule is consulted. That holds for every connection the agent opens, each reconnect included, because the first reply to an outbound connection already counts as established. A policy can be empty or wrong in every rule, and the stream that would carry its correction stays up.

Under outbound enforcement the guarantee stops being inherited. An output chain evaluates the agent's own connection attempts before any of them is established, so a reconnect is exactly the traffic a wrong outbound rule would refuse. The agent's path to its control plane would have to be carved out deliberately, kept out of reach of every policy an operator can write, and tested like any other safety property. That is real work, and it belongs in a release about outbound, not as a precondition of the first one.

Reserved, not retrofitted

Deferring a direction is safe only if bringing it back is cheap. The expensive way to add outbound is to find, with a fleet already running, that the policy format has no place for it: a new field on every rule, a migration of every stored policy, a contract change every agent must understand before any of it means anything.

Innerwall's contract has carried direction from its first version. The Direction enum defines DIRECTION_INBOUND and DIRECTION_OUTBOUND, and a rule orients by it rather than by separate source and destination fields: a ruleset's scope selects the local end, a rule's peers are the remote end, and the direction says which way the connection runs. What this version does with the reserved value is refuse it at admission. validateRule, in the control plane's policy package, rejects any rule whose direction is DIRECTION_OUTBOUND, naming the field (rules[0].direction for the first rule) and the reason:

policy: outbound rules are not admitted in this version; only DIRECTION_INBOUND is compiled (ADR-0010)

The admission test pins that error to that field. An unstated direction is refused as well, because DIRECTION_UNSPECIFIED is not a direction, so no rule reaches the renderer without saying which way it points.

Admission is the gate but not the only line. The renderer skips any rule that is not inbound, so that inbound-only, as its comment puts it, "holds even if a row reached the table by another path." The reservation runs through the evidence path too. Every flow record carries the same Direction, present on the wire so that records are "self-describing when outbound observation lands", and ingestion refuses a record that claims any direction but inbound. Policy and evidence reserve the other direction with the same enum, and both refuse it at their front door.

Enabling outbound is therefore, in the wire contract's design record, "a validation change, not a schema change", and the record is exact about the rest: outbound "changes admission validation and the rendered model, not the authored Rule shape." The admission check relaxes, the renderer learns the other direction, and new design records settle the layering. No rule written today has to be rewritten to keep meaning what it means.

It is the pattern of Part 5's signing interface: one backend shipped, the others deferred behind a shape that already holds them. Decided-not-yet is a state the system represents, not an absence.

What it costs

Inbound-only has a price, and the record states it without softening.

Version 1 observes inbound only. Each agent reports the connections that reach its own workload, so a connection from a managed workload to an unmanaged destination is not seen at all, as Part 1 said when it chose the viewpoint. Outbound observation arrives with outbound enforcement, not before it.

The record did not always say so. Until 2026-10-06, ADR-0010 said that outbound "remains observed (full flow visibility in both directions)". A conformance audit read the record against the code and found that sentence "was never true of the shipped agent": the collector reports inbound connections only, ingestion accepts inbound records only, and the wire contract defers outbound observation. The amendment, dated the day before this post, corrects the sentence in place and says when and why. That is the discipline the series has leaned on throughout, working in public. When the record and the code disagree about what the system does, the record is corrected to the code and the correction is itself recorded.

The consequence follows, and the record words it without hedging: "Compromised-host egress control, and the egress visibility it would rest on, are honestly deferred and documented as such." A compromised managed host can call out to the internet or to any unmanaged system, and this version neither stops nor records it. An estate that needs egress control today needs another control for it today.

Next: opening the box

Six parts have treated the host's firewall as a trusted executor: hand it a chain, and it enforces exactly that chain. The last part opens the box. It covers how a rendered policy becomes kernel rules in one atomic transaction, how an accepted connection is attributed back to the rule that admitted it, and what the host does when everything else is gone: the control plane, the stream, the daemon. That is Part 7, Native Firewalls as the Enforcement Plane: Compiling Policy Atomically.

Summary

  • Enforcing at every destination checks each connection between managed workloads exactly once. A workload that admits only its declared dependents is a hard target regardless of its neighbors' egress.
  • The destination is also where Part 1 observes and Part 2 gathers would-block evidence, so the map, the simulation, and the rule describe one host matching one source address.
  • Outbound is a different problem: its failures present as host misbehavior rather than blocked flows, and shared infrastructure forces duplication or holes without a platform-owned baseline layer, whose composition is open design work.
  • An established-and-related accept ahead of every rule, under inbound-only scope, means no rule can sever the agent's own control-plane stream. Outbound enforcement would have to engineer that guarantee.
  • Direction is in the contract from the first version and refused at admission, so outbound is a validation change. The cost, inbound-only observation and no egress control, is stated in the record, corrected in public the day before this post.

Next in the series: Native Firewalls as the Enforcement Plane: Compiling Policy Atomically.