Running it

Configuration

Every setting each component reads, its default, and the two settings people get wrong.

Environment variables, by component. Defaults are the lab's, and every default assumes the *.uma.lab names the lab issues certificates for.

The authorization server

Variable Default Meaning
UMA_AS_ISSUER https://alice-as.uma.lab The issuer this authority claims in tokens it mints
UMA_AS_SIGNING_KEY /keys/uma-as-ed25519.pem Signing key for RPTs, receipts and PATs
UMA_AS_KID uma-as-1 The key identifier every token this authority signs carries
UMA_AS_PREVIOUS_KEYS unset Comma-separated PEM paths of keys this authority no longer signs with and still publishes, so grants they signed stay verifiable. With more than one replica, rotation is two rollouts: first the new key goes here, so every replica accepts it before any signs with it; then it becomes UMA_AS_SIGNING_KEY under a new UMA_AS_KID and the old one moves here. One step would leave replicas not yet rolled refusing grants signed under a kid they have never seen
UMA_AS_PREVIOUS_KIDS unset The kids of those keys, in order. A key without one is identified by its thumbprint
UMA_AS_OWNER_AUTH oidc Comma-separated: oidc, local-key, or both. Each accepted credential is independently sufficient
UMA_AS_OWNER_KEY /keys/owner-ed25519.pub Her enrolled device key, for local-key. Public half only
UMA_AS_OWNER_AUTHORITY host part of the issuer The authority her signature base is rebuilt against. Configuration, never the request
UMA_AS_OWNER_ISSUER https://keycloak.uma.lab/realms/alice The issuer the owner's tokens must claim
UMA_AS_OWNER_METADATA_URL {OWNER_ISSUER}/.well-known/openid-configuration Where to fetch her identity provider's metadata
UMA_AS_OWNER alice The one owner this authority serves. Set it and every request naming anybody else is refused at the door rather than served and filtered. Leave it unset for a deployment holding many, where UMA_AS_DEFAULT_OWNER names the one an unqualified request means
UMA_AS_DEFAULT_OWNER alice On a server holding many owners, the one an unqualified request means. Ignored where UMA_AS_OWNER is set, which is its own answer
UMA_AS_OWNER_KEY_OWNER the default owner Which owner the enrolled device key speaks for. A signature proves a holder, not a name, so on a server holding many owners the binding has to be configured rather than inferred
UMA_AS_RS_META_TTL 300 Seconds a resource server's RFC 9728 document is cached after it checked out
UMA_AS_RS_MISS_TTL 30 Seconds a resource that did not check out is remembered as refused. Bounds how much outbound fetching an unauthenticated caller can cause — the opposite policy to UMA_AS_DIRECTORY_TTL above, and the reasoning is in many owners
UMA_AS_RS_MAX_BYTES 65536 How much of a document fetched from a caller-named origin is read before it is refused
UMA_AS_SEED_RS 1 Whether to seed a resource server this authority was provisioned alongside. 0 seeds none, which is the case where the authority is the owner's and nobody could have configured both ends — the resource server introduces itself instead
UMA_AS_PEND_BUDGET 5 How many requests from agents with no standing, and nobody checkable behind them, may wait for her at once. 0 makes her authority introduce-yourself-first
UMA_AS_PEND_BUDGET_ATTRIBUTED 40 The same cap for agents whose named operator published their key. A separate lane, so a flood of the cheap kind cannot fill it
UMA_AS_MAX_REASON 512 How much the requesting side may write about its own errand. Small on purpose: a sentence for a person to read in an approval, not a document
UMA_AS_TRAJECTORY_WINDOW 7d How far back a rule about an agent's recent behaviour looks. One window for all of them, so a rule reads recently and the deployment says how long that is
UMA_AS_DIRECTORY_TTL 300 Seconds an operator key directory is cached for a hit only. A miss is always re-fetched, because a stale hit keeps attesting a key the operator has disowned while a stale miss merely fails to recognise one just published
UMA_AS_OWNER_CLIENTS meridian-portal Comma-separated audiences accepted on owner tokens
UMA_AS_PENDING_TTL 3600 How long a held ask-me ticket stays valid, in seconds
UMA_AS_RPT_AUDIENCE https://gateway.uma.lab The aud on every grant this authority signs: the enforcement point they are for. One in the lab; a deployment with several names the one it protects
UMA_AS_STORE memory memory or postgres
UMA_AS_DATABASE_URL — Required when the store is postgres
UMA_AS_AGENT_ISSUERS — Comma-separated origins of the agent providers whose agent tokens are believed. An issuer not named is refused before anything is fetched from it, and with none named no agent token is accepted. The lab names its person server, https://ps.uma.lab
UMA4A_CA_BUNDLE — Trust bundle used when dereferencing agent-token issuers

Replicated deployments need postgres and a shared signing key. Three instances minting their own keys are three authorities wearing one name.

UMA_AS_PENDING_TTL defaults to an hour because the premise is that the owner may be asleep. That is only safe because the requesting side hands the wait up rather than holding a call open across it.

The enforcement point

Variable Default Meaning
UMA_AS_PUBLIC https://alice-as.uma.lab The authority's public identifier, put in challenges
UMA_AS_INTERNAL http://uma-as:9000 Where to reach it for protection API calls
UMA_AS_RS_CLIENT_ID meridian-gateway This resource server's client id, for PAT issuance. Read only where there is a secret — an authority nobody provisioned this pair against is one this resource server has no name at, so it registers under its own origin instead and this is ignored
UMA_AS_RS_CLIENT_SECRET gateway-dev-secret Its secret, where one authority was provisioned alongside it. Changing it takes effect on the next PAT request; the stored registration follows it and keeps its status. Setting it empty is a decision rather than an omission: it selects the other identity, where the credential is a key published at the resource's own origin and the owner approves it once
UMA_PEP_RS_SECRETS {"<owner>": "<secret>"} Per owner, and the interesting part is who is missing. An owner named here is one whose authority this resource server holds a credential for. Any other owner it serves is one it must introduce itself to, by signing with the key it publishes at its own origin
UMA_EXTRA_OWNERS — Comma-separated. Every owner named gets /mcp/<owner>, with her own tool namespace, her own PAT and her own RFC 9728 metadata
UMA_OWNER_AUTHORITIES — JSON, owner → {public, internal}. Which authority governs which owner. This is the one thing that stays configuration: which server speaks for a person is a fact only that person holds, so she tells the resource server, the way she tells it an address
UMA_REALM alice-vault Protection realm named in the challenge
UMA_OWNER alice The owner whose resources are protected
UMA_PEP_SIGNING_KEY /keys/uma-pep-ed25519.pem, and /keys/vault-ed25519.pem in the resource that enforces for itself Key for signed_metadata and signed queries, and the one an authority fetches to decide whether this resource server is who it says it is. Two defaults because two hosts run this code: a deployment that shares one key between them has to name it in both
UMA_EXPECTED_AUTHORITY gateway.uma.lab The authority used to rebuild the RFC 9421 signature base
UMA_ALLOWED_ORIGINS derived from the authority Origins accepted on MCP requests
UMA_PEP_SCHEME https The scheme of the URLs it publishes. An authority reads a resource's metadata and keys only over https, so http suits nothing that registers with one

UMA_EXPECTED_AUTHORITY is the setting to get right. The signature base needs an authority, and taking it from the Host header gives the caller control of an authorization input. It also breaks portability: behind a proxy, Host frequently arrives as the authorization service's own address, and no configuration change fixes it after the fact.

The resource

Variable Default Meaning
ENFORCEMENT_MODE gateway gateway or embedded
UMA_EXPECTED_AUTHORITY gateway.uma.lab As above, when enforcing in-process
UMA_AS_INTERNAL, UMA_AS_PUBLIC as above Read by the embedded enforcement core
UMA_VAULT_TLS_CERT, UMA_VAULT_TLS_KEY unset Under embedded, serve https with this certificate. For a resource with nothing in front of it to terminate TLS

Under gateway the resource holds no authorization code. Under embedded it runs the same enforcement core in-process and there is no gateway in the authorization path.

The owner's portal

Variable Default Meaning
UMA_AS_INTERNAL http://uma-as:9000 Where to reach the owner API
VAULT_MCP_URL http://alice-vault-mcp:9020/mcp The resource, for her own reads
PORTAL_AUTH oidc oidc, or none for a stack with no identity provider. The portal refuses to start on any other value
OIDC_ISSUER https://keycloak.uma.lab/realms/alice The issuer her tokens must claim
OIDC_METADATA_URL derived from the issuer Where to fetch that provider's metadata
PORTAL_PUBLIC_URL — The address her browser reaches the portal at
OIDC_CLIENT_ID alice-portal Client id
PORTAL_SESSION_SECRET dev-session-secret Session signing secret. Set it in any real deployment

The agent shim

Variable Default Meaning
UMA4A_GATEWAY https://gateway.uma.lab/mcp The resource endpoint
UMA4A_KEYSTORE ~/.uma4agents/agent-key.pem The agent's signing key
UMA4A_RECEIPTS <keystore dir>/receipts Where the agent's copies of receipts are kept
UMA4A_CACERT certs/rootCA.pem Trust bundle for the lab's names
UMA4A_STANDING_MAX_EXPIRES 604800 Longest expires_in a standing configuration will accept
UMA4A_AGENT_ISSUER — Set to run identified rather than pseudonymous
UMA4A_PERSON_TOKEN — The person token used to obtain an agent token
UMA4A_PEND_HANDBACK 15 Seconds before the wait is handed up to the agent's user
UMA4A_TRACING 1 0, false or no disables traceparent generation

Each agent gets its own keystore. The key is the pseudonymous identity, so two agents sharing a keystore are one agent to the owner. Point UMA4A_KEYSTORE somewhere distinct per agent and each gets its own connection, its own terms agreements and its own grants.

The organization's authority

Only present where an organization owns resources that are shared with members — see shared ownership. With none of it set, every line of that layer is inert.

Variable Default Meaning
ORG_ISSUER https://northwind-org.uma.lab The organization's own origin. Members' authorities verify its notices, and enforcement points verify the grants it signs, against the keys published here
ORG_ID / ORG_NAME northwind / Northwind Capital What members are shown
OPA_URL http://opa:8181 The policy engine. The charter's declarative conditions and the administrator's own Rego are both evaluated there
ORG_ADMIN_ISSUER …/realms/northwind The realm administrators sign in to. Deliberately not a member's realm — an identity provider that minted both would collapse the two layers
ORG_ADMIN_CLIENTS meridian-org-console Which client's tokens the admin API accepts
ORG_ADMIN_TOKEN unset A static administrator credential, for a stack with no identity provider. The service refuses to start with it and ORG_ADMIN_ISSUER both set; the lab's checks sign in as the administrator instead
CONSOLE_AUTH oidc The administrators' console: oidc, or none for a stack with no identity provider. It refuses to start on any other value
ORG_RS_TOKEN org-rs-dev-token What an enforcement point presents to read membership and check the grants this service signs
ORG_JOIN_CODE unset (the lab sets NW-7K2F-QX) A shared enrolment code, off unless set. It admits whoever holds it under whatever name they give, which suits onboarding a team and nothing more. An invitation, addressed to one person and shown once to the administrator who creates it, and the organization's identity provider vouching for an employee are the paths that know who is joining. Every join names the charter version the member agreed to
ORG_BREAK_GLASS_AUDIENCE https://gateway.uma.lab Who an override is issued for. Configuration rather than a field on the request: an audience the caller chooses is one it can aim at another resource server that also trusts this organization
ORG_OPA_GRACE_S 60 How long a decision may be answered from cache when the engine cannot be reached. Past it the answer is a refusal — a charter is the organization's protection of its own data, and a request that slipped through while the engine was down is exactly what it exists to prevent

A member's side of it

Read by the owner's authorization server. Naming an organization is not enrolment and grants nothing: until she enters a code or accepts an invitation, none of it does anything.

Variable Default Meaning
UMA_AS_ORG_ISSUER unset Where an enrolment code is redeemed. One organization, because a code says nothing about who issued it; a deployment with many needs a directory, which the lab does not pretend to have
UMA_AS_ORG_CALLBACK the AS issuer The address the organization posts notices back to — its view of this server, which need not be the issuer an agent is challenged with
UMA_AS_ORG_TTL_S 30 How often the ceiling is re-read
UMA_AS_ORG_STALE_MAX_S 600 How long a copy that could not be refreshed still stands. Past it, requests over the organization's resources are refused: a ceiling nobody can read is not a ceiling
UMA_AS_RESOURCE_REFRESH_S 15 How often her own resource listing re-reads what the resource server publishes, while she is enrolled. What an organization shares with her changes elsewhere, and on a replicated authority only one replica is notified — so the listing repairs itself on a clock rather than waiting for a miss
UMA_AS_ORG_TIMEOUT_S 5 Per-request timeout when talking to the organization. A refusal, not a hang: her own resources are unaffected either way
UMA_AS_ORG_RETRY_S 15 After a refresh of the ceiling fails, how long before it is tried again. Meanwhile the copy she has stands, within UMA_AS_ORG_STALE_MAX_S, and a request over her own resources does not wait on an organization that is down

The enforcement point's side of it

Variable Default Meaning
UMA_PEP_ORG_ISSUER unset The organization above the owners this gateway fronts. Configured here rather than discovered from an owner's authority — that is the point: an owner may name any authorization server she likes, so the check that the organization's ceiling was applied has to come from somewhere she does not control
UMA_PEP_ORG_INTERNAL the issuer Where to reach it on the cluster network
UMA_PEP_ORG_TOKEN unset What this gateway presents to it
UMA_PEP_MEMBERSHIP_TTL_S 10 How long a cached answer about who is a member may be acted on. The window is somebody's access to the organization's resources after it was withdrawn, so it is short. Listings are always read fresh
UMA_PEP_MANDATE_TTL_S 30 How long the electorate of a jointly held resource may be reused before the mandate is re-read from where the tally publishes it. Short: a holder leaving should stop counting in seconds, not at a restart
UMA_PEP_HOLDER_JWKS_TTL_S 300 How long a co-owner's published keys are reused for verifying her verdicts. The window is a rotated-away key still verifying — ordinary key-rotation latency, and longer than the electorate's on purpose
UMA_PEP_CLOCK_SKEW_S 60 How far a jointly held grant's remaining life may run past the lifetime its holders' verdicts state. Verdicts are signed a moment before the grant, so exact equality would refuse honest grants
UMA_PEP_REQUIRE_CONTENT_DIGEST unset Set to true to refuse a tool call whose signature does not cover a Content-Digest. Unset, a digest that is covered is still checked against the body the gateway received
UMA_PEP_SHARED_PREFIX mcp/shared The path an organization's resources are reached at, one segment per member
UMA_PEP_SHARED_NAMESPACE northwind-vault The resource-id namespace those resources publish under

The tally, for resources held jointly

Read by the party that counts verdicts over a resource with several owners of equal standing — see joint ownership. With none of it set, no account is jointly held and the whole layer is inert.

Variable Default Meaning
TALLY_ISSUER https://joint-tally.uma.lab Its own origin. Holders' authorities verify its requests, and enforcement points verify its grants, against the keys published here
TALLY_MANDATES / TALLY_MANDATES_FILE unset The mandates it counts for. Configuration rather than an API, because a mandate names the electorate and a coordinator that could edit it would be deciding who gets a say
TALLY_THRESHOLD_FLOOR 0 The fewest holders who must agree to release anything, which the holders may not vote themselves below. It counts people, not weight: a mandate under which fewer holders than this could release a resource together is refused at startup, by name. This is what an account agreement or a regulator supplies in the world, and the only answer to what quorum sets the quorum
TALLY_RS_SECRET tally-rs-dev-secret What an enforcement point presents to mint tickets and introspect. It buys nothing that matters: the verdicts inside a grant are checked against the holders' published keys, not against this
TALLY_SIGNING_KEY /keys/tally-ed25519.pem Persisted, not generated per process — both holders' authorities cache what this service publishes, and a key that changed on restart reads as a broken mandate
TALLY_MAX_OPEN_NEGOTIATIONS 200 Open negotiations per account, in two lanes counted apart: those nobody has signed for yet, and those an agent committed to while the holders are asked. Any caller the enforcement point challenges starts one, and each costs a quote from every holder's authority; past the cap in either lane a new one is refused with 503. Abandoned ones are swept once their ticket has lapsed, and grants once they expire

And on the enforcement point:

Variable Default Meaning
UMA_PEP_JOINT_TALLY unset The tally it will accept grants from. Named rather than read off a token: a resource server that accepted whichever issuer embedded a plausible mandate would be taking the electorate from the party that assembled it
UMA_PEP_JOINT_ACCOUNTS unset Which accounts it fronts, at /mcp/joint/<account>
UMA_PEP_ORG_DISCOVERY_TTL_S 120 How long a refusal may be worded from a cached description of the organization. Never affects a decision — only what an agent is told about how membership is come by

Cross App Access, where an organization federates identity

Read by the exchange endpoint that mints identity assertions for an organization's employees — see Cross App Access. With no identity_provider in the charter, none of it is consulted and members enrol with a code as before.

Variable Default Meaning
XAA_ISSUER https://northwind-xaa.uma.lab Its own origin. A member's authority verifies assertions against the keys published here, and accepts them only from the issuer her organization's charter names
XAA_IDP_ISSUER https://northwind-idp.uma.lab/realms/employees The customer's own directory, not Meridian's. A subject token signed by anything else is not an employee assertion, whatever it claims — the provider that authenticates people into Meridian's surfaces has no standing to say who a client company employs
XAA_CLIENTS {} Applications registered with the provider, and the secret each authenticates the exchange with
XAA_SEED_CONNECTIONS [] The edges an administrator approved — one requesting application, one authorization server it may be sent to, one resource, and the widest scope the enterprise will assert for. Policy about which applications may talk at all, never about what may be done to a resource. The broker holds connections in memory and applies these at every start, logging each as connection.seeded, so a withdrawal made at run time lasts until the next restart
XAA_ADMINS dana Who may configure connections, by realm username
XAA_JAG_TTL_S 300 How long an assertion lives. Short by construction — it is spent immediately at one authorization server and is not a credential anybody should hold

And in the charter, which is where a member reads it before agreeing:

Field Meaning
identity_provider.issuer The provider whose assertions this organization's members' authorities will accept. Must be https — it is a trust root
identity_provider.assertion id-jag, the only assertion this profile understands
identity_provider.enabled Whether federation is on. Configured-but-off is a real state, so an administrator switching it off does not lose the provider she typed in
identity_provider.directory Where employees sign in. Blank discovers it from the provider's own metadata, which is the normal case
identity_provider.enrol Whether the provider vouching for somebody is enough to enrol, instead of an enrolment code
identity_provider.subject_claim Which claim names the person, where a tenant's claim set makes it ambiguous. Blank compares preferred_username, email, its local part and sub
identity_provider.subject_map What the provider asserts, mapped to the member it means here. A real tenant's sub is local to that tenant, so somebody has to say who it is — and the organization is the party that knows, since they are its people

The two settings people get wrong

Issuer versus metadata URL. An issuer is an identifier — what a token claims. A metadata URL is a fetch location — where keys are read from. Normally the same origin answers both, and they come apart the moment a deployment is reachable under two names: a tunnel, a preview environment, an internal address alongside a public one. The token then claims the address the browser used, while this process can only reach the internal one.

Configure them separately. UMA_AS_OWNER_ISSUER and UMA_AS_OWNER_METADATA_URL, OIDC_ISSUER and OIDC_METADATA_URL, are the same split applied twice.

Trust bundles that replace rather than add. On some runtimes, setting a certificate file replaces the system trust store. That is fine inside a mesh where every name is yours, and it breaks the instant a component has to fetch something from a public URL. Symptom: TLS failures reaching a real identity provider or a tunnelled hostname, while everything internal works. Keep the private bundle for private names and leave platform trust in place for everything else.

Split-horizon deployments

When the deployment is reachable under two names, these are the settings that have to be told about the public one:

Component Setting
Identity provider Its own hostname setting, plus dynamic backchannel resolution
Portal PORTAL_PUBLIC_URL, OIDC_ISSUER, OIDC_METADATA_URL
Authorization server UMA_AS_OWNER_ISSUER, UMA_AS_OWNER_METADATA_URL
Identity provider clients Redirect URIs matching the public address

The lab's Codespaces path does this in .devcontainer/expose-web.sh, which is worth reading as a worked example rather than copied.

What's next