Interservice ACL

Microbus’s interservice authorization mechanism is precise and true-to-code. The model authorizes every microservice-to-microservice call at the NATS broker, using an allow-list of subjects derived from the caller’s own source code. A compromised microservice can call only the endpoints its code already invokes today, at the granularity of the exact {host, port, method, path} tuple.

The Model in Two Halves

Interservice ACL has two halves that work together.

Identity certification. Every message’s sender is broker-verified at the NATS layer, not application-attested. The NATS broker pins each connected microservice to publish only under its own hostname segment. Receivers parse the segment off the inbound subject and trust it as broker-verified.

Precision authorization. Each microservice’s .creds file carries an explicit allow-list of {host, port, method, path} tuples derived from its source code. The allow-list is true to code. Endpoints a microservice could call but doesn’t are absent from the allow-list, by construction, so a compromised microservice cannot reach them.

The combination is strictly tighter than mTLS-style service mesh authentication. TLS authenticates the connection once, and then the client can hit any endpoint on the destination. Interservice ACL authenticates every message and authorizes per endpoint.

The mechanism that makes both halves work is the NATS subject layout. Every subject carries a <src> segment broker-pinned to the caller’s identity, plus a <dest>.<port>.<method>.<path> tuple that together define a precision call path from source to destination. An ACL rule must match the full tuple, so the destination host along with its port, method, and path are all part of the authorization decision.

How It Works

The mechanism is straightforward and rests entirely on NATS. There is no per-message cryptography, no registration step, and no envelope-signing protocol.

  1. Each microservice holds its own signed .creds. The .creds file is a NATS user JWT signed by an account NKey held by the operator, paired with the connection’s NKey seed. It carries the connection’s public NKey, the microservice’s hostname (for logging only), and an explicit allow-list of NATS subjects this connection can PUB and SUB. Anything not in the list is denied at the broker.
  2. The allow-list is true to code. gencreds scans each microservice’s source code at deploy time and emits exactly the subjects the code actually publishes to and subscribes from. A compromised microservice can only do what its code actually does today, not anything its typed clients could do.
  3. The PUB allow-lists pin the source segment. Every PUB rule for a given microservice has the same <src> value, that microservice’s flattened hostname. The broker rejects any publish with a different <src>. There is no header, claim, or in-band override that bypasses this.
  4. Receivers trust the broker. On the receiver side, the framework parses the <src> segment off the NATS subject (which the broker delivered with broker-enforced contents) and unconditionally overwrites the publisher-set Microbus-From-Host header with the broker-verified value before any handler runs. Application code that reads Microbus-From-Host gets the verified caller.

The wire format is what makes this work. The subject’s structure dedicates one segment to source identity, and ACLs match by segment. The trust segment immediately after the plane (safe for non-:666, danger for :666, reply for response subjects) splits dangerous-tier traffic from the rest, so ACLs can express “any safe port” as one pattern without colliding with trust-root rules.

Threat Model

Interservice ACL is designed for one threat: a peer already on the bus that should not be trusted. Two realistic origins:

  1. Compromised legitimate microservice. A publicly-exposed microservice is popped via deserialization bug, SSRF, image-processing CVE, vulnerable dependency, or supply-chain compromise. The attacker now executes code inside a microservice that holds valid NATS credentials.
  2. Exfiltrated credentials. The credentials file of one of the microservices (nats.creds) leaks. The attacker can connect to NATS as that one microservice, from anywhere.

Both collapse to one primitive. The attacker holds a microservice’s NATS identity and can therefore connect to the bus. The HTTP ingress proxy cannot help here, because it guards against external HTTP traffic, not bus-internal traffic. What stops the attacker from spreading laterally is interservice ACL.

How a Compromise Is Contained Laterally

Suppose that a microservice is fully compromised. The attacker controls the microservice’s process and its nats.creds. What can the attacker do?

  • Cannot impersonate any other microservice. The microservice’s connection credentials permit publishing only under <plane>.*.*.<hostname>.>. The broker rejects any publish with a different <hostname> segment. There is no header, claim, or in-band override that lets the microservice publish under the identity of another.
  • Cannot reach endpoints that the microservice’s code doesn’t already invoke. The allow-list is true to code. It grants precise {host, port, method, path} access-rights based on the microservice’s source code. If the microservice’s code never calls :666/admin-action on a peer, its .creds file has no permission for that subject, even if the microservice regularly calls :443/get-status on that same peer. The attacker is bounded to the current usage surface, not the full capability surface.
  • Cannot mint fresh access tokens unless privileged. Access token minting lives in the trust-root tier (port :666), and access is granted only to highly-privileged microservices.
  • Cannot escalate by forging actor JWTs. Even with full code execution inside the microservice, the attacker cannot construct a valid actor JWT signed by the access token service’s key. All receivers verify the signature via JWKS pinning, and only the access token service holds the signing key.
  • Can replay any actor JWT in the microservice’s request frames during the compromise window. Actor JWTs are short-lived (operator-tunable, typically seconds or minutes). Once they expire, replay stops working.
  • Can subscribe only to the microservice’s allow-list. Typically that’s the microservice’s own destination plus control-plane broadcasts, narrow by construction. Blast radius is small for most microservices because each microservice subscribes to its own inbound traffic, not to peers’. Per-service identity is what keeps it that way.

Net effect: lateral spread from a compromised microservice is bounded by its existing code-derived capability set. Compromising one microservice does not give the attacker access to everything the bus carries. It gives access only to the things the code already does. Attackers must compromise each microservice individually to get that microservice’s capabilities, and there is no shortcut.

What About a Leaked nats.creds from a high-privilege Microservice?

If the leaked nats.creds belong to a microservice with broad permissions such as the HTTP ingress proxy or Foreman with their forwarding wildcard grants, the blast radius scales with that microservice’s capabilities. The mitigation is operational:

  • Time-bound the credentials. gencreds --expiration <duration> sets an exp claim on every emitted .creds, so the broker auto-rejects it once the time has passed. This requires a redeploy cadence shorter than the expiration, since each deploy reissues fresh .creds and the broker keeps accepting the new ones. The default is 0 (no expiration). A leaked .creds without an exp remains valid until its public key is revoked.
  • Revoke explicitly. nsc revocations add-user invalidates a specific user public key immediately, even if old binaries are still running. This is the only mechanism that kills a leaked nats.creds before its exp, or at all when --expiration was not used.
  • Rotate per deploy. gencreds’s default is fresh user NKeys on every run. Rotation does not retroactively invalidate older creds, but it limits exposure prospectively. A leak from one build cannot be used to predict or impersonate the identity of any other build.
  • Minimize broad-permission microservices. A microservice whose ACL grants safe.*.*.*.*.> is a high-value target. Where possible, prefer code that calls specific downstreams over dynamic dispatch. gencreds detects the difference, and narrower code produces narrower ACLs automatically.

What’s Not in the Threat Model

  • External HTTP attackers with no bus foothold. The HTTP ingress and port-block rules already handle this. Interservice ACL is the next layer in if those fail.
  • Root on the host. An attacker with host-level access can read any nats.creds, dump any process’s memory, and forge anything. Out of scope for any in-process control.
  • Same-process bundled microservices attacking each other. Microservices in one OS process share an address space and trust each other by definition. Bundling is an explicit operator decision to collapse identities.
  • Compromised NATS broker. Interservice ACL trusts the broker to enforce the per-microservice ACL. A broker that delivers forged messages would defeat the source-pinning guarantee.

In-process short-circuit

When two microservices live in the same OS process, requests bypass NATS entirely and dispatch through an in-process trie. The receiving microservice still parses the source segment from the subject and overwrites Microbus-From-Host, so the verified-source path runs uniformly across both transports.

There is no broker to enforce ACLs in-process, so short-circuit’s source segment is publisher-attested rather than broker-verified. This is acceptable because short-circuit only delivers between microservices that already share an OS process boundary. An attacker who can write into the source segment in-process has already compromised the address space and can do anything those microservices can.