LDAP / Active Directory
One connector class serves two shapes of directory over LDAPv3: Active Directory, and generic LDAP — OpenLDAP, 389-DS, ApacheDS, eDirectory. They share the transport and diverge on schema; the AD-only parts below are marked.
It is the connector partners implement first, and the one that touches accounts most destructively — it creates them, moves them between containers, and at the end of a leaver's grace window deletes them outright. So this page is less a feature list than an account of what it does to your directory, and where it deliberately stops.
Two templates, one engine
The catalogue ships the directory twice; the choice seeds a different schema and a different default posture:
| Active Directory | Generic LDAP | |
|---|---|---|
| Vendor key | active_directory |
ldap |
| Engine template | active_directory |
openldap |
| Default role | Target | Bidirectional |
| User class | user (person objects) |
inetOrgPerson |
| Group class | group |
groupOfNames |
| Login attribute | sAMAccountName |
uid |
| Stable object id | objectGUID |
uid value (DN if absent) |
| Account status | userAccountControl bit 0x2 |
no standard attribute |
| Account types shipped | three (below) | none |
AD defaults to Target because identities are normally HR-authoritative and the
directory holds accounts that correlate to them; a small LDAP directory is as
often the source of record, so the generic template defaults to bidirectional.
Both are overridable. The last three rows matter most: generic LDAP has no
standard status attribute, so enable and disable are unavailable there, and it
identifies accounts by uid rather than an immutable directory id, so a changed
uid presents to reconciliation as a new object.
Authentication, and where the connector runs
Simple bind with a bind DN and password, over an encrypted channel. That is the whole list of methods; both templates advertise exactly one. SASL/GSSAPI binding — Kerberos, NTLM — is not implemented, and a directory requiring it cannot be bound to today.
Certificates are verified by default, with the hostname checked. The same two
controls as the HTTP engines apply: your tenant's Trusted TLS certificates
(an internal CA or a self-signed domain controller certificate), and a
per-connector skip-verify escape hatch for internal targets only. A connector
can use ldaps:// (port 636, the default in the AD template) or ldap:// with
StartTLS (engine_config.tls.start_tls), which upgrades the connection before
the bind so the password only ever travels encrypted. A bind over plain
ldap:// is refused with an error naming those options; the only way round it
is the explicit lab opt-in engine_config.tls.allow_insecure_bind, which logs a
warning on every connection and still never allows a password to be written.
A connector that bound over plain ldap:// before this check fails on its first
bind until it is switched to LDAPS, StartTLS or the lab opt-in.
The connection is made by wherever the connector runs, and that is normally the agent; its trusted certificates travel with the connector configuration. An on-premises directory is by definition unreachable from a SaaS control plane, so agent mode is the normal posture — the connector runs inside your network on the tier-3 agent, and the bind credential is resolved from the agent's own encrypted store rather than travelling from the control plane.
Tunnel mode is impossible for LDAP, and the platform refuses it rather than degrading. Tunnelling bridges HTTP calls through the agent; LDAP is raw TCP with no HTTP layer to intercept, so there is nothing to bridge. Configuring it fails loudly when the engine is built, instead of falling back to a control-plane socket that would never reach the directory. The same refusal covers the other raw-socket engines (SQL, SFTP, SMB). In-process execution remains available for a lab directory the control plane can reach; the write primitives, the wizard's per-object test and the connectivity probe all travel over the agent protocol, so agent mode is not a reduced feature set.
Finding your domain: rootDSE base-DN detection
A directory template must ship some search base, and the placeholder is
inevitably wrong — CN=Users,DC=corp,DC=local is nobody's actual domain. A new
connector's first act is then to return zero records, which reads as breakage
rather than as an unedited default.
So the connectivity probe asks the server where it lives. Every LDAP server
publishes its naming contexts in the rootDSE, readable with a base-scope
search on the empty DN, and defaultNamingContext is the domain root. The wizard
re-homes each search base onto that domain while preserving the leaf container:
CN=Users,DC=corp,DC=local against a discovered root of DC=rapidvalue,DC=demo
becomes CN=Users,DC=rapidvalue,DC=demo.
Three properties make this safe to have on by default:
It rides the existing probe rather than being a separate call. The read happens on the connection the health check already opened and bound, and the naming contexts return on the existing connectivity result — no new connector method, no second round trip, and the agent path stays symmetrical with the in-process one.
It is best-effort and never downgrades a healthy bind. If the read fails — an unusual server, a bind too restricted to perform it — the failure is swallowed and the successful bind stands: connectivity reports healthy with no naming contexts, and the bases stay as they were. A discovery convenience is not allowed to make a working directory look broken.
It only rewrites a base still equal to the pristine template default. The
wizard rebuilds the vendor template, collects the base DNs it would have
produced, and rewrites only those; a base an administrator has edited is never
touched, and a one-shot flag stops a re-entered step rewriting twice. A DN with no
DC= components — ou=people,o=acme, a directory that was never AD-shaped — is
returned unchanged, because there is no domain suffix to swap.
Referrals are never chased
A subtree search against a real AD naming context routinely returns referrals — pointers to other domain controllers holding subordinate parts of the tree. Most LDAP client libraries, including this one's, follow them by default. From this position that is almost always wrong: the referral target is typically a host the agent cannot resolve or reach, so the follow-up connection fails and takes the whole search down with it.
The resulting failure is why this is called out rather than quietly configured.
Referral chasing fails with an invalid server address socket error — and it
fails only on searches. The health check binds and reads the rootDSE without a
subtree search, so it stays green. You get a connector reporting a healthy
connection, valid credentials and a reachable server, while every user and group
read fails with what looks like a network problem on a server you can plainly see
is up.
So referral chasing is disabled unconditionally: the connector only reads entries beneath the search bases it was given, and following a referral is never wanted.
Binary attributes must leave the connector JSON-safe
AD returns objectGUID and objectSid — and any schema extension declared binary
— as raw bytes. Two things break on that.
The first is transport: in agent mode the read result is posted to the control
plane as JSON, and raw bytes are not serialisable. The task dies mid-transfer, so
the sync sits at 0 read instead of reporting a clear failure.
The second makes this a correctness concern rather than a formatting one. On
Active Directory objectGUID is the account's external id — the rename-stable
anchor tying a directory object to the account row that governs it. Stringifying
raw bytes with a language default yields a representation of the byte buffer, not
the identifier: unstable, and nothing like what an administrator sees in a
directory tool. Every sync would mint new accounts and orphan the previous ones.
So values are normalised at the search boundary — objectGUID to a canonical GUID
string (decoded little-endian, as AD stores it), objectSid to a canonical
S-1-5-… string, any other binary value to base64. This covers the raw attribute
dictionary as well as the derived identifier, so no byte value reaches the control
plane by any route, in either execution mode.
One limit, because the format invites the assumption: objectSid is formatted,
not resolved. The connector renders the SID; it does not look up the principal it
names. A SID in a read is a SID, not an account name.
Reading the directory
Each object can carry several search bases with their own filters, so a directory whose users live under three OUs does not need three connectors; results are unioned, de-duplicated and paged. A failure on any one path aborts the whole read, naming the offending base DN — a partial union is never presented as success, because a mistyped OU that silently returned nothing would look like a population that had simply shrunk, and reconciliation treats a missing account as a removed one.
Nested groups are read. AD and LDAP list a child group in the same member
attribute as users, so the connector derives group-in-group links from the one
group search it already does — no extra round trips and no setting to turn on.
Large AD groups are read in full: a group's member is returned in ranges, and
the connector walks every range itself.
An incomplete read is never treated as an empty directory. A search that stops half-way (size limit, no access to a base, a busy server) fails loudly instead of returning the pages it got, and a group whose member ranges could not all be read is held as an incomplete scope rather than reported as having lost the missing members. Without that, a partial read would have produced leavers and revoked access nobody meant to revoke.
⚠ The
nested_groupsoption does nothingOlder configurations may still carry a
nested_groupskey for the AD in-chain matching rule. No engine reads it and the catalogue no longer seeds it; stored values are inert. Nesting needs no toggle.
What the connector writes
| Operation | Active Directory | Generic LDAP |
|---|---|---|
| Create account | ADD under the account type's write-home OU | ADD under the write-home OU |
| Update attributes | MODIFY (replace) | MODIFY (replace) |
| Enable / disable | userAccountControl bit flip |
not available |
| Grant group membership | add member (already a member counts as success) | add member (same) |
| Revoke group membership | remove member (not a member counts as success) | remove member (same) |
| Move to another OU | MODRDN | MODRDN |
| Delete account | DELETE | DELETE |
Enable and disable are AD-only, and the connector says so rather than pretending:
on a generic directory both return an explicit failure naming the missing
userAccountControl attribute.
On create, the connector owns the structural attributes — object classes, the CN
that forms the DN, the initial userAccountControl — while your provisioning
mappings supply everything else and win over the connector's own defaults for the
same attribute. The division stops a mapping fighting the DN placement or the
status bit-math. See Provisioning Mappings, and
Read/Write Parity for what happens when the write path
and the correlation rules drift apart.
Account state at creation, and the initial password
An AD account cannot be created enabled without a password, and AD accepts a password only over an encrypted connection. The connector therefore decides the account's state at creation from the identity and the channel:
| Identity | Channel | Result |
|---|---|---|
| Not yet active (inside its pre-validity window) | any | Created disabled, no password |
| Active | LDAPS or StartTLS | Created enabled with a generated password, set to change at next logon |
| Active | plain ldap:// |
Created disabled, no password, with a warning that encryption is required |
A later enable follows the same rule: over an encrypted channel the password and the enable go in one modify; over plain LDAP the enable is refused with the reason and the account stays disabled. An account type can opt in to being enabled a set number of days before the start date. Generic LDAP has no status attribute and keeps creating accounts without a password.
The generated password is never stored, logged or shown in the platform. It is
mailed once, to the recipients of the credential-delivery rule that matches the
system and account type (configured under Notifications → Rules; up to five
recipients, timed at account creation, at enable, or a number of days before
the start date). With no matching rule nothing is mailed and the account
records that. In agent mode the platform never sees the value: the agent sets
it, and a connector can be set so the agent also sends the mail through your own
SMTP server. Password attributes (userPassword, unicodePwd) are never
stored from a sync and never served by the accounts API.
Each account type gets its own creation OU
Directories separate account kinds by container: ordinary users in one place, administrative accounts in another, service accounts in a third. A connector with a single creation base would drop every new account into the same OU regardless of what it is. So the creation OU belongs to the account type, not to the connector: when provisioning routes a new account onto a type — a decision the access model has already made — that type's write-home is handed to the connector, which creates the object beneath it. A type with no write-home falls back to the connector's first users search base, so directories that never set one are unaffected.
Keeping accounts in their OU. Reconciliation can also compare where an account
actually is with where its type says it belongs (the write-home, or the leaver OU for a
disabled leaver) and move it back on drift. This is a container (OU) placement row you
add in the mappings editor; it is not in the AD template, so an existing directory is never
moved into one OU by default. The row can instead carry an expression in the mapping
template language that computes the OU per account, for example
{if_eq(status, 'terminated', 'OU=Leavers,DC=example,DC=com', 'OU=Users,DC=example,DC=com')}.
Account creation uses the same computation, so a new account lands where reconciliation
expects it. Values are escaped for a DN, an expression that renders blank or fails is
skipped with a warning and never moves anything, and the leaver timing does not change: a
terminated person's account is only compared after the leaver disable. On a system that
reviews account moves first, the move waits on the system's Review tab until a person
releases it.
Three account types ship for Active Directory:
| Type | Purpose class | Created under | Identified by |
|---|---|---|---|
| Personal accounts | personal | CN=Users |
everything not matched below |
| Privileged admin accounts | administrative | OU=Admins |
login-name prefixes adm-, adm_, a-, admin-, da- |
| Service accounts (NHI) | service | OU=ServiceAccounts |
login-name prefixes svc-, svc_, service-, sa-, srv- |
All three are enabled by default, and each OU leaf is combined with your real
domain suffix at onboarding, so OU=Admins becomes
OU=Admins,DC=yourdomain,DC=example. The prefixes are conventions, not standards,
and every rule here is editable per system.
Personal is deliberately the catch-all — no filter of its own, taking whatever
the admin and service rules do not claim. A directory naming its admin accounts
ops.jdoe therefore classifies them as personal until you say otherwise, which is
visible and correctable, where a personal type with its own restrictive filter
would leave them matching nothing at all. Service accounts are typed as non-human
identities, which puts them on a different lifecycle: a longer dormancy threshold
and NHI ownership review rather than a manager. Admin and service ship without
an ownership rule, because such an account shares its human owner but not that
human's email address — the email correlation that works for personal accounts
would silently fail, and a blank to configure beats a rule that cannot match. See
Account Types and Account Rules. The
generic template ships no account types; conventions outside AD are too varied
to guess.
The leaver sequence
1. Move. When a terminated identity's account is being disabled, the connector first re-parents it into that account type's configured leaver OU — an MODRDN keeping the relative name and changing only the parent, so the account keeps its name and moves house. Every part is gated: only when the owning identity is terminated, only when the engine can move accounts, only when a leaver OU is configured for the type. It is idempotent, and best-effort — a failed move is logged and the flow continues to the disable. A successful move is audited.
The terminated-only guard matters more than it looks. An emergency lockout also disables accounts, but a lockout is a reversible security override; moving that account would leave it in the wrong container when the lockout is lifted. Only the permanent leaver path moves anything.
A moved account is still found by the writes that follow. A leaver OU usually sits outside
the users search paths, and the leaver flow still removes the account's groups after the
move. On AD, a group grant or revoke and an attribute update that miss the account in the
users paths ask again from the base DN, for exactly one user object; two matches are logged
and nothing is guessed. A generic LDAP directory keeps to the users paths, because uid is
not unique across a directory.
2. Disable. The userAccountControl disable bit is set. On a generic directory
this step is unavailable and the account stays enabled in place until the delete
window elapses — a real gap, and a reason to prefer AD for the full leaver flow.
3. Delete — only after a grace window, and only if you asked for one. A daily sweep looks at accounts that are disabled, whose owning identity is terminated, and which record when they were disabled. It resolves a retention window from the account type, falling back to the tenant lifecycle default. If no window is configured, nothing is deleted — the account stays disabled indefinitely, which is the default posture. Where a window is configured and has elapsed, the account is deleted in the directory and the platform's own account row moves to pending delete: the row is kept, so the audit trail survives the removal of the object it describes.
⚠ A deliberate, narrow exception to a platform-wide rule
RapidValue does not hard-delete governed objects. Dormant-access remediation soft-revokes; identity erasure pseudonymises; audit rows are immutable at the database level. Directory account deletion is the one place that rule is broken, and it is broken because a directory accumulating every leaver's account forever is itself a governance failure.
So it is fenced. It applies only to accounts whose identity is terminated. It requires a retention window to be configured — off unless set. It runs only once that window has elapsed, measured from the disable timestamp. It is audited. And it applies only to connectors that declare the capability: an engine with no real delete primitive marks the account pending-delete and removes nothing, rather than reporting a deletion that never occurred. If the directory refuses the delete, the account stays disabled and the sweep retries next window rather than escalating.
Known edges
Stated inline above and collected here: no SASL/GSSAPI; a plain ldap:// bind is refused unless you use StartTLS or
the lab opt-in; the nested_groups option does nothing (nesting needs none);
objectSid is formatted, not resolved; enable and disable are AD-only;
generic LDAP identifies accounts by uid, not an immutable id; tunnel mode is refused rather than
degraded; and the shipped classification prefixes are conventions that will miss a
directory naming things differently — which is why the onboarding wizard runs them
against real sampled accounts, so coverage is visible before activation rather
than after.
Which buttons to click in your directory, which rights to delegate on which OU, and what each error code means are deliberately not on this page: those live in the connector's setup checklist inside the product, beside the wizard step that needs them. That list is worth reading before enabling writes — a least-privilege bind account needs separate, explicit rights to create, modify, move and delete objects, each on a different container.
Further reading: