> ## Documentation Index
> Fetch the complete documentation index at: https://astronomer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Configure LDAP authentication

Astro Private Cloud can authenticate users directly against an LDAP directory server, so people who already have accounts in your Active Directory (AD) or OpenLDAP directory can sign in without an intermediate identity provider. When you enable LDAP, Houston binds to the directory on each sign-in, verifies the user's credentials, provisions the account on first sign-in, and optionally maps LDAP groups to Astro Private Cloud Teams and system roles.

Choose LDAP when your organization runs an on-premises AD or OpenLDAP directory that can't or shouldn't be exposed through an OIDC bridge, and when you want the simplest possible path from a directory account to a working Astro Private Cloud user.

<Note>
  **Astro Private Cloud 2.1**

  This feature was introduced in Astro Private Cloud 2.1. To access this feature, upgrade your Astro Private Cloud installation to 2.1 or later.
</Note>

## Overview

LDAP authentication in Astro Private Cloud gives you:

* Username and password sign-in that goes directly to your directory. Houston never stores or caches the password.
* Group-to-team reconciliation. Users' directory groups can be turned into Astro Private Cloud Teams so team membership tracks the directory.
* Group-to-system-role assignment. Directory groups can grant `SYSTEM_ADMIN`, `SYSTEM_EDITOR`, or `SYSTEM_VIEWER` roles on Astro Private Cloud.
* Multiple group-resolution strategies to match how your directory represents membership. Houston supports four modes — direct `memberOf`, AD nested groups, sub-scoped search, and client-side recursive walk.

Supported directories: Active Directory Domain Services (AD DS), Microsoft Entra Domain Services, and OpenLDAP. Any RFC 4511 LDAP server works for basic authentication; nested-group behavior depends on which resolution mode the server supports.

<Info>The Astro CLI doesn't accept LDAP credentials through its username and password prompt. LDAP users obtain an OAuth token by signing in to the Astro Private Cloud UI and paste it into `astro login`. See [Sign in to the Astro CLI](/docs/astro-private-cloud/v-2-x/log-in-to-private-cloud#sign-in-to-the-astro-cli) for the exact CLI flow.</Info>

## Prerequisites

Before you enable LDAP, confirm:

* The LDAP or LDAPS host is reachable from the Astro Private Cloud control plane. Houston makes outbound TCP connections to the host and port defined by `auth.ldap.host` and `auth.ldap.port`.
* You have a service account in the directory with permission to search under the configured `searchBase`. Houston uses this account for the initial bind (the "service bind"). It doesn't need to modify entries — search-only permission is enough.
* You know the base distinguished name (DN) of your directory and the DN structure for users and groups. You use these when you set `bindDn`, `searchBase`, and the `groups.*` fields.
* You have write access to the Astro Private Cloud `values.yaml` file and can apply Helm-values changes. See [Apply a config change](/docs/astro-private-cloud/v-2-x/apply-platform-config).

## Configure the APC API through Helm values

Add an `auth.ldap` block to your `astronomer.houston.config` section in `values.yaml`. The following example shows every field you can set, with production-safe defaults. Field-by-field details for TLS, attribute mapping, group resolution, team reconciliation, and system-role assignment follow in later sections.

```yaml wrap theme={null}
astronomer:
  houston:
    # Uncomment this block to source bindCredentials from a Kubernetes Secret
    # (recommended for production). See the "Secure bindCredentials with a
    # Kubernetes Secret" section of this document.
    # secret:
    #   - envName: "AUTH__LDAP__BIND_CREDENTIALS"
    #     secretName: "houston-ldap-bind"
    #     secretKey: "password"
    config:
      auth:
        ldap:
          enabled: true
          host: ldap.corp.example.com
          # port is optional. If unset, Houston derives it from tls.mode
          # (636 for ldaps, 389 for none and starttls).
          # port: 636
          tls:
            mode: ldaps          # one of: none, starttls, ldaps
            verifyServerCert: true
          bindDn: "cn=houston-svc,ou=svc,dc=corp,dc=example,dc=com"
          # For dev and test only. In production, source this from a Kubernetes
          # Secret through the astronomer.houston.secret block above.
          bindCredentials: "plaintext-only-for-dev"
          searchBase: "ou=users,dc=corp,dc=example,dc=com"
          searchFilter: "(uid={{username}})"
          attributes:
            email: mail
            name: cn
          groups:
            enabled: true
            reconcileTeams: true
            nestedGroups: false          # or "ad", "recursive", "search"
            searchBase: "ou=users,dc=corp,dc=example,dc=com"
            searchFilter: "(member={{dn}})"
            nameAttribute: cn
            maxDepth: 10
            teamFilterRegex: ""
            manageSystemPermissions:
              enabled: false
              systemAdmin: []
              systemEditor: []
              systemViewer: []
```

After you save `values.yaml`, apply the change to your platform through the standard Helm upgrade flow. See [Apply a config change](/docs/astro-private-cloud/v-2-x/apply-platform-config).

## Secure `bindCredentials` with a Kubernetes Secret

The `bindCredentials` field accepts an inline value, but that inline value ends up on disk in `values.yaml` and in Helm release history. For production installs, source it from a Kubernetes Secret instead.

Astro Private Cloud uses the same `astronomer.houston.secret[]` mechanism that the OIDC guide uses for `clientSecret` and that the install guide uses for `EMAIL__SMTP_URL`. Each entry in the list becomes a `valueFrom.secretKeyRef` environment variable on the Houston pod, and Houston reads the LDAP bind password from that environment variable rather than from the config file.

<Steps>
  <Step title="Create a Kubernetes secret">
    ```bash wrap theme={null}
    kubectl create secret generic houston-ldap-bind \
      --from-literal=password='<the-bind-password>' \
      -n astronomer
    ```

    Replace `<the-bind-password>` with the password for the service account named in `bindDn`.
  </Step>

  <Step title="Reference the secret from values.yaml">
    Add the `secret:` block under `astronomer.houston` and remove the inline `bindCredentials` line:

    ```yaml wrap theme={null}
    astronomer:
      houston:
        secret:
          - envName: "AUTH__LDAP__BIND_CREDENTIALS"
            secretName: "houston-ldap-bind"
            secretKey: "password"
        config:
          auth:
            ldap:
              enabled: true
              host: ldap.corp.example.com
              bindDn: "cn=houston-svc,ou=svc,dc=corp,dc=example,dc=com"
              # bindCredentials is now sourced from AUTH__LDAP__BIND_CREDENTIALS,
              # which the Secret block above wires up. Omit the inline value.
              searchBase: "ou=users,dc=corp,dc=example,dc=com"
              searchFilter: "(uid={{username}})"
    ```

    The environment variable takes precedence when both an inline value and a `secretKeyRef` are present. `secretKey` is optional and defaults to `value`. Apply the change with `helm upgrade` and Houston restarts with the new environment variable bound.
  </Step>
</Steps>

<Tip>The same `astronomer.houston.secret[]` block already carries OIDC client secrets and the SMTP connection string. If you already use one of those, add the LDAP entry as another list item rather than creating a second block.</Tip>

## Configure TLS

LDAP transports credentials over the network. Any deployment outside a fully isolated test environment must use TLS. Houston supports three transport modes through `auth.ldap.tls.mode`.

| Mode       | Wire behavior                                                                              | Default port | When to use                                                                       |
| ---------- | ------------------------------------------------------------------------------------------ | ------------ | --------------------------------------------------------------------------------- |
| `none`     | Plain LDAP on 389, no encryption at any point.                                             | 389          | Isolated test environments only. Don't use in production.                         |
| `starttls` | Plain TCP on 389, upgraded to TLS through the StartTLS extended operation before any bind. | 389          | AD deployments that only expose 389 and require encryption before authentication. |
| `ldaps`    | TLS-wrapped from the first byte on 636.                                                    | 636          | Any deployment where 636 is reachable. Simplest, most common.                     |

Set `auth.ldap.tls.verifyServerCert: true` (the default) to have Houston validate the LDAP server's certificate chain against its trusted CAs. This applies to both `starttls` and `ldaps` modes. Set it to `false` only for dev environments where the server presents a self-signed certificate that isn't in Houston's trust store.

If your LDAP server presents a certificate signed by a private certificate authority, add the CA certificate to Houston's trust store through the platform configuration and keep `verifyServerCert: true`. See [Configure private CAs](/docs/astro-private-cloud/v-2-x/configure-private-cas).

`auth.ldap.port` is optional. When unset, Houston derives it from `tls.mode`: 636 for `ldaps`, 389 for `none` and `starttls`. Set the field explicitly only when your directory listens on a non-standard port.

## Attribute mapping

Houston reads two attributes from each user's LDAP entry to build the corresponding Astro Private Cloud user account:

* `attributes.email` (default `mail`) — the value used as the user's email address in Astro Private Cloud. Houston converts the value to lowercase before it stores the user. If the attribute is multi-valued in your directory, Houston takes the first value.
* `attributes.name` (default `cn`) — the value used as the user's full name in Astro Private Cloud.

For AD deployments, `displayName` produces a friendlier full name than `cn`:

```yaml wrap theme={null}
attributes:
  email: mail
  name: displayName
```

### Group name attribute

By default, when Houston resolves groups through the direct `memberOf` mode, it takes the group's name from the leftmost relative distinguished name (RDN) of the group's DN. For example, a `memberOf: cn=engineering,ou=groups,dc=example,dc=com` value produces a group name of `engineering`.

To use a different attribute for the group name — such as `description` — set `groups.nameAttribute`:

```yaml wrap theme={null}
groups:
  enabled: true
  nestedGroups: false
  nameAttribute: description
```

When `nameAttribute` is anything other than `cn`, Houston fetches each group's entry individually to read the configured attribute. This is one additional LDAP query per group, so the direct-`memberOf` mode is slower with a custom `nameAttribute`. The default `nameAttribute: cn` skips the per-group fetch and stays on the fast path.

## Configure group resolution

If you enable `groups.enabled: true`, Houston resolves the user's directory groups on each sign-in. Houston supports four resolution strategies through `groups.nestedGroups`.

| Mode              | `nestedGroups`    | How Houston finds groups                                                                                                                                   | Directory requirement                                                    | Cost per sign-in                                                  |
| ----------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| Direct `memberOf` | `false` (default) | Reads the `memberOf` attribute on the user's entry.                                                                                                        | AD, or OpenLDAP with the `memberof` overlay populated.                   | One round-trip, plus one per group if `nameAttribute` isn't `cn`. |
| Nested groups     | `"ad"`            | Runs a single sub-scoped search under `groups.searchBase` using AD's `LDAP_MATCHING_RULE_IN_CHAIN` operator (object identifier `1.2.840.113556.1.4.1941`). | Active Directory. The rule is AD-specific.                               | One round-trip regardless of nesting depth.                       |
| Search            | `"search"`        | Runs a sub-scoped search under `groups.searchBase` using `groups.searchFilter`, substituting the user's DN into every `{{dn}}` placeholder.                | Any RFC 4511 LDAP server.                                                | One round-trip.                                                   |
| Recursive         | `"recursive"`     | Walks the user's `memberOf` attribute, then each group's `memberOf`, up to `groups.maxDepth`.                                                              | Non-AD directory where `memberOf` is populated on both users and groups. | One round-trip per level, up to `maxDepth`.                       |

Choose the mode that matches how your directory represents nested membership. Every mode returns the same shape of result — a flat list of group names — so downstream reconciliation and role assignment don't depend on which mode you pick.

### Direct `memberOf` mode

The default. Use for Active Directory (where `memberOf` is native) or for OpenLDAP with the `memberof` overlay populated.

```yaml wrap theme={null}
groups:
  enabled: true
  reconcileTeams: true
  nestedGroups: false
```

Verify that `memberOf` is populated on your user entry:

```bash wrap theme={null}
ldapsearch -x -H ldap://ldap.corp.example.com -D "cn=houston-svc,..." -w "..." \
  -b "ou=users,dc=corp,dc=example,dc=com" \
  "(uid=alice)" memberOf
```

The response must include one `memberOf:` line per group the user belongs to. If it's missing, either enable the `memberof` overlay on your directory or switch to `nestedGroups: "search"`.

### Nested groups mode

Use for Active Directory when you need transitive group membership — for example, when your users are direct members of a role group that is nested inside a broader access group and you want Houston to see both.

```yaml wrap theme={null}
groups:
  enabled: true
  reconcileTeams: true
  nestedGroups: "ad"
  searchBase: "cn=Users,dc=corp,dc=example,dc=com"
```

This mode requires `groups.searchBase`. Houston constructs the transitive query internally; you don't need to configure the matching rule OID.

Verify with `ldapsearch`:

```bash wrap theme={null}
ldapsearch -x -H ldaps://ad.corp.example.com:636 -D "cn=houston-svc,..." -w "..." \
  -b "cn=Users,dc=corp,dc=example,dc=com" \
  "(member:1.2.840.113556.1.4.1941:=CN=alice,CN=Users,DC=corp,DC=example,DC=com)" cn
```

The response includes every group the user reaches through direct or nested membership.

### Search mode

Use when your directory doesn't populate `memberOf` but does index group `member` entries.

```yaml wrap theme={null}
groups:
  enabled: true
  reconcileTeams: true
  nestedGroups: "search"
  searchBase: "ou=groups,dc=corp,dc=example,dc=com"
  searchFilter: "(member={{dn}})"
```

`groups.searchBase` and `groups.searchFilter` are both required. Houston substitutes `{{dn}}` with the signed-in user's DN before it runs the search. Multiple `{{dn}}` placeholders are supported — for example, to match either `member` or `uniqueMember`:

```yaml wrap theme={null}
searchFilter: "(|(member={{dn}})(uniqueMember={{dn}}))"
```

Houston escapes the user's DN according to [RFC 4515](https://datatracker.ietf.org/doc/html/rfc4515) before substitution. DN values that contain filter special characters (`*`, `(`, `)`, `\`, `NUL`) are handled safely.

### Recursive mode

Use for non-AD directories where `memberOf` is populated on user entries and on group entries, and where you need transitive group traversal.

```yaml wrap theme={null}
groups:
  enabled: true
  reconcileTeams: true
  nestedGroups: "recursive"
  maxDepth: 10
```

`groups.maxDepth` caps the walk. Houston stops walking once it reaches the configured depth, even if more parent groups exist above that point. The default of `10` is enough for most nesting patterns.

Recursive mode starts from the user's `memberOf` attribute and walks up the group hierarchy. If your OpenLDAP directory doesn't populate `memberOf`, enable the `memberof` overlay on the directory server, or switch to `nestedGroups: "search"`, which reads group membership from group entries instead.

## Reconcile groups into Astro Private Cloud Teams

When `groups.reconcileTeams: true`, every resolved group becomes an Astro Private Cloud Team, and the signed-in user is added to the corresponding team.

```yaml wrap theme={null}
groups:
  enabled: true
  reconcileTeams: true
```

Teams that Houston creates through LDAP reconciliation are tagged with `provider='ldap'` in the Houston database. They don't collide with Teams from OIDC providers or Teams created manually — the two provider spaces are independent. For the extended cross-provider details, see [Import identity provider (IdP) groups](/docs/astro-private-cloud/v-2-x/import-idp-groups).

### Filter which groups become Teams

`groups.teamFilterRegex` restricts which directory groups become Teams. Only groups whose name matches the regular expression are reconciled; the rest are ignored.

```yaml wrap theme={null}
groups:
  reconcileTeams: true
  teamFilterRegex: "^astro-"
```

The regex is JavaScript syntax and is applied case-sensitively. It runs after group resolution completes, so it filters the exact names Houston sees — for direct-`memberOf` mode, those are the CN components of each `memberOf` DN. Only LDAP groups are affected; OIDC has its own separate filter.

## Assign system roles from directory groups

Houston can grant `SYSTEM_ADMIN`, `SYSTEM_EDITOR`, or `SYSTEM_VIEWER` platform roles to Teams that come from specific directory groups.

```yaml wrap theme={null}
groups:
  enabled: true
  reconcileTeams: true
  manageSystemPermissions:
    enabled: true
    systemAdmin: ["astro-platform-admins"]
    systemEditor: ["astro-workspace-editors"]
    systemViewer: ["astro-observers"]
```

Each list holds group names that map to that system role. Houston normalizes DN values in these lists to their CN components before matching, so you can list either short names (`astro-platform-admins`) or full DN values (`cn=astro-platform-admins,ou=groups,dc=corp,dc=example,dc=com`).

Priority is `SYSTEM_ADMIN` > `SYSTEM_EDITOR` > `SYSTEM_VIEWER`. If the same group appears in more than one list, the highest role wins. Roles apply to the Team that Houston creates from the LDAP group — individual users inherit the role through their team membership rather than getting a direct role binding.

Houston reconciles system roles on every sign-in. When a user is removed from a role-mapping group in the directory, they lose the corresponding system role the next time they sign in.

<Warning>
  Setting `manageSystemPermissions.enabled: false` stops future synchronization from directory groups but doesn't revoke roles that were previously auto-assigned. Existing system role assignments remain in place until you remove them manually in the Astro Private Cloud UI, in **Settings** > **Teams**. The OIDC equivalent (`auth.openidConnect.manageSystemPermissionsViaIdpGroups.enabled`) behaves the same way.
</Warning>

## Security guards

Houston enforces the following guards on every LDAP sign-in.

### Service and user bind are separate

Houston uses `bindDn` and `bindCredentials` only for the search that resolves the user's DN. The actual authentication is a second bind that Houston performs with the user's own DN and the password they submitted. The service account never authenticates users, and user passwords never travel outside the sign-in request.

### LDAP doesn't auto-link to existing accounts

If a user with the same email already exists on the platform through local auth or an OIDC provider, LDAP sign-in for that email is rejected. Linking an existing account to LDAP requires an explicit `OAuthCredential(provider='ldap', ...)` row. See [Sign-in migration paths](#sign-in-migration-paths).

### Deactivated users can't sign in

Only Astro Private Cloud users in the `ACTIVE` or `PENDING` status can complete an LDAP sign-in. Deactivating a user through Houston blocks their LDAP sign-in even if their directory account remains active.

### Last-admin protection

Houston prevents removing the last user with the `SYSTEM_ADMIN` role. This applies to system-role reconciliation as well as manual role changes.

### Error messages don't distinguish failure modes

A failed bind produces a generic "Invalid username or password" message regardless of whether the user doesn't exist, the password is wrong, or the account is deactivated. This prevents an attacker from probing the directory for valid usernames.

### Passwords are never stored or cached

Houston uses the user's password only during the sign-in bind. It doesn't persist the password to the database, log it, or hold it in memory beyond the request.

### Group-resolution failure isn't sign-in failure

If group resolution returns an error or no groups, Houston still signs the user in. Their teams and system roles aren't updated, but they can use the platform with whatever team and role state they already have. See [Diagnose configuration issues](#diagnose-configuration-issues) for the log lines that surface this case.

### LDAP filters escape user DN values

When Houston builds a filter that includes the user's DN — for example, `(member={{dn}})` in search mode — it escapes filter special characters (`*`, `(`, `)`, `\`, `NUL`) to their `\XX` hex-pair form according to RFC 4515. A crafted DN can't alter the semantics of the query.

## Diagnose configuration issues

Houston emits warn-level log lines that surface two silent-failure classes operators otherwise miss.

### Invalid `nestedGroups` value

If `groups.nestedGroups` holds anything other than `false`, `"ad"`, `"recursive"`, or `"search"` — for example the boolean `true`, an integer, or a typo like `"recursvie"` — Houston logs a warn identifying the invalid value and falls back to direct-`memberOf` mode. Example:

```text wrap theme={null}
Invalid auth.ldap.groups.nestedGroups value: true. Valid values are false, "ad", "recursive", "search". Falling back to direct-memberOf mode.
```

Sign-in continues to work under the fallback, but the mode you configured is silently overridden. Search the Houston pod logs for `Invalid auth.ldap.groups.nestedGroups` after a config change to confirm your value was accepted.

### Empty group resolution when reconciliation is expected

If you set `groups.reconcileTeams: true` or `groups.manageSystemPermissions.enabled: true` but group resolution returned zero groups for the signed-in user, Houston logs a warn that includes the user's DN and a mode-specific mitigation hint. Example in direct-`memberOf` mode:

```text wrap theme={null}
LDAP group resolution returned no groups for user cn=alice,ou=users,dc=corp,dc=example,dc=com — configured team reconciliation / role assignment cannot proceed. nestedGroups=false; check that memberOf is populated on the user's LDAP entry, or set nestedGroups='recursive', 'ad', or 'search' depending on your directory.
```

The hint text is mode-aware. In search mode, the hint suggests checking `searchBase` and `searchFilter`. In recursive mode, the hint points at the `memberof` overlay.

Filter the Houston pod logs for `LDAP group resolution returned no groups` when a user reports that their teams or system roles aren't being applied.

## Sign-in migration paths

When both local auth and LDAP are enabled on the same platform, Astro Private Cloud user accounts aren't automatically linked across providers. This is intentional — the account-takeover guard requires an explicit `OAuthCredential(provider='ldap', ...)` row before an LDAP sign-in can attach to an existing user. Choose one of three operator-driven paths to migrate.

### Backfill

Recommended for platforms with a working user base you want to preserve. For each existing user who is moving to LDAP, insert an `OAuthCredential(provider='ldap', oauthUserId=<identity>)` row where `<identity>` is the value that matches `auth.ldap.searchFilter` for that user (typically their `mail` or `uid`).

Backfill preserves `User.id`, existing team memberships, role bindings, and the audit trail across the migration. Users notice no change other than the sign-in flow itself.

### Re-create

Clean-slate path. The Astro Private Cloud administrator deletes existing `User` rows for the affected users; the deletion cascades through `Email`, `OAuthCredential`, `RoleBinding`, and `_TeamToUser` in the Houston database. Users then sign in through LDAP as new accounts.

Re-creation is simpler operationally but every affected user receives a new `User.id`, and existing audit entries in your security information and event management (SIEM) system continue to reference the old ID. Choose this path only if audit continuity across the migration isn't a requirement.

### Coexistence

Keep both providers active with no migration. Each user keeps whichever credential they were created with — local users continue to sign in with their email and password, LDAP users sign in through the directory. Astro Private Cloud doesn't attempt to unify accounts even when the emails match.

<Info>A self-serve, in-platform "link my LDAP account to this Astro Private Cloud user" flow isn't part of Astro Private Cloud 2.1.0. The three paths in the preceding section are the supported options.</Info>

## Verify the setup

After you apply the LDAP configuration and Houston restarts, verify end-to-end sign-in.

<Steps>
  <Step title="Sign in through the UI">
    Open the Astro Private Cloud UI and sign in with a directory user's credentials. Successful sign-in indicates that Houston can reach the directory, the service bind succeeded, the user search filter matched, and the user's bind succeeded.
  </Step>

  <Step title="Confirm the database state">
    For a successful first sign-in, Houston writes:

    * One row in `User` with `status = 'active'`
    * One row in `Email` for the user's directory email
    * One row in `OAuthCredential` with `provider = 'ldap'`
    * One row per resolved LDAP group in `Team` with `provider = 'ldap'` (when you enable `reconcileTeams`)
    * One `RoleBinding` per Team-to-system-role mapping (when you enable `manageSystemPermissions`)

    You can confirm from a Houston pod with `psql`:

    ```sql wrap theme={null}
    SELECT u.id, u.username, u."fullName", u.status, oc.provider
    FROM "User" u
    JOIN "OAuthCredential" oc ON oc."userId" = u.id
    WHERE oc.provider = 'ldap'
      AND u.username = 'alice@corp.example.com';
    ```
  </Step>

  <Step title="Verify the TLS mode on the wire">
    The LDAP server logs show which TLS transport Houston used:

    * `mode: none` — plain `BIND` lines with `ssf=0`.
    * `mode: starttls` — an `EXT oid=1.3.6.1.4.1.1466.20037` line and `TLS established tls_ssf=N` before the bind.
    * `mode: ldaps` — `TLS established tls_ssf=N` immediately after the `ACCEPT` on port 636.

    If the server-side log shows `ssf=0` when you expected TLS, review the `tls.mode` value and confirm the client and server ports agree.
  </Step>
</Steps>

## Audit logging

Every LDAP sign-in attempt produces an audit record with `action: auth.login`, identical in shape to sign-ins through local auth and OIDC. Existing SIEM rules that filter on `auth.login` catch LDAP sign-ins uniformly.

The record's `entity.identity` field holds the LDAP username submitted at sign-in, and the password field is redacted by the platform's existing redaction rules. Failed binds produce records with `outcome: failure` and the generic sign-in error message. Underlying cert or network errors go to the Houston pod log, not the audit record.

## Limitations

* The Astro CLI doesn't accept LDAP credentials through its username and password prompt. LDAP users authenticate to the CLI by signing in to the Astro Private Cloud UI, retrieving an OAuth token, and pasting it into `astro login`. See [Sign in to the Astro CLI](/docs/astro-private-cloud/v-2-x/log-in-to-private-cloud#sign-in-to-the-astro-cli) for the token flow.
* OIDC and LDAP can coexist, but accounts aren't automatically linked between providers even when the emails match. See [Sign-in migration paths](#sign-in-migration-paths).
* Airflow UI authentication is unchanged. LDAP configuration on Astro Private Cloud doesn't affect the Airflow security model. Airflow continues to use its Flask AppBuilder (FAB) authentication.
* A self-serve account-linking UI isn't available. Linking an existing Astro Private Cloud user to their LDAP identity requires the Backfill path described earlier.

## Reference: `auth.ldap` fields

| Field                                         | Environment variable                                           | Default                 | Description                                                                                                         |
| --------------------------------------------- | -------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `enabled`                                     | `AUTH__LDAP__ENABLED`                                          | `false`                 | Turn LDAP sign-in on or off.                                                                                        |
| `host`                                        | `AUTH__LDAP__HOST`                                             | —                       | LDAP or LDAPS server hostname.                                                                                      |
| `port`                                        | `AUTH__LDAP__PORT`                                             | derived from `tls.mode` | TCP port. When unset, `636` for `ldaps`, `389` otherwise.                                                           |
| `tls.mode`                                    | `AUTH__LDAP__TLS__MODE`                                        | `none`                  | One of `none`, `starttls`, `ldaps`.                                                                                 |
| `tls.verifyServerCert`                        | `AUTH__LDAP__TLS__VERIFY_SERVER_CERT`                          | `true`                  | Validate the server's TLS certificate chain when TLS is in use.                                                     |
| `bindDn`                                      | `AUTH__LDAP__BIND_DN`                                          | —                       | Service-account DN used for the user-search bind.                                                                   |
| `bindCredentials`                             | `AUTH__LDAP__BIND_CREDENTIALS`                                 | —                       | Service-account password. Source from a Kubernetes Secret in production.                                            |
| `searchBase`                                  | `AUTH__LDAP__SEARCH_BASE`                                      | —                       | Base DN under which Houston searches for user entries.                                                              |
| `searchFilter`                                | `AUTH__LDAP__SEARCH_FILTER`                                    | `(uid={{username}})`    | Filter with `{{username}}` substituted from the sign-in form.                                                       |
| `attributes.email`                            | `AUTH__LDAP__ATTRIBUTES__EMAIL`                                | `mail`                  | LDAP attribute Houston reads for the user's email.                                                                  |
| `attributes.name`                             | `AUTH__LDAP__ATTRIBUTES__NAME`                                 | `cn`                    | LDAP attribute Houston reads for the user's full name.                                                              |
| `groups.enabled`                              | `AUTH__LDAP__GROUPS__ENABLED`                                  | `false`                 | Turn group resolution on. Required for `reconcileTeams` and `manageSystemPermissions`.                              |
| `groups.reconcileTeams`                       | `AUTH__LDAP__GROUPS__RECONCILE_TEAMS`                          | `false`                 | Turn resolved LDAP groups into Astro Private Cloud Teams tagged `provider='ldap'`.                                  |
| `groups.nestedGroups`                         | `AUTH__LDAP__GROUPS__NESTED_GROUPS`                            | `false`                 | Group-resolution mode. One of `false`, `"ad"`, `"recursive"`, `"search"`.                                           |
| `groups.searchBase`                           | `AUTH__LDAP__GROUPS__SEARCH_BASE`                              | —                       | Base DN for group searches. Required for `"ad"` and `"search"` modes.                                               |
| `groups.searchFilter`                         | `AUTH__LDAP__GROUPS__SEARCH_FILTER`                            | `(member={{dn}})`       | Filter used in `"search"` mode. `{{dn}}` is RFC 4515-escaped before substitution.                                   |
| `groups.nameAttribute`                        | `AUTH__LDAP__GROUPS__NAME_ATTRIBUTE`                           | `cn`                    | Attribute Houston reads for each group's name. Non-`cn` values trigger a per-group fetch in direct-`memberOf` mode. |
| `groups.maxDepth`                             | `AUTH__LDAP__GROUPS__MAX_DEPTH`                                | `10`                    | Recursion depth cap for `"recursive"` mode.                                                                         |
| `groups.teamFilterRegex`                      | `AUTH__LDAP__GROUPS__TEAM_FILTER_REGEX`                        | —                       | Regex applied to group names; only matches become Teams.                                                            |
| `groups.manageSystemPermissions.enabled`      | `AUTH__LDAP__GROUPS__MANAGE_SYSTEM_PERMISSIONS__ENABLED`       | `false`                 | Turn group-to-system-role mapping on.                                                                               |
| `groups.manageSystemPermissions.systemAdmin`  | `AUTH__LDAP__GROUPS__MANAGE_SYSTEM_PERMISSIONS__SYSTEM_ADMIN`  | `[]`                    | Group names that grant `SYSTEM_ADMIN` on Astro Private Cloud.                                                       |
| `groups.manageSystemPermissions.systemEditor` | `AUTH__LDAP__GROUPS__MANAGE_SYSTEM_PERMISSIONS__SYSTEM_EDITOR` | `[]`                    | Group names that grant `SYSTEM_EDITOR`.                                                                             |
| `groups.manageSystemPermissions.systemViewer` | `AUTH__LDAP__GROUPS__MANAGE_SYSTEM_PERMISSIONS__SYSTEM_VIEWER` | `[]`                    | Group names that grant `SYSTEM_VIEWER`.                                                                             |
