Initial Commit

This commit is contained in:
Patrick de Ruiter 2025-12-25 12:36:39 +01:00
commit c556664056
Signed by: pderuiter
GPG Key ID: 5EBA7F21CF583321
18 changed files with 2433 additions and 0 deletions

23
.env.example Normal file
View File

@ -0,0 +1,23 @@
# Required
LDAP_DOMAIN=example.com
LDAP_ORGANISATION=Example ORG
LDAP_ADMIN_PASSWORD=Yourpasswordhere
# Optional - TLS
LDAP_TLS_ENABLED=true
LDAP_TLS_CERT_FILE=/certs/ldap.crt
LDAP_TLS_KEY_FILE=/certs/ldap.key
LDAP_TLS_CA_FILE=/certs/ca.crt
# Optional - Service accounts
LDAP_CREATE_SERVICE_ACCOUNTS=true
# If not set, random passwords will be generated and saved to /var/lib/openldap/service-passwords.txt
# LDAP_SERVICE_KEYCLOAK_PASSWORD=
# LDAP_SERVICE_NEXTCLOUD_PASSWORD=
# LDAP_SERVICE_GITEA_PASSWORD=
# LDAP_SERVICE_POSTFIX_PASSWORD=
# LDAP_SERVICE_DOVECOT_PASSWORD=
# LDAP_SERVICE_SSSD_PASSWORD=
# Optional - Logging
LDAP_LOG_LEVEL=256

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Env files usualy contain credentials, we do not want to store those in git
.env
# No Claude code stuff in here either
.claude

49
Dockerfile Normal file
View File

@ -0,0 +1,49 @@
FROM alpine:3.23
LABEL maintainer="WeBuildYourCloud"
LABEL description="Enterprise OpenLDAP container with rfc2307bis, memberOf, and custom schemas"
# Install OpenLDAP and required packages
RUN apk add --no-cache \
openldap \
openldap-clients \
openldap-back-mdb \
openldap-overlay-memberof \
openldap-overlay-refint \
openldap-overlay-unique \
openldap-overlay-ppolicy \
openssl \
argon2 \
&& mkdir -p /var/lib/openldap/openldap-data \
&& mkdir -p /etc/openldap/slapd.d \
&& mkdir -p /run/openldap \
&& mkdir -p /certs \
&& chown -R ldap:ldap /var/lib/openldap \
&& chown -R ldap:ldap /etc/openldap/slapd.d \
&& chown -R ldap:ldap /run/openldap
# Copy custom schemas
COPY schema/*.schema /etc/openldap/schema/
# Copy initialization scripts
COPY scripts/ /scripts/
RUN chmod +x /scripts/*.sh
# Copy LDIF templates
COPY ldif/ /ldif/
# Copy entrypoint
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Expose ports
EXPOSE 389 636
# Volumes for persistence
VOLUME ["/var/lib/openldap/openldap-data", "/etc/openldap/slapd.d", "/certs"]
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ldapsearch -x -H ldap://localhost -b "" -s base "objectClass=*" || exit 1
ENTRYPOINT ["/docker-entrypoint.sh"]

669
README.md Normal file
View File

@ -0,0 +1,669 @@
# OpenLDAP Container Operations Guide
## Introduction
This document provides operational guidance for deploying and managing the Enterprise OpenLDAP container. It explains not just the how, but also the what and why behind each configuration decision.
### What This Container Provides
This is a purpose-built OpenLDAP container designed to serve as a central identity provider for an enterprise environment. It provides:
- **Centralized user authentication** for all services (Keycloak, Nextcloud, Gitea, mail, etc.)
- **POSIX account support** for Linux systems via SSSD integration
- **Group-based access control** with automatic membership tracking
- **Service account isolation** so each application has its own credentials with limited permissions
### Why OpenLDAP?
LDAP (Lightweight Directory Access Protocol) remains the standard for centralized identity management because:
1. **Universal support** - Nearly every enterprise application supports LDAP authentication
2. **Hierarchical organization** - Natural fit for organizational structures (users, groups, departments)
3. **Fine-grained access control** - Different applications can have different levels of access
4. **Proven reliability** - Decades of production use in demanding environments
### Key Design Decisions
**RFC2307bis instead of NIS schema**: Traditional LDAP setups use the NIS schema where `posixGroup` is a STRUCTURAL class. This prevents combining POSIX groups with other group types. We use RFC2307bis where `posixGroup` is AUXILIARY, allowing groups to have both POSIX attributes (gidNumber) and member tracking (member attribute). This is essential for the memberOf overlay to work correctly.
**Pre-configured overlays**: The container includes four overlays that are critical for enterprise use:
- **memberOf** - Automatically maintains reverse group membership on user objects
- **refint** - Maintains referential integrity when users are deleted
- **unique** - Prevents duplicate UIDs, email addresses, and ID numbers
- **ppolicy** - Enforces password complexity and expiration policies
**Service accounts**: Instead of sharing the admin password with applications, each service gets its own account with read-only access to the directory. This follows the principle of least privilege and provides audit trails.
---
## Architecture Overview
### Directory Structure
The container creates a directory tree optimized for enterprise use:
```
dc=example,dc=com ← Your organization's base
├── ou=People ← All user accounts live here
│ └── uid=jdoe ← Individual user
├── ou=Groups ← Authorization and access groups
│ ├── cn=admins ← LDAP administrators
│ └── cn=developers ← Example application group
├── ou=Services ← Application service accounts
│ ├── cn=keycloak ← For Keycloak LDAP federation
│ ├── cn=nextcloud ← For Nextcloud user backend
│ ├── cn=gitea ← For Gitea authentication
│ ├── cn=postfix ← For mail routing lookups
│ ├── cn=dovecot ← For mail authentication
│ └── cn=sssd ← For Linux PAM/NSS
├── ou=Domains ← Virtual mail domains (for mail server)
├── ou=Policies ← Password and security policies
│ └── cn=default ← Default password policy
└── ou=Kerberos ← Reserved for Kerberos integration
```
**Why this structure?**
- **Separate OUs for each purpose** makes ACLs (access control lists) simpler and more secure
- **Service accounts in their own OU** allows restricting what parts of the tree they can access
- **Policies OU** keeps password policies separate from user data
- **Domains OU** is specifically for mail server virtual domain configuration
### How Authentication Works
When an application authenticates a user:
1. Application connects to LDAP using its service account (e.g., `cn=keycloak,ou=Services,...`)
2. Application searches for the user in `ou=People` by uid or email
3. Application attempts to bind (authenticate) as that user with their password
4. LDAP verifies the password and returns success/failure
5. Application can then query the user's groups via the `memberOf` attribute
The service account never sees user passwords - it only searches for users. The actual password verification happens when the user's DN is used for a bind operation.
---
## Getting Started
### Prerequisites
Before deploying, ensure you have:
- Docker installed and running
- Decided on your domain name (this determines your base DN)
- Generated a strong admin password
- (Optional) TLS certificates if you need encrypted connections
### Step 1: Basic Deployment
The simplest deployment requires just three pieces of information:
```bash
docker run -d --name openldap \
-e LDAP_DOMAIN=example.com \
-e LDAP_ORGANISATION="Example Corporation" \
-e LDAP_ADMIN_PASSWORD=your-secure-password \
-p 389:389 \
enterprise-openldap:latest
```
**What happens on first run:**
1. The container detects this is a fresh start (no existing database)
2. It generates the base DN from your domain (`example.com``dc=example,dc=com`)
3. Creates the cn=config database with your settings
4. Loads all required schemas in the correct order
5. Configures the overlays (memberOf, refint, unique, ppolicy)
6. Creates the directory structure (OUs)
7. Sets up ACLs to protect sensitive data
8. Optionally creates service accounts
9. Starts the LDAP server
On subsequent restarts, steps 2-8 are skipped - the existing database is used.
### Step 2: Verify the Deployment
Test that the server is responding:
```bash
# Anonymous query to verify the server is up
ldapsearch -x -H ldap://localhost:389 -b "" -s base "(objectClass=*)"
```
Test admin authentication:
```bash
ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "your-secure-password" \
-b "dc=example,dc=com" "(objectClass=organizationalUnit)"
```
You should see the OUs (People, Groups, Services, etc.) listed.
### Step 3: Enable Service Accounts (Recommended)
Service accounts allow applications to connect with limited, auditable credentials:
```bash
docker run -d --name openldap \
-e LDAP_DOMAIN=example.com \
-e LDAP_ORGANISATION="Example Corporation" \
-e LDAP_ADMIN_PASSWORD=your-secure-password \
-e LDAP_CREATE_SERVICE_ACCOUNTS=true \
-p 389:389 \
enterprise-openldap:latest
```
The generated passwords are saved to `/var/lib/openldap/service-passwords.txt` inside the container. Retrieve them:
```bash
docker exec openldap cat /var/lib/openldap/service-passwords.txt
```
**Important**: Save these passwords securely, then delete the file:
```bash
docker exec openldap rm /var/lib/openldap/service-passwords.txt
```
### Step 4: Add Persistence
Without volumes, your data is lost when the container is removed. For production, always use volumes:
```bash
docker run -d --name openldap \
-v openldap-data:/var/lib/openldap/openldap-data \
-v openldap-config:/etc/openldap/slapd.d \
-e LDAP_DOMAIN=example.com \
-e LDAP_ORGANISATION="Example Corporation" \
-e LDAP_ADMIN_PASSWORD=your-secure-password \
-e LDAP_CREATE_SERVICE_ACCOUNTS=true \
-p 389:389 \
enterprise-openldap:latest
```
**Volume purposes:**
| Volume | Contains | Why it matters |
|--------|----------|----------------|
| `/var/lib/openldap/openldap-data` | The actual LDAP database (MDB format) | All your users, groups, and data |
| `/etc/openldap/slapd.d` | The cn=config configuration | Schema, overlays, ACLs, indexes |
### Step 5: Enable TLS (Production)
For production environments, you should enable TLS to encrypt connections:
```bash
docker run -d --name openldap \
-v openldap-data:/var/lib/openldap/openldap-data \
-v openldap-config:/etc/openldap/slapd.d \
-v /path/to/certs:/certs:ro \
-e LDAP_DOMAIN=example.com \
-e LDAP_ORGANISATION="Example Corporation" \
-e LDAP_ADMIN_PASSWORD=your-secure-password \
-e LDAP_TLS_ENABLED=true \
-p 389:389 -p 636:636 \
enterprise-openldap:latest
```
The `/certs` directory must contain:
- `ldap.crt` - Server certificate
- `ldap.key` - Private key (readable only by root/ldap user)
- `ca.crt` - CA certificate (for client verification)
**Port 389 vs 636:**
- Port 389 (LDAP) - Unencrypted, but supports STARTTLS upgrade
- Port 636 (LDAPS) - TLS from the start, like HTTPS
Most modern applications support STARTTLS on port 389, which is generally preferred.
---
## Configuration Reference
### Required Environment Variables
These must be set for the container to start:
| Variable | Purpose | Example |
|----------|---------|---------|
| `LDAP_DOMAIN` | Your organization's domain. Converted to base DN automatically. | `example.com``dc=example,dc=com` |
| `LDAP_ORGANISATION` | Human-readable organization name, stored in the base entry. | `Example Corporation` |
| `LDAP_ADMIN_PASSWORD` | Password for the directory administrator account. **Use cleartext** - the container hashes it automatically with `slappasswd`. | `Str0ng!Passw0rd` |
### Optional Environment Variables
#### General Settings
| Variable | Default | Purpose |
|----------|---------|---------|
| `LDAP_BASE_DN` | Generated from domain | Override if you need a non-standard base DN |
| `LDAP_CONFIG_PASSWORD` | Random | Password for cn=config administrative access. Only needed for advanced configuration changes. |
| `LDAP_LOG_LEVEL` | `256` | Controls logging verbosity. `256` logs connections and operations. See [Log Levels](#understanding-log-levels). |
#### TLS Settings
| Variable | Default | Purpose |
|----------|---------|---------|
| `LDAP_TLS_ENABLED` | `true` | Master switch for TLS. Set `false` to disable completely. |
| `LDAP_TLS_CERT_FILE` | `/certs/ldap.crt` | Path to server certificate inside container |
| `LDAP_TLS_KEY_FILE` | `/certs/ldap.key` | Path to private key inside container |
| `LDAP_TLS_CA_FILE` | `/certs/ca.crt` | Path to CA certificate for client verification |
| `LDAP_TLS_VERIFY_CLIENT` | `try` | Client cert policy: `never` (don't ask), `try` (optional), `demand` (required) |
#### Service Account Settings
| Variable | Default | Purpose |
|----------|---------|---------|
| `LDAP_CREATE_SERVICE_ACCOUNTS` | `false` | Set `true` to create pre-configured service accounts |
| `LDAP_SERVICE_KEYCLOAK_PASSWORD` | Random | Explicit password for Keycloak account |
| `LDAP_SERVICE_NEXTCLOUD_PASSWORD` | Random | Explicit password for Nextcloud account |
| `LDAP_SERVICE_GITEA_PASSWORD` | Random | Explicit password for Gitea account |
| `LDAP_SERVICE_POSTFIX_PASSWORD` | Random | Explicit password for Postfix account |
| `LDAP_SERVICE_DOVECOT_PASSWORD` | Random | Explicit password for Dovecot account |
| `LDAP_SERVICE_SSSD_PASSWORD` | Random | Explicit password for SSSD account |
**When to set explicit passwords:** If you're deploying with configuration management (Ansible, Terraform) and need predictable credentials, set them explicitly. For manual deployments, random passwords are more secure.
---
## Day-to-Day Operations
### Adding Users
Users belong in `ou=People`. Here's a complete user entry:
```ldif
dn: uid=jdoe,ou=People,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: jdoe
cn: John Doe
sn: Doe
givenName: John
mail: john.doe@example.com
uidNumber: 10001
gidNumber: 10000
homeDirectory: /home/jdoe
loginShell: /bin/bash
userPassword: {SSHA}hashed-password
```
**Object classes explained:**
- `inetOrgPerson` - Standard LDAP person with email, phone, etc.
- `posixAccount` - UNIX account attributes (uid/gid numbers, home directory, shell)
- `shadowAccount` - Password aging and expiration
Add the user:
```bash
ldapadd -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "admin-password" \
-f user.ldif
```
### Adding Groups
Groups use `groupOfMembers` (for member tracking) combined with `posixGroup` (for UNIX gidNumber):
```ldif
dn: cn=developers,ou=Groups,dc=example,dc=com
objectClass: groupOfMembers
objectClass: posixGroup
cn: developers
gidNumber: 10001
description: Development team
member: uid=jdoe,ou=People,dc=example,dc=com
```
**Why groupOfMembers instead of groupOfNames?**
`groupOfNames` requires at least one member - you can't have an empty group. `groupOfMembers` allows empty groups, which is often needed when setting up access control before adding members.
### Checking Group Membership
Thanks to the memberOf overlay, you can see a user's groups directly:
```bash
ldapsearch -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "admin-password" \
-b "uid=jdoe,ou=People,dc=example,dc=com" \
memberOf
```
This returns all groups the user belongs to, without having to search each group.
### Modifying Entries
To change an attribute:
```ldif
dn: uid=jdoe,ou=People,dc=example,dc=com
changetype: modify
replace: mail
mail: j.doe@example.com
```
```bash
ldapmodify -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "admin-password" \
-f modify.ldif
```
### Deleting Entries
```bash
ldapdelete -x -H ldap://localhost:389 \
-D "cn=admin,dc=example,dc=com" \
-w "admin-password" \
"uid=jdoe,ou=People,dc=example,dc=com"
```
The refint overlay automatically removes the user from any groups they belonged to.
---
## Connecting Applications
### Keycloak
Keycloak uses LDAP for user federation. Configure it with:
| Setting | Value |
|---------|-------|
| Connection URL | `ldap://openldap:389` |
| Bind DN | `cn=keycloak,ou=Services,dc=example,dc=com` |
| Bind Credential | (from service-passwords.txt) |
| Users DN | `ou=People,dc=example,dc=com` |
| Username Attribute | `uid` |
| RDN Attribute | `uid` |
| UUID Attribute | `entryUUID` |
### Nextcloud
In Nextcloud's LDAP settings:
| Setting | Value |
|---------|-------|
| Host | `openldap` |
| Port | `389` |
| User DN | `cn=nextcloud,ou=Services,dc=example,dc=com` |
| Password | (from service-passwords.txt) |
| Base DN | `dc=example,dc=com` |
| User Filter | `(&(objectClass=inetOrgPerson)(uid=%uid))` |
| Group Filter | `(objectClass=groupOfMembers)` |
### SSSD (Linux PAM/NSS)
For Linux systems to authenticate against LDAP:
```ini
# /etc/sssd/sssd.conf
[sssd]
services = nss, pam
domains = example.com
[domain/example.com]
id_provider = ldap
auth_provider = ldap
ldap_uri = ldap://openldap.example.com
ldap_search_base = dc=example,dc=com
ldap_default_bind_dn = cn=sssd,ou=Services,dc=example,dc=com
ldap_default_authtok = sssd-password-here
ldap_user_search_base = ou=People,dc=example,dc=com
ldap_group_search_base = ou=Groups,dc=example,dc=com
```
---
## Backup and Recovery
### Why Backups Matter
The LDAP directory contains:
- All user accounts and credentials
- Group memberships and access control
- Service account configurations
- Password policies
Losing this data means losing access to all integrated systems.
### Creating Backups
Export the directory to LDIF format:
```bash
# Backup user data (database 1)
docker exec openldap slapcat -n 1 > ldap-data-$(date +%Y%m%d).ldif
# Backup configuration (database 0) - optional but recommended
docker exec openldap slapcat -n 0 > ldap-config-$(date +%Y%m%d).ldif
```
**Backup frequency recommendation:**
- Daily for the data backup
- After any configuration changes for the config backup
### Restoring from Backup
**Warning**: This destroys all existing data!
```bash
# Stop the container
docker stop openldap
docker rm openldap
# Remove existing volumes
docker volume rm openldap-data openldap-config
# Create fresh container with backup
docker run --rm \
-v openldap-data:/var/lib/openldap/openldap-data \
-v $(pwd)/ldap-data-20240101.ldif:/backup.ldif:ro \
enterprise-openldap:latest \
sh -c "slapadd -n 1 -l /backup.ldif && chown -R ldap:ldap /var/lib/openldap"
# Start normally
docker run -d --name openldap \
-v openldap-data:/var/lib/openldap/openldap-data \
-v openldap-config:/etc/openldap/slapd.d \
... # your normal settings
```
---
## Troubleshooting
### Understanding Log Levels
The `LDAP_LOG_LEVEL` controls what gets logged. Common values:
| Level | What it shows | When to use |
|-------|---------------|-------------|
| `0` | Nothing | Never in production |
| `256` | Connections, operations, results | Normal operation |
| `384` | Above + ACL decisions | Debugging access issues |
| `512` | Stats with entry counts | Performance analysis |
| `-1` | Everything | Last resort debugging |
Increase temporarily for debugging:
```bash
docker run -e LDAP_LOG_LEVEL=384 ...
```
### Common Issues
#### Container exits immediately
**Symptom**: Container starts and stops within seconds.
**Cause**: Missing required environment variables.
**Solution**: Check that `LDAP_DOMAIN`, `LDAP_ORGANISATION`, and `LDAP_ADMIN_PASSWORD` are all set.
```bash
docker logs openldap # Will show which variable is missing
```
#### Cannot authenticate as admin
**Symptom**: `ldap_bind: Invalid credentials (49)`
**Causes**:
1. Wrong password
2. Wrong bind DN (check the domain components match your LDAP_DOMAIN)
3. Typing `dc=example.com` instead of `dc=example,dc=com`
**Solution**: Verify the exact bind DN:
```bash
# Your bind DN is always: cn=admin,<your-base-dn>
# If LDAP_DOMAIN=example.com, then it's: cn=admin,dc=example,dc=com
```
#### TLS connection refused
**Symptom**: Cannot connect to port 636.
**Cause**: Certificates not found or not readable.
**Solution**: Check the logs for TLS errors:
```bash
docker logs openldap | grep -i tls
```
Verify certificates are mounted correctly:
```bash
docker exec openldap ls -la /certs/
```
#### Service account cannot search
**Symptom**: Service account binds successfully but searches return empty.
**Cause**: ACLs restrict what the service account can see.
**Solution**: Service accounts can only read specific OUs. Verify you're searching the correct base:
```bash
# Correct - searching People OU
ldapsearch -D "cn=keycloak,ou=Services,..." -b "ou=People,dc=example,dc=com" ...
# Wrong - searching the entire tree
ldapsearch -D "cn=keycloak,ou=Services,..." -b "dc=example,dc=com" ...
```
#### Users don't have memberOf attribute
**Symptom**: User exists in group but `memberOf` attribute is empty.
**Cause**: User was added before the memberOf overlay, or group uses wrong attribute.
**Solution**: The memberOf overlay only tracks `member` attribute on `groupOfMembers` objects. Verify:
1. Group has `objectClass: groupOfMembers`
2. Group uses `member` attribute (not `memberUid`)
To fix existing users, remove and re-add them to groups.
---
## Security Recommendations
### Password Security
1. **Use strong admin password** - At least 16 characters with mixed case, numbers, symbols
2. **Rotate service account passwords** periodically
3. **Delete service-passwords.txt** after retrieving the passwords
4. **Never commit passwords** to version control
### Network Security
1. **Use TLS** for all production deployments
2. **Restrict port access** - Only allow LDAP ports from trusted networks
3. **Use internal Docker networks** when possible:
```bash
docker network create ldap-net
docker run --network ldap-net --name openldap ...
# Other containers on ldap-net can access via hostname "openldap"
```
### Access Control
1. **Use service accounts** instead of sharing admin credentials
2. **Principle of least privilege** - Service accounts only get read access
3. **Audit regularly** - Check who has access to what
---
## Docker Compose Reference
Complete production-ready example:
```yaml
version: '3.8'
services:
openldap:
image: enterprise-openldap:latest
container_name: openldap
hostname: ldap.example.com
environment:
LDAP_DOMAIN: example.com
LDAP_ORGANISATION: "Example Corporation"
LDAP_ADMIN_PASSWORD: ${LDAP_ADMIN_PASSWORD:?Set LDAP_ADMIN_PASSWORD}
LDAP_TLS_ENABLED: "true"
LDAP_CREATE_SERVICE_ACCOUNTS: "true"
LDAP_LOG_LEVEL: "256"
volumes:
- openldap-data:/var/lib/openldap/openldap-data
- openldap-config:/etc/openldap/slapd.d
- ./certs:/certs:ro
ports:
- "389:389"
- "636:636"
networks:
- backend
restart: unless-stopped
healthcheck:
test: ["CMD", "ldapsearch", "-x", "-H", "ldap://localhost", "-b", "", "-s", "base"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
backend:
driver: bridge
volumes:
openldap-data:
openldap-config:
```
Create a `.env` file:
```bash
LDAP_ADMIN_PASSWORD=your-secure-password-here
```
Start:
```bash
docker compose up -d
```

31
docker-compose.yml Normal file
View File

@ -0,0 +1,31 @@
services:
openldap:
build: .
image: enterprise-openldap:latest
container_name: openldap
hostname: ldap.example.com
environment:
LDAP_DOMAIN: example.com
LDAP_ORGANISATION: "Example Corporation"
LDAP_ADMIN_PASSWORD: ${LDAP_ADMIN_PASSWORD:-changeme}
LDAP_TLS_ENABLED: "true"
LDAP_CREATE_SERVICE_ACCOUNTS: "true"
LDAP_LOG_LEVEL: "256"
volumes:
- ldap_data:/var/lib/openldap/openldap-data
- ldap_config:/etc/openldap/slapd.d
- ./certs:/certs:ro
- ./ldif:/ldif/custom:ro
ports:
- "389:389"
- "636:636"
networks:
- ldap_net
restart: unless-stopped
volumes:
ldap_data:
ldap_config:
networks:
ldap_net:

115
docker-entrypoint.sh Normal file
View File

@ -0,0 +1,115 @@
#!/bin/sh
set -e
# Source utility functions
. /scripts/utils.sh
# LDAPI socket URL - must use URL-encoded path for Alpine
LDAPI_SOCKET="ldapi://%2Frun%2Fopenldap%2Fldapi"
export LDAPI_SOCKET
# Validate required environment variables
if [ -z "$LDAP_DOMAIN" ]; then
log_error "LDAP_DOMAIN is required"
exit 1
fi
if [ -z "$LDAP_ORGANISATION" ]; then
log_error "LDAP_ORGANISATION is required"
exit 1
fi
if [ -z "$LDAP_ADMIN_PASSWORD" ]; then
log_error "LDAP_ADMIN_PASSWORD is required"
exit 1
fi
# Generate base DN from domain if not provided
if [ -z "$LDAP_BASE_DN" ]; then
LDAP_BASE_DN=$(echo "$LDAP_DOMAIN" | sed 's/^/dc=/; s/\./,dc=/g')
fi
export LDAP_BASE_DN
# Extract DC component for base entry
LDAP_DC=$(echo "$LDAP_DOMAIN" | cut -d'.' -f1)
export LDAP_DC
# Set defaults for optional variables
export LDAP_CONFIG_PASSWORD="${LDAP_CONFIG_PASSWORD:-$(generate_password)}"
export LDAP_TLS_ENABLED="${LDAP_TLS_ENABLED:-true}"
export LDAP_TLS_CERT_FILE="${LDAP_TLS_CERT_FILE:-/certs/ldap.crt}"
export LDAP_TLS_KEY_FILE="${LDAP_TLS_KEY_FILE:-/certs/ldap.key}"
export LDAP_TLS_CA_FILE="${LDAP_TLS_CA_FILE:-/certs/ca.crt}"
export LDAP_TLS_VERIFY_CLIENT="${LDAP_TLS_VERIFY_CLIENT:-try}"
export LDAP_LOG_LEVEL="${LDAP_LOG_LEVEL:-256}"
export LDAP_READONLY="${LDAP_READONLY:-false}"
log_info "OpenLDAP Container Starting"
log_info "Domain: $LDAP_DOMAIN"
log_info "Base DN: $LDAP_BASE_DN"
log_info "Organisation: $LDAP_ORGANISATION"
# Check if already initialized
if [ ! -f /var/lib/openldap/openldap-data/data.mdb ]; then
log_info "First run - initializing OpenLDAP..."
# Initialize cn=config
/scripts/init-config.sh
# Load schemas in order
/scripts/init-schemas.sh
# Configure overlays
/scripts/init-overlays.sh
# Create base DIT
/scripts/init-dit.sh
# Configure ACLs
/scripts/init-acls.sh
# Create service accounts if requested
if [ "$LDAP_CREATE_SERVICE_ACCOUNTS" = "true" ]; then
/scripts/init-services.sh
fi
# Process custom LDIF files if present
if [ -d /ldif/custom ] && [ "$(ls -A /ldif/custom 2>/dev/null)" ]; then
log_info "Processing custom LDIF files..."
for ldif in /ldif/custom/*.ldif; do
if [ -f "$ldif" ]; then
log_info "Loading: $ldif"
ldapadd -x -H "$LDAPI_SOCKET" -D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" -f "$ldif" || \
log_warn "Failed to load $ldif (may already exist)"
fi
done
fi
log_info "Initialization complete."
else
log_info "Database exists - starting normally."
fi
# Ensure proper ownership
chown -R ldap:ldap /var/lib/openldap
chown -R ldap:ldap /etc/openldap/slapd.d
chown -R ldap:ldap /run/openldap
# Build slapd arguments
SLAPD_URLS="ldap:/// $LDAPI_SOCKET"
if [ "$LDAP_TLS_ENABLED" = "true" ]; then
if [ -f "$LDAP_TLS_CERT_FILE" ] && [ -f "$LDAP_TLS_KEY_FILE" ]; then
SLAPD_URLS="ldap:/// ldaps:/// $LDAPI_SOCKET"
log_info "TLS enabled - listening on ldaps://"
else
log_warn "TLS enabled but certificates not found - skipping ldaps://"
fi
fi
log_info "Starting slapd with URLs: $SLAPD_URLS"
# Start slapd
exec /usr/sbin/slapd -h "$SLAPD_URLS" \
-F /etc/openldap/slapd.d \
-u ldap -g ldap \
-d "${LDAP_LOG_LEVEL}"

2
ldif/.gitkeep Normal file
View File

@ -0,0 +1,2 @@
# Place custom LDIF files here for automatic loading during initialization
# Files will be loaded in alphabetical order after base DIT creation

206
schema/enterprise.schema Normal file
View File

@ -0,0 +1,206 @@
# ===========================================================================
# Enterprise Platform LDAP Schema
# Version: 1.0
#
# OID Base: 1.3.6.1.4.1.99999 (DEVELOPMENT - Apply for your own from IANA)
#
# Structure:
# 1.3.6.1.4.1.99999.1 - Enterprise Platform
# 1.3.6.1.4.1.99999.1.1 - Attribute Types
# 1.3.6.1.4.1.99999.1.2 - Object Classes
#
# Includes:
# - Virtual Mail (Postfix/Dovecot)
# - Nextcloud integration
# - Service access control
#
# Dependencies (must be loaded first):
# - core.schema
# - cosine.schema
# - inetorgperson.schema
# - rfc2307bis.schema
# ===========================================================================
# ===========================================================================
# SECTION 1: VIRTUAL MAIL ATTRIBUTES AND OBJECTS
# ===========================================================================
# ---------------------------------------------------------------------------
# Mail Attribute Types
# ---------------------------------------------------------------------------
attributetype ( 1.3.6.1.4.1.99999.1.1.1
NAME 'mailDomain'
DESC 'Virtual mail domain name'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.99999.1.1.2
NAME 'mailTransport'
DESC 'Postfix transport (e.g., lmtp:unix:private/dovecot-lmtp)'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.99999.1.1.3
NAME 'mailbox'
DESC 'Relative mailbox path (e.g., domain.com/user/)'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.99999.1.1.4
NAME 'mailQuota'
DESC 'Mailbox quota in bytes'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.99999.1.1.5
NAME 'mailEnabled'
DESC 'Mail account or domain enabled (TRUE/FALSE)'
EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.99999.1.1.6
NAME 'maildrop'
DESC 'Final delivery address or forward destination'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.4.1.99999.1.1.7
NAME 'mailAlias'
DESC 'Additional email addresses for this account'
EQUALITY caseIgnoreIA5Match
SUBSTR caseIgnoreIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.4.1.99999.1.1.8
NAME 'mailHomeDirectory'
DESC 'Base path for mail storage'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.99999.1.1.9
NAME 'domainQuota'
DESC 'Total quota for all accounts in domain (bytes)'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.99999.1.1.10
NAME 'domainMaxAccounts'
DESC 'Maximum number of accounts in domain'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.99999.1.1.11
NAME 'domainMaxAliases'
DESC 'Maximum number of aliases in domain'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
# ---------------------------------------------------------------------------
# Mail Object Classes
# ---------------------------------------------------------------------------
objectclass ( 1.3.6.1.4.1.99999.1.2.1
NAME 'mailDomainObject'
DESC 'Virtual mail domain'
SUP top STRUCTURAL
MUST ( mailDomain $ mailEnabled )
MAY ( mailTransport $ mailHomeDirectory $ domainQuota $
domainMaxAccounts $ domainMaxAliases $ description ) )
objectclass ( 1.3.6.1.4.1.99999.1.2.2
NAME 'mailAccountObject'
DESC 'Virtual mail account - extends inetOrgPerson'
SUP inetOrgPerson STRUCTURAL
MUST ( mail $ mailEnabled )
MAY ( mailbox $ mailQuota $ maildrop $ mailAlias $
mailHomeDirectory $ description ) )
objectclass ( 1.3.6.1.4.1.99999.1.2.3
NAME 'mailAliasObject'
DESC 'Mail alias or distribution list'
SUP top STRUCTURAL
MUST ( mail $ maildrop $ mailEnabled )
MAY ( cn $ description ) )
# ===========================================================================
# SECTION 2: NEXTCLOUD ATTRIBUTES AND OBJECTS
# Using official Nextcloud OIDs (1.3.6.1.4.1.49213.1) for compatibility
# ===========================================================================
attributetype ( 1.3.6.1.4.1.49213.1.1.1
NAME 'nextcloudEnabled'
DESC 'Whether user or group should be available in Nextcloud'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
attributetype ( 1.3.6.1.4.1.49213.1.1.2
NAME 'nextcloudQuota'
DESC 'Nextcloud disk quota (e.g., 15 GB)'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
objectclass ( 1.3.6.1.4.1.49213.1.2.1
NAME 'nextcloudUser'
DESC 'Nextcloud user account'
SUP top AUXILIARY
MAY ( nextcloudEnabled $ nextcloudQuota ) )
objectclass ( 1.3.6.1.4.1.49213.1.2.2
NAME 'nextcloudGroup'
DESC 'Nextcloud group'
SUP top AUXILIARY
MAY ( nextcloudEnabled ) )
# ===========================================================================
# SECTION 3: SERVICE ACCESS CONTROL
# Based on PADL ldapns schema (OID 1.3.6.1.4.1.5765)
# ===========================================================================
attributetype ( 1.3.6.1.4.1.5765.100.1
NAME 'authorizedService'
DESC 'Service authorized for this account'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
objectclass ( 1.3.6.1.4.1.5765.100.2
NAME 'authorizedServiceObject'
DESC 'Service authorization object'
SUP top AUXILIARY
MAY ( authorizedService ) )
# ===========================================================================
# SECTION 4: ADDITIONAL UTILITY OBJECTS
# ===========================================================================
# Service account object for bind DNs
objectclass ( 1.3.6.1.4.1.99999.1.2.10
NAME 'serviceAccount'
DESC 'Service account for application binding'
SUP top STRUCTURAL
MUST ( cn )
MAY ( description $ userPassword ) )

291
schema/kerberos.schema Normal file
View File

@ -0,0 +1,291 @@
# ===========================================================================
# MIT Kerberos LDAP Schema
#
# This schema enables storing Kerberos principals in OpenLDAP.
# It is loaded by default but remains DORMANT until Kerberos is enabled.
#
# OID Base: 2.16.840.1.113719.1.301
#
# When Kerberos is enabled:
# 1. krbPrincipalAux objectClass is added to user entries
# 2. MIT KDC is deployed with LDAP backend
# 3. Principals are created via kadmin
#
# Source: MIT Kerberos source code (src/plugins/kdb/ldap/libkdb_ldap/kerberos.schema)
# Reference: https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_ldap.html
#
# Dependencies:
# - core.schema
# ===========================================================================
# ---------------------------------------------------------------------------
# Attribute Types
# ---------------------------------------------------------------------------
attributetype ( 2.16.840.1.113719.1.301.4.1.1
NAME 'krbPrincipalName'
DESC 'Kerberos principal name (e.g., user@REALM)'
EQUALITY caseExactIA5Match
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.2.1
NAME 'krbPrincipalKey'
DESC 'Kerberos principal key data (managed by KDC)'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
attributetype ( 2.16.840.1.113719.1.301.4.3.1
NAME 'krbTicketPolicyReference'
DESC 'DN of ticket policy'
EQUALITY distinguishedNameMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.4.1
NAME 'krbPrincipalExpiration'
DESC 'Principal expiration time'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.5.1
NAME 'krbPasswordExpiration'
DESC 'Password expiration time'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.6.1
NAME 'krbMaxTicketLife'
DESC 'Maximum ticket lifetime in seconds'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.7.1
NAME 'krbMaxRenewableLife'
DESC 'Maximum renewable ticket lifetime in seconds'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.8.1
NAME 'krbTicketFlags'
DESC 'Kerberos ticket flags'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.9.1
NAME 'krbPrincipalType'
DESC 'Kerberos principal type'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.10.1
NAME 'krbPwdPolicyReference'
DESC 'DN of password policy'
EQUALITY distinguishedNameMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.11.1
NAME 'krbPrincipalReferences'
DESC 'DN of associated principal entries'
EQUALITY distinguishedNameMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
attributetype ( 2.16.840.1.113719.1.301.4.12.1
NAME 'krbLastPwdChange'
DESC 'Time of last password change'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.13.1
NAME 'krbLastSuccessfulAuth'
DESC 'Time of last successful authentication'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.14.1
NAME 'krbLastFailedAuth'
DESC 'Time of last failed authentication'
EQUALITY generalizedTimeMatch
ORDERING generalizedTimeOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.15.1
NAME 'krbLoginFailedCount'
DESC 'Number of consecutive failed login attempts'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.16.1
NAME 'krbExtraData'
DESC 'Extra data for Kerberos'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
attributetype ( 2.16.840.1.113719.1.301.4.17.1
NAME 'krbAllowedToDelegateTo'
DESC 'Services this principal can delegate to (S4U2Proxy)'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
# ---------------------------------------------------------------------------
# Realm Container Attributes
# ---------------------------------------------------------------------------
attributetype ( 2.16.840.1.113719.1.301.4.20.1
NAME 'krbSubTrees'
DESC 'DNs of subtrees containing principals'
EQUALITY distinguishedNameMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
attributetype ( 2.16.840.1.113719.1.301.4.21.1
NAME 'krbSearchScope'
DESC 'Search scope for principals (0=base, 1=one, 2=sub)'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.22.1
NAME 'krbPrincContainerRef'
DESC 'DN of principal container'
EQUALITY distinguishedNameMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
attributetype ( 2.16.840.1.113719.1.301.4.23.1
NAME 'krbMaxPwdLife'
DESC 'Maximum password lifetime in realm'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.24.1
NAME 'krbMinPwdLife'
DESC 'Minimum password lifetime in realm'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.25.1
NAME 'krbPwdMinDiffChars'
DESC 'Minimum number of character classes in password'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.26.1
NAME 'krbPwdMinLength'
DESC 'Minimum password length'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.27.1
NAME 'krbPwdHistoryLength'
DESC 'Number of passwords to keep in history'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.28.1
NAME 'krbPwdMaxFailure'
DESC 'Maximum password failures before lockout'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.29.1
NAME 'krbPwdFailureCountInterval'
DESC 'Failure count reset interval in seconds'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 2.16.840.1.113719.1.301.4.30.1
NAME 'krbPwdLockoutDuration'
DESC 'Lockout duration in seconds'
EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
# ---------------------------------------------------------------------------
# Object Classes
# ---------------------------------------------------------------------------
# Auxiliary class for adding Kerberos attributes to user entries
# This is what gets added to users when Kerberos premium feature is enabled
objectclass ( 2.16.840.1.113719.1.301.6.8.1
NAME 'krbPrincipalAux'
DESC 'Auxiliary class for Kerberos principal attributes'
SUP top AUXILIARY
MAY ( krbPrincipalName $ krbPrincipalKey $ krbTicketPolicyReference $
krbPrincipalExpiration $ krbPasswordExpiration $
krbMaxTicketLife $ krbMaxRenewableLife $ krbTicketFlags $
krbPrincipalType $ krbPwdPolicyReference $ krbPrincipalReferences $
krbLastPwdChange $ krbLastSuccessfulAuth $ krbLastFailedAuth $
krbLoginFailedCount $ krbExtraData $ krbAllowedToDelegateTo ) )
# Structural class for standalone principal entries (less common)
objectclass ( 2.16.840.1.113719.1.301.6.9.1
NAME 'krbPrincipal'
DESC 'Structural class for Kerberos principals'
SUP top STRUCTURAL
MUST krbPrincipalName
MAY ( krbPrincipalKey $ krbTicketPolicyReference $
krbPrincipalExpiration $ krbPasswordExpiration $
krbMaxTicketLife $ krbMaxRenewableLife $ krbTicketFlags $
krbPrincipalType $ krbPwdPolicyReference $ krbPrincipalReferences $
krbLastPwdChange $ krbLastSuccessfulAuth $ krbLastFailedAuth $
krbLoginFailedCount $ krbExtraData $ krbAllowedToDelegateTo ) )
# Container for Kerberos realm
objectclass ( 2.16.840.1.113719.1.301.6.1.1
NAME 'krbRealmContainer'
DESC 'Container for Kerberos realm'
SUP top STRUCTURAL
MUST cn
MAY ( krbSubTrees $ krbSearchScope $ krbPrincContainerRef $
krbMaxTicketLife $ krbMaxRenewableLife $ krbTicketFlags ) )
# Ticket policy object
objectclass ( 2.16.840.1.113719.1.301.6.2.1
NAME 'krbTicketPolicy'
DESC 'Kerberos ticket policy'
SUP top STRUCTURAL
MUST cn
MAY ( krbMaxTicketLife $ krbMaxRenewableLife $ krbTicketFlags ) )
# Password policy object for Kerberos
objectclass ( 2.16.840.1.113719.1.301.6.3.1
NAME 'krbPwdPolicy'
DESC 'Kerberos password policy'
SUP top STRUCTURAL
MUST cn
MAY ( krbMaxPwdLife $ krbMinPwdLife $ krbPwdMinDiffChars $
krbPwdMinLength $ krbPwdHistoryLength $ krbPwdMaxFailure $
krbPwdFailureCountInterval $ krbPwdLockoutDuration ) )
# Service principal container
objectclass ( 2.16.840.1.113719.1.301.6.4.1
NAME 'krbService'
DESC 'Kerberos service'
SUP krbPrincipal STRUCTURAL )

29
schema/openssh-lpk.schema Normal file
View File

@ -0,0 +1,29 @@
#
# OpenSSH LDAP Public Key Schema
# Used by: SSH servers, Gitea, GitLab CE (with LDAP sync)
#
# OID: 1.3.6.1.4.1.24552.500.1.1 (OpenSSH project registered OID)
#
# Installation:
# Include in slapd.conf or convert to LDIF for cn=config
#
# Usage:
# Add 'ldapPublicKey' objectClass to user entries
# Add 'sshPublicKey' attribute with public key(s)
#
# SSH Server Config (/etc/ssh/sshd_config):
# AuthorizedKeysCommand /usr/local/bin/ldap-ssh-keys.sh
# AuthorizedKeysCommandUser nobody
#
attributetype ( 1.3.6.1.4.1.24552.500.1.1.1.13
NAME 'sshPublicKey'
DESC 'OpenSSH Public Key'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
objectclass ( 1.3.6.1.4.1.24552.500.1.1.2.0
NAME 'ldapPublicKey'
DESC 'OpenSSH LDAP Public Key Objectclass'
SUP top AUXILIARY
MAY ( sshPublicKey $ uid ) )

340
schema/rfc2307bis.schema Normal file
View File

@ -0,0 +1,340 @@
# ===========================================================================
# RFC2307bis Schema
#
# This schema provides POSIX account and group support with the critical
# difference from RFC2307 (nis.schema) that posixGroup is AUXILIARY,
# allowing it to be combined with groupOfNames/groupOfMembers for
# proper memberOf overlay support.
#
# Source: https://github.com/jtyr/rfc2307bis
# See also: https://tools.ietf.org/html/draft-howard-rfc2307bis-02
#
# Key differences from nis.schema (RFC2307):
# - posixGroup is AUXILIARY (not STRUCTURAL)
# - Allows combining with groupOfNames/groupOfMembers
# - memberOf overlay works correctly
# - Empty groups are allowed
#
# Dependencies:
# - core.schema
# - cosine.schema
#
# DO NOT load nis.schema alongside this schema - they conflict!
#
# NOTE: On Alpine Linux, uidNumber and gidNumber are built-in to OpenLDAP
# and must NOT be redefined. They are commented out below.
# ===========================================================================
# Attribute types from RFC 2307
# uidNumber is built-in on Alpine Linux OpenLDAP
#attributetype ( 1.3.6.1.1.1.1.0 NAME 'uidNumber'
# DESC 'An integer uniquely identifying a user in an administrative domain'
# EQUALITY integerMatch
# ORDERING integerOrderingMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
# SINGLE-VALUE )
# gidNumber is built-in on Alpine Linux OpenLDAP
#attributetype ( 1.3.6.1.1.1.1.1 NAME 'gidNumber'
# DESC 'An integer uniquely identifying a group in an administrative domain'
# EQUALITY integerMatch
# ORDERING integerOrderingMatch
# SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
# SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.2 NAME 'gecos'
DESC 'The GECOS field; the common name'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.3 NAME 'homeDirectory'
DESC 'The absolute path to the home directory'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.4 NAME 'loginShell'
DESC 'The path to the login shell'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.6 NAME 'shadowMin'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.7 NAME 'shadowMax'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.8 NAME 'shadowWarning'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.9 NAME 'shadowInactive'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.10 NAME 'shadowExpire'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.11 NAME 'shadowFlag'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.12 NAME 'memberUid'
DESC 'Member UID - for backwards compatibility with RFC2307'
EQUALITY caseExactMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple'
DESC 'Netgroup triple'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
attributetype ( 1.3.6.1.1.1.1.15 NAME 'ipServicePort'
DESC 'Service port number'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol'
DESC 'Service protocol name'
SUP name )
attributetype ( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber'
DESC 'IP protocol number'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber'
DESC 'ONC RPC number'
EQUALITY integerMatch
ORDERING integerOrderingMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber'
DESC 'IPv4 addresses as a dotted decimal omitting leading zeros or IPv6 addresses as defined in RFC 4291'
SUP name )
attributetype ( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber'
DESC 'IP network omitting leading zeros, eg. 192.168'
SUP name
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber'
DESC 'IP netmask omitting leading zeros, eg. 255.255.255.0'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.22 NAME 'macAddress'
DESC 'MAC address in maximal, colon separated hex notation, eg. 00:00:92:90:ee:e2'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.23 NAME 'bootParameter'
DESC 'rpc.bootparamd parameter'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.24 NAME 'bootFile'
DESC 'Boot image name'
EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.26 NAME 'nisMapName'
DESC 'Name of a generic NIS map'
SUP name )
attributetype ( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry'
DESC 'A generic NIS entry'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.28 NAME 'nisPublicKey'
DESC 'NIS public key'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.29 NAME 'nisSecretKey'
DESC 'NIS secret key'
EQUALITY octetStringMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.40
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.30 NAME 'nisDomain'
DESC 'NIS domain'
EQUALITY caseIgnoreIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
attributetype ( 1.3.6.1.1.1.1.31 NAME 'automountMapName'
DESC 'automount Map Name'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.32 NAME 'automountKey'
DESC 'Automount Key value'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
attributetype ( 1.3.6.1.1.1.1.33 NAME 'automountInformation'
DESC 'Automount information'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
# Object classes from RFC 2307bis
objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount'
DESC 'Abstraction of an account with POSIX attributes'
SUP top AUXILIARY
MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
MAY ( userPassword $ loginShell $ gecos $ description ) )
# THIS IS THE KEY DIFFERENCE FROM RFC2307:
# posixGroup is AUXILIARY, not STRUCTURAL
# This allows combining with groupOfNames/groupOfMembers
objectclass ( 1.3.6.1.1.1.2.2 NAME 'posixGroup'
DESC 'Abstraction of a group of accounts'
SUP top AUXILIARY
MUST ( cn $ gidNumber )
MAY ( userPassword $ memberUid $ description ) )
objectclass ( 1.3.6.1.1.1.2.1 NAME 'shadowAccount'
DESC 'Additional attributes for shadow passwords'
SUP top AUXILIARY
MUST uid
MAY ( userPassword $ description $
shadowLastChange $ shadowMin $ shadowMax $
shadowWarning $ shadowInactive $
shadowExpire $ shadowFlag ) )
objectclass ( 1.3.6.1.1.1.2.3 NAME 'ipService'
DESC 'Abstraction an Internet Protocol service. Maps an IP port and protocol (such as tcp or udp) to one or more names'
SUP top STRUCTURAL
MUST ( cn $ ipServicePort $ ipServiceProtocol )
MAY description )
objectclass ( 1.3.6.1.1.1.2.4 NAME 'ipProtocol'
DESC 'Abstraction of an IP protocol. Maps a protocol number to one or more names'
SUP top STRUCTURAL
MUST ( cn $ ipProtocolNumber )
MAY description )
objectclass ( 1.3.6.1.1.1.2.5 NAME 'oncRpc'
DESC 'Abstraction of an Open Network Computing (ONC) Remote Procedure Call (RPC) binding'
SUP top STRUCTURAL
MUST ( cn $ oncRpcNumber )
MAY description )
objectclass ( 1.3.6.1.1.1.2.6 NAME 'ipHost'
DESC 'Abstraction of a host, an IP device. The distinguished value of the cn attribute denotes the hosts canonical name'
SUP top AUXILIARY
MUST ( cn $ ipHostNumber )
MAY ( userPassword $ l $ description $ manager ) )
objectclass ( 1.3.6.1.1.1.2.7 NAME 'ipNetwork'
DESC 'Abstraction of a network. The distinguished value of the cn attribute denotes the networks canonical name'
SUP top STRUCTURAL
MUST ipNetworkNumber
MAY ( cn $ ipNetmaskNumber $ l $ description $ manager ) )
objectclass ( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup'
DESC 'Abstraction of a netgroup. May refer to other netgroups'
SUP top STRUCTURAL
MUST cn
MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )
objectclass ( 1.3.6.1.1.1.2.9 NAME 'nisMap'
DESC 'A generic abstraction of a NIS map'
SUP top STRUCTURAL
MUST nisMapName
MAY description )
objectclass ( 1.3.6.1.1.1.2.10 NAME 'nisObject'
DESC 'An entry in a NIS map'
SUP top STRUCTURAL
MUST ( cn $ nisMapEntry $ nisMapName )
MAY description )
objectclass ( 1.3.6.1.1.1.2.11 NAME 'ieee802Device'
DESC 'A device with a MAC address'
SUP top AUXILIARY
MAY macAddress )
objectclass ( 1.3.6.1.1.1.2.12 NAME 'bootableDevice'
DESC 'A device with boot parameters'
SUP top AUXILIARY
MAY ( bootFile $ bootParameter ) )
objectclass ( 1.3.6.1.1.1.2.14 NAME 'nisKeyObject'
DESC 'An object with a public and secret key'
SUP top AUXILIARY
MUST ( cn $ nisPublicKey $ nisSecretKey )
MAY ( uidNumber $ description ) )
objectclass ( 1.3.6.1.1.1.2.15 NAME 'nisDomainObject'
DESC 'Associates a NIS domain with a naming context'
SUP top AUXILIARY
MUST nisDomain )
objectclass ( 1.3.6.1.1.1.2.16 NAME 'automountMap'
DESC 'An group of related automount objects'
SUP top STRUCTURAL
MUST ( automountMapName )
MAY description )
objectclass ( 1.3.6.1.1.1.2.17 NAME 'automount'
DESC 'An automount entry'
SUP top STRUCTURAL
MUST ( automountKey $ automountInformation )
MAY description )
# groupOfMembers - like groupOfNames but allows empty groups
# This is essential for memberOf overlay support
objectclass ( 2.16.840.1.113730.3.2.33 NAME 'groupOfMembers'
DESC 'A group with members (like groupOfNames but member is optional)'
SUP top STRUCTURAL
MUST cn
MAY ( member $ businessCategory $ description $ o $ ou $ owner $ seeAlso ) )

44
scripts/init-acls.sh Normal file
View File

@ -0,0 +1,44 @@
#!/bin/sh
set -e
. /scripts/utils.sh
log_info "Configuring ACLs..."
# Socket URL for ldapi - must use URL-encoded path
LDAPI_SOCKET="ldapi://%2Frun%2Fopenldap%2Fldapi"
# Start slapd temporarily
log_info "Starting slapd temporarily for ACL configuration..."
/usr/sbin/slapd -h "$LDAPI_SOCKET" -F /etc/openldap/slapd.d -u ldap -g ldap
sleep 2
# Wait for slapd
wait_for_slapd 30 "$LDAPI_SOCKET"
# Configure ACLs
log_info "Applying ACL rules..."
cat > /tmp/acls.ldif << EOF
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to * by dn.exact="cn=admin,${LDAP_BASE_DN}" manage by * break
olcAccess: {1}to attrs=userPassword by self write by anonymous auth by * none
olcAccess: {2}to dn.children="ou=People,${LDAP_BASE_DN}" by self read by * break
olcAccess: {3}to dn.subtree="ou=People,${LDAP_BASE_DN}" by dn.exact="cn=keycloak,ou=Services,${LDAP_BASE_DN}" read by dn.exact="cn=nextcloud,ou=Services,${LDAP_BASE_DN}" read by dn.exact="cn=gitea,ou=Services,${LDAP_BASE_DN}" read by dn.exact="cn=sssd,ou=Services,${LDAP_BASE_DN}" read by * break
olcAccess: {4}to dn.subtree="ou=Groups,${LDAP_BASE_DN}" by dn.exact="cn=keycloak,ou=Services,${LDAP_BASE_DN}" read by dn.exact="cn=nextcloud,ou=Services,${LDAP_BASE_DN}" read by dn.exact="cn=gitea,ou=Services,${LDAP_BASE_DN}" read by dn.exact="cn=sssd,ou=Services,${LDAP_BASE_DN}" read by * break
olcAccess: {5}to dn.subtree="ou=Domains,${LDAP_BASE_DN}" by dn.exact="cn=postfix,ou=Services,${LDAP_BASE_DN}" read by dn.exact="cn=dovecot,ou=Services,${LDAP_BASE_DN}" read by * break
olcAccess: {6}to * by users read by * none
EOF
ldapmodify -Y EXTERNAL -H "$LDAPI_SOCKET" -f /tmp/acls.ldif
# Stop temporary slapd
log_info "Stopping temporary slapd..."
pkill slapd || true
sleep 2
# Cleanup
rm -f /tmp/acls.ldif
log_info "ACL configuration complete"

107
scripts/init-config.sh Normal file
View File

@ -0,0 +1,107 @@
#!/bin/sh
set -e
. /scripts/utils.sh
log_info "Initializing cn=config..."
# Generate password hashes
LDAP_ADMIN_PASSWORD_HASH=$(hash_password "$LDAP_ADMIN_PASSWORD")
LDAP_CONFIG_PASSWORD_HASH=$(hash_password "$LDAP_CONFIG_PASSWORD")
export LDAP_ADMIN_PASSWORD_HASH LDAP_CONFIG_PASSWORD_HASH
# Create initial slapd.d configuration
rm -rf /etc/openldap/slapd.d/*
# Create base cn=config LDIF
cat > /tmp/init-config.ldif << EOF
dn: cn=config
objectClass: olcGlobal
cn: config
olcArgsFile: /run/openldap/slapd.args
olcPidFile: /run/openldap/slapd.pid
olcLogLevel: ${LDAP_LOG_LEVEL}
dn: cn=module{0},cn=config
objectClass: olcModuleList
cn: module{0}
olcModulePath: /usr/lib/openldap
olcModuleLoad: back_mdb.so
olcModuleLoad: memberof.so
olcModuleLoad: refint.so
olcModuleLoad: unique.so
olcModuleLoad: ppolicy.so
dn: cn=schema,cn=config
objectClass: olcSchemaConfig
cn: schema
dn: olcDatabase={-1}frontend,cn=config
objectClass: olcDatabaseConfig
objectClass: olcFrontendConfig
olcDatabase: {-1}frontend
olcSizeLimit: 500
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
olcAccess: {1}to dn.base="" by * read
olcAccess: {2}to dn.base="cn=subschema" by * read
dn: olcDatabase={0}config,cn=config
objectClass: olcDatabaseConfig
olcDatabase: {0}config
olcRootDN: cn=admin,cn=config
olcRootPW: ${LDAP_CONFIG_PASSWORD_HASH}
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break
dn: olcDatabase={1}mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: {1}mdb
olcSuffix: ${LDAP_BASE_DN}
olcRootDN: cn=admin,${LDAP_BASE_DN}
olcRootPW: ${LDAP_ADMIN_PASSWORD_HASH}
olcDbDirectory: /var/lib/openldap/openldap-data
olcDbIndex: objectClass eq
olcDbIndex: cn eq,pres,sub
olcDbIndex: entryCSN eq
olcDbIndex: entryUUID eq
olcDbMaxSize: 1073741824
EOF
# Add TLS configuration if enabled and certs exist
if [ "$LDAP_TLS_ENABLED" = "true" ] && [ -f "$LDAP_TLS_CERT_FILE" ] && [ -f "$LDAP_TLS_KEY_FILE" ]; then
log_info "Adding TLS configuration..."
cat >> /tmp/init-config.ldif << EOF
dn: cn=config
changetype: modify
add: olcTLSCertificateFile
olcTLSCertificateFile: ${LDAP_TLS_CERT_FILE}
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: ${LDAP_TLS_KEY_FILE}
EOF
if [ -f "$LDAP_TLS_CA_FILE" ]; then
cat >> /tmp/init-config.ldif << EOF
-
add: olcTLSCACertificateFile
olcTLSCACertificateFile: ${LDAP_TLS_CA_FILE}
EOF
fi
cat >> /tmp/init-config.ldif << EOF
-
add: olcTLSVerifyClient
olcTLSVerifyClient: ${LDAP_TLS_VERIFY_CLIENT}
EOF
fi
# Import the configuration
log_info "Importing cn=config with slapadd..."
/usr/sbin/slapadd -n 0 -F /etc/openldap/slapd.d -l /tmp/init-config.ldif
# Set proper ownership
chown -R ldap:ldap /etc/openldap/slapd.d
chown -R ldap:ldap /var/lib/openldap
log_info "cn=config initialization complete"

118
scripts/init-dit.sh Normal file
View File

@ -0,0 +1,118 @@
#!/bin/sh
set -e
. /scripts/utils.sh
log_info "Creating base DIT structure..."
# Socket URL for ldapi - must use URL-encoded path
LDAPI_SOCKET="ldapi://%2Frun%2Fopenldap%2Fldapi"
# Start slapd temporarily
log_info "Starting slapd temporarily for DIT creation..."
/usr/sbin/slapd -h "$LDAPI_SOCKET" -F /etc/openldap/slapd.d -u ldap -g ldap
sleep 2
# Wait for slapd
wait_for_slapd 30 "$LDAPI_SOCKET"
# Create base DIT LDIF
cat > /tmp/base-dit.ldif << EOF
# Base entry
dn: ${LDAP_BASE_DN}
objectClass: top
objectClass: dcObject
objectClass: organization
dc: ${LDAP_DC}
o: ${LDAP_ORGANISATION}
# People OU
dn: ou=People,${LDAP_BASE_DN}
objectClass: organizationalUnit
ou: People
description: User accounts
# Groups OU
dn: ou=Groups,${LDAP_BASE_DN}
objectClass: organizationalUnit
ou: Groups
description: Authorization groups
# Services OU
dn: ou=Services,${LDAP_BASE_DN}
objectClass: organizationalUnit
ou: Services
description: Service accounts for application binding
# Domains OU (for virtual mail domains)
dn: ou=Domains,${LDAP_BASE_DN}
objectClass: organizationalUnit
ou: Domains
description: Virtual mail domains
# Policies OU
dn: ou=Policies,${LDAP_BASE_DN}
objectClass: organizationalUnit
ou: Policies
description: Password and access policies
# Kerberos OU (for future use)
dn: ou=Kerberos,${LDAP_BASE_DN}
objectClass: organizationalUnit
ou: Kerberos
description: Kerberos realm container (premium feature)
EOF
# Add base DIT
log_info "Adding base organizational units..."
ldapadd -x -H "$LDAPI_SOCKET" -D "cn=admin,${LDAP_BASE_DN}" -w "${LDAP_ADMIN_PASSWORD}" -f /tmp/base-dit.ldif
# Create default password policy
log_info "Creating default password policy..."
cat > /tmp/default-policy.ldif << EOF
dn: cn=default,ou=Policies,${LDAP_BASE_DN}
objectClass: pwdPolicy
objectClass: device
cn: default
pwdAttribute: userPassword
pwdMaxAge: 7776000
pwdExpireWarning: 1209600
pwdInHistory: 5
pwdCheckQuality: 2
pwdMinLength: 12
pwdMaxFailure: 5
pwdLockout: TRUE
pwdLockoutDuration: 900
pwdGraceAuthNLimit: 3
pwdFailureCountInterval: 900
pwdMustChange: FALSE
pwdAllowUserChange: TRUE
pwdSafeModify: FALSE
EOF
ldapadd -x -H "$LDAPI_SOCKET" -D "cn=admin,${LDAP_BASE_DN}" -w "${LDAP_ADMIN_PASSWORD}" -f /tmp/default-policy.ldif || \
log_warn "Password policy may already exist"
# Create default admin group
log_info "Creating default admin group..."
cat > /tmp/admin-group.ldif << EOF
dn: cn=admins,ou=Groups,${LDAP_BASE_DN}
objectClass: groupOfMembers
objectClass: posixGroup
cn: admins
gidNumber: 10000
description: LDAP Administrators
EOF
ldapadd -x -H "$LDAPI_SOCKET" -D "cn=admin,${LDAP_BASE_DN}" -w "${LDAP_ADMIN_PASSWORD}" -f /tmp/admin-group.ldif || \
log_warn "Admin group may already exist"
# Stop temporary slapd
log_info "Stopping temporary slapd..."
pkill slapd || true
sleep 2
# Cleanup
rm -f /tmp/base-dit.ldif /tmp/default-policy.ldif /tmp/admin-group.ldif
log_info "Base DIT creation complete"

88
scripts/init-overlays.sh Normal file
View File

@ -0,0 +1,88 @@
#!/bin/sh
set -e
. /scripts/utils.sh
log_info "Configuring overlays..."
# Socket URL for ldapi - must use URL-encoded path
LDAPI_SOCKET="ldapi://%2Frun%2Fopenldap%2Fldapi"
# Start slapd temporarily to add overlays via LDAP
log_info "Starting slapd temporarily for overlay configuration..."
/usr/sbin/slapd -h "$LDAPI_SOCKET" -F /etc/openldap/slapd.d -u ldap -g ldap
sleep 2
# Wait for slapd
wait_for_slapd 30 "$LDAPI_SOCKET"
# 1. memberof overlay
log_info "Configuring memberof overlay..."
cat > /tmp/overlay-memberof.ldif << EOF
dn: olcOverlay=memberof,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcMemberOf
olcOverlay: memberof
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfMembers
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf
EOF
ldapadd -Y EXTERNAL -H "$LDAPI_SOCKET" -f /tmp/overlay-memberof.ldif 2>/dev/null || \
log_warn "memberof overlay may already exist"
# 2. refint (Referential Integrity) overlay
log_info "Configuring refint overlay..."
cat > /tmp/overlay-refint.ldif << EOF
dn: olcOverlay=refint,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcRefintConfig
olcOverlay: refint
olcRefintAttribute: member
olcRefintAttribute: memberOf
EOF
ldapadd -Y EXTERNAL -H "$LDAPI_SOCKET" -f /tmp/overlay-refint.ldif 2>/dev/null || \
log_warn "refint overlay may already exist"
# 3. unique (Attribute Uniqueness) overlay
log_info "Configuring unique overlay..."
cat > /tmp/overlay-unique.ldif << EOF
dn: olcOverlay=unique,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcUniqueConfig
olcOverlay: unique
olcUniqueURI: ldap:///ou=People,${LDAP_BASE_DN}?uid?sub
olcUniqueURI: ldap:///ou=People,${LDAP_BASE_DN}?mail?sub
olcUniqueURI: ldap:///ou=People,${LDAP_BASE_DN}?uidNumber?sub
olcUniqueURI: ldap:///ou=Groups,${LDAP_BASE_DN}?gidNumber?sub
EOF
ldapadd -Y EXTERNAL -H "$LDAPI_SOCKET" -f /tmp/overlay-unique.ldif 2>/dev/null || \
log_warn "unique overlay may already exist"
# 4. ppolicy (Password Policy) overlay
log_info "Configuring ppolicy overlay..."
cat > /tmp/overlay-ppolicy.ldif << EOF
dn: olcOverlay=ppolicy,olcDatabase={1}mdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcPPolicyConfig
olcOverlay: ppolicy
olcPPolicyDefault: cn=default,ou=Policies,${LDAP_BASE_DN}
olcPPolicyHashCleartext: TRUE
olcPPolicyUseLockout: TRUE
EOF
ldapadd -Y EXTERNAL -H "$LDAPI_SOCKET" -f /tmp/overlay-ppolicy.ldif 2>/dev/null || \
log_warn "ppolicy overlay may already exist"
# Stop the temporary slapd
log_info "Stopping temporary slapd..."
pkill slapd || true
sleep 2
# Cleanup
rm -f /tmp/overlay-*.ldif
log_info "Overlay configuration complete"

116
scripts/init-schemas.sh Normal file
View File

@ -0,0 +1,116 @@
#!/bin/sh
set -e
. /scripts/utils.sh
log_info "Loading schemas..."
SCHEMA_DIR="/etc/openldap/schema"
# We use slaptest to convert all schemas at once into the cn=config format
# This is more reliable than trying to load individual schemas
log_info "Converting schemas using slaptest..."
# Create a temporary slapd.conf with all schemas
TEMP_DIR="/tmp/schema-convert-$$"
mkdir -p "$TEMP_DIR/slapd.d"
cat > "$TEMP_DIR/slapd.conf" << EOF
# Core schemas (built-in)
include ${SCHEMA_DIR}/core.schema
include ${SCHEMA_DIR}/cosine.schema
include ${SCHEMA_DIR}/inetorgperson.schema
# Custom schemas - rfc2307bis replaces nis.schema
include ${SCHEMA_DIR}/rfc2307bis.schema
EOF
# Add openssh-lpk if it exists
if [ -f "${SCHEMA_DIR}/openssh-lpk.schema" ]; then
echo "include ${SCHEMA_DIR}/openssh-lpk.schema" >> "$TEMP_DIR/slapd.conf"
fi
# Add kerberos if it exists
if [ -f "${SCHEMA_DIR}/kerberos.schema" ]; then
echo "include ${SCHEMA_DIR}/kerberos.schema" >> "$TEMP_DIR/slapd.conf"
fi
# Add enterprise if it exists
if [ -f "${SCHEMA_DIR}/enterprise.schema" ]; then
echo "include ${SCHEMA_DIR}/enterprise.schema" >> "$TEMP_DIR/slapd.conf"
fi
log_info "Schema config file:"
cat "$TEMP_DIR/slapd.conf"
# Convert schemas to cn=config format using slaptest
log_info "Running slaptest to convert schemas..."
if /usr/sbin/slaptest -f "$TEMP_DIR/slapd.conf" -F "$TEMP_DIR/slapd.d" 2>&1; then
log_info "Schema conversion successful"
else
log_error "Schema conversion failed"
rm -rf "$TEMP_DIR"
exit 1
fi
# Copy the converted schema files to our slapd.d
log_info "Installing converted schemas..."
if [ -d "$TEMP_DIR/slapd.d/cn=config/cn=schema" ]; then
mkdir -p /etc/openldap/slapd.d/cn=config/cn=schema
cp -a "$TEMP_DIR/slapd.d/cn=config/cn=schema/"* /etc/openldap/slapd.d/cn=config/cn=schema/
log_info "Schemas installed:"
ls -la /etc/openldap/slapd.d/cn=config/cn=schema/
else
log_error "No schema directory found after conversion"
rm -rf "$TEMP_DIR"
exit 1
fi
# Cleanup
rm -rf "$TEMP_DIR"
# Fix ownership
chown -R ldap:ldap /etc/openldap/slapd.d
# Now add indexes for schema-defined attributes
log_info "Adding database indexes..."
# Socket URL for ldapi - must use URL-encoded path
LDAPI_SOCKET="ldapi://%2Frun%2Fopenldap%2Fldapi"
# Start slapd temporarily to add indexes
/usr/sbin/slapd -h "$LDAPI_SOCKET" -F /etc/openldap/slapd.d -u ldap -g ldap
sleep 2
wait_for_slapd 30 "$LDAPI_SOCKET"
cat > /tmp/add-indexes.ldif << EOF
dn: olcDatabase={1}mdb,cn=config
changetype: modify
add: olcDbIndex
olcDbIndex: uid eq,pres,sub
-
add: olcDbIndex
olcDbIndex: uidNumber eq
-
add: olcDbIndex
olcDbIndex: gidNumber eq
-
add: olcDbIndex
olcDbIndex: mail eq,pres,sub
-
add: olcDbIndex
olcDbIndex: memberOf eq
-
add: olcDbIndex
olcDbIndex: member eq
EOF
ldapmodify -Y EXTERNAL -H "$LDAPI_SOCKET" -f /tmp/add-indexes.ldif || log_warn "Some indexes may already exist"
# Stop temporary slapd
pkill slapd || true
sleep 2
rm -f /tmp/add-indexes.ldif
log_info "Schema loading complete"

108
scripts/init-services.sh Normal file
View File

@ -0,0 +1,108 @@
#!/bin/sh
set -e
. /scripts/utils.sh
log_info "Creating service accounts..."
# Socket URL for ldapi - must use URL-encoded path
LDAPI_SOCKET="ldapi://%2Frun%2Fopenldap%2Fldapi"
# Start slapd temporarily
log_info "Starting slapd temporarily for service account creation..."
/usr/sbin/slapd -h "$LDAPI_SOCKET" -F /etc/openldap/slapd.d -u ldap -g ldap
sleep 2
# Wait for slapd
wait_for_slapd 30 "$LDAPI_SOCKET"
# Generate passwords for each service if not provided
LDAP_SERVICE_KEYCLOAK_PASSWORD="${LDAP_SERVICE_KEYCLOAK_PASSWORD:-$(generate_password)}"
LDAP_SERVICE_NEXTCLOUD_PASSWORD="${LDAP_SERVICE_NEXTCLOUD_PASSWORD:-$(generate_password)}"
LDAP_SERVICE_GITEA_PASSWORD="${LDAP_SERVICE_GITEA_PASSWORD:-$(generate_password)}"
LDAP_SERVICE_POSTFIX_PASSWORD="${LDAP_SERVICE_POSTFIX_PASSWORD:-$(generate_password)}"
LDAP_SERVICE_DOVECOT_PASSWORD="${LDAP_SERVICE_DOVECOT_PASSWORD:-$(generate_password)}"
LDAP_SERVICE_SSSD_PASSWORD="${LDAP_SERVICE_SSSD_PASSWORD:-$(generate_password)}"
# Create service accounts LDIF
cat > /tmp/service-accounts.ldif << EOF
# Keycloak service account
dn: cn=keycloak,ou=Services,${LDAP_BASE_DN}
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: keycloak
description: Keycloak LDAP federation service account
userPassword: ${LDAP_SERVICE_KEYCLOAK_PASSWORD}
# Nextcloud service account
dn: cn=nextcloud,ou=Services,${LDAP_BASE_DN}
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: nextcloud
description: Nextcloud LDAP backend service account
userPassword: ${LDAP_SERVICE_NEXTCLOUD_PASSWORD}
# Gitea service account
dn: cn=gitea,ou=Services,${LDAP_BASE_DN}
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: gitea
description: Gitea LDAP authentication service account
userPassword: ${LDAP_SERVICE_GITEA_PASSWORD}
# Postfix service account
dn: cn=postfix,ou=Services,${LDAP_BASE_DN}
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: postfix
description: Postfix virtual mailbox lookup service account
userPassword: ${LDAP_SERVICE_POSTFIX_PASSWORD}
# Dovecot service account
dn: cn=dovecot,ou=Services,${LDAP_BASE_DN}
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: dovecot
description: Dovecot authentication service account
userPassword: ${LDAP_SERVICE_DOVECOT_PASSWORD}
# SSSD service account
dn: cn=sssd,ou=Services,${LDAP_BASE_DN}
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: sssd
description: SSSD NSS/PAM service account
userPassword: ${LDAP_SERVICE_SSSD_PASSWORD}
EOF
# Add service accounts
ldapadd -x -H "$LDAPI_SOCKET" -D "cn=admin,${LDAP_BASE_DN}" -w "${LDAP_ADMIN_PASSWORD}" -f /tmp/service-accounts.ldif || \
log_warn "Some service accounts may already exist"
# Output generated passwords to a file for reference
cat > /var/lib/openldap/service-passwords.txt << EOF
# Service Account Passwords (generated on first run)
# IMPORTANT: Store these securely and delete this file after noting passwords
LDAP_SERVICE_KEYCLOAK_PASSWORD=${LDAP_SERVICE_KEYCLOAK_PASSWORD}
LDAP_SERVICE_NEXTCLOUD_PASSWORD=${LDAP_SERVICE_NEXTCLOUD_PASSWORD}
LDAP_SERVICE_GITEA_PASSWORD=${LDAP_SERVICE_GITEA_PASSWORD}
LDAP_SERVICE_POSTFIX_PASSWORD=${LDAP_SERVICE_POSTFIX_PASSWORD}
LDAP_SERVICE_DOVECOT_PASSWORD=${LDAP_SERVICE_DOVECOT_PASSWORD}
LDAP_SERVICE_SSSD_PASSWORD=${LDAP_SERVICE_SSSD_PASSWORD}
EOF
chmod 600 /var/lib/openldap/service-passwords.txt
chown ldap:ldap /var/lib/openldap/service-passwords.txt
log_info "Service account passwords saved to /var/lib/openldap/service-passwords.txt"
log_warn "IMPORTANT: Retrieve these passwords and delete the file for security"
# Stop temporary slapd
log_info "Stopping temporary slapd..."
pkill slapd || true
sleep 2
# Cleanup
rm -f /tmp/service-accounts.ldif
log_info "Service account creation complete"

91
scripts/utils.sh Normal file
View File

@ -0,0 +1,91 @@
#!/bin/sh
# Utility functions for OpenLDAP initialization
# Logging functions
log_info() {
echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_warn() {
echo "[WARN] $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2
}
log_error() {
echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1" >&2
}
# Generate a random password
generate_password() {
head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 24
}
# Hash password using SSHA
hash_password() {
local password="$1"
/usr/sbin/slappasswd -s "$password"
}
# Wait for slapd to be ready
# Args: max_attempts [socket_url]
wait_for_slapd() {
local max_attempts="${1:-30}"
local socket_url="${2:-ldapi://%2Frun%2Fopenldap%2Fldapi}"
local attempt=0
while [ $attempt -lt $max_attempts ]; do
if ldapsearch -x -H "$socket_url" -b "" -s base "objectClass=*" >/dev/null 2>&1; then
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
log_error "slapd did not become ready in time"
return 1
}
# Template substitution - replaces ${VAR} with environment variable values
process_template() {
local template="$1"
local output="$2"
# Use envsubst-like behavior with sed
cp "$template" "$output"
# Replace known variables
sed -i "s|\${LDAP_BASE_DN}|${LDAP_BASE_DN}|g" "$output"
sed -i "s|\${LDAP_DC}|${LDAP_DC}|g" "$output"
sed -i "s|\${LDAP_DOMAIN}|${LDAP_DOMAIN}|g" "$output"
sed -i "s|\${LDAP_ORGANISATION}|${LDAP_ORGANISATION}|g" "$output"
sed -i "s|\${LDAP_ADMIN_PASSWORD_HASH}|${LDAP_ADMIN_PASSWORD_HASH}|g" "$output"
sed -i "s|\${LDAP_CONFIG_PASSWORD_HASH}|${LDAP_CONFIG_PASSWORD_HASH}|g" "$output"
sed -i "s|\${LDAP_TLS_CERT_FILE}|${LDAP_TLS_CERT_FILE}|g" "$output"
sed -i "s|\${LDAP_TLS_KEY_FILE}|${LDAP_TLS_KEY_FILE}|g" "$output"
sed -i "s|\${LDAP_TLS_CA_FILE}|${LDAP_TLS_CA_FILE}|g" "$output"
sed -i "s|\${LDAP_TLS_VERIFY_CLIENT}|${LDAP_TLS_VERIFY_CLIENT}|g" "$output"
}
# Default LDAPI socket URL
LDAPI_SOCKET="${LDAPI_SOCKET:-ldapi://%2Frun%2Fopenldap%2Fldapi}"
# Check if a DN exists
dn_exists() {
local dn="$1"
ldapsearch -x -H "$LDAPI_SOCKET" -b "$dn" -s base "objectClass=*" >/dev/null 2>&1
}
# Add LDIF if it doesn't cause errors (ignore "already exists" errors)
ldif_add_safe() {
local ldif_file="$1"
local result
result=$(ldapadd -x -H "$LDAPI_SOCKET" -D "cn=admin,$LDAP_BASE_DN" -w "$LDAP_ADMIN_PASSWORD" -f "$ldif_file" 2>&1) || {
if echo "$result" | grep -q "Already exists"; then
log_warn "Entry already exists, skipping"
return 0
else
log_error "Failed to add LDIF: $result"
return 1
fi
}
}