Identity HBAC Policy
Identity HBAC (Identity Handler-Based Access Control) is a way to restrict which users can obtain tokens from which applications. Think of it as a firewall between your users and your OAuth2 clients: you decide who can log in to what, and what data each application is allowed to receive.
The basic idea
Without any policies, every authenticated user can get a token for any registered client. Once you create at least one policy, Ahdapa starts enforcing rules — a user can only get a token if at least one live policy explicitly allows it.
Policies follow the same mental model as FreeIPA’s HBAC rules for SSH access, extended to the OAuth2 world:
| Traditional HBAC | Identity HBAC |
|---|---|
| User logs in to a server | User logs in to an application (OAuth2 client) |
| Access to a service (SSH, FTP…) | Access via specific scopes (openid, email, groups…) |
Common use cases
Restrict an application to a specific group
Only members of hr-staff can log in to the HR portal:
Rule: "HR portal access"
Users: (any) → User groups: hr-staff
Client: hr-portal
Scopes: openid, email, profile
Lock down a sensitive client to named individuals
Only alice and bob may access the payroll system:
Rule: "Payroll access"
Users: alice, bob
Client: payroll-app
Scopes: openid, email
Require MFA for a privileged client
Finance users need a second factor before getting tokens for the reporting tool:
Rule: "Finance reporting — MFA required"
User groups: finance-team
Client: reporting-tool
MFA bypass: off (MFA step-up required)
Scopes: openid, profile, email, groups
Allow any user for a low-sensitivity app, but restrict scopes
All users can use the wiki, but it only receives openid and email — no
group membership or POSIX attributes:
Rule: "Wiki — any user, limited scopes"
Users: any user
Client: company-wiki
Scopes: openid, email
Restrict by network
Internal dashboard is only accessible from the corporate network:
Rule: "Internal dashboard — office network only"
User groups: employees
Client: internal-dashboard
Source networks: 10.0.0.0/8, 172.16.0.0/12
Scopes: openid, profile, groups
Control OBO delegation targets
A pipeline agent is allowed to perform token exchange on behalf of users, but
only when delegating to the specific backend service. Because all HBAC rules
default to mfa_bypass=false, any rule covering a machine-to-machine flow
must set mfa_bypass=true explicitly:
Rule: "Pipeline agent OBO delegation"
Users: (any)
Client: pipeline-agent
Scopes: openid, email
MFA bypass: true (required for M2M flows — default is false = MFA required)
Delegation targets: host/backend.example.com
To allow the agent to delegate to any service without an explicit allowlist:
Rule: "Pipeline agent — wildcard delegation"
Users: (any)
Client: pipeline-agent
Scopes: openid, email
MFA bypass: true
Delegation target category: true (wildcard — any target_service is permitted)
Two-rule pattern for OBO deployments with multiple clients
When any deployment has at least one live HBAC rule (enforcement is active), machine-to-machine clients that issue client credentials tokens also need coverage. A reliable pattern uses two rules:
Rule 1 — CC base (covers all clients for client_credentials):
Rule: "M2M base — client credentials for all clients"
client_category: all
user_category: all
scope_category: all
mfa_bypass: true
delegation_targets: ["_cc_only"] ← sentinel SPN prevents OBO delegation
The _cc_only sentinel is an SPN that no real service will ever request via
target_service. When a token exchange request specifies a real backend SPN,
this rule fails the delegation check and does not grant OBO access. The rule
covers only plain client credentials requests (where no target_service is
evaluated).
Rule 2 — OBO rule (explicit delegation to the backend):
Rule: "Agent OBO to backend"
clients: [pipeline-agent-id]
user_category: all
scope_category: all
mfa_bypass: true
delegation_targets: ["host/backend.example.com"]
With this two-rule setup:
- All clients can obtain CC tokens (
_cc_onlysentinel prevents accidental OBO). - Only the designated agent can perform OBO delegation to the backend.
- Both rules set
mfa_bypass=trueso the exchange is not rejected for missing AMR.
Delegation targets for OBO token exchange
When a client performs RFC 8693 token exchange with a target_service
parameter, the HBAC rule that grants the exchange must also explicitly permit
that Kerberos SPN. Two fields on each rule control this:
| Field | Default | Meaning |
|---|---|---|
delegation_targets | [] (empty) | List of Kerberos SPNs this rule permits as target_service. An empty list means no SPN is explicitly allowed unless delegation_target_category is set. |
delegation_target_category | false | Wildcard flag: if true, any target_service value is accepted regardless of delegation_targets. |
delegation_target_count | (read-only) | Count of SPNs in delegation_targets; returned in GET responses for display. |
When a token exchange request includes target_service, the server:
- Evaluates all regular HBAC axes (user, client, scope, network, device, ACR).
- Among the rules that match those axes, checks whether at least one permits the SPN.
- If none does →
403 access_denied.
When no target_service is provided, the delegation-target axis is not
evaluated and delegation_targets has no effect.
If target_service is provided but no live HBAC rules exist at all, the
request is denied (fail-closed), even though an empty rule set is normally
allow-all for user identity flows.
Unconstrained-axis warning: A rule with delegation_targets = []
(empty list) and delegation_target_category = false does not restrict
delegation at all — any target_service value will match it. When this
condition is detected at evaluation time, a WARN-level log entry is emitted
so operators can identify rules that may grant broader delegation than
intended. The request is still processed; no error is returned to the client.
To manage delegation targets via the API, use the PATCH (PUT) endpoint with
add_delegation_targets and remove_delegation_targets arrays:
# Allow delegation to a specific backend
curl -s -b session.jar \
-X PUT -H 'Content-Type: application/json' \
-d '{"add_delegation_targets": ["host/backend.example.com"]}' \
https://idp.example.com/api/admin/hbac/<rule-id>
# Enable wildcard (any target_service allowed)
curl -s -b session.jar \
-X PUT -H 'Content-Type: application/json' \
-d '{"delegation_target_category": true}' \
https://idp.example.com/api/admin/hbac/<rule-id>
Managing policies
Admin WebUI
Navigate to Identity HBAC policy in the sidebar. The list shows all active policies with their enabled status and a count of members.
Click any policy name to open the detail editor. Two collapsible sections keep the form focused:
- Users — who is allowed (specific users, user groups, or any user)
- PAM / SSH — hosts and services carried from FreeIPA rules; Ahdapa stores them but does not use them for OAuth2 evaluation
- OAuth2 — which clients, which scopes, source networks, device groups, and security requirements (MFA, ACR)
The General section at the top controls the policy name, description, and whether the policy is enabled.
Click Edit to make changes, Save to apply them, Delete to remove the policy.
On the Clients page, the “Identity HBAC policy” section at the bottom shows which policies apply to that client, and links directly to creating a new policy pre-scoped to that client.
Via the API
Administrators can also manage policies through the REST API. See the developer reference for the full endpoint list. A quick example:
# Restrict the HR portal to hr-staff members
curl -s -b session.jar \
-X POST -H 'Content-Type: application/json' \
-d '{
"name": "HR portal — hr-staff only",
"description": "Only hr-staff members can log in to the HR portal",
"enabled": true,
"user_groups": ["hr-staff"],
"clients": ["hr-portal"],
"allowed_scopes": ["openid", "email", "profile"]
}' \
https://idp.example.com/api/admin/hbac
Key behaviours to know
No policies → everyone allowed. If you have not created any policies, all authenticated users can get tokens for all clients. This preserves backward compatibility for fresh deployments.
First policy starts enforcement. The moment at least one live policy exists, access is evaluated. A user who does not match any policy is denied.
Machine-to-machine flows are excluded from user-identity HBAC. The
client_credentials grant type carries no user principal, so user-axis HBAC
evaluation is skipped for it. However, if you use any user-facing HBAC rule
at all (which starts enforcement), M2M clients that perform OBO token exchange
are evaluated via the subject token’s sub. Any HBAC rule that may match an
OBO or CC flow must set mfa_bypass: true explicitly — DWRegister
defaults to false (no enable-tags → disabled), so a rule without an
explicit mfa_bypass=true returns mfa_required=true, and the token exchange
handler rejects the request because machine-to-machine flows carry no AMR.
Token exchange (OBO) is subject to HBAC. RFC 8693 token exchange requests
are evaluated against the HBAC rule set using the subject token’s sub as the
identity. Group-based rules and network-axis rules do not apply during OBO
exchange (group membership is not embedded in access tokens; the originating
network is not available at token exchange time).
OBO with no live rules emits a warning but is allowed (without target_service). When a token exchange request arrives and no live HBAC rules exist, Ahdapa logs a WARN-level message noting that all policy checks will be skipped. If the request also carries a target_service parameter, the request is denied (403 access_denied) even with no live rules — preventing implicit Kerberos delegation in a misconfigured environment.
Malformed act chain returns 400 invalid_request. If the actor_token JWT carries an act claim that cannot be deserialized as a valid actor-claim chain, the token exchange endpoint returns 400 invalid_request rather than silently truncating the chain.
Disabled policies are ignored. A disabled policy has no effect — it does not grant or deny anything. Use the enabled toggle to temporarily suspend a policy without deleting it.
Multiple matching rules expand access. If two rules both match a request, the user gets the union of the scopes each rule allows. Rules are not ordered; the most permissive matching combination wins.
Policy changes replicate automatically. In a multi-node cluster, policy changes gossip to every node within seconds. You do not need to restart anything.
RBAC: who can manage policies
Assign the hbac:read and hbac:write permissions to roles that should
manage policies. A read-only role can view policies but not modify them:
[[rbac.role]]
name = "hbac-admin"
permissions = ["hbac:read", "hbac:write"]
[[rbac.role]]
name = "hbac-viewer"
permissions = ["hbac:read"]
[[rbac.group_role]]
group = "admins"
role = "hbac-admin"