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,passwordandclient_credentialsincluded, which hand out tokens to anyone holding the client secret with no browser flow. Re-running the script narrows the provider. - attaches the
openidandprofilescope mappings, resolved by their stablemanagedids. Without them authentik issues an ID token carryingsubalone 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-emailmapping foremailinstead of authentik's managed one, which hard-codesemail_verified: false. The registry links an identity to an existing account only when both sides' addresses are verified. The mapping reportsfalsefor a self-enrolled account (agentry_email_verified: false) until the registry's activation link confirms it,trueonce it has, andtruefor 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-devicescope mapping, which answers theagentryscope (the remembered-browser choice and the account's directory id), and sets the provider's invalidation flow to the blueprint'sagentry-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'send-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 tonext.
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 (
ChrisYbecomeschrisy); 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'spkg/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_FIELDis non-empty on the pods (the worker reads it when it applies the blueprint). The overlay sets ittrueunless.envsets it empty; production'sglobal.envsets ittrue. Any non-empty value turns it on,falseincluded: 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'sagentry_promo_codeattribute (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. Theagentryscope sends it as theagentry_promo_codeclaim, from theagentry-remember-devicemapping the provider names; the registry passes it to its extensions aspromoCodeinuser.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_FILEnames a file in the directory's environment. The file is an HTML fragment whose root element carriesdata-terms-version="<version>", for example<article data-terms-version="2026-09-30">…</article>. The site's file ispublic/legal/terms.html, the file the/termspage renders: dev mountspublic/legalread-only at/agentry-legal(a folder, not the file, since a single-file bind mount keeps the old inode after agit pull) and production mounts the chart'sagentry-directory-termsConfigMap there; the variable is/agentry-legal/terms.htmlon 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 recordsagentry_terms_accepted,agentry_terms_versionandagentry_terms_accepted_at. Accounts that existed before the variable was set are not asked. - Registry activation. With
AGENTRY_DIRECTORY_REGISTRY_ACTIVATIONset 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-verificationonAGENTRY_REGISTRY_INTERNAL_URL, withAGENTRY_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'sagentry-verified-emailmapping: a mapping that asserts every address verified would vouch for unconfirmed accounts. The chart reads the entry inauthentik.global.envto give the provider that mapping, andscripts/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.pyconverts 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_KEYandAUTHENTIK_CAPTCHA_SECRET_KEYbefore opening sign-up to the internet (production: keys ofauthentik-config);AUTHENTIK_CAPTCHA_JS_URLand_API_URLpoint 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_writeevent a flow records withcreatedand the client's address; a mail is the budget's owncustom_agentry_account_mailevent, 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'semail_sentevent 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_HOURsets 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.htmlis 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-minuteagentry_remember_intentcookie 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 shortensAGENTRY_AUTH_TRUSTED_DEVICE_TTLchanges 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-intentpolicy, 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. Theagentry-remember-devicescope mapping answers theagentryscope withagentry_remember_deviceandagentry_directory_id, the signed-in account's directory id. The registry mints a record only when the claim istrue, the token'samrnames a second factor and itsauth_timeis within five minutes: a live directory session answers a later sign-in silently, repeating the claim andamrof the sign-in that started it, and onlyauth_timeshows 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-skippolicy, on the sign-in's MFA stage, asks the registry before the code page, at<AGENTRY_REGISTRY_INTERNAL_URL><AGENTRY_REGISTRY_CALLBACK_PATH>/trusted-devicewith{token, sub}and the bearer secretAGENTRY_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 explicittrusted: true. The blueprint restates the MFA binding in policy modealland binds the policy withfailure_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 withAUTHENTIK_DEBUGwhere 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 carryingX-Forwarded-FororForwarded, so a directory that reaches the API through a proxy of its own is refused too. In production two policies,trust-check-egressandtrust-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 setAGENTRY_AUTH_DIRECTORY_COOKIE_DOMAINto 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, defaultagentry-staff; give the server the same value); - a
rolesscope 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 isall, 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_emailand 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-classifylooks 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 ascustom_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
mfainamr. - 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.shclears 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_AVATARSonly seeds a first install), soscripts/directory-social-apply.shsets it toinitialsand 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-userandagentry-org-oidc-username 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'samragainst the source's JWKS, because authentik reads userinfo and never checks the ID token. - The flows
agentry-org-enrollmentandagentry-org-authenticationboth carry the gateagentry-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 methodagentry_org_sso, withmfaonly on the organization's own evidence (a class of the one signed SAML assertion, or the verifiedamr). - 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, withnextkept; 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-guardis bound to the registry's application (agentry), which runs its policies in modeall: 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'sPLATFORMset with its source. - The claims. The
agentryscope addsagentry_org_sourceandagentry_email_domain_verifiedto a sign-in through an organization's source. Theemailscope 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.
- Take a
pg_dumpof theauthentikdatabase first (production's nightlybackup-dbis one; take another before the hop). Dev's database is the stack's Postgres. - 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. - After the last hop, look at what depends on the image:
- the mail logo's path: check
authentik/stages/email/utils.pyin the new image, since a moved logo path would attach authentik's own logo; - the flow page and theme: compare the image's
flows/templates/if/flow.htmlandcore/templates/base/header_js.htmlwithfiles/authentik/templates/if/flow.htmlandbase/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.shchecks the rest of the template against the image and that the code stage, password page and first page the block writes into still exist; - the CAPTCHA controller:
scripts/acceptance/directory-captcha-check.pypins the frontend files the Turnstile adapter hooks into; review them and run the real widget test; - regenerate with
scripts/directory-csp.pyif a template changed; - re-run
scripts/authentik-bootstrap.sh(dev) so the provider's grant, mappings and flows are as above, andscripts/directory-social-apply.sh; - the sign-in flow's title (see Brand).
- On dev,
docker image prune -aafterwards: 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.
- Is authentik up?
curl -fsS https://auth.<site>/-/health/ready/, anddocker compose … ps authentik-server authentik-workeron dev orkubectl -n agentry get podsin production. A crash loop that says "Major version skips are not allowed" is the release-line rule above. - Did the blueprints apply? In the admin console (Customisation →
Blueprints) "Agentry - Account flows" must be successful and the
agentry-invalidationflow must exist; the worker's log names the entry of a blueprint that fails.scripts/directory-social-apply.shre-applies and reads back every instance. On a fresh install a blueprint that names the account flows' objects reportserroruntil the next hourly discovery; restarting the worker applies it at once. Production's admin console is not public: reach it through a port-forward tosvc/agentry-authentik-server. - 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 withinvalid_request), the application's launch URL and the brand's default application. Read individual keys from.envrather thansource-ing it (values with spaces are not shell-quoted). In production it is theagentry-site.yamlblueprint; apply it as in step 2. - Do the containers know where to return?
AGENTRY_PUBLIC_WEB_URL(sign-out) andAGENTRY_ACCOUNT_URL(password and authenticator changes) must reach theauthentik-*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. - The code is asked every time, or "remember" does nothing.
AGENTRY_IDP_TRUST_CHECK_TOKENmust be set on the directory and the registry with the same value,AGENTRY_REGISTRY_INTERNAL_URLmust reach the registry's API directly, andAGENTRY_REGISTRY_CALLBACK_PATHmust 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. - 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. - A sign-up stops at the terms step. The directory refuses every sign-up
while
AGENTRY_DIRECTORY_TERMS_FILEcannot be read or has nodata-terms-version. The file must be readable by uid 1000 and mounted in the server and the worker. - 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, allowingmeans the counter itself failed. - 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: checkAGENTRY_SMTP_*there. - 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. - An erasure left a directory account behind. See Account erasure: the
X-Agentry-Directory-Erasureheader sayspending, and afailopen.directory_erasurelog line or auser.directory_eraserow withabandonedmeans 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.