Table of Contents

Security: identity, authorization, and enforcement

This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at security.md, and llms.txt lists every page.

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).

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 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 ILatticeCredentialAuthenticators. 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 - a Microsoft Entra ID (Azure AD) credential authenticator. Its Azure CLI setup guide provisions an app registration and shows the host wiring end to end.
  • Orleans.Lattice.Membership.Entra.Graph - a Microsoft Graph-backed resolver for subjects whose group claims overflow the token.
  • Orleans.Lattice.Membership.Oidc - 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 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.

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 does), and a refusal throws LatticeTreeOwnershipDeniedException. See Tree Registry.

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 and its gRPC binding - 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 - 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 - 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.

The Explorer runs an extensible login challenge against an auth-enabled State API endpoint, and its sign-in mechanisms are a provider model a host can extend. Microsoft Entra ID sign-in ships as two such providers: Orleans.Lattice.Explorer.Entra, an interactive MSAL sign-in for hosts that do not use the hosted-web OpenID Connect cookie flow, and Orleans.Lattice.Explorer.Entra.Web 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. 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. 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.

Security posture and cost

The security posture 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.