Skip to content

Realm policies

A realm’s policies are its own settings: token lifetimes, the sign-in and password rules, client registration, mail and the environment posture (ADR-019, Decision 10). They are defined once, as a typed schema with a version, in libs/server/realm-policy. The admin API’s realm representation, the realm file, the diff of two configurations and the rule that decides which changes are security operations all come from it, and so does this page.

Most of them already live on the realm row: the access, development access and refresh token lifetimes, the verified-account, password and brute-force policy, the environment ceiling, the scopes a dynamically registered client may hold, whether and from which hosts the realm accepts a Client ID Metadata Document, and the mail transport and sender. The rest are still read from the environment, for every realm: the verification link’s lifetime and resend rule. Nothing reads the admin group yet. Once a policy lives on the realm row, the environment only seeds a new realm’s value, and the realm row owns it from then on.

A secret field, such as the SMTP password, is never in the policies: the realm keeps its value in its secret store, encrypted with the realm’s data-at-rest key, and the representation shows only whether it is set (passwordSet). The environment seeds the first realm’s secrets once, at the first boot that can encrypt them. A policy update that selects the smtp transport without its host, port, user, sender address and password, or resend without its sender address and API key, is refused, and so is one that selects smtp or changes its host, port or TLS mode without giving the password again, or that names localhost, a non-public IP address or a cloud metadata address as its SMTP host when the operator’s SMTP_ALLOWED_HOSTS does not list it (a metadata address never).

Schema version: 1-rc.0.

Each field says which way is stricter:

  • lower or higher: a number, such as a lifetime or a threshold.
  • true or false: a flag.
  • closer to a value: an enum with an explicit order, from the loosest value to the strictest.
  • fewer entries: a set of allowed values, such as scopes or trusted domains.
  • more entries: a set of required values.
  • not ordered: a change is neither a tightening nor a loosening.

A change is a tightening when it moves toward stricter, a loosening when it moves the other way, and neutral when the field is not ordered. A change the schema cannot place counts as a loosening, so an unknown direction fails closed: a value outside an enum’s order, a set change that both adds and removes an allowed value, and a direction the schema does not know.

The Class column says when a change needs qauth:admin:security instead of qauth:admin:write (ADR-019, Decision 7): never, only when it loosens, or on any change, in either direction. A security change carries the effects that make it one. A change of several fields needs what its strictest change needs.

  • The Default column is the value of a realm whose environment does not set the key in Seeded from. The environment only seeds: changing a key later changes no existing realm.
  • A field added within a schema version always has a default, so a stored document that predates it still parses.
  • Secret fields, such as the SMTP password, are write-only. Their value is never stored in the policy document and never returned: the realm representation shows only whether one is set, as <name>Set, and the realm file never holds one. A write sets one with its value and clears it with null; leaving it out keeps it, and a <name>Set sent back from the representation changes nothing. Setting or clearing a secret is always a security operation.
  • Environment-bound fields describe the environment a realm runs in, such as its mail transport or a trust list. Promoting a sandbox configuration to a live realm leaves them out unless each is selected (ADR-019, Decision 11).
FieldDefaultSeeded fromStricter whenClassDescription
tokens.accessTokenLifespanSeconds900ACCESS_TOKEN_LIFESPANlowersecurity when loosened (posture-loosening), else writeHow long an access token for a staging or production client is valid, in seconds, at most 86400 (a day).
tokens.developmentAccessTokenLifespanSeconds28800DEV_ACCESS_TOKEN_LIFESPANlowersecurity when loosened (posture-loosening), else writeHow long an access token for a development client is valid, in seconds, at most 86400 (a day); no staging or production client receives it.
tokens.refreshTokenLifespanSeconds604800REFRESH_TOKEN_LIFESPANlowersecurity when loosened (posture-loosening), else writeHow long a refresh token is valid, in seconds, at most 7776000 (90 days).
tokens.adminAccessTokenLifespanSeconds600nonelowersecurity when loosened (posture-loosening), else writeHow long an access token for the admin API is valid, in seconds, at most 900, so a token whose audience is the admin API never lives longer, whatever the other lifetimes say (ADR-019 Decision 7: admin tokens are short-lived).
FieldDefaultSeeded fromStricter whenClassDescription
accounts.requireVerifiedAccounttrueREQUIRE_VERIFIED_ACCOUNTtruesecurity when loosened (credential-path), else writeWhether the legacy login routes (POST /auth/login, POST /ui/login) refuse an unverified account’s right password with the answer a wrong password gets; the Interaction API verifies such an account inline with an emailed code, whatever this says.
accounts.allowSignUpfalseALLOW_SIGN_UPfalsesecurity when loosened (credential-path), else writeWhether a person may create a new account through the hosted sign-in, from an unknown upstream identity (#639) or with prompt=create (#640, for a client whose hosted_flows name register); the legacy POST /auth/register does not read it.
FieldDefaultSeeded fromStricter whenClassDescription
password.minLength15PASSWORD_MIN_LENGTHhighersecurity when loosened (credential-path), else writeThe fewest characters (Unicode code points, after NFC normalisation) a new password may have, from 8 to 64; below 15 is a recorded deviation from NIST SP 800-63B-4 while a password can be the only factor.
password.minScore2PASSWORD_MIN_SCOREhighersecurity when loosened (credential-path), else writeThe lowest zxcvbn strength score, from 0 to 4, a new password may have.
password.breachListSourceoffPASSWORD_BREACH_LIST_SOURCEcloser to k_anonymity (off < local < k_anonymity)security when loosened (credential-path), else writeWhich breach list new and presented passwords are checked against: off (the default), a list the operator supplies, or a k-anonymity range lookup through the egress client that sends only the first five hex characters of the password’s SHA-1; a failed check never blocks a sign-in.
FieldDefaultSeeded fromStricter whenClassDescription
bruteForce.enabledtrueFAILED_LOGIN_TRACKING_ENABLEDtruesecurity when loosened (credential-path), else writeWhether repeated failed sign-ins for one identifier lock it out for a while; when off, failures are only logged.
bruteForce.maxAttempts5FAILED_LOGIN_MAX_ATTEMPTSlowersecurity when loosened (credential-path), else writeHow many failed sign-ins within the window lock an identifier out.
bruteForce.windowSeconds900FAILED_LOGIN_WINDOWhighersecurity when loosened (credential-path), else writeThe window, in seconds, over which failed sign-ins are counted.
bruteForce.lockoutSeconds900FAILED_LOGIN_LOCKOUT_DURATIONhighersecurity when loosened (credential-path), else writeHow long, in seconds, a locked-out identifier is refused before any password check.
FieldDefaultSeeded fromStricter whenClassDescription
botChallenge.requiredAtSignUpfalseBOT_CHALLENGE_REQUIRED_AT_SIGN_UPtruesecurity when loosened (credential-path), else writeWhether every prompt=create registration passes the bot challenge before its address is asked for, whatever the client; it then needs JavaScript, a client that cannot serve the challenge cannot register, and the legacy POST /auth/register ignores it.
botChallenge.difficulty18BOT_CHALLENGE_DIFFICULTYhighersecurity when loosened (credential-path), else writeHow many leading zero bits the built-in proof of work asks for; each one doubles the work of the page that solves it, and 18 takes a browser a second or two.
FieldDefaultSeeded fromStricter whenClassDescription
registration.dynamicRegistrationAllowedScopesopenid, profile, email, offline_accessDEFAULT_DYNAMIC_REGISTRATION_SCOPESfewer entriessecurity when loosened (token-content), else writeThe scopes a dynamically registered client may hold; a scope outside the list is dropped from the registration, and no scope of the reserved qauth: namespace can be listed.
registration.cimd.enabledtrueCIMD_ENABLEDfalsesecurity when loosened (token-content), else writeWhether an HTTPS URL is accepted as a client_id, its metadata fetched from that URL (Client ID Metadata Documents).
registration.cimd.trustPolicyaccept-any-httpsCIMD_TRUST_POLICYcloser to allowlist (accept-any-https < allowlist)security when loosened (token-content), else writeWhich hosts a metadata-document client_id may have: any HTTPS host, or only the trusted domains.
registration.cimd.trustedDomainsemptyCIMD_TRUSTED_DOMAINSfewer entriessecurity when loosened (token-content), else writeThe hosts the allowlist policy accepts, stored in lower case; an entry *.example.com accepts every subdomain of example.com but not example.com itself. Environment-bound.
FieldDefaultSeeded fromStricter whenClassDescription
mail.providermockEMAIL_PROVIDERnot orderedsecurity on any change (credential-path)Which transport sends the realm mail: mock (logs only), resend or smtp. Environment-bound.
mail.fromAddressnoneEMAIL_FROM_ADDRESSnot orderedsecurity on any change (credential-path)The address the realm mail is sent from. Environment-bound.
mail.fromNameQAuthEMAIL_FROM_NAMEnot orderedsecurity on any change (credential-path)The sender name the realm mail shows. Environment-bound.
mail.smtp.hostnoneSMTP_HOSTnot orderedsecurity on any change (credential-path)The SMTP server the smtp transport connects to. Environment-bound.
mail.smtp.portnoneSMTP_PORTnot orderedsecurity on any change (credential-path)The port of the SMTP server. Environment-bound.
mail.smtp.securetrueSMTP_SECUREnot orderedsecurity on any change (credential-path)Whether the SMTP connection uses TLS from its start. Environment-bound.
mail.smtp.usernoneSMTP_USERnot orderedsecurity on any change (credential-path)The user name the smtp transport signs in with. Environment-bound.
mail.smtp.passwordnoneSMTP_PASSWORDnot orderedsecurity on any change (credential-path)The password the smtp transport signs in with. Secret: write-only, never stored in the policy document, shown as passwordSet. Environment-bound.
mail.resend.apiKeynoneRESEND_API_KEYnot orderedsecurity on any change (credential-path)The API key the resend transport sends with. Secret: write-only, never stored in the policy document, shown as apiKeySet. Environment-bound.
mail.verificationTokenExpirySeconds86400EMAIL_VERIFICATION_TOKEN_EXPIRYlowersecurity when loosened (posture-loosening), else writeHow long a verification link is valid, in seconds, at least one hour. Environment-bound.
mail.invalidateExistingTokensOnResendtrueEMAIL_VERIFICATION_INVALIDATE_EXISTING_ON_RESENDtruewriteWhether sending a new verification link makes the earlier ones invalid, so only the latest works. Environment-bound.
FieldDefaultSeeded fromStricter whenClassDescription
posture.maxEnvironmentLaxityproductionnonecloser to production (development < staging < production)security when loosened (posture-loosening), else writeThe laxest environment profile a client of the realm may get; a client set to a laxer one gets this one. Environment-bound. More.
FieldDefaultSeeded fromStricter whenClassDescription
admin.twoAdminMandatoryOperationsemptynonemore entriessecurity on any change (administration)The admin operations that need a second admin approval in this realm, by operation id.
admin.workloadIssuersemptynonefewer entriessecurity on any change (administration)The HTTPS issuers whose workload tokens a CI service account may exchange for admin API tokens. Environment-bound.

The schema version is a string. Until QAuth 1.0.0 it is 1-rc.<M>, and nothing about it is promised: a breaking change raises <M> (ADR-018 §5). From the release that ships 1.0.0 it is 1, and inside version 1 every change is additive: a new field comes with a default, and no field is removed. Other areas add their groups to the same schema, such as browser sessions and sign-in methods; a group’s name is taken once.

A realm keeps its policies under the schema version they were written in. A release that changes the version reads a document of an older one through an upgrade it ships, which never reads a field looser than stored, and its first boot stores the document under the new version. A document of a later version, which a newer node wrote during a rolling upgrade, is read with the older node’s schema and never written by it.

The effects named above are the closed list of 11 effect tags on Security operations.