When authentik expression policies in Python are the right tool
Use an authentik expression policy when a login, enrollment or recovery flow needs a decision that no built-in policy can make on its own, such as allowing access only to members of one group who also carry a specific user attribute. According to the authentik policies documentation, an expression policy runs custom Python logic with access to the flow context, prompt data, user data and request metadata, and it returns a pass or fail result that the flow acts on. Before writing any code, check whether the rule can be built from the standard policy types, such as group membership, attribute matching, GeoIP or reputation, and combined with policy bindings. Built-in policies are easier to review, test and explain to an auditor, so the expression policy should be the tool for the remaining gap, not the default for every rule.
The main risk is that an expression policy is code, and code-bearing configuration becomes an access control surface. Anyone who can create or edit a policy, prompt or blueprint can change who gets in, so the people who can edit those objects are as important as the rule itself. The authentik hardening guide suggests blocking the /api/v3/policies/expression*, /api/v3/propertymappings*, /api/v3/stages/prompt/prompts* and /api/v3/managed/blueprints* paths at the reverse proxy to restrict who can change this configuration. Before you approve a design, confirm which administrator roles can reach these objects in your deployment, who reviews changes, and whether a blueprint or the API could overwrite a reviewed policy. Document the answers, because they decide whether the expression policy is an acceptable control for your environment.
What an expression policy can read at evaluation time
Flow context describes the login, enrollment or recovery flow that is running when the policy is evaluated. It is where a policy checks which flow the user is in, which stage is executing and what earlier stages have already established. Because one expression can be bound to several flows, a flow context check is often how a single policy serves more than one journey. The policies documentation describes the policy types and where they can be attached.
- Current flow: whether the request runs inside a specific flow, so a rule can apply to enrollment without affecting a plain login.
- Stage position: which stage is running, which helps when a rule should pass only after an earlier challenge succeeded.
- Values from earlier stages: data that previous stages have placed in the flow, such as a selected source or an invitation identifier.
- Binding point: whether the policy is attached to a stage binding or a flow binding, which can change what is available at evaluation.
- Failure path: what the flow does when the expression returns false, so users are not stranded at a stage with no clear next step.
Confirm the exact flow context keys for your running version in the policies documentation, and test each flow binding with a non-administrative account before you rely on the result.
Prompt data holds the values a user typed or selected in a prompt stage, such as a username, email address, display name or a custom attribute collected during enrollment. A policy that reads prompt data can validate input before the flow advances, for example by checking that an email domain appears on an approved list. Prompt data exists only within the flow that collected it, so the order of stages determines whether a value is available when your policy runs.
- Field values: what the user entered in each prompt field, read as submitted.
- Validation before advancing: blocking a step when a value fails a rule, such as an allowed email domain.
- Custom fields: values from prompts your team created, which depend on the field identifiers set in the prompt stage.
- Optional or missing fields: optional prompts may be absent, so checks should handle missing values without raising an error.
- Stage ordering: a prompt placed after the policy has not been answered yet when the policy runs.
Confirm the field identifiers and how absent values appear in the policies documentation for your version, then submit an invalid value during testing to confirm the flow stops where you expect.
User data covers the account the policy evaluates, including its attributes, group membership and account state. Most access rules depend on this group, for example limiting an application to one team or requiring an attribute before enrollment completes. Group checks need particular care because the group model changed in a recent release, and older expressions may still read the deprecated field.
- Group membership: checks such as whether the user belongs to a named group, which underpin most access rules.
- Custom attributes: attributes stored on the user, including values set by administrators or synchronized from an upstream source.
- Groups accessor: since authentik 2026.2,
User.groupsis the supported way to read group membership.User.ak_groupsis deprecated, still works, and creates a configuration warning event. See the 2026.2 release notes for details. - Account state: whether the account status fields are exposed to expressions, which may matter in recovery and invitation flows.
- Unset attributes: how an expression behaves when an attribute is missing, which can raise an error or evaluate to an empty value depending on how it is written.
Confirm which user fields are available in the policies documentation for your version, and search existing expressions for ak_groups before you upgrade to 2026.2 or later.
Request metadata describes the HTTP request that triggered the flow, such as the client address, headers and the endpoint being requested. Policies use it for rules about where and how a sign-in is attempted, for example restricting administrative access to approved network ranges. Request metadata can pass through reverse proxies and load balancers, so its accuracy depends on how your edge is configured and which forwarded headers the server trusts.
- Client address: the source address the server sees, which may be the proxy address unless forwarded client addresses are trusted.
- Selected headers: HTTP headers from the request, useful for recognizing a specific gateway or client application.
- Target application or endpoint: which application or flow endpoint the request is directed at.
- Network ranges: matching client addresses against approved ranges for administrative or contractor access.
- Proxy trust settings: the configuration that decides which forwarded headers the server believes, which must match your reverse proxy.
Confirm the exact request attribute names and forwarded-header handling in the policies documentation, then test from inside and outside your approved network before you rely on an address-based rule.
How the 2026.2 change from ak_groups to groups affects existing expressions
User.ak_groups has been deprecated since authentik 2026.2 in favor of User.groups. Existing expression policies that read request.user.ak_groups still run after an upgrade, so nothing fails at login on the day you move to 2026.2. The cost is a configuration warning event each time the old usage is evaluated. Treat that warning as a work item rather than a cosmetic message, because the deprecated attribute is the kind of thing a later release may remove. Before you change anything, read the 2026.2 release notes for the exact wording of the deprecation and any other changes in the same release that touch policies.
Start the upgrade check by searching every expression policy for ak_groups, including expressions in policies bound to flows, applications and stages, and in any blueprint or configuration-as-code files that hold expression bodies. Your export or blueprint repository is usually faster than clicking through the admin interface. Replace each occurrence with groups in the same expression, then test the policy against a user who belongs to the group the check is meant to match. Keep a list of the policies you changed, because the warning event does not name the policy that caused it in every case, and you will need that list for change records and for reviewing the audit trail.
To verify the change, reproduce the path that triggered the warning, then check the event log for the configuration warning tied to the deprecated field and confirm that no new warning appears. Do this in a non-production instance on the same version first, and repeat it after production is upgraded because bound policies and flows run on live traffic. If a warning remains, the cause is usually an expression you did not find in the search, such as one stored only in the database or in a file outside your repository. Record the verification result with the upgrade ticket so that an auditor or a later maintainer can see when the deprecated usage was removed.
How to write and bind an expression policy safely
This sequence takes an access rule from a plain-language decision to a bound, tested and documented expression policy, so that each step leaves a check the next step depends on. It keeps the code, the binding and the change record in step, which is what makes the rule reviewable later.
Write the decision in plain language
Before any Python, the policy owner states who may pass, under which conditions, and what happens to everyone else, for example a group membership plus a condition on the request. Name the flow or stage where the decision will run, because the available context depends on that location. Done means a short written rule that a reviewer who has not seen the code can read and agree or disagree with, with the deny outcome spelled out.
Map the rule to available context
Using the authentik policies documentation, list which values the expression can read at that point: flow context, prompt data, user data and request metadata. Confirm the exact attribute names in your own instance, and confirm the group check uses User.groups for authentik 2026.2 and later, since User.ak_groups is deprecated there. Done means every input the rule needs is traced to a source that exists at the bound location.
Write and test the expression outside production
Write the expression in a non-production instance, keeping it to one decision that returns a pass or fail result and handling missing attributes explicitly so that an absent value does not silently allow access. Use the policy test function in the admin interface with sample users to confirm the expression evaluates as intended. Done means the expression returns the expected result for each sample case and fails closed when data is missing.
Bind the policy to a stage or policy binding
Attach the tested expression policy to the intended flow stage or policy binding, and set the binding order and the failure behavior so the policy runs where the decision belongs. Limit the binding to the smallest flow or application that needs it. Done means the binding list for the flow shows the policy in the intended position, and no unrelated flow references it.
Verify allow and deny paths with test users
Create or use dedicated test accounts that match each side of the rule, including one account that should be denied and one edge case such as a user missing the attribute. Sign in through the real flow for each account and record the outcome. Done means every allow and deny path behaves as written in the plain-language rule, and the results are saved with the test date and instance version.
Check the event log for configuration warnings
Review the event log after the tests for configuration warnings, particularly ones raised by use of the deprecated User.ak_groups attribute, and for errors raised during expression evaluation. Resolve each warning by updating the expression before the change is accepted. Done means the log shows no unresolved configuration warnings or evaluation errors for the tested flow.
Restrict editing and document the change
Expressions are code, so limit who can edit them by blocking the policy, property mapping, prompt and blueprint API paths at the reverse proxy, as the authentik hardening guide suggests, and record who may make changes. Document the plain-language rule, the expression, the binding, the test results and the log check in the change record. Done means editing rights are confirmed against the hardening guide, and a reviewer can trace the rule from decision to bound code.
Which check fits group, attribute and request-based rules
This table compares built-in authentik policy options with expression policies for group membership, attribute matching and request metadata checks. Use it to decide which rules justify custom Python and which should stay in configuration that is easier to review and upgrade.
| Rule type | Built-in policy or expression policy | Maintenance cost and upgrade risk | When to choose it |
|---|---|---|---|
| Group membership | Built-in group or policy bindings in the admin interface cover direct membership checks without code | Low: configuration is visible in the admin interface and survives most upgrades without edits | Use for plain yes or no membership gates on applications, stages or flows |
| Group membership with logic | Expression policy testing user.groups or nested conditions in Python | Medium: each change needs code review and a test login; field names must match the running version | Use when a rule combines several groups, exclusions or conditional logic that bindings cannot express |
| Attribute matching | Built-in attribute checks cover simple equality on known user attributes | Low to medium: simple attributes stay stable, but attribute names chosen by your team become a dependency | Use Python only when the check compares values, parses strings or handles missing attributes |
| Request metadata | Built-in options for source or client details are limited to what the product exposes | Medium to high: request fields and context keys can change between releases, so each upgrade needs a retest | Use expressions for headers, flow context or prompt data that no built-in policy reads |
| Upgrade exposure | Configuration objects are migrated by the product during upgrades | High for code tied to internal objects: deprecated fields such as User.ak_groups need replacing and can raise warnings | Record which expressions reference internal fields and check release notes before each upgrade |
| Ownership and review | Administrators can change bindings with less process overhead | Expressions run as code, so changes need a named owner, version control and a restricted edit path | Confirm who may edit policies and property mappings before adding any expression |
| Testing | Binding tests in the admin interface show outcomes for simple rules | Each expression needs test cases for allowed, denied and missing-data users | Ask your team to write a test plan for every expression that controls access |
Locking down who may edit expressions
Expression policies are Python code stored in authentik's configuration, so anyone who can edit them can change who gets in. The controls below work as layers: block the code-bearing API paths at the reverse proxy, separate admin roles, review every change, and keep the authoritative copy in blueprints. Test each layer before you rely on it.
Know what counts as code
Expression policies, property mappings, prompt stages and blueprints all carry logic or data that authentik evaluates during sign-in or provisioning. An editor of any of these can effectively change access rules, even without touching a user or group. Inventory every object type in your tenant that stores an expression or a template before you decide who may write to it. The hardening guide's list of paths is a starting point for that inventory, not a complete one.
Block code paths at the proxy
The hardening guide suggests blocking /api/v3/policies/expression*, /api/v3/propertymappings*, /api/v3/stages/prompt/prompts* and /api/v3/managed/blueprints* at the reverse proxy. Apply these blocks to the public hostname while keeping an internal route for approved automation. Confirm the exact path patterns against your authentik version, since the API surface changes between releases. Blocking at the proxy is only effective if the admin interface cannot reach the API through another hostname.
Separate admin roles
Create a dedicated group for people allowed to author expressions and keep it apart from the group that manages users, applications and outposts. Most administrators need read access to flows and policies but not write access to code-bearing objects. Review who holds superuser status, since it bypasses ordinary role boundaries. Record the membership of both groups and review it on a fixed schedule.
Review every expression change
Treat an expression edit like a code change: a named author, a second reviewer, and a record of the previous and new text. Use your version control system as the review record by keeping exported blueprints in a repository and merging changes through pull requests. Check each change for network calls, reads of sensitive attributes and unconditional allow results. Ask your team which events authentik logs for policy edits and forward them to your log store.
Use blueprints for change control
Blueprints let you declare policies and bindings as YAML files that authentik applies on a schedule or on demand. Storing the authoritative definition in Git means a manual edit in the UI can be reverted by the next reconciliation, which makes drift visible. Restrict who can apply blueprints, since blueprint import is itself a code-bearing path. Check how your deployment mounts blueprint files and who can write to that location.
Test blocks and admin tooling
Verify each block with an authenticated request from an account outside the authoring group and expect a refusal. Then verify the paths your pipelines need, such as an approved blueprint apply job or a Terraform run, still succeed from the internal route. Test a break-glass account separately so an emergency fix stays possible. Repeat these checks after each authentik upgrade, because new endpoints can appear outside the blocked patterns.
Questions teams ask before writing expression policies
How much effort does a typical authentik expression policy take to write?
Effort depends on how many conditions the rule has, how many systems the rule reads from, and how many flows it touches. A single group or attribute check is a small change, while a rule that combines group membership, request metadata and branching across several flows needs design, tests and a review step. Ask any partner to split the quote into rule design, build, test in a staging instance and handover, so you can see which part grows with complexity. Expect the testing and documentation work to take a meaningful share of the effort on any rule that controls sign-in for many users.
Can our own team write expression policies without outside help?
Yes, if your engineers know Python, can read the authentik data model, and have a staging instance where they can test changes safely. The harder part is usually not the code but deciding who may edit it and how changes are reviewed. Teams that build in-house should agree on a naming convention, a review step for every expression, and a rollback plan before the first policy goes live. A partner can take on the design and test plan, or review your work, if the rule governs a sensitive sign-in path.
What must our team provide before an expression policy engagement starts?
Plan to provide the following before build work starts:
- Test users covering each group, attribute and edge case the rule must allow or deny
- The flows and stages the policy will bind to, with a list of the stages each flow runs
- A staging instance that matches production's version, authentication sources and reverse proxy setup
- A named owner who can approve the decision logic and the exact access outcome
- Access to logs so the team can confirm each test result
Missing any of these usually adds time because decisions get made during the build instead of before it.
What breaks when we upgrade authentik and our policies use ak_groups?
The main known risk is the change to the group reference. Since authentik 2026.2, User.ak_groups is deprecated in favor of User.groups. Old usage still works, but it creates a configuration warning event, so the policy may appear to function while your logs fill with warnings. Before any upgrade, search every expression policy for the old attribute, update it, and test the result in staging. Also read the release notes for each version between your current and target release, because upgrades must follow the documented sequence.
How do we test expression policies without locking out administrators?
Test in a separate staging instance first, using test accounts for every case, including an administrator account that is deliberately outside the new rule. Bind the policy to a test flow rather than the production login flow, and keep a break-glass administrator account that the new rule does not affect. Record the expected result for each test user before you run it, then compare the result with the log entries. Only move the binding to production after the tests pass and someone has confirmed the rollback steps.
Who may edit expression policies, and how should we restrict it?
Anyone who can edit an expression policy can run Python in your identity provider, so treat this as a high-privilege change. The authentik hardening guide suggests blocking the API paths for expression policies, property mappings, prompt stages and managed blueprints at the reverse proxy, so only approved automation or administrators can change code-bearing configuration. Confirm the exact paths against your version and test the block, since the change affects the admin interface too. Keep the list of allowed editors short and log every change.
Does the Enterprise edition change how expression policies work?
The researched facts do not list expression policies as an Enterprise-only feature, so confirm this with authentik before you plan around it. The pricing page lists Enterprise features such as Google Workspace and Microsoft Entra ID integrations, mTLS client certificate authentication, privileged access management, password history and enhanced audit logging. Enterprise audit logging may matter if you need a record of who changed a policy, so check whether that feature fits your review process. Ask for the license terms in writing, because the enterprise directory of the repository is under a separate license.
Limits to confirm in your environment
Expression policies run custom code inside your identity flows, so their behavior depends on the exact authentik version, edition and proxy setup you deploy. The cards below list what to confirm on your own instance before you rely on an expression policy for access decisions.
Context variables in your version
The flow context, prompt data, user data and request metadata that an expression can read are documented, but the exact names and attributes available can differ by release. Check the policy's context in a test flow on the version you run, and log the values you depend on. Do not assume a variable from a blog post or an older release exists in yours.
Edition for each policy feature
Some features in authentik sit in the enterprise directory under a separate license, while most of the code is MIT licensed. Confirm which edition includes the specific policy capability, integration or audit logging your rule needs before you design around it. Edition boundaries on the pricing page can change, so check them against the current version.
Behavior when an expression errors
Test what happens when an expression raises an exception, returns an unexpected type or reads a missing attribute. Decide whether the failure denies, passes or stalls the flow, and confirm that the result appears in logs where your operators will see it. A policy that fails open on a bug can grant access that the rule was meant to block.
Performance under real load
Expression policies run Python on every evaluation they are bound to, so a slow expression can add latency to each login or application request. Measure evaluation time with realistic user counts, concurrent sign-ins and the number of bindings on a flow. Confirm the sizing of the server and worker containers and the PostgreSQL connection capacity before the rollout.
Reverse proxy rules versus API paths
The hardening guide suggests blocking expression, property mapping, prompt and blueprint API paths at the reverse proxy to limit who can change code-bearing configuration. Confirm that your proxy rules match the exact paths in your version, including any prefixes, and that the rules are tested from a non-admin account. A rule that misses one path leaves code editing open.
How DeployOpen reviews and tests policy code before it goes live
A scoped engagement starts with a review of the expression policies already in the instance, because the riskiest code is often the code nobody has read since it was pasted into the admin interface. DeployOpen lists each expression policy, the flow stage and binding it sits behind, the user and group attributes it reads, and any external calls or broad exception handling it contains. Each policy is then marked as keep, rewrite or retire, and the reasons are written down. Where a policy uses ak_groups, the review also records the move to groups from authentik 2026.2 so the change is planned rather than discovered during an upgrade. Ask your partner to show the inventory format before work begins, and confirm whether the review covers blueprints as well as the live database, since a blueprint can recreate a policy you thought you had removed.
New or rewritten expressions are built and exercised on a staging instance that mirrors production configuration, including the same flows, brands, groups and identity sources. Each policy gets written test cases for both the allow path and the deny path, using test users with known group memberships and attributes, plus at least one case where the expected attribute is missing. The deny cases matter as much as the allow cases: a policy that fails open on an unexpected value is the usual defect. Results are recorded against the policy version, and the same cases are rerun after any change. Confirm with your partner which test accounts exist, who holds their credentials, and that staging never shares identity sources or outbound integrations with production.
The final deliverable is documentation of who may change what. DeployOpen records which roles may edit expression policies, property mappings, prompt stages and blueprints, and how the reverse proxy controls match the authentik hardening guide, which suggests blocking the /api/v3/policies/expression* and related API paths for users who should not change code-bearing configuration. The document also names the change approval steps, the owner of each policy and the review date. Your team keeps the approval authority. DeployOpen does not promise that a policy is correct for your risk model; it delivers the review record, the test evidence and the access documentation so your security and compliance leads can judge the result. Agree the document format and sign-off roles in the written scope before the build starts.
How DeployOpen scopes and supports authentik policy work
DeployOpen plans, implements, tests and documents authentik expression policies inside your own infrastructure, so each rule is written against your groups, attributes and flows. Engagements are fixed-scope, with the scope, deliverables and timeline agreed in writing before work starts.
How we scope
- List each access rule the built-in policies cannot express, with the decision it makes and the flow it sits in.
- Confirm which authentik version runs in your environment so group checks use User.groups from 2026.2 onward.
- Agree who may edit expression policies, and how that restriction is enforced at the reverse proxy.
How we build
- Write each expression policy against the documented context: flow data, prompt data, user data and request metadata.
- Test every policy with allowed, denied and edge-case users in a non-production instance before promotion.
- Replace any deprecated ak_groups usage and confirm no configuration warning events remain after the change.
How we hand over
- Deliver a written record of each policy's purpose, inputs, expected results and the group or attribute it depends on.
- Document the blocked API paths and the access model for editing policies, property mappings, prompts and blueprints.
- Walk your administrators through reviewing, testing and rolling back a policy change in your own environment.
How we support
- Ongoing support, upgrades and continuous testing are available as a retainer after handover.
- Re-test expression policies when you upgrade authentik, since group and attribute behavior can change between releases.
- Ask any partner for the written scope, the handover pack and the upgrade path before signing.

