---
title: "Security: identity, authorization, and enforcement"
url: "https://nsta1.github.io/Orleans.Lattice/docs/lattice/security.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/lattice/security.md"
package: "Orleans.Lattice"
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/llms-full.txt"
---
# Security: identity, authorization, and enforcement

Part of the [Orleans.Lattice documentation](architecture.md).

Orleans.Lattice ships an **opt-in** security layer that turns an anonymous,
allow-all key-value store into an authenticated, authorized one. It is composed
of several small add-on packages that layer cleanly on top of the core library.
This page is the map: it explains what each capability does and links to the
package documentation that covers it in depth. Nothing here is enabled unless
the host registers it, and a cluster that registers none of it keeps the core
read/write path byte-for-byte unchanged (see
[Zero cost when absent](#zero-cost-when-absent)).

## The pipeline at a glance

A gated operation flows through four stages. Each stage is a separate package so
a deployment adopts only what it needs:

1. **Identity** turns the credential a caller presents into a stable *subject*
   (a subject id plus the transitive closure of the groups it belongs to).
2. **Authorization** compiles durable rules into a decision: given a subject, an
   operation, and a tree key or range, allow or deny.
3. **Enforcement** consults that decision on the core data path for every
   user-originated operation, fail-closed: a denied write throws and a denied
   point read reports absent.
4. **External surfaces** project the same gated data and control planes to
   non-.NET callers, operators, and the Explorer - all authorizing through the
   very same gate, never a bespoke bypass.

## Identity and membership

[`Orleans.Lattice.Membership`](../lattice.membership/README.md) owns a durable,
introspectable directory of users and groups (with nested, group-in-group
membership) and a credential-to-subject resolution pipeline. Authentication is
pluggable: a credential is mapped to a principal by one or more scheme-selected
`ILatticeCredentialAuthenticator`s. The package ships an anonymous authenticator
and a built-in **JWT authenticator** registered per trusted issuer; a host can
register its own. Resolution is cached with a configurable TTL.

Three optional companions integrate a corporate identity provider:

- [`Orleans.Lattice.Membership.Entra`](../lattice.membership.entra/README.md) -
  a Microsoft Entra ID (Azure AD) credential authenticator. Its
  [Azure CLI setup guide](../lattice.membership.entra/entra-setup.md) provisions
  an app registration and shows the host wiring end to end.
- [`Orleans.Lattice.Membership.Entra.Graph`](../lattice.membership.entra.graph/README.md) -
  a Microsoft Graph-backed resolver for subjects whose group claims overflow the
  token.
- [`Orleans.Lattice.Membership.Oidc`](../lattice.membership.oidc/README.md) -
  a generic OpenID Connect credential authenticator for any conformant provider
  (Okta, Auth0, Keycloak, Ping, Google), configured from the provider's
  discovery document. It is an additive sibling to the Entra authenticator:
  neither package depends on the other, and a silo can register both.

Membership on its own adds identity resolution and the directory; it enforces
nothing until the authorization package is also registered.

## Authorization: policy and decisions

[`Orleans.Lattice.Auth`](../lattice.auth/README.md) adds the durable policy
store, the decision engine, and the enforcing access gate. Rules grant or deny a
set of operations to a subject selector (a user or a group) at a scope (a whole
tree, a key prefix, or a single key). When several rules match, the engine
resolves them deterministically: the most-specific scope wins; within a scope
tier a user rule outranks a group rule (on by default, configurable), and
otherwise deny overrides allow; with no matching rule the configured default
effect applies. Cluster-wide capabilities that do not attach to a tree - such as
telemetry and app installation - are granted by a whole-tree rule on the
cluster-wide `*` scope. An opt-in all-trees tier
(`LatticeAuthOptions.AllTreesGrantsEnabled`, off by default) also lets such a
cluster-wide rule govern ordinary trees: an all-trees deny then wins outright, and
an all-trees allow applies only where the tree's own rules decide nothing. The
recommended and default
posture is **default-deny**. A small set of **bootstrap administrators** forms
the root-of-trust that seeds the first rules and performs break-glass operations.

### Consistency: eventual by default, strict on request

Policy propagation is **eventually consistent** by default: a rule change takes
effect once the destination's compiled snapshot rebuilds off the updated policy
tree, which happens continuously in the background. A tree whose writes must not
run ahead of a policy change can opt into a **strict epoch fence** by naming the
tree in the strict-consistency set: a user write to it is then rejected while this
cluster's compiled policy is older than a floor the caller stamped, and reads are
never fenced. Strict behaviour is opt-in and off by default. The trade-offs are
covered in the
[authorization README](../lattice.auth/README.md#consistency-modes).

## Enforcement on the data path

The core library exposes an access-gate seam that defaults to an allow-all null
gate. Registering the authorization package replaces it with the enforcing gate,
which every user-originated core read and write consults; the routing lookup
`GetRoutingAsync`, which resolves a tree's physical id and shard map for the
library's own coordinators, is deliberately ungated. Enforcement is fail-closed
throughout: writes and deletes throw on denial, point reads of a denied key
report absent, and range scans prune to the authorized subset server-side, while
a read that cannot be narrowed per key - `CountPerShardAsync`, a projection
digest, or a whole-tree report such as `DiagnoseAsync` - throws
`LatticeAuthorizationDeniedException` under a partial grant as well as a deny. A
range delete under partial authorization is **hard-denied** rather than silently
narrowed, so a caller never deletes a subset while believing it deleted a range.
The State API honours the same read-access visibility when membership and
authorization are registered, so a browsing operator only sees the keys the
subject may read.

Alias changes carry one further, ownership-based check that caller rights cannot
override: every alias assignment - resize, restore and remediation included - is
put to the core `ITreeOwnershipGuard` seam, which allows every alias unless the
host registers an ownership provider (the [apps package](../lattice.apps/README.md)
does), and a refusal throws `LatticeTreeOwnershipDeniedException`. See
[Tree Registry](tree-registry.md#ownership-bounded-aliasing).

## External surfaces

Transport-agnostic facades, each with a code-first gRPC binding, extend the
cluster to callers that do not embed the Orleans client, and every one of them
authorizes through the core gate rather than re-implementing authorization.
Three of them are this layer's own surfaces:

- [`Orleans.Lattice.Api.Auth`](../lattice.api.auth/README.md) and its
  [gRPC binding](../lattice.api.auth.grpc/README.md) - the **control plane**:
  administer membership and policy, and explain decisions. Every operation is
  administrator-gated through the same enforcement primitive the data path uses,
  and `ExplainAsync` produces its verdict from the same gate. The subject it
  evaluates is rebuilt from the membership directory (the named id and its
  transitive directory groups), so token-asserted or claim-projected groups are
  not part of it, and under the `TokenOnly` group-merge mode the explained
  subject can differ from a live caller's.
- [`Orleans.Lattice.Api.Data`](../lattice.api.data/README.md) - the write-capable
  external **data plane** for non-.NET clients. It routes every call through the
  gated `ILattice` surface, so per-tree and per-key rights are enforced
  automatically; a coarse transport-level authorizer that denies by default sits
  in front as an endpoint on/off switch.
- [`Orleans.Lattice.Api.State`](../lattice.api.state/README.md) - the read-only
  state-query surface, which honours the same read visibility.

The other facades - tree administration, backup, replication, schema, tenant
administration, app installation, and telemetry - authorize their operations
through the same gate, and the Model Context Protocol endpoint projects those
facades as agent tools. The full set is listed in [PACKAGES.md](../../PACKAGES.md).

The [Explorer](../lattice.explorer/connecting-to-an-auth-enabled-state-api.md)
runs an extensible login challenge against an auth-enabled State API endpoint,
and its sign-in mechanisms are a
[provider model](../lattice.explorer/adding-a-custom-auth-method.md) a host can
extend. Microsoft Entra ID sign-in ships as two such providers:
[`Orleans.Lattice.Explorer.Entra`](../lattice.explorer.entra/README.md), an
interactive MSAL sign-in for hosts that do not use the hosted-web OpenID Connect
cookie flow, and
[`Orleans.Lattice.Explorer.Entra.Web`](../lattice.explorer.entra.web/README.md)
for the hosted web console.

## Cross-cluster convergence

The membership directory and the policy store are ordinary `ILattice` trees, so
they replicate through the replication package's
[system-tree enrolment](../lattice.replication/system-tree-replication.md). This
is a **separate, explicit opt-in** from data replication: a rule authored or
revoked in one cluster converges to the others, in both state and enforcement.
Some deployments deliberately keep policy local (per-region security domains,
data-residency boundaries, or blast-radius containment); the trade-offs are
discussed in that document.

## Observability and audit

Every authorization decision, its latency, and the compiled-snapshot epoch and
age are published on a single meter, and an optional durable audit sink records a
decision trail. The full instrument catalogue and the audit-sink seam are
documented in [Authorization observability](../lattice.auth/observability.md).
The subject-resolution cache belongs to the membership package, so its hit and
miss counters live on the membership meter and are documented in
[Membership observability](../lattice.membership/observability.md).

## Security posture and cost

The [security posture](../lattice.auth/security-posture.md) page is the
authoritative reference for the threat model, the attack surface, the fail-closed
guarantees, the internal-grain trust boundary, TLS expectations, the
security-review findings with their resolutions, and the **measured
per-operation cost** of enforcement.

## Zero cost when absent

Every layer above is opt-in. The core registration installs only the allow-all
null gate, whose decision is a synchronously-completed, allocation-free allow
that never resolves a subject. A cluster that never registers authorization keeps
that null gate, so the data path is byte-for-byte what it was before the security
layer existed.

## Registration order

The layers must be registered in dependency order on the silo: the core lattice
first, then membership, then authorization, then any external facade. Each
add-on validates its prerequisites and fails fast at registration with an
actionable message rather than failing obscurely at silo start. The exact calls
are shown in each package README linked above.
