Password Protection
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 README.md, and llms.txt lists every page.This sample shows the built-in username/password authentication front door for the State API gRPC surface (
AddEnvVarCredentialAuthorizer) composed with the per-tree authorization layer (AddLatticeMembership+AddLatticeAuth) on a single silo. For the authorization layer on its own, see the Authorization sample.
What it shows
Two operator accounts protecting one in-process Orleans silo that exposes the read-only State API over gRPC:
- admin - a bootstrap administrator. It seeds the trees, authors rules, and can read everything.
- reader - an ordinary user granted read-only access to a single tree
(
orders) and nothing else.
Each account is a salted PBKDF2-SHA256 password hash (never the plaintext),
published in the LATTICE_STATE_USER_<name> environment variable the authorizer
looks up by username. In a real deployment an operator mints these with the
tools/New-LatticeStateCredential.ps1 helper and sets the variables out-of-band; the sample mints
them in-process so it runs with a single command.
Two layers cooperate on every call:
- Authentication (transport).
EnvVarCredentialAuthorizervalidates the inboundauthorization: Basic base64(user:pass)header against the stored hash. A wrong password or a missing credential is rejected before any tree is touched. This is an authentication front door only - it does not consult the target tree or operation. - Authorization (data plane). A small
ILatticeStateApiCredentialBridgelifts the authenticated username into the ambient caller credential, anILatticeCredentialAuthenticatormaps it to a subject id, and the default-denyAddLatticeAuthgate enforces the per-tree rule. The reader can readorders; theledgertree is hidden from it entirely.
The demo runs in four acts:
- Create accounts. Mint the two salted password hashes and publish them as environment variables.
- Stand up the silo. Start one silo plus the State API gRPC surface with
authentication required, then seed
orders(3 entries) andledger(2 entries) as the bootstrap administrator. - Authenticate. Present each account's Basic credential over gRPC. The
correct passwords authenticate; a wrong password and an anonymous call are
both rejected with
PermissionDenied. - Authorize per-tree. Scan both trees as each account. The admin sees
everything; the reader sees
ordersbut theledgertree reads as empty because the gate hides it rather than disclosing its existence.
Run it
dotnet run --project samples/PasswordProtection
Expected output
== Act 1: create two operator accounts ==
admin -> env LATTICE_STATE_USER_admin (salted pbkdf2-sha256) role: bootstrap administrator
reader -> env LATTICE_STATE_USER_reader (salted pbkdf2-sha256) role: read-only on tree 'orders'
== Act 2: start single silo + State API gRPC (auth required) ==
Silo + state-API gRPC listening on http://localhost:5223
Seeded tree 'orders' with 3 entries; tree 'ledger' with 2 entries.
== Act 3: authenticate over gRPC (username/password) ==
admin correct password -> authenticated
reader correct password -> authenticated
reader WRONG password -> rejected (PermissionDenied)
no credentials -> rejected (PermissionDenied)
== Act 4: authorize per-tree reads (tied to the authenticated user) ==
admin scan 'orders' -> 3 entries ; scan 'ledger' -> 2 entries (bootstrap admin: sees all)
reader scan 'orders' -> 3 entries ; scan 'ledger' -> 0 entries (granted 'orders' read; 'ledger' hidden)
[OK] username/password authenticated both users; per-tree rules limited 'reader' to 'orders'.
When to use
- Single-cluster deployments that expose the State API gRPC surface to operators or tools and want a simple username/password front door without wiring up an external identity provider.
- Deployments that need the password front door and per-tree authorization, so a given account can read only the trees it was granted.
When not to use
- Deployments that already authenticate with an OIDC/JWT identity provider - use a bearer-token bridge instead of the Basic credential authorizer. The authorization layer underneath is identical.
- Any surface exposed without TLS. The
Basiccredential is only as safe as the channel it rides: terminate TLS at the channel or an outer boundary so the credential is never sent in clear text. This sample uses plaintext h2c purely to stay dependency-free onlocalhost.
Not production-hardened: do not expose this sample publicly
This sample is a localhost teaching artifact. Even with the credentials changed and a TLS listener bolted on, the project as written is not safe to expose to the public internet. Adding TLS alone does not close the gaps below; each needs a deliberate change before this shape goes anywhere untrusted.
- It runs plaintext h2c, not TLS. The host binds Kestrel to
localhostwithHttpProtocols.Http2and no certificate, and the client speaks h2c by prior knowledge over anhttp://address. Binding without a certificate is the only thing that selects plaintext here - there is no app switch to unset. To serve real TLS you must bind a public interface and callUseHttps(...)with a real certificate; until you do, the listener has no encrypted path at all, and port-forwarding the sample as-is sends theBasiccredential in clear text. - The passwords are compiled into the binary. The demo declares the
passwords as constants and mints the hashes in-process at startup for a
one-command run. That means "change the password" would mean editing source and
shipping a recoverable secret in the assembly. A real deployment must set the
LATTICE_STATE_USER_*variables out-of-band (for example with thetools/credential helper) and delete the in-process minting entirely. Generate each hash once withtools/New-LatticeStateCredential.ps1(run it withpwshon any platform) - it prints only the salted hash, never the plaintext - and inject it as theLATTICE_STATE_USER_<username>variable. When deploying with Docker, pass that variable through your orchestrator's secret mechanism rather than baking it into the image: a Docker/Swarm or Kubernetes secret surfaced as an env var, ordocker run --env-filepointing at a file kept out of source control. Avoid-e LATTICE_STATE_USER_...=...on the command line (it lands in shell history anddocker inspect) and neverENV/ARGthe hash in theDockerfile(it is baked into an image layer). The value you inject is the hash, so a leak still forces an attacker through PBKDF2, but treat it as a secret regardless. - Unauthenticated requests can exhaust CPU. To keep the authorizer free of a user-existence timing oracle, every well-formed Basic attempt - including an unknown username and a locked-out account - spends a full, deliberately expensive PBKDF2 verification (only a missing or malformed header, or a username that is not a valid environment-variable name, is refused before any hashing). That safety property is also an amplifier: an unauthenticated flood of well-formed Basic credentials forces expensive hashing per request and can saturate CPU. A public deployment needs an upstream rate limiter / WAF / connection cap in front; the sample has none.
- A known username can be locked out on purpose. The failed-attempt lockout
is per-username, and once locked the account is refused until the window
expires - even with the correct password. Anyone who knows or guesses a
username (
adminis an obvious target) can keep the real operator locked out with wrong guesses. Public exposure wants IP-scoped throttling in front of the per-username lockout, not the lockout alone. - Failed-auth telemetry is silenced. The sample clears all logging providers for a clean console, which drops the authorizer's own failed-auth warnings. A public surface should capture those events for detection and alerting.
- The bootstrap administrator bypasses the data-plane gate entirely. That is by design, but it makes the admin credential the crown jewels: its compromise grants full read access to every tree. Keep the bootstrap set as small as possible and treat that secret accordingly.
Notes on this sample
- The password hash iteration count is deliberately not printed. It is an
implementation detail of
LatticePasswordHashthat tracks current guidance and changes over time; hard-coding it in sample output would go stale. - The bootstrap administrator seeds the trees before any rule grants access; the reader's rule is written through the silo-side policy store, which runs under system origin and never consults the gate. Production should keep the bootstrap set as small as possible and grant everything else through rules.
- The authorization gate reads a compiled policy snapshot that rebuilds off the policy-tree change feed, so the sample polls briefly after authoring the rule before exercising enforcement.