> ## Documentation Index
> Fetch the complete documentation index at: https://conductorone-hunner-patch-1.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up an LDAP connector

> C1 provides identity governance and just-in-time provisioning for LDAP. Integrate your LDAP server with C1 to run user access reviews (UARs), enable just-in-time access requests, and automatically provision and deprovision access.

## Capabilities

| Resource | Sync | Provision |
| :- | :- | :- |
| Accounts | <Icon icon="square-check" iconType="solid" color="#c937ae" /> | <Icon icon="square-check" iconType="solid" color="#c937ae" /> |
| Roles (`organizationalRole` in LDAP) | <Icon icon="square-check" iconType="solid" color="#c937ae" /> | <Icon icon="square-check" iconType="solid" color="#c937ae" /> |
| Groups (`groupOfNames`, `groupOfUniqueNames`, `posixGroup`, `groupOfURLs`, `group` in LDAP) | <Icon icon="square-check" iconType="solid" color="#c937ae" /> | <Icon icon="square-check" iconType="solid" color="#c937ae" /> |

The LDAP connector supports [automatic account provisioning and deprovisioning](/product/admin/account-provisioning).

### Group membership provisioning

A group's membership lives in `member` (a DN), `uniqueMember` (a DN) or `memberUid` (a login name), and which one it is cannot be derived from the entry's object classes: `posixGroup` is STRUCTURAL under RFC 2307 and AUXILIARY under rfc2307bis, and a client such as SSSD chooses the attribute it reads through its own `ldap_schema` setting. The connector therefore learns it per entry instead of assuming it:

* **Grant** writes the attribute the group entry already uses, or, when the entry holds no membership yet, the one its object classes suggest (`groupOfUniqueNames` alone: `uniqueMember`; `posixGroup` with no DN group class: `memberUid`; otherwise `member`). If the server rejects an attribute as not permitted by the entry, the next candidate is tried. A group whose membership has diverged across attributes is written to each of them. `memberUid` is written as the principal's `uid` or `cn`, whichever form the group's own values already use.
* Every write is confirmed by re-reading the entry on the connection that accepted it, and asking the same question sync asks -- is this principal a member? A write the server accepted but that cannot be confirmed is a retryable failure, and is never followed by a write to a second attribute.
* **Revoke** removes the membership from wherever it actually is, deleting the exact stored values from every attribute that holds the principal in one request. A revoke never acts on a guess or on a configured pin.
* A revoke acts on the group's own attributes and on nothing else, so a membership held only through a nested group is not revoked here: it answers `GrantAlreadyRevoked`, changes nothing in the directory, and reappears on the next sync — revoke it at the source group instead. The one membership the connector *does* report and cannot remove with a write to the group is the user's primary group, minted from the user entry's own `gidNumber`; that one is reported as an error naming it, whether or not this call also removed a direct membership. Otherwise the result is a plain success (a direct membership was removed) or `GrantAlreadyRevoked` (there was none).
* Set `--group-member-attribute` to `member`, `uniqueMember` or `memberUid` (default `auto`) to pin the attribute a **grant** writes, for directories the connector cannot learn from -- notably one with schema checking disabled, so that no attribute is ever rejected. The pin governs grants only; revokes still act on where the membership actually is. See the connector README for the full behavior and its limits.

### POSIX account provisioning

The LDAP connector supports provisioning `posixAccount` entries with automatic UID number assignment. When creating a new POSIX account, the connector can look up the highest `uidNumber` currently in use across all existing `posixAccount` entries in your directory and automatically assign the next available value.

To use this feature, configure the following account provisioning mappings:

| Mapping field | Destination value | Description | Example CEL expression |
| :- | :- | :- | :- |
| **RDN Key** | `rdnKey` | The RDN attribute for the new entry | `"uid"` |
| **RDN Value** | `rdnValue` | The value for the RDN attribute | `subject.profile.login` |
| **Path** | `path` | The DN path where the account will be created | `"ou=users,o=Example Org"` |
| **Suffix** | `suffix` | The top-level entry DN (naming context) | `"dc=example,dc=com"` |
| **Object Class(es)** | `objectClass` | Must include `posixAccount` | `["top", "person", "organizationalPerson", "posixAccount"]` |
| **Calculate the next valid UID Number** | `calculatePosixUIDNumber` | Set to `true` to enable automatic UID assignment | `true` |
| **Additional Attributes** | `additionalAttributes` | Other required POSIX attributes | `{"cn": "Jane Doe", "sn": "Doe", "homeDirectory": "/home/jdoe", "gidNumber": "5000"}` |

<Note>
  Automatic UID number calculation assigns `uidNumber` only. You must provide `gidNumber` manually in the **Additional Attributes** mapping. If you set **Calculate the next valid UID Number** to `true`, any `uidNumber` value provided in **Additional Attributes** is ignored.
</Note>

<Note>
  A mapping that resolves to an empty value is **omitted** from the LDAP add. A zero-length value is not portable across attribute types — a directory rejects the whole add with result 21 (`Invalid Attribute Syntax`) on most of them — so an optional field the account leaves unset cannot break account creation, and it is not stored as an empty value either. A list keeps its non-empty entries and is omitted only when none remain. Values such as `false` and `0` are real values and are still sent.

  `objectClass` is the exception: a mapping that resolves to no object class at all is rejected before the add, because an account cannot be created without one.

  This is the opposite of `update_profile`, where an empty `custom_attributes` value **clears** the attribute. See the `update_profile` rules under Connector actions.
</Note>

### Connector actions

Connector actions are custom capabilities that extend C1 automations with app-specific operations. You can use connector actions in the [Perform connector action](/product/admin/automations-steps-reference#perform-connector-action) automation step.

**Global actions** (connector-level):

| Action name | Additional fields | Description |
| - | - | - |
| `create_ou` | `name` (string, required), `parent_dn` (string), `description` (string) | Create an LDAP organizational unit (`organizationalUnit`) under a parent container. `parent_dn` defaults to the configured base DN; a parent outside the base DN is rejected. Idempotent — creating an OU that already exists succeeds. |
| `enable_user` | `user_id` (account resource ID, required) | Mark a user account as enabled by writing the attributes configured in `enable-user-attributes`. Registered only when that configuration is set. |
| `disable_user` | `user_id` (account resource ID, required) | Mark a user account as disabled by writing the attributes configured in `disable-user-attributes`. Registered only when that configuration is set. |

<Note>
  To use `create_ou`, the connector's bind account must have permission to add child entries under the target parent DN (by default, the configured base DN). For OpenLDAP and similar servers, this is write/add access on the parent container; for Active Directory, delegate the **Create Organizational Unit objects** right on the parent OU.
</Note>

#### User enable/disable attributes

Some directories do not use the standard `userAccountControl` (Active Directory) or `nsAccountLock` (FreeIPA) flags to mark an account's lifecycle state, but their own attribute — IDMWorks, for example, uses `revoke`, where `Y` means disabled and `N` means enabled. `enable-user-attributes` and `disable-user-attributes` map LDAP attribute names to the values that mean *enabled* and *disabled*:

```yaml theme={null}
disable-user-attributes:
  revoke: "Y"
enable-user-attributes:
  revoke: "N"
```

Both maps are unset by default. When configured:

* **Sync** reports an account as disabled when any configured attribute holds its disabled value, as enabled when any holds its enabled value, and otherwise falls back to `userAccountControl` then `nsAccountLock`. Values are compared case-insensitively and whitespace-trimmed; any value of a multi-valued attribute counts.
* **`disable_user` writes exactly the attributes named in `disable-user-attributes`**, and **`enable_user` writes exactly those in `enable-user-attributes`**. Neither action touches an attribute named only in the other direction.
* Because of that isolation, a configuration that sets both maps must name the **same** attributes in each — otherwise `enable_user` would leave an attribute at its disabled value while the synced status kept reporting the account as disabled. Asymmetric key sets are rejected at startup. To clear an attribute when enabling, give it an explicit empty value (`enable-user-attributes: {"revoke": ""}`), which keeps the attribute name present.
* The same attribute cannot be given the same value in both directions; attribute names must not be empty; `objectClass` and password attributes cannot be used as the marker; and the **disable** side cannot use an empty value. Each is rejected at startup. An absent attribute reads as enabled, so "disable by clearing the marker" would clear it, report success, and leave sync reporting the account enabled — an empty value stays legal only on the **enable** side, where it means clear-on-enable.
* Each action returns `success`, `status` (`"enabled"` / `"disabled"`), `applied` (the number of attributes changed; `0` means the account was already in the requested state), and `updated_user` (encoded from the entry the write was verified against; absent only if encoding it failed, since a failed re-read fails the action). The connector re-reads the entry and verifies the configured attributes now hold their configured values — or are absent, for a configured clear — before reporting success. An attribute it cannot write (the entry's RDN attribute, for example) fails the action rather than being reported as skipped, on the first call and on every retry alike.
* Unlike `update_profile`, these actions are **global**, not resource-scoped. `user_id` still takes the C1 account identifier rather than the LDAP DN, for the same reason.
* **Quote the values.** The configuration loader stringifies map values before the connector sees them, so an unquoted `TRUE`/`FALSE` arrives as the string `"true"` and is written that way; LDAP's Boolean syntax (RFC 4517) requires uppercase, so the modify is rejected instead of setting the attribute. The connector cannot tell a quoted `"true"` from an unquoted one by then, so this is not caught at startup. `Y` and `N` are not YAML booleans and are safe unquoted.
* **As an environment variable the value must be JSON**: `BATON_DISABLE_USER_ATTRIBUTES='{"revoke":"Y"}'` works, `BATON_DISABLE_USER_ATTRIBUTES='revoke=Y'` is rejected at startup rather than silently configuring nothing. A nested YAML map and repeated `--disable-user-attributes key=value` flags both work as written.

<Note>
  With a "clear on enable" definition (`revoke: "Y"` / `revoke: ""`) the enabled state is reached by *fall-through* rather than by matching a positive value: no configured attribute matches its disabled value, so the built-in rules apply and an unspecified status defaults to enabled. "Enabled" therefore means "the disabled marker is absent".
</Note>

**Resource-scoped actions (user):**

| Action name | Additional fields | Description |
| - | - | - |
| `update_profile` | `user_id` (account resource ID, required), `first_name` (string), `last_name` (string), `display_name` (string), `email` (string), `custom_attributes` (string map) | Set core profile fields (first name, last name, display name, email) and/or arbitrary custom LDAP attributes on an existing user. This action is scoped to the `user` resource type, which is what makes it discoverable and usable from C1's attribute-push-rule feature. It also backs C1's [attribute push rule](/product/admin/push-rules) flow for LDAP users. |

The `update_profile` action returns `success` (bool), `updated_user` (the modified user resource, re-fetched after the write; absent if the read-back failed, though the write itself still succeeded), `applied` (the number of attributes changed), and `skipped` (named fields or `custom_attributes` entries that were not written).

`updated_user` carries the resource identity, `displayName`, and the user trait — not the entry's full attribute set. A value the action just wrote appears there only when it also feeds one of those: `display_name` through `displayName`, `email` through the trait's email list, and a `custom_attributes` key only when it maps to a trait field (`mail`, `displayName`, `sAMAccountName`, `userPrincipalName`, a non-RDN `uid` or `cn`, `lastLogonTimestamp`, `authTimestamp`). `first_name`, `last_name`, and every other `custom_attributes` key reach the directory but do not appear in `updated_user`. Use `applied` to confirm those.

Two requirements come from the C1 side rather than from LDAP:

* **`user_id` takes the C1 account identifier, not the LDAP DN.** C1 resolves the account to the connector's resource before dispatching the action. A DN fails inside C1 with `resource <dn> with type user was not found` and never reaches the connector, so it produces no connector log line and leaves the directory untouched.
* **An attribute push rule must map from a single-valued attribute.** The connector's user profile carries only attributes that hold exactly one value on the entry, so a multi-valued source resolves to nothing: the rule saves and enables, and each push reports zero attributes applied.

<Note>
  `update_profile` is intended for generic LDAP directories (Active Directory and FreeIPA have their own connectors). It applies the following rules:

  * **Named fields:** `first_name` → `givenName`, `last_name` → `sn`, `display_name` → `displayName`, `email` → `mail`. A named field is applied only when present and non-empty; a present-but-empty named field cannot clear the attribute and is instead reported in `skipped`.
  * **`inetOrgPerson` requirement:** only `last_name` (`sn`) is universal — it's a MUST attribute of the base `person` object class. `first_name` (`givenName`, defined in RFC 4519), `display_name` (`displayName`, RFC 2798), and `email` (`mail`, RFC 4524) are permitted on an entry only by RFC 2798's `inetOrgPerson` object class, so writing one of them to an entry that doesn't carry `inetOrgPerson` fails loudly with LDAP result code 65 ("Object Class Violation") — an atomic, clearly-signaled failure with no partial write, not a silent no-op.
  * **Custom attributes:** `custom_attributes` maps arbitrary raw LDAP attribute names to values; an empty value clears the attribute (unlike the named fields above, where empty just means "not supplied"). Keys are used **verbatim** as attribute names — the named-field mapping above applies only to the named arguments, never to `custom_attributes`. `{"user_id": "x"}` therefore writes an attribute literally named `user_id`; it does not write `uid`. A name the directory does not define is refused by the server, and the result code depends on the implementation: OpenLDAP returns 17 ("Undefined Attribute Type"), ApacheDS returns 16 ("No Such Attribute"). The same goes for baton profile field names such as `login` and `path`: they are attempted as literal attribute names rather than skipped.
  * **Collisions:** a `custom_attributes` key is dropped (never merged with, or overwriting, a named field's slot) and reported once in `skipped` when it case-insensitively matches either one of the four named field names, or the LDAP attribute a supplied named field is writing (`givenName`, `sn`, `displayName`, `mail`). The latter only applies when that named field was actually supplied and non-empty; otherwise `{"givenName": "Jane"}` is an ordinary raw write.
  * **Not modifiable:** password attributes (`userPassword`, or any name containing `password` — use credential rotation instead) and `objectClass` are rejected; the user's RDN attribute (for example `cn` when the DN is `cn=jdoe,...`) is skipped, since renaming requires a different operation.
  * **Multi-valued attributes:** setting (not clearing) a value on an attribute that currently holds more than one value returns an error instead of silently discarding the extra values; clearing (an empty value) still removes all values.
  * **Value types:** `custom_attributes` carries one string per attribute, so binary attributes (`jpegPhoto`, `userCertificate;binary`) and option-tagged attributes (`;lang-xx`) cannot be set through this action.
  * **Scope:** only entries within the configured user search scope (`user-search-dn`, falling back to `base-dn`) can be modified; out-of-scope or non-user DNs are rejected.

  The connector's bind account must have permission to modify the target entry.
</Note>

## Gather LDAP credentials

Configuring the connector requires you to pass in credentials for LDAP. Gather these credentials before you move on.

Here's the set of credentials you'll need when setting up the connector:

* The username and password of an LDAP account
* URL of the LDAP server, which can use either `ldap:` or `ldaps:` schemes, and optionally includes a port number

**Done.** Next, move on to the connector configuration instructions.

## Configure the LDAP connector

<Warning>
  To complete this task, you'll need:

  * The **Connector Administrator** or **Super Administrator** role in C1
  * Access to the set of LDAP credentials generated by following the instructions above
</Warning>

<Tabs>
  <Tab title="Cloud-hosted">
    **Follow these instructions to use a built-in, no-code connector hosted by C1.**

    *Cloud-hosted connector not currently available.*
  </Tab>

  <Tab title="Self-hosted">
    **Follow these instructions to use the LDAP connector, hosted and run in your own environment.**

    When running in service mode on Kubernetes, a self-hosted connector maintains an ongoing connection with C1, automatically syncing and uploading data at regular intervals. This data is immediately available in the C1 UI for access reviews and access requests.

    ### Resources

    * [Official download center](https://dist.conductorone.com/ConductorOne/baton-ldap): For stable binaries (Windows/Linux/macOS) and container images.

    * [GitHub repository](https://github.com/conductorone/baton-ldap): Access the source code, report issues, or contribute to the project.

    ### Step 1: Set up a new LDAP connector

    <Steps>
      <Step>
        In C1, navigate to **Apps** > **Connectors** > **Add connector**.
      </Step>

      <Step>
        Search for **Baton** and click **Add**.
      </Step>

      <Step>
        Choose where to add the connector: **Create a new app**, or **Add to an existing app** (then select the app).

        If you're creating a new app, choose whether to link it to an application discovered from your identity provider: select **Yes** and pick the IdP application, or **No** to continue with just the connector.
      </Step>

      <Step>
        Set the connector's **Name** and, optionally, a **Description**.
      </Step>

      <Step>
        Click the pencil icon next to **Owners** to choose who can configure and manage this connector.
      </Step>

      <Step>
        Click **Add**. The connector is created and its configuration page opens.
      </Step>

      <Step>
        In the **Settings** area of the page, click **Edit**.
      </Step>

      <Step>
        Click **Rotate** to generate a new Client ID and Secret.

        Carefully copy and save these credentials. We'll use them in Step 2.
      </Step>
    </Steps>

    ### Step 2: Create Kubernetes configuration files

    Create two Kubernetes manifest files for your LDAP connector deployment:

    #### Secrets configuration

    ```yaml expandable theme={null}
    # baton-ldap-secrets.yaml
    apiVersion: v1
    kind: Secret
    metadata:
      name: baton-ldap-secrets
    type: Opaque
    stringData:
      # C1 credentials
      BATON_CLIENT_ID: <C1 client ID>
      BATON_CLIENT_SECRET: <C1 client secret>
      
      # LDAP credentials
      BATON_BIND_DN: <Username to bind to the LDAP server with>
      BATON_PASSWORD: <Password to bind to the LDAP server with>
      BATON_URL: <URL to the LDAP server, optionally including port number>

      # Optional: include if you want C1 to provision access using this connector
      BATON_PROVISIONING: true
    ```

    See the connector's README or run `--help` to see all available configuration flags and environment variables.

    #### Deployment configuration

    ```yaml expandable theme={null}
    # baton-ldap.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: baton-ldap
      labels:
        app: baton-ldap
    spec:
      selector:
        matchLabels:
          app: baton-ldap
      template:
        metadata:
          labels:
            app: baton-ldap
            baton: true
            baton-app: ldap
        spec:
          containers:
          - name: baton-ldap
            image: public.ecr.aws/conductorone/baton-ldap:latest
            imagePullPolicy: IfNotPresent
            env:
            - name: BATON_HOST_ID
              value: baton-ldap
            envFrom:
            - secretRef:
                name: baton-ldap-secrets
    ```

    ### Step 3: Deploy the connector

    <Steps>
      <Step>
        Create a namespace in which to run C1 connectors (if desired), then apply the secret config and deployment config files.
      </Step>

      <Step>
        Check that the connector data uploaded correctly. In C1, click **Apps**. On the **Managed apps** tab, locate and click the name of the application you added the LDAP connector to. LDAP data should be found on the **Entitlements** and **Accounts** tabs.
      </Step>
    </Steps>

    **Done.** Your LDAP connector is now pulling access data into C1.
  </Tab>
</Tabs>
