Kubernetes Deployment

Deploying OpenLDAP on Kubernetes is technically possible — but it's a decision that deserves careful thought. LDAP is the backbone of authentication for most organizations. Putting it on Kubernetes introduces risks that don't exist with simpler deployment models.

The Hard Truth About LDAP on Kubernetes

LDAP is not a typical stateless microservice. It's a stateful, persistent, security-critical service that every other application depends on. This creates a fundamental tension with Kubernetes.

Why Teams Hesitate

LDAP is the foundation of authentication. If LDAP goes down, nobody can log in — not to Kubernetes itself, not to any application that authenticates against it. Deploying the service that controls access to your infrastructure inside that infrastructure creates a circular dependency. Stateful workloads on Kubernetes are hard. Despite StatefulSets, Kubernetes was built for stateless services. Pod rescheduling, node failures, and storage reattachment can cause data corruption if not handled carefully. The MDB database that OpenLDAP uses requires consistent storage — not storage that might disappear during a node drain. The blast radius is massive. A misconfigured Kubernetes upgrade, a storage class issue, or a pod eviction at the wrong time doesn't just break one service — it breaks authentication for everything. Multi-master replication adds complexity. OpenLDAP's syncRepl protocol expects stable network identities and persistent connections. Kubernetes pod IPs change on restart. DNS resolution can be delayed. This can cause replication conflicts or split-brain scenarios.

What the Industry Does

Most organizations that run LDAP in production keep it outside Kubernetes:
  • Dedicated VMs or bare metal with Docker / Docker Compose
  • Managed directory services (AWS Directory Service, Azure AD DS)
  • Standalone containers on long-lived hosts
The applications inside Kubernetes connect to LDAP via an external service endpoint. This is the most common and safest pattern.

When Kubernetes Makes Sense

There are legitimate reasons to run LDAP on Kubernetes:
  • All-in-one clusters where everything runs on K8s and no external infrastructure is available
  • Development and staging environments that mirror production topology
  • Edge deployments where a single K8s cluster is the only compute available
  • Teams with strong K8s expertise who understand StatefulSet operations deeply
If you decide to proceed, the deployment below is production-hardened.

StatefulSet Deployment

Namespace

apiVersion: v1
kind: Namespace
metadata:
  name: ldap

Secret

apiVersion: v1
kind: Secret
metadata:
  name: openldap-credentials
  namespace: ldap
type: Opaque
stringData:
  LDAP_ADMIN_PASSWORD: "changeme"
  LDAP_CONFIG_PASSWORD: "changeme"
🔒

Security: Use a secrets manager (Vault, Sealed Secrets, External Secrets Operator) instead of plain manifests in production.

StatefulSet — Single Node

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: openldap
  namespace: ldap
spec:
  serviceName: openldap
  replicas: 1
  selector:
    matchLabels:
      app: openldap
  template:
    metadata:
      labels:
        app: openldap
    spec:
      containers:
        - name: openldap
          image: ghcr.io/vibhuvioio/openldap:latest
          ports:
            - containerPort: 389
              name: ldap
            - containerPort: 636
              name: ldaps
          env:
            - name: LDAP_DOMAIN
              value: "company.com"
            - name: LDAP_ORGANIZATION
              value: "Company Inc"
            - name: INCLUDE_SCHEMAS
              value: "cosine,inetorgperson,nis"
            - name: ENABLE_MONITORING
              value: "true"
          envFrom:
            - secretRef:
                name: openldap-credentials
          volumeMounts:
            - name: ldap-data
              mountPath: /var/lib/ldap
            - name: ldap-config
              mountPath: /etc/openldap/slapd.d
          livenessProbe:
            exec:
              command:
                - ldapsearch
                - "-x"
                - "-H"
                - "ldap://localhost:389"
                - "-b"
                - ""
                - "-s"
                - "base"
            initialDelaySeconds: 30
            periodSeconds: 30
            timeoutSeconds: 5
          readinessProbe:
            exec:
              command:
                - ldapsearch
                - "-x"
                - "-H"
                - "ldap://localhost:389"
                - "-b"
                - ""
                - "-s"
                - "base"
            initialDelaySeconds: 15
            periodSeconds: 10
            timeoutSeconds: 5
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "1000m"
  volumeClaimTemplates:
    - metadata:
        name: ldap-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard
        resources:
          requests:
            storage: 1Gi
    - metadata:
        name: ldap-config
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard
        resources:
          requests:
            storage: 100Mi

Service

apiVersion: v1
kind: Service
metadata:
  name: openldap
  namespace: ldap
spec:
  type: ClusterIP
  selector:
    app: openldap
  ports:
    - name: ldap
      port: 389
      targetPort: 389
    - name: ldaps
      port: 636
      targetPort: 636

Applications inside the cluster connect to openldap.ldap.svc.cluster.local:389.

External Access (Optional)

To expose LDAP outside the cluster:

apiVersion: v1
kind: Service
metadata:
  name: openldap-external
  namespace: ldap
spec:
  type: NodePort
  selector:
    app: openldap
  ports:
    - name: ldap
      port: 389
      targetPort: 389
      nodePort: 30389
⚠️

Warning: Exposing LDAP externally without TLS sends passwords in plaintext. Always enable TLS for external access.

Multi-Master on Kubernetes

Running multi-master replication on Kubernetes requires a headless service for stable DNS names:

apiVersion: v1
kind: Service
metadata:
  name: openldap-headless
  namespace: ldap
spec:
  clusterIP: None
  selector:
    app: openldap-cluster
  ports:
    - name: ldap
      port: 389

Each pod gets a stable DNS name: openldap-cluster-0.openldap-headless.ldap.svc.cluster.local.

The StatefulSet for multi-master needs each pod to know its SERVER_ID and peers. This typically requires an init container or startup script that derives the server ID from the pod ordinal:
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: openldap-cluster
  namespace: ldap
spec:
  serviceName: openldap-headless
  replicas: 3
  selector:
    matchLabels:
      app: openldap-cluster
  template:
    metadata:
      labels:
        app: openldap-cluster
    spec:
      containers:
        - name: openldap
          image: ghcr.io/vibhuvioio/openldap:latest
          ports:
            - containerPort: 389
          env:
            - name: LDAP_DOMAIN
              value: "company.com"
            - name: LDAP_ORGANIZATION
              value: "Company Inc"
            - name: INCLUDE_SCHEMAS
              value: "cosine,inetorgperson,nis"
            - name: ENABLE_REPLICATION
              value: "true"
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          envFrom:
            - secretRef:
                name: openldap-credentials
          command:
            - /bin/sh
            - -c
            - |
              # Derive SERVER_ID from pod ordinal (openldap-cluster-0 → 1)
              ORDINAL=${POD_NAME##*-}
              export SERVER_ID=$((ORDINAL + 1))
              
              # Build peer list excluding self
              PEERS=""
              for i in 0 1 2; do
                if [ "$i" != "$ORDINAL" ]; then
                  PEER="openldap-cluster-${i}.openldap-headless.ldap.svc.cluster.local"
                  PEERS="${PEERS:+${PEERS},}${PEER}"
                fi
              done
              export REPLICATION_PEERS="$PEERS"
              
              # Start OpenLDAP
              exec /docker-entrypoint.sh
          volumeMounts:
            - name: ldap-data
              mountPath: /var/lib/ldap
            - name: ldap-config
              mountPath: /etc/openldap/slapd.d
          livenessProbe:
            exec:
              command: ["ldapsearch", "-x", "-H", "ldap://localhost:389", "-b", "", "-s", "base"]
            initialDelaySeconds: 45
            periodSeconds: 30
          readinessProbe:
            exec:
              command: ["ldapsearch", "-x", "-H", "ldap://localhost:389", "-b", "", "-s", "base"]
            initialDelaySeconds: 30
            periodSeconds: 10
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "1000m"
  volumeClaimTemplates:
    - metadata:
        name: ldap-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard
        resources:
          requests:
            storage: 1Gi
    - metadata:
        name: ldap-config
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard
        resources:
          requests:
            storage: 100Mi

Known Risks on Kubernetes

RiskImpactMitigation
Pod evictionLDAP unavailable during rescheduleUse PodDisruptionBudget with minAvailable: 2
Storage lossData corruption or lossUse reliable StorageClass with reclaimPolicy: Retain
DNS delayReplication peers can't find each otherUse headless service, increase retry intervals
Node drainAll LDAP pods may land on same nodeUse podAntiAffinity to spread across nodes
Circular dependencyK8s can't authenticate if LDAP is downConsider external LDAP or bootstrap credentials

Pod Disruption Budget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: openldap-pdb
  namespace: ldap
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: openldap-cluster

Pod Anti-Affinity

Add to the StatefulSet pod spec to spread pods across nodes:

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: openldap-cluster
        topologyKey: kubernetes.io/hostname
For most organizations, the safest approach is:
  1. 1 Run LDAP outside Kubernetes — Docker Compose on dedicated hosts
  2. 2 Create an ExternalName Service in Kubernetes pointing to the LDAP host
  3. 3 Applications inside K8s connect via the service name
apiVersion: v1
kind: Service
metadata:
  name: openldap
  namespace: ldap
spec:
  type: ExternalName
  externalName: ldap.internal.company.com

Or use Endpoints for IP-based external services:

apiVersion: v1
kind: Service
metadata:
  name: openldap
  namespace: ldap
spec:
  ports:
    - port: 389
---
apiVersion: v1
kind: Endpoints
metadata:
  name: openldap
  namespace: ldap
subsets:
  - addresses:
      - ip: 10.0.1.50
    ports:
      - port: 389
This gives Kubernetes workloads a stable service name (openldap.ldap.svc.cluster.local) while keeping LDAP safely outside the cluster.

Next Steps