Skip to content

The directory (authentik): operations

The directory is authentik. It holds people's sign-in accounts, passwords and second factors, and it runs the account flows: sign-up, recovery, password change, authenticator setup and sign-out. The registry signs people in through it as an OpenID provider (authentik on the registry's side) and keeps sessions, personal access tokens and roles itself. The edge serves the directory as auth.<site> (and on :8443 on dev, where there is no such DNS name). Nobody browses it: auth.<site>/ and the directory's user panel send people back to the site, and Settings → Authentication is where they change their password or authenticators.

Everything that configures the directory is a file in this repository. Nothing is taken from the core at build or deploy time.

What File
Account flows (sign-up, recovery, password change, authenticator setup, sign-out), policies, scope mappings deploy/helm/secureagentry/files/authentik/blueprints/agentry-account-flows.yaml
The staff group's retirement …/blueprints/agentry-groups.yaml
Social sign-in (Google, GitHub) deploy/authentik/blueprints/agentry-social.yaml
The mails (confirmation, reset) and their frame …/templates/agentry/*.html, *.txt
The escaped JSON bootstrap helper and the verbatim upstream theme bootstrap, which the CSP below hashes …/templates/agentry/directory_config.html, …/templates/base/theme.html
The flow page, with the remembered-browser block, the sign-up link and the terms courtesy …/templates/if/flow.html
The mails' mark …/brand/mail-logo.png (source mail-logo.svg)
The brand's stylesheet deploy/authentik/branding.css
The registry's OAuth2 provider and application, the brand object, the registry's service account (production) deploy/helm/secureagentry/templates/authentik-blueprints.yaml, its agentry-site.yaml entry

(… is deploy/helm/secureagentry/files/authentik.)

Both deployments read the same files. Production's umbrella chart renders them into ConfigMaps (templates/authentik-blueprints.yaml, authentik-mail-templates.yaml, authentik-flow-template.yaml, authentik-mail-logo.yaml, authentik-terms.yaml) and mounts them in authentik's server and worker pods. Dev's compose overlay, deploy/compose.site.yaml, defines redis, authentik-db-init, authentik-server and authentik-worker itself ("The directory (authentik)" block) and bind-mounts the same files:

Mount in the directory containers Source
/blueprints/custom files/authentik/blueprints
/blueprints/site deploy/authentik/blueprints
/templates (mails under agentry/, if/flow.html, base/theme.html) files/authentik/templates
/web/dist/assets/icons/icon_left_brand.png files/authentik/brand/mail-logo.png
/agentry-legal public/legal (the site's Terms of Service)

Production mounts the same pieces from the chart's ConfigMaps at /templates/agentry, /templates/if, /templates/base, /agentry-legal and the logo path (values-vultr-prod.yaml, authentik.global.volumes). Both pods, server and worker, get every mount: the worker applies the blueprints and sends the mails, the server renders the flow page.

Configuration

The variables below are read by the directory's server and worker. On dev they come from the stack's .env through the overlay's environment block (the two services share one anchor); in production they are authentik.global.env in values-vultr-prod.yaml or keys of terraform's authentik-config Secret, which authentik reads as its environment once, at start.

Variable Meaning
AUTHENTIK_SECRET_KEY Generated once (openssl rand -base64 48). It signs sessions and seals stored secrets: keep it, and back it up with the database.
AUTHENTIK_BOOTSTRAP_PASSWORD, _TOKEN, _EMAIL First boot creates akadmin with that password and email and an API token with that value. The scripts below use the token.
AGENTRY_PUBLIC_WEB_URL The site's origin. Sign-out and "end of flow" return there.
AGENTRY_ACCOUNT_URL Where password and authenticator changes return: the site's Settings → Authentication page (the overlay and production set <site>/settings/authentication).
AGENTRY_BRAND_NAME The deployment's name in flow titles and mail subjects (default Agentry).
AUTHENTIK_ACCOUNT_FLOW_MAX_PER_HOUR The per-IP budget below (default 10).
AUTHENTIK_CAPTCHA_SITE_KEY, _SECRET_KEY, _JS_URL, _API_URL Sign-up's CAPTCHA below.
AUTHENTIK_EMAIL__HOST, __PORT, __USERNAME, __PASSWORD, __USE_TLS, __FROM Mail delivery below.
AGENTRY_IDP_TRUST_CHECK_TOKEN The secret of the remembered-browser and address-confirmation calls to the registry (at least 32 characters; the registry holds the same value).
AGENTRY_REGISTRY_INTERNAL_URL The registry's API as the directory reaches it, never through the edge (http://server:8080 on dev, the agentry-api Service in production).
AGENTRY_REGISTRY_CALLBACK_PATH The path on that URL the flows call. Empty or unset means /internal/directory, the registry's built-in mount. Where the enterprise build's directory extension serves the directory's calls the path is /internal/x/directory; dev's .env sets it. Production's values leave it unset.
AGENTRY_DIRECTORY_REGISTRY_ACTIVATION Registry activation, below. Any non-empty value is on.
AGENTRY_DIRECTORY_PROMO_FIELD The sign-up's promo code field, below. Any non-empty value is on, false included.
AGENTRY_DIRECTORY_TERMS_FILE The terms step's HTML fragment, below.
AGENTRY_SOCIAL_<P>_CLIENT_ID, _CLIENT_SECRET Social sign-in keys, below.
AUTHENTIK_DISABLE_UPDATE_CHECK, AUTHENTIK_DISABLE_STARTUP_ANALYTICS, AUTHENTIK_ERROR_REPORTING__ENABLED=false Set on every pod: the directory calls no authentik server.

A blueprint reads the environment when the worker applies it, so a changed value reaches the directory after the worker (and, for what the server reads, the server) restarts. On dev, up -d --force-recreate --no-deps authentik-server authentik-worker does it. In production the deploy restarts both pods itself when its apply changes authentik-config.

First boot and the registry's client

Dev. Set AUTHENTIK_SECRET_KEY, AUTHENTIK_BOOTSTRAP_PASSWORD, AUTHENTIK_BOOTSTRAP_TOKEN and AUTHENTIK_BOOTSTRAP_EMAIL in the stack's .env, bring the stack up, then run scripts/authentik-bootstrap.sh (idempotent). It needs AUTHENTIK_URL, AUTHENTIK_BOOTSTRAP_TOKEN, AGENTRY_AUTHENTIK_CLIENT_SECRET (the client secret to set), AGENTRY_REDIRECT_URI (<site>/api/v1/auth/oidc/authentik/callback) and AGENTRY_PUBLIC_WEB_URL. It makes the OAuth2 provider agentry-registry and the application agentry, and:

  • allows one grant, the authorization code (with PKCE), the only one the registry uses. authentik 2026.8 made a provider's grants explicit: a provider made through the API gets none and refuses every sign-in with invalid_request, and one migrated from an older release gets all of them, password and client_credentials included, which hand out tokens to anyone holding the client secret with no browser flow. Re-running the script narrows the provider.
  • attaches the openid and profile scope mappings, resolved by their stable managed ids. Without them authentik issues an ID token carrying sub alone and the registry can only create a placeholder account (<handle>@authentik.social.local) instead of linking to the existing one.
  • attaches the account flows' agentry-verified-email mapping for email instead of authentik's managed one, which hard-codes email_verified: false. The registry links an identity to an existing account only when both sides' addresses are verified. The mapping reports false for a self-enrolled account (agentry_email_verified: false) until the registry's activation link confirms it, true once it has, and true for an account without the attribute (created by an operator, provisioned from an upstream directory, or enrolled before activation existed). While the false marker is set the mapping asks the registry about the linked subject and the same address; activation never changes the marker.
  • attaches the agentry-remember-device scope mapping, which answers the agentry scope (the remembered-browser choice and the account's directory id), and sets the provider's invalidation flow to the blueprint's agentry-invalidation.
  • points the application's launch URL at the registry's sign-in start and the brand's default application at it, so a sign-in, sign-up or recovery that ends without a return lands in the site signed in, and unsets the brand's user-settings flow.
  • prints AGENTRY_IDP_LOGOUT_URL: the provider's end-session/ endpoint wrapped in authentik's cancel view (https://auth.<site>/flows/-/cancel/?next=/application/o/agentry/end-session/). authentik answers an empty page, and does not sign out, when the directory session still holds a flow plan (a flow the person walked away from); the cancel view drops that plan first and then continues to next.

The account flows' blueprint is mounted at /blueprints/custom and applied on boot and whenever the file changes, so the flows exist before the script runs. Then run scripts/authentik-brand.sh (make brand) and, with social keys set, scripts/directory-social-apply.sh (below).

Production. The chart does the script's work declaratively. The agentry-site.yaml entry of templates/authentik-blueprints.yaml makes the same provider (one grant, the authorization code), the application, the scope mappings (agentry-verified-email only where registry activation is on, a plain verified-email mapping otherwise), the brand object and the registry's service account. The client secret is AGENTRY_AUTHENTIK_CLIENT_SECRET in authentik-config. On a fresh install the blueprint can run before the account flows exist; it reports error until the next discovery (hourly), and restarting the worker applies it at once. Blueprints are applied apart from each other on purpose: CI refuses a blueprint that meta-applies another of ours, since a nested apply races the other over the same prompt rows and Postgres kills one with a deadlock.

The registry's settings. The registry signs in through the directory as its OpenID provider, under the id authentik, which every account it linked carries (another id would sign each of them in as a stranger), and asks for the agentry scope, which carries the directory's own claims:

AGENTRY_AUTH_OIDC_ID=authentik
AGENTRY_AUTH_OIDC_CLIENT_ID=agentry-registry
AGENTRY_AUTH_OIDC_CLIENT_SECRET=…          # the provider's secret
AGENTRY_AUTH_OIDC_ISSUER=https://auth.<site>/application/o/agentry/
AGENTRY_AUTH_OIDC_SCOPES=openid,email,profile,agentry
AGENTRY_IDP_LOGOUT_URL=https://auth.<site>/flows/-/cancel/?next=/application/o/agentry/end-session/
AGENTRY_IDP_ADMIN_URL=…                    # the directory's API as the registry reaches it
AGENTRY_IDP_ADMIN_TOKEN=…                  # the registry's service-account token
AGENTRY_IDP_TRUST_CHECK_TOKEN=…            # shared with the directory

The registry keeps no password or second factor of its own (from core v0.6.15), so the directory is the only way in. A browser signs in through /api/v1/auth/oidc/authentik/start?next=<path>; the callback answers 303 to that path (or to the sign-in page with ?error=…) once the session cookie is set.

The enterprise build's directory extension talks to the directory when the server's environment has AGENTRY_IDP_ADMIN_URL and AGENTRY_IDP_ADMIN_TOKEN. Its settings routes are /api/v1/x/directory/settings and /api/v1/x/directory/devices/{kind}/{deviceId}; its callbacks for the flows are at /internal/x/directory (so AGENTRY_REGISTRY_CALLBACK_PATH names that path). On dev the admin token is the bootstrap token. In production it belongs to the service account agentry-registry, which the blueprint makes with a role of exactly what the registry calls: view and delete users, view OAuth source connections, view and delete authenticator devices. Its token is AGENTRY_IDP_ADMIN_TOKEN (agentry-app), rotated by the procedure in docs/ops/deployment.md ("Rotating the registry's directory token"), whose sweep is scripts/authentik-sweep-tokens.sh.

The account flows

The blueprint defines both self-service routes and the controls on them, and adds the "Sign up" and "Forgot username or password?" links to the sign-in page. The flow page (if/flow.html) also puts "Need an account? Sign up." on the password page that follows the first one. Both sign-up links carry what was typed into the sign-in's field: a name opens the form with the username filled in, an address with the email (/if/flow/agentry-enrollment/?username=… or ?email=…, the sign-in's query kept). The link shows whether or not the name exists, so it reveals nothing about which accounts do.

To run invite-only, remove the blueprint (delete the file, or disable its instance in authentik's admin console); accounts an operator created are unaffected.

Sign-up (agentry-enrollment): CAPTCHA, a form (username, optional display name, email, password), the Terms of Service step, then the account is written, then authenticator setup and sign-in.

  • Username. It becomes the registry handle at the first sign-in, so the form refuses one that is not a valid handle (letters, digits and hyphens, at most 39) or that the registry reserves for its own paths and names. Any case is accepted and the username is written in lower case (ChrisY becomes chrisy); a username that differs from an existing one only by case is refused as taken. The policy holds its own copy of the registry's handle rules; scripts/directory-handle-check.sh <core checkout> fails when the copy differs from the core's pkg/handle. Users created in the directory some other way are not checked; the registry maps such a username to a valid handle.
  • Display name. Optional. Left blank, the account is written with its username as its name, which is what the registry shows until the person changes it. A blank display name keeps the case it was typed in.
  • Promo code. An optional last field, shown only where AGENTRY_DIRECTORY_PROMO_FIELD is non-empty on the pods (the worker reads it when it applies the blueprint). The overlay sets it true unless .env sets it empty; production's global.env sets it true. Any non-empty value turns it on, false included: a blueprint cannot compare strings, so only an empty value (or none) turns it off. Off, the field is a hidden input and authentik resets a hidden field to its empty default whatever is posted. On, the form holds a code of 4–32 letters, digits and hyphens, writes it to the account's agentry_promo_code attribute (so it survives a confirmation link opened on another device), and fills it in from ?promo= when the flow is opened as /if/flow/agentry-enrollment/?promo=CODE. The agentry scope sends it as the agentry_promo_code claim, from the agentry-remember-device mapping the provider names; the registry passes it to its extensions as promoCode in user.registered, upper-cased, only on the sign-in that creates the account and only from the directory's token. CI checks the field, the provider's mapping and the pods' variable in the rendered chart.
  • Terms of Service step. Shown when AGENTRY_DIRECTORY_TERMS_FILE names a file in the directory's environment. The file is an HTML fragment whose root element carries data-terms-version="<version>", for example <article data-terms-version="2026-09-30">…</article>. The site's file is public/legal/terms.html, the file the /terms page renders: dev mounts public/legal read-only at /agentry-legal (a folder, not the file, since a single-file bind mount keeps the old inode after a git pull) and production mounts the chart's agentry-directory-terms ConfigMap there; the variable is /agentry-legal/terms.html on both. The chart refuses to render without the file. The step follows the form: the terms in a scrolling box and an "I have read and agree" box that the page enables, with Continue, only once the box has been scrolled to the end. A static flow script supplies the scroll courtesy; without JavaScript it is absent. The server refuses a submission without the agreement whatever the page does, and refuses every sign-up while the file cannot be read or has no version. The account records agentry_terms_accepted, agentry_terms_version and agentry_terms_accepted_at. Accounts that existed before the variable was set are not asked.
  • Registry activation. With AGENTRY_DIRECTORY_REGISTRY_ACTIVATION set to any non-empty value (on in both deployments), sign-up writes the account active but unactivated (agentry_email_verified: false) and goes straight to authenticator setup and sign-in, with no mail to wait for. At that first sign-in the registry mails the activation link through its own SMTP settings (AGENTRY_SMTP_*): with none set the link is only logged and nobody can activate. Until the link is followed the account is read-only in the registry: it reads everything and refuses every write except signing in and out, asking for another link (at most one every five minutes), its display name and cookie choice, deleting or clearing itself, and removing its own sessions, authenticators, tokens and SSH keys (403 account-unactivated; git pushes are refused too). A link works once, for 24 hours, and an earlier one keeps working after a new one is sent. Following it confirms the address in the registry. The directory reads that confirmation through <AGENTRY_REGISTRY_CALLBACK_PATH>/email-verification on AGENTRY_REGISTRY_INTERNAL_URL, with AGENTRY_IDP_TRUST_CHECK_TOKEN; it requires the exact linked authentik subject and the same address and withholds verification on a refused, unreachable or malformed answer. The false attribute stays unchanged, and activation needs no directory write. Set the switch only where the registry's provider uses the blueprint's agentry-verified-email mapping: a mapping that asserts every address verified would vouch for unconfirmed accounts. The chart reads the entry in authentik.global.env to give the provider that mapping, and scripts/registry-activation-check.sh (CI) renders the chart with the switch off and on and refuses it set on one pod alone. Unset, sign-up writes the account inactive and the directory's own confirmation mail activates it. scripts/directory-convert-unactivated.py converts accounts created while the switch was off.

Recovery (agentry-recovery): identification, an emailed reset link, the authenticator check, then a new password. The identification stage runs with pretend_user_exists, so an unknown address gets the same answer a real one does. Recovery does not activate an inactive account: an operator-deactivated user must not reactivate themselves with a reset. (With registry activation on a self-enrolled account is active from the start, and confirming its address is the registry's link.) Recovery verifies the authenticator before the password may change; a user with none is sent through the same setup stage. An account with no password and no authenticator, such as one a social provider created, is refused after the reset mail with the provider to sign in with, so the mailbox alone never opens it. Recovery also keeps authentik's own per-user control on the email stage (recovery_max_attempts, recovery_cache_timeout).

Password policy. Both forms enforce the same policy: 12 characters minimum plus a zxcvbn strength score. There is no Have I Been Pwned lookup: it needs egress from the container, and an outage there would block sign-ups and resets outright.

Authenticators. Enrollment ends by offering authenticator (TOTP) setup: the person sets one up there, or skips it and adds one later from Settings (agentry-totp-setup, agentry-webauthn-setup). An account without an authenticator signs in with its password alone. Lost-authenticator recovery is an operator action in the authentik admin console; email alone never resets both factors.

Password change and authenticator flows return to AGENTRY_ACCOUNT_URL through the flow plan's redirect, which the blueprint's agentry-return-to-settings policy sets. A redirect stage would leave the flow's plan parked in the session, and the next sign-out would answer an empty page.

Sign-out runs through the blueprint's agentry-invalidation flow, which ends the directory session and redirects to AGENTRY_PUBLIC_WEB_URL.

Nobody changes their own directory email. The registry links accounts by the address the directory holds and the provider asserts it verified, so a self-service change would let a person sign in as whoever owns the address they typed. authentik's default-user-settings-flow has an email field and stays reachable by URL even with the settings panel unset; the only thing refusing a change there is the system setting "Allow users to change email" (off by default), which a blueprint cannot pin. The blueprint therefore binds a policy that always denies to that flow. An operator who must correct an address does it in the admin console, after confirming the new address belongs to the person.

No authentik device cookie. authentik's User Login stage writes a "known device" cookie, authentik_device, for 30 days by default. It is a signed token the directory keeps no record of: it cannot be listed or revoked per browser, and it survives a password change, a logout and a rotation of AUTHENTIK_SECRET_KEY. The blueprint sets remember_device: seconds=0 on every login stage (sign-in, sign-up, recovery and authentik's two source stages), so the directory never writes it. Leave each stage's remember_me_offset at zero as well: above zero authentik shows its own "Remember me" box beside the remembered-browser one, and a login made with that box's answer never writes the cookie at all, so the two settings interact without saying so. A login stage added in the admin console must carry both settings.

Mail

The two mails the directory sends, the sign-up confirmation and the password reset, are the site's templates in files/authentik/templates/agentry/ (account_confirmation.html/.txt, password_reset.html/.txt, with base.html as their frame), not authentik's stock ones. authentik reads templates from /templates; the account flows' email stages name them as agentry/<name>.html and authentik looks for the text twin by replacing every html in the name with txt. The chart refuses to render when a stage names a template that lacks its text twin, and scripts/directory-mail-check.sh (CI) checks the mount in both pods, read-only, at /templates/agentry.

The mails' header shows the site's mark inline: the HTML names cid:logo, and authentik attaches the PNG at its logo path, /web/dist/assets/icons/icon_left_brand.png, to any mail that names it. files/authentik/brand/mail-logo.png is mounted there in the server and the worker; without it the mail would carry authentik's own logo, so the chart refuses to render a template naming cid:logo with no logo file. Nothing in a mail is a remote image. A changed PNG reaches dev only when the containers are recreated (up -d --force-recreate --no-deps authentik-server authentik-worker), and production mounts it with a subPath, which the kubelet never refreshes, so the agentry.dev/mail-logo-sha256 annotation in values-vultr-prod.yaml rolls the pods; directory-mail-check.sh requires it to equal the file's SHA-256. mail-logo.svg is the source of the PNG.

With registry activation on, the registry mails the activation link; the directory's confirmation mail is sent only by an enrolment that still binds the email stage, such as the untrusted social providers below.

Delivery. Both mails go through authentik's own SMTP settings (AUTHENTIK_EMAIL__*), separate from the registry's AGENTRY_SMTP_*. Dev sends to the stack's mailpit unless .env names a provider (AUTHENTIK_EMAIL_HOST and the rest); production's settings are keys of authentik-config, with the transactional sender the registry's mail also uses. authentik 2026.8 sends mail from its worker, so the worker's settings are the ones that count.

Abuse controls on the flows

Two layers, and the defaults are not production values.

  • CAPTCHA. A CAPTCHA stage guards sign-up, configured for Cloudflare Turnstile. Without keys it uses Cloudflare's published test keys, which always pass: that keeps a dev stack working out of the box and stops nobody. Set AUTHENTIK_CAPTCHA_SITE_KEY and AUTHENTIK_CAPTCHA_SECRET_KEY before opening sign-up to the internet (production: keys of authentik-config); AUTHENTIK_CAPTCHA_JS_URL and _API_URL point the stage at a different provider. The widget renders directly in the flow card through a static adapter that uses the vendor SDK and the stage's own callback and server verifier, with no blob document and no inline renderer.
  • A per-IP budget caps how much work one address can cause: once an address has caused 10 accounts created plus mails sent in the last hour, a new sign-up or reset from it is refused at the start. It counts from authentik's own event log. An account is the user_write event a flow records with created and the client's address; a mail is the budget's own custom_agentry_account_mail event, which the sign-up's and the reset's mail stages record with the client's address for a flow's first mail and for each resend from the "check your mail" page. The budget records its own because the worker sends the mail and authentik's email_sent event carries no address. Ordinary use costs 2 (an account and its confirmation mail) to sign up and 1 to reset a password; with registry activation on, the confirmation mail is the registry's, held to its own per-account limit, and a sign-up costs 1 here. The check is made when a flow starts, so a flow already open finishes, and its resends are held by the mail stage's own per-account limit. AUTHENTIK_ACCOUNT_FLOW_MAX_PER_HOUR sets the budget (default 10); the blueprint formats it into the policy at apply time. The per-IP limit is what stops one source mailing a thousand different victims.

The policy fails open on an unexpected error: a broken counter must not become an outage on the only route into the product. The directory's server logs it as agentry: rate limit check failed, allowing, and a mail that could not be counted as agentry: counting the account flow's mail failed. Alert on those two lines; the registry's FailOpenEvents metric covers the registry's own fail-open paths, not this one. The client address the budget sees is the one the edge forwards: in production authentik trusts X-Forwarded-For from the pod network only (AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS), and the directory's ingress policy admits only the edge and the registry's API.

scripts/acceptance/directory-budget.sh proves the counts and the refusal on a throwaway directory.

Second factor and remembered browsers

Public publishing. The registry keeps no second factor of its own, so its gate on public publishing is off unless AGENTRY_AUTH_REQUIRE_IDP_MFA=true. With it set, the gate is satisfied when the account has signed in at least once with an ID token whose amr claim (RFC 8176) names a second factor (mfa, otp, hwk, webauthn, never pwd alone), which the registry records as users.idp_mfa_at. authentik emits amr on its ID tokens. Check one sign-in's token (or its user.login audit row) before enabling the flag: with it on and a directory that never asserts a factor, nobody can publish publicly. GET /api/v1/meta reports the gate's state as features.publicPublish2FA.

Remembering a browser that answered the second factor. The sign-in's code page offers an unticked "Remember this device for 30 days". The registry records the browser, and a later sign-in that presents it is not asked for the code. The credential skips the second factor and nothing else: the password is still required, it is not a session and does not lengthen one, and it carries no scope. Its lifetime is absolute, AGENTRY_AUTH_TRUSTED_DEVICE_TTL from issue (default and ceiling 30 days; a larger value is refused at the registry's start). Only the token's SHA-256 is stored, beside the issuing address and user agent, which describe the browser to its owner and are not consulted in the decision. A lookup that cannot be answered (an outage, a revoked record, an expired one, somebody else's) demands the code. Signing out leaves remembered browsers alone; the person clears them from Settings, which revokes all of them.

The pieces on the directory's side:

  • The checkbox. files/authentik/templates/if/flow.html is authentik's own flow page plus one marked block (agentry:begin remembered-browser) that draws the box on the code page. The box is offered on the sign-in flow only. A ticked box becomes a ten-minute agentry_remember_intent cookie on the directory's host, __Host- prefixed on https so no other host under the same parent domain can set it; every load of the flow page clears it. The label says 30 days; a deployment that shortens AGENTRY_AUTH_TRUSTED_DEVICE_TTL changes the label to match. The chart refuses to render a page without the block or without <ak-flow-executor>. The box is not offered on a passkey sign-in or on authentik's simplified flow page for old browsers, which ask for the code as before.
  • The claim. The blueprint's agentry-remember-intent policy, on the sign-in's login stage, copies the choice into the sign-in's record, but only when a second factor was validated in the same flow. The agentry-remember-device scope mapping answers the agentry scope with agentry_remember_device and agentry_directory_id, the signed-in account's directory id. The registry mints a record only when the claim is true, the token's amr names a second factor and its auth_time is within five minutes: a live directory session answers a later sign-in silently, repeating the claim and amr of the sign-in that started it, and only auth_time shows it is old. The cookie is httpOnly and scoped to the directory's flow executor (Path=/api/v3/flows/executor/).
  • The skip. The agentry-remembered-browser-skip policy, on the sign-in's MFA stage, asks the registry before the code page, at <AGENTRY_REGISTRY_INTERNAL_URL><AGENTRY_REGISTRY_CALLBACK_PATH>/trusted-device with {token, sub} and the bearer secret AGENTRY_IDP_TRUST_CHECK_TOKEN. The answer is {"trusted": true} only for a live record owned by the active account linked to that subject; a store that cannot answer is a 503, which the directory treats as "ask for the code". The policy reads the secret and URL from the containers' environment when it runs, so the secret is never stored in the directory's database. It skips the code only on the registry's explicit trusted: true. The blueprint restates the MFA binding in policy mode all and binds the policy with failure_result: true; without both, the stock passwordless policy would hide the skip and a policy error would skip the code. Do not run the directory with AUTHENTIK_DEBUG where the secret matters: authentik's debug HTTP session logs outgoing requests with their headers.
  • The route stays private. The check is for the directory on the private network, calling the API directly. The edge never routes /internal/, and the registry answers 404 to any request carrying X-Forwarded-For or Forwarded, so a directory that reaches the API through a proxy of its own is refused too. In production two policies, trust-check-egress and trust-check-ingress, let the directory's server (not its worker) reach the API pods on 8080 and nothing else.
  • The cookie's domain. The registry sets the cookie on the callback, at its public URL's host, and the directory has to receive it. A directory on a subdomain of the registry's host (auth.<site> under <site>) gets a cookie for the registry's host; for sibling hosts set AGENTRY_AUTH_DIRECTORY_COOKIE_DOMAIN to a parent domain of both. A parent-domain cookie reaches every host under that domain at the executor's path: keep staging and development on a name of their own.

With the secret unset on either side the code is asked every time. The rotation of the secret in production is in docs/ops/deployment.md ("Remembered browsers on the directory's sign-in"). scripts/acceptance/directory-remember.sh checks the flow template against the pinned image and plays the sign-in against a stand-in registry.

Platform staff, the staff mirror and the Dashboards sign-in

Platform staff is membership of the staff organization (the registry's AGENTRY_STAFF_ORG at AGENTRY_STAFF_DOMAIN), granted by that organization's admins in the registry. The directory decides none of it. The blueprint agentry-groups.yaml removes the old agentry-platform-staff group, so a membership in it means nothing; AGENTRY_IDP_STAFF_GROUP and AGENTRY_IDP_STAFF_DOMAIN grant nothing, and the server logs a warning at start while either is set.

An application that signs in through the directory and should admit staff alone, such as the log stack's OpenSearch Dashboards, can read only what the directory's tokens carry. AGENTRY_IDP_STAFF_MIRROR_GROUP (default name agentry-staff) names a directory group the registry keeps equal to its platform staff: for every active account with the flag and a verified address, the directory account it signed in with, and nobody else. That account is found by address and added only when its uid matches the registry's record of the sign-in (the subject, under the hashed_user_id mode the provider uses), so a second directory account at a staff member's address gains nothing. It needs the staff organization and AGENTRY_IDP_ADMIN_URL/AGENTRY_IDP_ADMIN_TOKEN, and the token needs rights over groups as well as users. The group is a mirror, never a source of staff: anyone added by hand is removed at the next run, and each change is audited as directory.staff_mirror. It runs seconds after a staff change, every ten minutes, and soon after the server starts. While runs fail (the directory unreachable, or a token without rights over groups) a removed member keeps the group until a run succeeds; every failure is logged. A directory or application session already open is not ended by it. Where the directory extension supplies the directory, the extension keeps the group and the registry keeps no mirror of its own.

When AGENTRY_DASHBOARDS_URL is set, scripts/authentik-bootstrap.sh makes the Dashboards side (idempotent, like the rest):

  • the mirror group (AGENTRY_IDP_STAFF_MIRROR_GROUP, default agentry-staff; give the server the same value);
  • a roles scope mapping whose claim is ["agentry-reader"] for a member of that group and [] for anyone else;
  • the OAuth2 provider agentry-dashboards (authorization code only, redirect <url>/auth/openid/login) and its application, which admits members of the group alone. The application uses an expression policy rather than a group binding, because authentik deletes a group's bindings along with the group and the expression then fails closed; its policy engine mode is all, so a policy bound later cannot admit anyone by itself;
  • the client id and secret, written to AGENTRY_DASHBOARDS_SECRET_FILE (default ~/.config/agentry/dashboards-oidc.env, mode 0600, outside any checkout) and never printed. A re-run keeps the secret in an existing file.

It prints the provider's OpenID configuration URL, which Dashboards' OpenID settings take. The chart's blueprint makes no Dashboards client.

Account erasure and the settings page

POST /user/delete pseudonymizes the registry account; the sign-in account (email, display name, password hash) lives in authentik. With AGENTRY_IDP_ADMIN_URL and AGENTRY_IDP_ADMIN_TOKEN set, erasure also deletes the person's directory account, never a superuser or a service account. The account is the one the directory's own id names (the agentry_directory_id claim, recorded at every sign-in) confirmed by its uid; for an identity that has not signed in since, the account at the registry's address carrying that uid; and for an account that never signed in through the directory, the account at its address only when exactly one account holds it (authentik does not keep addresses unique). Each attempt is a user.directory_erase audit event (deleted, not-found, unresolved, failed, not-configured or abandoned), and the erasure's 204 carries the outcome in the X-Agentry-Directory-Erasure header. A failure does not undo the registry erasure: the request answers pending and the retries (directory.erase_retry, every 30 seconds) try again with backoff (fourteen attempts, about four and a half hours). They run in the registry's api, like every job of the directory extension that calls the directory: the api is the one role of the registry the directory's server admits. When the retries run out the last attempt is audited abandoned and the server logs failopen.directory_erasure at ERROR and counts FailOpenEvents{path="directory_erasure"}: alert on either, then delete the sign-in account in the admin console by hand (the requester supplies the former address; the registry no longer holds it). An erasure that could not tell the account apart (two accounts at the address, a recorded id that now names an account with another uid, or a superuser) is unresolved: nothing is deleted, it is not retried, and it is raised the same way.

The registry's settings page reads the directory with the same admin token: the social provider the account first connected through (firstSocialProvider) and whether it has a password (hasPassword). The account is the one carrying the caller's linked uid, never one found by address alone; only OAuth sources count; and the token needs authentik_sources_oauth.view_useroauthsourceconnection as well as users. The password answer is the account's agentry_has_password attribute, which the blueprint's agentry-record-has-password policy sets when a flow writes a password and refreshes at every sign-in (an account a social provider created holds an empty password, which counts as none). Until an account has signed in or written a password under this blueprint the field is absent, and so is either field when the directory does not answer.

Brand

The brand object (title, wordmark, favicon, flow background and the custom stylesheet) is the site's. On dev scripts/authentik-brand.sh (make brand; needs AUTHENTIK_URL, AUTHENTIK_BOOTSTRAP_TOKEN and ASSET_BASE, the origin the assets in public/brand are served from) patches the default brand through the directory's API and sets the sign-in flow's title. In production the agentry-site.yaml blueprint carries the same brand. Either way each asset URL carries ?v=<content hash>, because Cloudflare gives /brand/ a four-hour browser cache, so a new mark at the same URL would stay hidden that long; re-run the script (or deploy) after the mark changes. branding.css is the stylesheet; its palette follows the site's design tokens (app/globals.css). authentik's own default blueprint sets the sign-in flow's title too and re-applies when an authentik release changes that file, so after an upgrade the title is authentik's until the site's blueprint (or the brand script) runs again.

The flows' return targets read AGENTRY_PUBLIC_WEB_URL and AGENTRY_ACCOUNT_URL, and their titles and mail subjects format AGENTRY_BRAND_NAME.

Social sign-in

deploy/authentik/blueprints/agentry-social.yaml is sign-up and sign-in with social providers (Google and GitHub so far) through the directory's sources, under the trust model below. authentik reads no provider's verified-address flag, and the directory tells the registry every address is verified, so a source added as authentik ships it would be an account-takeover path. The blueprint holds what keeps the claim true:

  • Verified only on the provider's own flag. A property mapping per provider reads Google's verified_email and the verified flag of GitHub's primary address. With the flag set, a new address signs up active after the username prompt (prefilled, checked by the handle policy) and the Terms of Service step. Without it, the person also gets the address field and the confirmation mail, and the account stays inactive until it is confirmed, as a password sign-up does without registry activation. The address prompt refuses an address any account holds.
  • Matching is ours. Every source matches by its own identifier only; the policy agentry-social-classify looks a provider's address up case-blind. A match whose address the provider verified links to that account; any other asks for that account's password, then its authenticator. Two accounts at one address, an operator-disabled account and the administrator's are refused. An account that was never confirmed is removed (audited as custom_agentry_unconfirmed_account_removed) when the provider has verified the address, and refused otherwise.
  • The second factor. An account with an authenticator is asked for it on every social sign-in, and the ID token then carries mfa in amr.
  • No provider token is kept. The flows drop the provider's access and refresh tokens before authentik saves the connection, and scripts/directory-social-apply.sh clears any a connection still holds.
  • No avatar lookup. The directory shows initials, never a picture fetched by the hash of a person's email address. The mode is a tenant field that a blueprint cannot write (AUTHENTIK_AVATARS only seeds a first install), so scripts/directory-social-apply.sh sets it to initials and reads it back.
  • Recovery refuses an account with no password and no authenticator after the reset mail, naming the provider to sign in with.

Keys. Each provider's app is the operator's, named Agentry, with the callback https://auth.<domain>/source/oauth/callback/<slug>/ (slugs google, github). Its keys are AGENTRY_SOCIAL_<P>_CLIENT_ID and AGENTRY_SOCIAL_<P>_CLIENT_SECRET in the directory's environment: on dev the stack's .env, which the overlay passes to the two authentik containers alone (the registry reads the same names for its own dormant providers and must not get them from here); in production terraform's authentik-config Secret, from the GitHub secrets SOCIAL_<P>_PROD_CLIENT_ID/_SECRET, which the deploy workflow passes to its apply as TF_VAR_social_<p>_client_id/_secret. A provider whose client id is empty has its source disabled and no logo on the sign-in page. Square PNG logos for the providers' app registrations are rendered by scripts/brand-logos.mjs.

Applying it. Dev mounts deploy/authentik/blueprints at /blueprints/site in both authentik containers; the umbrella chart carries the same file in its blueprint ConfigMap through the symlink deploy/helm/secureagentry/files/authentik/agentry-social.yaml, and CI fails if the rendered copy ever differs. The blueprint names the account flows, and a first boot may apply it before them, so after a deploy that starts or restarts the directory, or changes this file or the keys, run:

scripts/directory-social-apply.sh            # the dev stack's worker; or pass a container name
scripts/directory-social-apply.sh --cluster  # production: kubectl exec on deployment/agentry-authentik-worker in agentry

Production runs the second form at the end of every deploy (.github/workflows/deploy.yaml), after restarting both directory pods whenever the apply changed authentik-config, since authentik reads its environment only at start.

It makes sure authentik has discovered the files, applies the account flows when they are not applied for their current file, then the social blueprint unless it is applied for its current file and its sources match the worker's keys, applies once more any other enabled blueprint that is not successful, and fails on any enabled blueprint that still is not. It then clears provider tokens, sets the avatar mode and reads back both flows, each source's enabled against its keys, and the sign-in page's logos. The deploy log is not masked for the directory's keys, so what the script prints is kept free of that environment's values. scripts/acceptance/directory-social.sh (CI job directory-social) proves the whole model against a throwaway directory with a stand-in Google and GitHub.

Organization sign-in

deploy/authentik/blueprints/agentry-org-sources.yaml holds the shared objects every organization's SAML or OIDC source references. The sources themselves are made by the registry's console, one per organization and kind, slugged <organization>-saml or <organization>-oidc; this blueprint makes none. authentik's sources do not keep one organization apart from another as shipped (an assertion can set an account's type, path and groups, matching runs across the whole directory, and nothing ties a source to an organization), so the tenancy lives in these objects:

  • The user mappings agentry-org-saml-user and agentry-org-oidc-user name every account a source makes <organization>:<name>, from the source's slug, and keep only username, email and name. The OIDC mapping verifies the ID token's amr against the source's JWKS, because authentik reads userinfo and never checks the ID token.
  • The flows agentry-org-enrollment and agentry-org-authentication both carry the gate agentry-org-gate: a newcomer only at one of the organization's verified domains, an existing account linked only there and never a privileged one or one of two holding the address, and only a same-site path as the post-sign-in destination. The enrolment shows the Terms of Service step and writes the account active; the login step names the method agentry_org_sso, with mfa only on the organization's own evidence (a class of the one signed SAML assertion, or the verified amr).
  • Routing. agentry-org-route, a redirect step at order 15 of the main sign-in flow, sends an address at an organization's verified domain to its source, with next kept; any other address, and every operator account, goes on to the password. No organization source is listed on the sign-in page.
  • The guard. agentry-org-guard is bound to the registry's application (agentry), which runs its policies in mode all: a sign-in with a password or through Google or GitHub passes; through any other source, a code is issued only when the registry confirms the source is that organization's and the person belongs to it. A new social provider is added to the guard's PLATFORM set with its source.
  • The claims. The agentry scope adds agentry_org_source and agentry_email_domain_verified to a sign-in through an organization's source. The email scope counts an organization's account's address as verified only at one of that organization's verified domains.
  • Recovery refuses an organization's account after the mail.

The registry's answers. Nothing about an organization is written into the directory. Its policies ask the registry at sign-in, with the directory's shared secret, through agentry-org-registry (AGENTRY_IDP_TRUST_CHECK_TOKEN, AGENTRY_REGISTRY_INTERNAL_URL, AGENTRY_REGISTRY_CALLBACK_PATH, as the account flows' checks), two seconds per call:

Call Body Answer
POST …/org-config organization sources, verifiedDomains, mfaAuthnContexts, mfaAmr, ssoRequired; 404 for an organization it does not know
POST …/org-route domain source: the enabled source for a verified domain, or null
POST …/org-member organization, source, sub, username, email allow

No answer is no: the gate refuses, the guard refuses, the claims say the address is not verified, and routing lets the password follow.

The console's token. The registry's console makes each organization's source with AGENTRY_IDP_SOURCES_TOKEN, which the deployment sets in the directory's environment (where this blueprint makes the role agentry-registry-sources, its service account and the token with that key) and in the registry server's; it never reaches a browser. On the cluster, terraform generates it and writes the one value into authentik-config (the directory's environment) and agentry-app, from which the chart gives it to the registry's api, which makes the sources and, every ten minutes, reads each of them back (directory.org_source_watch): one changed or gone is marked drifted and the registry vouches for no sign-in through it until an administrator saves it again, logged at ERROR and counted in DirectoryOrgSourceDrift. Each run logs directory: organization sources read back with how many it read and how they stood. The watch, the erasure retries and the staff mirror run in the api, on its own queue: the registry's worker, which opens untrusted archives, holds no directory token, and the directory's server does not admit it (scripts/directory-egress-check.sh checks both on the rendered chart). The deploy restarts the directory when authentik-config changes, and scripts/directory-social-apply.sh then applies this file again, since its token entries exist only where the key is set (rotation: the deployment, "The console's sources token"). Without it the blueprint makes none of the four and the console has no single sign-on to set up. The role may create SAML and OAuth sources and certificate pairs and read the flows and mappings a source names, and through Initial Permissions it may read, change and delete what it made and nothing else: it has no right on any token, account, group, flow, policy, mapping or role. Even so, a source it makes could sign anyone in, which is why the guard does not trust that sources were made as the console makes them. The directory-org job proves its limits.

Applying and drift. scripts/directory-social-apply.sh applies this file after the account flows and the social blueprint, and its read-back fails when a policy or mapping here differs from the file, when one of them logs its executions (each logged evaluation is an event that queues the worker), when a flow or the routing step is missing, when the registry's application is not in mode all or has no guard bound, or when the console's role holds a right its file does not grant. A directory without the registry's application (a throwaway one in a test) gets no guard and is not failed for it. The application's mode is set where the application is made: the chart's agentry-site.yaml in production, scripts/authentik-bootstrap.sh on compose.

Egress. authentik fetches from addresses an organization supplies at every OIDC sign-in (the token and userinfo endpoints) and has no guard against private or internal addresses of its own. On the cluster the chart's directory-egress NetworkPolicy is that guard for the directory's server and worker: besides DNS and the database, they may reach only public IPv4 addresses on ports 443 and 587. RFC 1918 (which holds the cluster's pod, Service and VPC ranges), shared address space, loopback, link-local (the cloud's metadata address), 0.0.0.0/8, 192.0.0.0/24, 198.18.0.0/15 and everything from 224.0.0.0 up are left out, and no IPv6 destination is allowed. The server may also reach the registry's api for the sign-in checks above, which answer only with the directory's shared secret. So an organization's endpoint on another port, or one that resolves to a private address, fails the sign-in. A policy never sees a pod's traffic to its own loopback: the console keeps the source's endpoints https and its keys inline, and certificate verification refuses authentik's own listeners, but a redirect answered by the provider's endpoint can still reach them, without the source's credentials. scripts/directory-egress-check.sh (CI) holds the rendered policies to this; check it on a cluster from inside the server pod, where a private address must time out and a public one on 443 connect.

On the cluster the directory's server admits the edge and the registry's api alone; nothing else in the cluster may reach it, because authentik believes X-Forwarded-For from the pod network.

Directory CSP

The edge enforces a Content Security Policy on the public /if/flow HTML, including HTML errors and limiter refusals. It rejects arbitrary inline scripts and event handlers: eight SHA-256 permissions authorize the static bootstraps and customizations the templates carry, by their data-id: shady-dom, authentik-config, flow-config, theme-script, agentry-remember-device, agentry-signup-link, agentry-terms and agentry-turnstile. Other inline scripts and event handlers are refused. Dynamic directory config, brand and flow values stay escaped JSON data (agentry/directory_config.html); a permission is never generated by hashing a rendered response or values a user supplied. The policy permits the directory's own scripts and Cloudflare Turnstile, disables script attributes, and permits same-origin WebSockets for flow updates. style-src 'self' 'unsafe-inline' is required by the upstream Lit components and their shadow trees and grants no script permission; same-origin frames support upstream flow embedding. API and media responses keep their upstream policy, and the registry's nonce CSP is separate.

scripts/directory-csp.py generates the policy from the templates:

python3 scripts/directory-csp.py          # rewrite the edge map and the pod annotations
python3 scripts/directory-csp.py --check  # CI: fail when either differs from the templates

It writes the generated block of deploy/edge/nginx.site.conf.template (the $directory_flow_csp map) and the agentry.dev/flow-template-sha256, directory-config-sha256 and theme-template-sha256 annotations of values-vultr-prod.yaml, so a change to any of the three templates rolls both directory pods. A template that adds an executable script must name it in the script's SCRIPT_IDS and be reviewed.

Template changes and the policy have to reach production together. A simultaneous first-time rolling upgrade could otherwise deny an old bootstrap before its directory pod is replaced, and the pod annotations alone do not make an edge and directory rollout atomic. So for a change to the templates that changes a script permission: release and read back the templates in ready pods first, then activate the matching edge header. The edge check renders both actual nginx configurations to prove injection refusal, the HTML and error scope, media, API and registry preservation and a real WebSocket; the directory-social job also drives the pinned frontend through nginx with disposable accounts, fixture terms and the actual Turnstile widget using its published test keys. On dev, recreate both directory services after a template change and the edge (up -d --force-recreate --no-deps edge) after a change to the edge config.

Checks

Check What it proves
scripts/directory-csp.py --check The edge's script permissions and the pods' template-hash annotations match the templates.
scripts/directory-mail-check.sh The mail templates the flows name are in the chart's ConfigMap with their text twins; the flow page, theme, terms and mail logo are mounted read-only in both pods; the logo annotation matches the file.
scripts/registry-activation-check.sh The activation switch, the provider's mapping and the registry's role agree, and a switch on one pod alone is refused.
scripts/directory-handle-check.sh <core checkout> The username policy's handle rules equal the core's pkg/handle.
scripts/compose-render-check.sh The four directory services render from this repository alone, mount nothing from the core checkout, and run the image the chart pins, tag and digest.
the directory-social job Social sign-in, the CSP and the pinned frontend, on a throwaway directory.
the directory-org job (scripts/acceptance/directory-org.sh) Organization sign-in on a throwaway directory built from the blueprints, with a stand-in SAML IdP, OIDC IdP and registry: D12 and D13 on both kinds, the attribute whitelist, next, routing, the claims and amr, the guard (a rogue source, the registry down, a Google sign-in), and the read-back failing on a tampered policy, mapping or application.
scripts/acceptance/directory-{budget,email,promo,remember,terms}.sh The per-IP budget, the mails and the logo, the promo field, the remembered-browser flow and the terms step, each on a throwaway directory on the pinned image.

Upgrading authentik

authentik refuses to skip a release line: every YYYY.M line between the running version and the target must be passed through in order, and the check runs before any migration (a refused container crash-loops with "Major version skips are not allowed" and leaves the database untouched). To move from 2025.6 to 2026.8 the path is 2025.12, 2026.2, 2026.5, 2026.8.

The image is pinned by tag and digest together, in two places that CI holds equal: authentik.global.image in deploy/helm/secureagentry/values.yaml and the image: of authentik-server and authentik-worker in deploy/compose.site.yaml (scripts/compose-render-check.sh). The chart dependency authentik in deploy/helm/secureagentry/Chart.yaml (and Chart.lock) moves with the line.

  1. Take a pg_dump of the authentik database first (production's nightly backup-db is one; take another before the hop). Dev's database is the stack's Postgres.
  2. For each hop, set the tag and the digest in both places and bring the directory up on it: on dev docker compose … up -d authentik-server authentik-worker; in production, deploy and wait for the rollout. Wait for the server to report ready (/-/health/ready/) before the next hop.
  3. After the last hop, look at what depends on the image:
  4. the mail logo's path: check authentik/stages/email/utils.py in the new image, since a moved logo path would attach authentik's own logo;
  5. the flow page and theme: compare the image's flows/templates/if/flow.html and core/templates/base/header_js.html with files/authentik/templates/if/flow.html and base/theme.html. The two reviewed bootstraps read escaped JSON data instead of interpolating runtime values into executable scripts; review their contracts before updating the hashes. scripts/acceptance/directory-remember.sh checks the rest of the template against the image and that the code stage, password page and first page the block writes into still exist;
  6. the CAPTCHA controller: scripts/acceptance/directory-captcha-check.py pins the frontend files the Turnstile adapter hooks into; review them and run the real widget test;
  7. regenerate with scripts/directory-csp.py if a template changed;
  8. re-run scripts/authentik-bootstrap.sh (dev) so the provider's grant, mappings and flows are as above, and scripts/directory-social-apply.sh;
  9. the sign-in flow's title (see Brand).
  10. On dev, docker image prune -a afterwards: each hop's image is about 1.4 GB and a full chain has filled a small root volume.

The registry needs no restart: the OIDC issuer, client and brand ride the database.

Backups

All of the directory's durable state is in its Postgres database, authentik, and in AUTHENTIK_SECRET_KEY. Redis holds only cache and queues, and authentik-media (dev) holds uploaded media; the brand is by URL, so neither is a record.

Production's authentik database is in the managed Postgres, covered by its point-in-time recovery and by the nightly backup-db dump of the agentry and authentik databases to the backup bucket (docs/ops/deployment.md, "Production on Vultr: backups and the restore drill"). The dump holds the directory's password hashes; the bucket is readable with the backup key alone. AUTHENTIK_SECRET_KEY is in authentik-config, which terraform owns: a restored database needs the same key. Dev's database is the stack's Postgres; pg_dump authentik from its container is the copy.

Troubleshooting

People cannot sign in but the registry is up: existing registry sessions and personal access tokens keep working, since the directory is consulted at sign-in and not per request.

  1. Is authentik up? curl -fsS https://auth.<site>/-/health/ready/, and docker compose … ps authentik-server authentik-worker on dev or kubectl -n agentry get pods in production. A crash loop that says "Major version skips are not allowed" is the release-line rule above.
  2. Did the blueprints apply? In the admin console (Customisation → Blueprints) "Agentry - Account flows" must be successful and the agentry-invalidation flow must exist; the worker's log names the entry of a blueprint that fails. scripts/directory-social-apply.sh re-applies and reads back every instance. On a fresh install a blueprint that names the account flows' objects reports error until the next hourly discovery; restarting the worker applies it at once. Production's admin console is not public: reach it through a port-forward to svc/agentry-authentik-server.
  3. Is the registry's client still what authentik has? On dev re-run scripts/authentik-bootstrap.sh (idempotent): it restores the provider's redirect URIs (authorization and logout), scope mappings, its one grant (a provider with none answers every sign-in with invalid_request), the application's launch URL and the brand's default application. Read individual keys from .env rather than source-ing it (values with spaces are not shell-quoted). In production it is the agentry-site.yaml blueprint; apply it as in step 2.
  4. Do the containers know where to return? AGENTRY_PUBLIC_WEB_URL (sign-out) and AGENTRY_ACCOUNT_URL (password and authenticator changes) must reach the authentik-* containers; the blueprint formats them into sign-out's redirect and the settings flows' return policy. After changing them recreate the directory containers, not the registry alone.
  5. The code is asked every time, or "remember" does nothing. AGENTRY_IDP_TRUST_CHECK_TOKEN must be set on the directory and the registry with the same value, AGENTRY_REGISTRY_INTERNAL_URL must reach the registry's API directly, and AGENTRY_REGISTRY_CALLBACK_PATH must name the path the registry serves (/internal/x/directory, the directory extension's mount). The registry answers 404 on the check while its secret is unset.
  6. A flow page is blank or its scripts are blocked. The CSP's hashes differ from the templates: run scripts/directory-csp.py --check, and see "Directory CSP" for the rollout order.
  7. A sign-up stops at the terms step. The directory refuses every sign-up while AGENTRY_DIRECTORY_TERMS_FILE cannot be read or has no data-terms-version. The file must be readable by uid 1000 and mounted in the server and the worker.
  8. Sign-up or reset is refused for one address. The per-IP budget: look for the account and mail events in the admin console's event log. The log line agentry: rate limit check failed, allowing means the counter itself failed.
  9. Mail does not arrive. Check AUTHENTIK_EMAIL__* on the worker and its log; on dev read the stack's mailpit. With registry activation on, the activation link is the registry's mail: check AGENTRY_SMTP_* there.
  10. A person cannot complete MFA. That is an operator action in the admin console (/if/admin/ still serves on dev); email alone never resets both factors.
  11. An erasure left a directory account behind. See Account erasure: the X-Agentry-Directory-Erasure header says pending, and a failopen.directory_erasure log line or a user.directory_erase row with abandoned means delete the account by hand, never a superuser or a service account.

Verify a repair: GET https://<site>/api/v1/auth/oidc/authentik/start?next=%2Fdashboard in a browser ends on that page with no directory screen in between; https://auth.<site>/ answers 302 to the site; the provider's end-session URL lands in the agentry-invalidation flow, whose first challenge (the executor API) is a redirect to the same home.