Directory sign-in and groups¶
A company that runs Active Directory, OpenLDAP or FreeIPA already knows who works there and which teams they belong to. This page covers the three ways AgenticOS can use that knowledge. People can sign in with their directory account. A domain-joined browser can sign them in with its Kerberos ticket. And directory groups can decide which organizations people join, with which role, and which groups they are in.
Three pieces make that up, and they work independently:
| Piece | What it does | Configured by |
|---|---|---|
| Groups | Named sets of an organization's members that a resource can be shared with | Members with members:manage, in the console |
| Directory group mappings | "Everyone in this directory group joins as this role, in this group" | Members with members:manage and roles:manage |
| Directory sign-in | LDAP password sign-in, and Kerberos (SPNEGO) sign-in | The operator, in backend/.env |
The mappings also apply to people who sign in through OIDC, when the identity provider reports their groups. If your company already runs Keycloak or Entra ID in front of its directory, you may not need the native LDAP sign-in at all. See Through an identity provider instead.
Groups¶
A group is a named set of members inside one organization: Finance, Support team, Platform admins. You share an agent, a skill, a collection, a context file, a vault secret or an artifact with a group the same way you share it with a person, from the resource's Sharing panel. One grant then reaches everyone in the group.
A group grant reaches whoever is in the group when access is checked. Somebody who joins the group next month reaches the resource; somebody who leaves stops reaching it. There is nothing to revoke per person.
A group carries no role. What a member may do in the organization is still their membership's role, and a group only adds the grants made to it. Effective access stays the rule described in Permissions:
When a person reaches a row through several grants, the highest level wins. Read through their own grant and edit through their group's is edit.
| Action | Who may |
|---|---|
| List groups and see who is in them | Any member of the organization |
| Create, rename or delete a group; add or remove people | members:manage |
| Share a resource with a group | Whoever may edit that resource |
Groups live on Organizations → Members → Groups. Only members of the organization can be added, and removing somebody from the organization removes them from all of its groups. Deleting a group deletes every grant made to it and every directory mapping that names it, so it takes the authority deleting those mappings takes: an Admin cannot delete a group an Admin-level mapping points at.
Directory group mappings¶
A mapping says what one directory group means inside one organization:
Everyone in
CN=Finance,OU=Groups,DC=corp,DC=example,DC=comis a member here, and is in the group Finance.
Mappings live on Organizations → Members → Directory. Reading them takes
members:manage. Creating or deleting one takes members:manage and
roles:manage, because a mapping both admits people and gives them a role.
The role is bounded the same way an invitation's is. You can only map a
directory group to a role your own role strictly outranks. Nobody can map a group
to owner, and an Admin cannot map a group to Admin. Deleting a mapping takes the
same authority, because it demotes everybody it placed.
What happens at sign-in¶
Every time somebody signs in with LDAP, with Kerberos, or through OIDC with
OIDC_GROUPS_CLAIM set, their directory groups
are read against every organization's mappings:
- An organization with a matching mapping is joined if they are not a member yet, with the mapped role. The membership is marked directory.
- A directory membership gets its role brought up to date with the mappings.
- They join the mapped groups, and leave the directory-made group memberships no mapping names any more.
- A directory membership with no matching mapping left is removed, together with the person's group memberships in that organization.
Groups are compared without regard to case or surrounding spaces, because every directory compares them that way.
When several mappings match in one organization, the role is the first of these that any of them names:
admin → builder → operator → member → viewer
Roles are not ordered by one line of authority. A builder and an operator each hold something the other does not, so the order is stated here rather than worked out.
What the sync never touches¶
The sync only changes rows it made itself. Four rules follow from that:
- A membership an administrator made keeps its role. Somebody invited by hand keeps the role they were given, even if a mapping matches them. They still join the mapped groups.
- Changing a directory member's role by hand takes the membership over. It becomes a manual membership, and later sign-ins leave its role alone.
- A group membership added by hand stays when the directory would take the person out of that group.
- An owner is never demoted or removed, and no mapping can make one. Ownership moves through Transfer ownership only.
A personal organization is never joined through a mapping, and a mapping cannot be created for one.
Deleting a mapping removes the people it placed
Removal happens at each person's next sign-in, not at once. To keep somebody after you delete the mapping that brought them in, change their role by hand first. That takes the membership over.
Mappings admit new accounts¶
A mapping counts as an invitation. On an invite_only deployment, or one with an
allowed-domains list, a first sign-in whose groups match any organization's
mapping creates the account. Somebody who holds both members:manage and
roles:manage decided that everyone in that group belongs. A closed deployment
still refuses it. See Who may register.
Signing in with a directory account¶
Set LDAP_URL and the sign-in page offers a directory form. People type the
username they use everywhere else and their directory password. AgenticOS checks
the password with the standard two-step bind:
- It searches
LDAP_USER_BASE_DNwithLDAP_USER_FILTER, using the service account (LDAP_BIND_DN), for exactly one account matching the username. - It binds as that account with the typed password. Only the directory checks the password. It is not stored, hashed or logged here.
The account is keyed on the directory's own stable identifier: LDAP_ID_ATTRIBUTE,
objectGUID on Active Directory and entryUUID on OpenLDAP and FreeIPA. It is not
keyed on the address, so a renamed person keeps their history. On their first
sign-in the account is created under the sign-up
policy, and an existing account with the same
address is linked to it.
# backend/.env - Active Directory over LDAPS
LDAP_URL=ldaps://dc01.corp.example.com
LDAP_CA_CERT_FILE=/etc/ssl/certs/corp-ca.pem
LDAP_BIND_DN=CN=agenticos,OU=Service Accounts,DC=corp,DC=example,DC=com
LDAP_BIND_PASSWORD=...
LDAP_USER_BASE_DN=OU=Staff,DC=corp,DC=example,DC=com
LDAP_ID_ATTRIBUTE=objectGUID
# frontend - show the form on the sign-in page
OAUTH_PROVIDERS=ldap
LDAP_DISPLAY_NAME=Active Directory
Every setting and its default is in Configuration.
What the sign-in refuses¶
| Refused | Why |
|---|---|
| An empty password | An LDAP bind with a name and no password is an unauthenticated bind, which many servers accept (RFC 4513). It never reaches the directory |
| Two accounts matching one username | Binding as whichever came back first could sign one person in as another |
| A username carrying filter syntax | It is escaped (RFC 4515), so * finds nobody rather than everybody |
| An account with no address or no identifier | The account, the invitation and the sign-up policy are all keyed on them |
ldap:// without StartTLS |
It sends every password in the clear. The process refuses to start unless LDAP_ALLOW_PLAINTEXT=true says so on purpose |
A wrong username and a wrong password get the same answer. A directory that is down, a refused service account and a search the directory could not answer (a wrong base DN, say) are reported as the directory being unavailable. They are not reported as a wrong password, which would send every person in the company to reset theirs.
The certificate is verified against the system trust store, or against
LDAP_CA_CERT_FILE for a company CA, and so is the host name. The sign-in is
rate-limited per address and per username, like the password sign-in.
Groups from the directory¶
By default a person's groups are the DNs in their memberOf attribute. A directory
without memberOf can be searched instead: set LDAP_GROUP_BASE_DN, and
LDAP_GROUP_FILTER with {dn} or {username} in it. For Active Directory's
nested groups, search with the matching rule that follows the chain:
LDAP_GROUP_BASE_DN=OU=Groups,DC=corp,DC=example,DC=com
LDAP_GROUP_FILTER=(member:1.2.840.113556.1.4.1941:={dn})
To let only one group sign in at all, put it in LDAP_USER_FILTER:
(&(objectClass=user)(sAMAccountName={username})(memberOf=CN=AgenticOS Users,OU=Groups,DC=corp,DC=example,DC=com)).
Integrated Windows sign-in (Kerberos)¶
On a domain-joined machine, a browser pointed at a host its policy trusts answers
WWW-Authenticate: Negotiate with a Kerberos ticket. With KERBEROS_ENABLED=true
the sign-in page offers a button that signs the person in with that ticket, and
nobody types a password.
The ticket names a principal, jane@CORP.EXAMPLE.COM. AgenticOS resolves it
through the directory above, with LDAP_KERBEROS_FILTER
((userPrincipalName={principal}) by default), to the same account the LDAP
password sign-in reaches. So Kerberos needs LDAP_URL as well, and the directory
decides the address and the groups the same way for both.
What it takes:
- An image built with the
kerberosextra.gssapicompiles against the system Kerberos libraries, so it is not in the default image:docker build --build-arg EXTRAS="--extra kerberos" -f backend/Dockerfile . - A service principal and its keytab. On Active Directory, create a service
account, give it the SPN
HTTP/agenticos.corp.example.com, and export a keytab withktpass. Mount it into the backend container. -
The backend settings.
-
The button, with
kerberosin the frontend'sOAUTH_PROVIDERSandKERBEROS_DISPLAY_NAME=Windows sign-inif you want it named that way. - Browser policy that lets the browser negotiate with the API host. That
means the local intranet zone on Windows, or
AuthServerAllowlistfor Chrome and Edge, ornetwork.negotiate-auth.trusted-urisfor Firefox.
The service principal must name the host the browser reaches the API on. That
is PUBLIC_API_URL, because the negotiation happens there, not on the console's
host.
A browser that cannot answer the challenge is sent straight back to the sign-in page with a sentence saying so. That covers any machine outside the domain, and any browser whose policy does not list the host. Only a ticket accepted in one step is accepted at all. NTLM is not offered.
Through an identity provider instead¶
If your company already puts an identity provider in front of its directory, point AgenticOS at that instead, over OIDC. Use its groups claim for the mappings. The native LDAP and Kerberos sign-in are then not needed. This is also the answer for multi-factor authentication, which the native sign-in does not do.
Keycloak federates both. Under User federation, add an LDAP provider
for your directory, and switch on Allow Kerberos authentication there if you
want integrated sign-in. Its group-ldap-mapper brings the directory's groups
across. Then give the AgenticOS client a Group Membership mapper whose token
claim is groups, and set:
The mapping's directory group is then what Keycloak puts in the claim: the group
path, such as /Finance, or the bare name if Full group path is off.
Microsoft Entra ID, with groups synchronized from Active Directory, puts a
groups claim in the token once the app registration has one under Token
configuration. By default it carries each group's object id, and that id is
what you map.
The groups claim over OIDC¶
OIDC_GROUPS_CLAIM names the claim that lists a person's groups. With it unset,
OIDC sign-ins leave memberships alone. With it set, each sign-in of the
deployment's own provider applies the mappings exactly as a directory sign-in
does. Google sign-in never does, because only the deployment's own provider is
trusted to say which of its groups somebody is in.
The claim is read from the ID token, or from the UserInfo endpoint when the token does not carry it. A configured claim that is absent counts as no groups. Keycloak and Okta leave an empty list out rather than send one, and somebody removed from their last group has to lose what it gave them. If the token lacks the claim and UserInfo cannot be reached, the sign-in is refused instead: that says nothing about the person's groups, and reading it as none would strip them.
OIDC_GROUPS_CLAIM and LDAP_URL cannot be set together. Each reports groups in
its own identifiers, and each sign-in would take away the memberships the other
made, so the process refuses to start with both.
Entra ID group overage is refused
Past 200 groups, Entra ID sends a pointer to Microsoft Graph instead of the
groups (_claim_names). Such a token does not say which groups the person is
in, so the sign-in is refused with a sentence asking for the fix. Reading it
as "no groups" would strip every membership the directory gave them, and
ignoring it would keep ones it may have taken away. Configure the app
registration to send groups assigned to the application.
What this does not do yet¶
- Nothing re-checks the directory between sign-ins. Disabling an account in
the directory stops its next sign-in, and the next sign-in is when mappings are
re-read. A session already open lasts until its refresh token expires, which is
REFRESH_TOKEN_EXPIRE_MINUTESand seven days by default. To end one sooner, deactivate the account under Admin → Users, which refuses it everywhere at once. There is no SCIM and no scheduled directory sync. - A directory account can still use the password reset and magic link. They go to the account's address, like every other account's. If the directory is meant to be the only way in, keep those addresses under the company's own control, as you would for OIDC accounts.
- No SAML. A SAML-only identity provider can reach AgenticOS through Keycloak, which speaks SAML to it and OIDC to AgenticOS.