---
title: "Security posture"
url: "https://nsta1.github.io/Orleans.Lattice/docs/lattice.auth/security-posture.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/lattice.auth/security-posture.md"
package: "Orleans.Lattice.Auth"
version: "9.9.0"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
bundle: "https://nsta1.github.io/Orleans.Lattice/docs/lattice.auth/llms-full.txt"
---
# Security posture

Part of the [Auth documentation](README.md).

This page describes the security posture of the Orleans.Lattice authorization
layer (`Orleans.Lattice.Auth` and the identity packages it builds on). It
summarises the threat model, enumerates the attack surface, states the
fail-closed guarantees the layer makes, defines the trust boundary for internal
grain calls, describes the bootstrap-administrator root of trust, and sets out
the transport-security expectations.

The review that produced this page, and the regression coverage that locks the
fixes in, are tracked in
[issue #1103](https://github.com/NSTA1/Orleans.Lattice/issues/1103).

## Threat model summary

The authorization layer defends a lattice cluster against callers that should
not be able to read or mutate data, or to reconfigure the authorization control
plane. The assets under protection are:

- **Tree data** - the key/value entries in each logical tree, including the
  entries surfaced through views.
- **The authorization control plane** - the membership graph (users, groups,
  memberships) and the policy rule set that together decide who can do what.
- **Existence metadata** - the mere fact that a given tree, view, or key exists
  can be sensitive, so the catalog and read surfaces must not leak it to a
  caller who cannot read the underlying data.

The layer assumes the following trust model:

- **Silos are trusted.** Every silo in the cluster is inside the trust boundary.
  Code running on a silo may establish internal capabilities (system-origin,
  view scopes, the internal-origin marker) because it is already trusted.
- **External clients are untrusted.** A caller reaching the cluster through the
  external data API (the gRPC state/data gateway) or as a plain Orleans client
  is untrusted. Its identity is whatever the registered credential authenticator
  resolves from its presented credential, defaulting to the anonymous subject
  when no valid credential is presented.
- **The Orleans clustering transport is a trust boundary, not an authorization
  boundary.** A party that can already issue arbitrary in-cluster grain calls is
  inside the trust boundary. The defense-in-depth internal-origin assertion (see
  below) hardens against a direct in-cluster grain call that skips the facade,
  but the primary control against external callers is that the external gateway
  can only reach the public facade grain.

## Attack surface

| Surface | Entry point | Enforcement |
|---|---|---|
| Data plane (external) | gRPC state / data API to the `ILattice` facade grain | The facade calls the registered access gate on every operation. |
| Control plane (external) | The admin API (`ILatticeAuthAdmin`) | Every auth-admin call, reads included, is authorized against the reserved authorization namespace, which is fail-closed (see [Fail-closed guarantees](#fail-closed-guarantees)). |
| App lifecycle (external) | The app control, role re-binding, and catalogue facades (`ILatticeAppsControl`, `ILatticeAppRoleBindings`, `ILatticeAppCatalog`) and their gRPC bindings | Every facade verb except the advisory capability probes - and every app-registry and activation transition behind it, even when called directly - authorizes the scopeless `AppInstall` capability over the `*` sentinel, which is fail-closed (see [Fail-closed guarantees](#fail-closed-guarantees)). |
| App workspace and UI bridge (external) | The per-user app workspace and app data bridge (`ILatticeAppWorkspace`, `ILatticeAppBridge`) and their gRPC bindings | Gated per caller rather than on `AppInstall`: the caller must hold a role of the enabled install in its active tenant and is otherwise answered exactly as for an app that does not exist; a bridge call then runs on the data path under the caller's own identity, so the access gate applies as well (see [`Orleans.Lattice.Api.Apps`](../lattice.api.apps/README.md)). |
| Read catalog (external) | State-API catalog and structure endpoints | Existence of a tree/view/key is hidden from a caller who cannot read the underlying source (see [Fail-closed guarantees](#fail-closed-guarantees)). |
| Explorer (operator tool) | The Explorer's gRPC client to the state API | Credentials only attach over a transport gRPC can confirm is secure (see [Transport-security (TLS) expectations](#transport-security-tls-expectations)). |
| Internal grain calls | Direct in-cluster calls to the shard, leaf, and write-ahead-log shard grains, to the internal coordinator and saga grains, and to the facade grain's internal-only system-tree and replication-apply interfaces | Defense-in-depth internal-origin assertion (see [Trust boundary for internal grain calls](#trust-boundary-for-internal-grain-calls)). |
| Credential smuggling | Reserved `RequestContext` capability keys | The capability-stripping incoming call filter re-derives every internal capability from the real caller identity on each hop (see below). |

## Fail-closed guarantees

The layer is designed to deny by default and to fail closed on every mutating
path:

- **Recommended default effect is deny.** With the recommended configuration an
  operation that matches no allow rule is denied.
- **The control plane is deny-by-default regardless of the data-plane default.**
  Even when the data-plane default effect is configured to allow, an unmatched
  decision in the reserved authorization namespace resolves to deny, so only a
  bootstrap administrator (or an explicitly modelled grant) is ever an
  administrator. The same isolation governs the tenant-registry (`sys-tenant-*`)
  namespace, the installable-apps (`sys-app-*`) namespace (the app registry, activation status and `sys-app-trees` tree ownership ledger), the tenant-administration capability ids, and a cluster-wide
  capability request on the `*` sentinel (such as `Telemetry` or `AppInstall`): each
  is granted only by an explicit matched allow rule.
- **App-owned rules are write-protected.** A policy-store write or delete of a rule
  id in the app-owned `app:` namespace (`LatticeAppRuleIds.Prefix`) is rejected with
  `LatticeAppOwnedRuleException` unless the caller is already running under system
  origin, as the app compiler is; a bootstrap administrator is no exception. The
  check runs before anything is read or written, so a rejected delete does not
  disclose whether the rule exists.
- **Denied mutations leave no partial state.** A denied single-key write,
  delete, range delete, CRDT apply, batch write, atomic multi-key write, or bulk
  load throws before any leg of the operation is applied. The adversarial
  regression suite asserts this fail-closed property for every operation class.
- **Existence is hidden on a read-around.** A caller who cannot read a tree's
  source data receives an empty or not-found result from the read and catalog
  surfaces, and cannot distinguish "exists but I cannot read it" from "does not
  exist". This is enforced on the facade grain itself:
  `ILattice.TreeExistsAsync` authorizes a whole-tree read and answers `false` for
  a denied caller, so the guarantee holds for a direct in-cluster grain call and
  not only for the state-API surfaces layered above it.
- **Tree metadata is read-authorized, not public.** A tree's volumetric and
  structural metadata is source data for this purpose, so
  `ILattice.DiagnoseAsync`, `GetStorageUsageAsync`, and
  `GetHistoryRetentionAsync` each authorize a whole-tree read before disclosing
  anything. Because those reports aggregate over every key in the tree, a partial
  (prefix) grant is refused rather than narrowed: a per-shard key count or byte
  total cannot be pruned per key without still disclosing the keys it counted.
  `GetRoutingAsync` remains deliberately ungated - it resolves routing for
  internal coordinator grains and discloses no user data or volumetrics.

## Trust boundary for internal grain calls

Access-gate enforcement lives on the grains the public API routes a caller to:
the `ILattice` facade grain, the durable cursor it opens, and the coordinator a
cross-tree atomic write runs on. The physical shard, leaf, and write-ahead-log
shard grains beneath them enforce no policy of their own, so a direct in-cluster
grain call to one of those keys would otherwise bypass policy.

Two mechanisms harden this boundary:

1. **The capability-stripping incoming call filter.** A silo-wide incoming grain
   call filter re-derives the internal capability markers from the real caller
   identity on every hop. The reserved `RequestContext` capability keys - the
   system-origin gate-bypass marker, the view read/write scopes, the
   internal-origin marker, and the replication / maintenance origin markers - are
   the signals that cause access-gate enforcement to be skipped without any
   authentication, so an external caller must never be able to assert them. The
   filter strips every such key from a call that arrives from a genuine external
   Orleans client, and stamps a fresh internal-origin marker (re-derived, never
   trusted from the wire) on a call that is silo-sourced or comes from this
   cluster's own in-silo hosted client. A malicious client that manually seeds
   the system-origin or internal-origin marker on its outbound `RequestContext`
   therefore cannot smuggle a forged capability into a grain call. The filter
   strips the routed tree-identity pair the same way - the logical and physical
   tree ids the routing tier stamps for mutation observers, the `ol.rlt` and
   `ol.rpt` keys - so an external client, or one presenting a forged `hosted-*`
   client id, cannot make observers attribute its writes to another logical tree. The in-silo
   hosted client keeps the pair.

2. **The internal-origin assertion.** The shard, leaf, and write-ahead-log shard
   grains assert, on their read entry points as well as their mutation entry
   points, that the current turn carries the internal-origin marker
   (established only inside the trust boundary). A direct external client call
   carries no such marker - any forged one having been stripped by the filter -
   and is refused. The same assertion guards the other internal surfaces that
   skip the gate: the atomic-write saga, the cross-tree receiver, the structural
   lifecycle coordinators (tree deletion, merge, reshard, resize, snapshot, and
   shard split and consolidation), the schema-remediation coordinator that
   `Orleans.Lattice.Schema` adds, and the facade grain's internal-only
   system-tree and replication-apply interfaces. The assertion is keyed on the
   presence of the filter (a sentinel the authorization layer registers beside
   it), so it activates exactly when the filter that establishes the marker is
   present. A no-auth cluster, or
   a cluster that registers a custom access gate without the filter, never sets
   the marker and pays nothing.

### In-silo hosted client discriminator

The in-silo hosted client that in-silo infrastructure uses (for example the
co-hosted gRPC gateway and the authorization initializer) is a client from the
Orleans runtime's point of view, but it is inside the trust boundary. The
Orleans hosted-client grain id prefix (`hosted-`) alone is not a trust signal:
the caller's source id is client-supplied and unvalidated, so an external client
can announce an arbitrary `hosted-*` id. Recognition therefore requires the
`hosted-` prefix **plus** validation that the silo address embedded in the id is
either this silo's own address or a currently active member of the local
cluster. Only then does the filter exempt the call from stripping and let it
establish the internal capabilities it legitimately needs; a forged `hosted-*`
id whose embedded address is not a live cluster silo is treated as an external
client and has its forged capabilities stripped.

## Bootstrap administrators (break-glass root of trust)

`LatticeAuthOptions.BootstrapAdministrators` is a set of **subject ids** that the
gate treats as a break-glass root of trust. A request whose resolved subject id
is in the set is short-circuited to allow *before* the decision engine is
consulted, so it works even against a cold or empty policy snapshot. It exists
for one reason: to stop a deployment locking every operator out of the
authorization tree itself. Under the recommended deny-by-default posture the
reserved control-plane namespace is forced closed (see
[Fail-closed guarantees](#fail-closed-guarantees)), so with no
rules yet authored nobody could seed the first one. A bootstrap administrator
seeds that first policy and repairs a misconfiguration that would otherwise be
unrecoverable.

Its security properties, and the constraints that keep it safe, are worth stating
plainly:

- **It is only as strong as the authenticator that resolves the subject.** The
  gate matches `request.Subject.SubjectId`, which is the output of the membership
  resolution pipeline, not a string the caller supplies to the gate. Binding a
  bootstrap id to an unforgeable, cryptographically-validated identity (for
  example a Microsoft Entra `oid` from a signed token) means impersonating it
  requires forging a signed token. Binding it to an identity minted by a
  trusted-token authenticator that maps a plaintext token verbatim to a subject
  id (as several shipped samples do for brevity) turns the bootstrap id into an
  unsigned bearer secret - acceptable for an in-process demo, **never** for a
  deployment reachable by anything untrusted.
- **The bypass is cluster-wide god mode, not just policy repair.** A bootstrap
  administrator is allowed every operation on every tree - the data plane as well
  as the control plane - and is exempt from strict epoch fencing. It is a
  break-glass identity, not a day-to-day admin role. Grant ordinary
  administrative rights through explicit rules; reserve the bootstrap set for
  recovery.
- **Keep the set as small as possible.** It is empty by default. Every id in it
  is a full-cluster master key, so the set should hold the smallest possible
  number of break-glass operator identities, be sourced from a strong identity
  provider, and be audited. The option is live-reloadable, so an id can be added
  for a recovery window and removed again afterwards.

## Transport-security (TLS) expectations

- **Credentials only flow over a transport the runtime can confirm is secure.**
  The Explorer's gRPC client, and the replication transports, only lift the
  gRPC insecure-channel safeguard for an endpoint that is genuinely plaintext
  (an `http` address) and only when the operator has explicitly opted into
  unencrypted transport. For an `https` endpoint the safeguard stays active and
  credentials still attach over the confirmed-secure channel.
- **Production deployments should terminate TLS at or before the cluster's
  external endpoints.** The plaintext opt-in exists for local development and for
  deployments that terminate TLS at a trusted proxy; it should not be enabled on
  an endpoint that is reachable by an untrusted network.
