Imported sources.

This commit is contained in:
2026-05-04 07:07:52 +02:00
commit a3f3105081
26 changed files with 12475 additions and 0 deletions

205
bootstrap/init.py Normal file
View File

@@ -0,0 +1,205 @@
#!/usr/bin/env python3
import csv
import os
import socket
import subprocess
import time
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
LDAPI_SOCKET = "/var/run/slapd/ldapi"
LDIF_DIR = Path("/bootstrap/ldif")
CSV_DIR = Path("/bootstrap/accounts")
INITIALIZED_FLAG = Path("/var/lib/ldap/.initialized")
SLAPD_D = Path("/etc/ldap/slapd.d")
base_dn = os.environ["LDAP_BASE_DN"]
password = os.environ["LDAP_PASSWORD"]
tls_enabled = os.environ.get("TLS_ENABLED") == "1"
admin_dn = f"cn=admin,{base_dn}"
def hash_password(password: str) -> str:
return subprocess.check_output(
["slappasswd", "-h", "{SSHA}", "-s", password],
text=True,
).strip()
def run_command(cmd: list, input: str) -> None:
proc = subprocess.run(cmd, input=input, text=True, capture_output=True, check=False)
if proc.stdout:
print(proc.stdout, end="")
if proc.stderr:
print(proc.stderr, end="")
if proc.returncode != 0:
raise subprocess.CalledProcessError(proc.returncode, proc.args, output=proc.stdout, stderr=proc.stderr)
def apply_ldif(path: Path, env: Environment, command: str = "ldapadd", **ctx) -> None:
print(f"Applying LDIF: {path.name}")
rendered = env.get_template(path.name).render(**ctx)
run_command([command, "-Q", "-Y", "EXTERNAL", "-H", "ldapi:///"], rendered)
print(f"Finished LDIF: {path.name}")
def wait_for_slapd(timeout: int = 30) -> None:
for _ in range(timeout):
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(LDAPI_SOCKET)
sock.close()
return
except OSError:
time.sleep(1)
raise RuntimeError("slapd did not become ready in time")
def stop_slapd(proc: subprocess.Popen, timeout: int = 30) -> None:
proc.terminate()
try:
proc.wait(timeout=timeout)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
def group_exists(name: str, group_ou: str) -> bool:
# fmt: off
result = subprocess.run(
[
"ldapsearch", "-Q", "-Y", "EXTERNAL", "-H", "ldapi:///",
"-b", f"ou={group_ou},{base_dn}", "-s", "one",
f"(&(objectClass=groupOfNames)(cn={name}))",
"cn",
],
capture_output=True,
text=True,
)
# fmt: on
return "numEntries: 1" in result.stdout
def read_users(path: Path, fields: list[str]) -> list[dict]:
users = []
with open(path, newline="") as f:
for row in csv.reader(f):
if not row or row[0].strip().startswith("#"):
continue
users.append(dict(zip(fields, (c.strip() for c in row))))
return users
def main():
print("First run — starting initialisation...")
env = Environment(loader=FileSystemLoader(str(LDIF_DIR)), keep_trailing_newline=True)
# fmt: off
proc = subprocess.Popen([
"slapd",
"-d", "0",
"-F", str(SLAPD_D),
"-u", "openldap",
"-g", "openldap",
"-h", "ldapi:///",
])
# fmt: on
wait_for_slapd()
print("slapd is ready.")
apply_ldif(LDIF_DIR / "config-memberof-module.ldif", env)
apply_ldif(LDIF_DIR / "config-memberof-overlay.ldif", env)
apply_ldif(LDIF_DIR / "config-indexes.ldif", env)
apply_ldif(LDIF_DIR / "config-performance.ldif", env)
apply_ldif(LDIF_DIR / "config-acl.ldif", env, base_dn=base_dn, admin_dn=admin_dn)
if tls_enabled:
apply_ldif(LDIF_DIR / "config-tls.ldif", env)
print("cn=config updated.")
password_hash = hash_password(password)
ctx = dict(
base_dn=base_dn,
password_hash=password_hash,
)
apply_ldif(LDIF_DIR / "base.ldif", env, **ctx)
apply_ldif(LDIF_DIR / "service-accounts.ldif", env, **ctx)
# Process optional account CSV files.
user_files = {
"users.csv": {
"name": "users",
"description": "Regular users",
"fields": ["uid", "gn", "sn", "mail"],
"group_ou": "groups",
"member_ou": "users",
},
"admins.csv": {
"name": "admins",
"description": "Administrators",
"fields": ["uid", "gn", "sn", "mail"],
"group_ou": "privileged-groups",
"member_ou": "users",
},
"posix-users.csv": {
"name": "users",
"description": "POSIX users",
"fields": ["uid", "gn", "sn", "mail", "uidNumber", "gidNumber"],
"group_ou": "groups",
"member_ou": "users",
},
}
for filename, group in user_files.items():
path = CSV_DIR / filename
if not path.exists():
# Backward-compatible fallback for older bind-mount layout.
path = Path("/bootstrap") / filename
if path.exists():
print(f' Reading {group["name"]} from {filename}...')
users = read_users(path, group["fields"])
if users:
ldif = Path(filename).stem + ".ldif"
for user in users:
print(f' Adding user: {user["uid"]}')
apply_ldif(
LDIF_DIR / ldif,
env,
base_dn=base_dn,
password_hash=password_hash,
**user,
)
uids = [u["uid"] for u in users]
if group_exists(group["name"], group["group_ou"]):
apply_ldif(
LDIF_DIR / "add-member.ldif",
env,
base_dn=base_dn,
name=group["name"],
group_ou=group["group_ou"],
member_ou=group["member_ou"],
uids=uids,
)
else:
apply_ldif(
LDIF_DIR / "group.ldif",
env,
base_dn=base_dn,
name=group["name"],
description=group["description"],
group_ou=group["group_ou"],
member_ou=group["member_ou"],
uids=uids,
)
INITIALIZED_FLAG.touch()
stop_slapd(proc)
print("Bootstrap complete.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,6 @@
dn: cn={{ name }},ou={{ group_ou }},{{ base_dn }}
changeType: modify
add: member
{% for uid in uids -%}
member: uid={{ uid }},ou={{ member_ou }},{{ base_dn }}
{% endfor %}

View File

@@ -0,0 +1,9 @@
dn: uid={{ uid }},ou=users,{{ base_dn }}
objectClass: inetOrgPerson
objectClass: shadowAccount
uid: {{ uid }}
givenName: {{ gn }}
sn: {{ sn }}
cn: {{ gn }} {{ sn }}
mail: {{ mail }}
userPassword: {{ password_hash }}

19
bootstrap/ldif/base.ldif Normal file
View File

@@ -0,0 +1,19 @@
dn: ou=users,{{ base_dn }}
objectClass: organizationalUnit
ou: users
description: All users
dn: ou=groups,{{ base_dn }}
objectClass: organizationalUnit
ou: groups
description: Regular groups
dn: ou=privileged-groups,{{ base_dn }}
objectClass: organizationalUnit
ou: privileged-groups
description: Privileged groups
dn: ou=service-accounts,{{ base_dn }}
objectClass: organizationalUnit
ou: service-accounts
description: Service accounts

View File

@@ -0,0 +1,44 @@
dn: olcDatabase={-1}frontend,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to *
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
by dn.exact="cn=admin,{{ base_dn }}" manage
by group.exact="cn=admins,ou=privileged-groups,{{ base_dn }}" manage
by * break
olcAccess: {1}to dn.exact=""
by * read
olcAccess: {2}to dn.base="cn=Subschema"
by * read
dn: olcDatabase={0}config,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to *
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
by dn.exact="cn=admin,{{ base_dn }}" manage
by group.exact="cn=admins,ou=privileged-groups,{{ base_dn }}" manage
by * break
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to attrs=userPassword
by self write
by anonymous auth
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
by dn.exact="{{ admin_dn }}" manage
by group.exact="cn=admins,ou=privileged-groups,{{ base_dn }}" manage
by * none
olcAccess: {1}to attrs=shadowLastChange
by self write
by * read
olcAccess: {2}to dn.base=""
by * read
olcAccess: {3}to *
by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" manage
by dn.exact="{{ admin_dn }}" manage
by dn.exact="cn=readonly,ou=service-accounts,{{ base_dn }}" read
by group.exact="cn=admins,ou=privileged-groups,{{ base_dn }}" manage
by self read
by * none

View File

@@ -0,0 +1,7 @@
dn: olcDatabase={1}mdb,cn=config
changetype: modify
add: olcDbIndex
olcDbIndex: mail eq,pres
-
add: olcDbIndex
olcDbIndex: memberOf eq

View File

@@ -0,0 +1,4 @@
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: memberof

View File

@@ -0,0 +1,10 @@
dn: olcOverlay=memberof,olcDatabase={1}mdb,cn=config
changetype: add
objectClass: olcOverlayConfig
objectClass: olcMemberOf
olcOverlay: memberof
olcMemberOfDangling: error
olcMemberOfRefint: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf

View File

@@ -0,0 +1,9 @@
dn: cn=config
changetype: modify
replace: olcThreads
olcThreads: 4
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcDbMaxSize
olcDbMaxSize: 134217728

View File

@@ -0,0 +1,13 @@
dn: cn=config
changetype: modify
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ldap/certs/ca_cert.pem
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ldap/certs/server_cert.pem
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ldap/certs/server_key.pem
-
add: olcTLSVerifyClient
olcTLSVerifyClient: never

View File

@@ -0,0 +1,7 @@
dn: cn={{ name }},ou={{ group_ou }},{{ base_dn }}
objectClass: groupOfNames
cn: {{ name }}
description: {{ description }}
{% for uid in uids -%}
member: uid={{ uid }},ou={{ member_ou }},{{ base_dn }}
{% endfor %}

View File

@@ -0,0 +1,14 @@
dn: uid={{ uid }},ou=users,{{ base_dn }}
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: {{ uid }}
givenName: {{ gn }}
sn: {{ sn }}
cn: {{ gn }} {{ sn }}
mail: {{ mail }}
userPassword: {{ password_hash }}
uidNumber: {{ uidNumber }}
gidNumber: {{ gidNumber }}
homeDirectory: /home/{{ uid }}
loginShell: /usr/bin/bash

View File

@@ -0,0 +1,14 @@
dn: uid={{ uid }},ou=users,{{ base_dn }}
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: {{ uid }}
givenName: {{ gn }}
sn: {{ sn }}
cn: {{ gn }} {{ sn }}
mail: {{ mail }}
userPassword: {{ password_hash }}
uidNumber: {{ uidNumber }}
gidNumber: {{ gidNumber }}
homeDirectory: /home/{{ uid }}
loginShell: /usr/bin/bash

View File

@@ -0,0 +1,6 @@
dn: cn=readonly,ou=service-accounts,{{ base_dn }}
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: readonly
description: Read-only service account
userPassword: {{ password_hash }}

View File

@@ -0,0 +1,9 @@
dn: uid={{ uid }},ou=users,{{ base_dn }}
objectClass: inetOrgPerson
objectClass: shadowAccount
uid: {{ uid }}
givenName: {{ gn }}
sn: {{ sn }}
cn: {{ gn }} {{ sn }}
mail: {{ mail }}
userPassword: {{ password_hash }}