First commit from the robot
This commit is contained in:
13
scripts/__init__.py
Normal file
13
scripts/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
LDAP Docker Management Scripts
|
||||
|
||||
This package contains management and utility scripts for the LDAP Docker
|
||||
development environment.
|
||||
|
||||
Modules:
|
||||
- cli: Command-line interface for managing LDAP server
|
||||
- generate_certs: SSL/TLS certificate generation utility
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = ["cli", "generate_certs"]
|
||||
463
scripts/cli.py
Normal file
463
scripts/cli.py
Normal file
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CLI tool for managing the LDAP Docker development environment.
|
||||
|
||||
This tool provides convenient commands for starting, stopping, and testing
|
||||
the LDAP server, as well as managing test users and certificates.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
try:
|
||||
import ldap3
|
||||
from ldap3 import ALL, Connection, Server
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
except ImportError:
|
||||
ldap3 = None
|
||||
|
||||
|
||||
# Constants
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
CERTS_DIR = PROJECT_ROOT / "certs"
|
||||
LDIF_DIR = PROJECT_ROOT / "ldif"
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_PORT = 389
|
||||
DEFAULT_LDAPS_PORT = 636
|
||||
DEFAULT_BASE_DN = "dc=testing,dc=local"
|
||||
DEFAULT_ADMIN_DN = "cn=admin,dc=testing,dc=local"
|
||||
DEFAULT_ADMIN_PASSWORD = "admin_password"
|
||||
|
||||
|
||||
def run_command(cmd: list[str], cwd: Optional[Path] = None, check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a shell command and return the result."""
|
||||
if cwd is None:
|
||||
cwd = PROJECT_ROOT
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check,
|
||||
)
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
click.echo(f"Error running command: {' '.join(cmd)}", err=True)
|
||||
click.echo(f"Exit code: {e.returncode}", err=True)
|
||||
if e.stdout:
|
||||
click.echo(f"stdout: {e.stdout}", err=True)
|
||||
if e.stderr:
|
||||
click.echo(f"stderr: {e.stderr}", err=True)
|
||||
raise
|
||||
|
||||
|
||||
def check_docker():
|
||||
"""Check if Docker is available."""
|
||||
try:
|
||||
result = run_command(["docker", "version"], check=False)
|
||||
if result.returncode != 0:
|
||||
click.echo("Error: Docker is not running or not installed.", err=True)
|
||||
click.echo("Please ensure Docker (or Rancher Desktop) is running.", err=True)
|
||||
sys.exit(1)
|
||||
except FileNotFoundError:
|
||||
click.echo("Error: Docker command not found.", err=True)
|
||||
click.echo("Please install Docker or Rancher Desktop.", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def check_certificates():
|
||||
"""Check if SSL certificates exist."""
|
||||
required_files = ["ca.crt", "server.crt", "server.key"]
|
||||
missing = [f for f in required_files if not (CERTS_DIR / f).exists()]
|
||||
|
||||
if missing:
|
||||
click.echo("⚠️ Warning: Missing SSL certificate files:", err=True)
|
||||
for f in missing:
|
||||
click.echo(f" - {CERTS_DIR / f}", err=True)
|
||||
click.echo("\nYou can:", err=True)
|
||||
click.echo(" 1. Copy your dev-ca certificates to the certs/ directory", err=True)
|
||||
click.echo(" 2. Generate self-signed certificates: ldap-docker certs generate", err=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="0.1.0")
|
||||
def cli():
|
||||
"""LDAP Docker Development Tool - Manage your OpenLDAP development environment."""
|
||||
pass
|
||||
|
||||
|
||||
@cli.group()
|
||||
def server():
|
||||
"""Manage the LDAP server container."""
|
||||
pass
|
||||
|
||||
|
||||
@server.command("start")
|
||||
@click.option("--detach", "-d", is_flag=True, default=True, help="Run in detached mode")
|
||||
@click.option("--build", is_flag=True, help="Build images before starting")
|
||||
def server_start(detach: bool, build: bool):
|
||||
"""Start the LDAP server and optional phpLDAPadmin."""
|
||||
check_docker()
|
||||
check_certificates()
|
||||
|
||||
cmd = ["docker-compose", "up"]
|
||||
if detach:
|
||||
cmd.append("-d")
|
||||
if build:
|
||||
cmd.append("--build")
|
||||
|
||||
click.echo("Starting LDAP server...")
|
||||
result = run_command(cmd)
|
||||
|
||||
if result.returncode == 0:
|
||||
click.echo("✅ LDAP server started successfully!")
|
||||
click.echo(f"\nLDAP server is available at:")
|
||||
click.echo(f" - LDAP: ldap://localhost:389")
|
||||
click.echo(f" - LDAPS: ldaps://localhost:636")
|
||||
click.echo(f" - Admin: http://localhost:8080 (phpLDAPadmin)")
|
||||
click.echo(f"\nAdmin credentials:")
|
||||
click.echo(f" - DN: {DEFAULT_ADMIN_DN}")
|
||||
click.echo(f" - Password: {DEFAULT_ADMIN_PASSWORD}")
|
||||
|
||||
if detach:
|
||||
click.echo("\nWaiting for server to be ready...")
|
||||
time.sleep(5)
|
||||
# Try to check if server is responding
|
||||
result = run_command(
|
||||
["docker-compose", "ps", "--filter", "status=running"],
|
||||
check=False
|
||||
)
|
||||
if "ldap-server" in result.stdout:
|
||||
click.echo("✅ Server is running")
|
||||
|
||||
|
||||
@server.command("stop")
|
||||
def server_stop():
|
||||
"""Stop the LDAP server."""
|
||||
check_docker()
|
||||
|
||||
click.echo("Stopping LDAP server...")
|
||||
run_command(["docker-compose", "stop"])
|
||||
click.echo("✅ LDAP server stopped")
|
||||
|
||||
|
||||
@server.command("restart")
|
||||
def server_restart():
|
||||
"""Restart the LDAP server."""
|
||||
check_docker()
|
||||
|
||||
click.echo("Restarting LDAP server...")
|
||||
run_command(["docker-compose", "restart"])
|
||||
click.echo("✅ LDAP server restarted")
|
||||
|
||||
click.echo("\nWaiting for server to be ready...")
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
@server.command("down")
|
||||
@click.option("--volumes", "-v", is_flag=True, help="Remove volumes (deletes all data)")
|
||||
def server_down(volumes: bool):
|
||||
"""Stop and remove the LDAP server containers."""
|
||||
check_docker()
|
||||
|
||||
cmd = ["docker-compose", "down"]
|
||||
if volumes:
|
||||
if not click.confirm("⚠️ This will delete all LDAP data. Continue?"):
|
||||
click.echo("Aborted.")
|
||||
return
|
||||
cmd.append("-v")
|
||||
|
||||
click.echo("Removing LDAP server containers...")
|
||||
run_command(cmd)
|
||||
click.echo("✅ Containers removed")
|
||||
|
||||
|
||||
@server.command("logs")
|
||||
@click.option("--follow", "-f", is_flag=True, help="Follow log output")
|
||||
@click.option("--tail", "-n", default=100, help="Number of lines to show from the end")
|
||||
@click.option("--service", default="openldap", help="Service to show logs for")
|
||||
def server_logs(follow: bool, tail: int, service: str):
|
||||
"""View LDAP server logs."""
|
||||
check_docker()
|
||||
|
||||
cmd = ["docker-compose", "logs", f"--tail={tail}"]
|
||||
if follow:
|
||||
cmd.append("-f")
|
||||
cmd.append(service)
|
||||
|
||||
# For follow mode, we want to pass through to the terminal
|
||||
try:
|
||||
subprocess.run(cmd, cwd=PROJECT_ROOT)
|
||||
except KeyboardInterrupt:
|
||||
click.echo("\n")
|
||||
|
||||
|
||||
@server.command("status")
|
||||
def server_status():
|
||||
"""Check LDAP server status."""
|
||||
check_docker()
|
||||
|
||||
result = run_command(["docker-compose", "ps"], check=False)
|
||||
click.echo(result.stdout)
|
||||
|
||||
|
||||
@cli.group()
|
||||
def certs():
|
||||
"""Manage SSL/TLS certificates."""
|
||||
pass
|
||||
|
||||
|
||||
@certs.command("generate")
|
||||
@click.option("--force", is_flag=True, help="Overwrite existing certificates")
|
||||
@click.option("--hostname", default="ldap.testing.local", help="Server hostname")
|
||||
def certs_generate(force: bool, hostname: str):
|
||||
"""Generate self-signed SSL certificates for development."""
|
||||
script_path = PROJECT_ROOT / "scripts" / "generate_certs.py"
|
||||
|
||||
if not script_path.exists():
|
||||
click.echo(f"Error: Certificate generation script not found: {script_path}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
cmd = [sys.executable, str(script_path), "--hostname", hostname]
|
||||
if force:
|
||||
cmd.append("--force")
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
click.echo("Failed to generate certificates", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@certs.command("check")
|
||||
def certs_check():
|
||||
"""Verify SSL certificates."""
|
||||
required_files = {
|
||||
"ca.crt": "CA Certificate",
|
||||
"server.crt": "Server Certificate",
|
||||
"server.key": "Server Private Key",
|
||||
}
|
||||
|
||||
click.echo("Checking SSL certificates...\n")
|
||||
|
||||
all_exist = True
|
||||
for filename, description in required_files.items():
|
||||
filepath = CERTS_DIR / filename
|
||||
if filepath.exists():
|
||||
size = filepath.stat().st_size
|
||||
click.echo(f"✅ {description}: {filepath} ({size} bytes)")
|
||||
else:
|
||||
click.echo(f"❌ {description}: {filepath} (missing)")
|
||||
all_exist = False
|
||||
|
||||
if all_exist:
|
||||
click.echo("\n✅ All required certificates are present")
|
||||
|
||||
# Try to verify the certificate chain
|
||||
try:
|
||||
result = run_command([
|
||||
"openssl", "verify", "-CAfile",
|
||||
str(CERTS_DIR / "ca.crt"),
|
||||
str(CERTS_DIR / "server.crt")
|
||||
], check=False)
|
||||
|
||||
if result.returncode == 0:
|
||||
click.echo("✅ Certificate chain is valid")
|
||||
else:
|
||||
click.echo("⚠️ Certificate chain verification failed")
|
||||
click.echo(result.stderr)
|
||||
except FileNotFoundError:
|
||||
click.echo("ℹ️ OpenSSL not found, skipping certificate verification")
|
||||
else:
|
||||
click.echo("\n❌ Some certificates are missing")
|
||||
click.echo("Run 'ldap-docker certs generate' to create them")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.group()
|
||||
def test():
|
||||
"""Test LDAP server connectivity and queries."""
|
||||
pass
|
||||
|
||||
|
||||
@test.command("connection")
|
||||
@click.option("--host", default=DEFAULT_HOST, help="LDAP server host")
|
||||
@click.option("--port", default=DEFAULT_PORT, help="LDAP server port")
|
||||
@click.option("--use-ssl", is_flag=True, help="Use LDAPS instead of LDAP")
|
||||
def test_connection(host: str, port: int, use_ssl: bool):
|
||||
"""Test basic connection to LDAP server."""
|
||||
if ldap3 is None:
|
||||
click.echo("Error: ldap3 library not installed", err=True)
|
||||
click.echo("Install it with: uv pip install ldap3", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
if use_ssl:
|
||||
port = DEFAULT_LDAPS_PORT
|
||||
url = f"ldaps://{host}:{port}"
|
||||
else:
|
||||
url = f"ldap://{host}:{port}"
|
||||
|
||||
click.echo(f"Testing connection to {url}...")
|
||||
|
||||
try:
|
||||
server = Server(url, get_info=ALL, use_ssl=use_ssl)
|
||||
conn = Connection(server, auto_bind=True)
|
||||
|
||||
click.echo("✅ Successfully connected to LDAP server")
|
||||
click.echo(f"\nServer info:")
|
||||
click.echo(f" Vendor: {server.info.vendor_name if server.info else 'Unknown'}")
|
||||
click.echo(f" Version: {server.info.vendor_version if server.info else 'Unknown'}")
|
||||
|
||||
conn.unbind()
|
||||
|
||||
except LDAPException as e:
|
||||
click.echo(f"❌ Connection failed: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@test.command("auth")
|
||||
@click.option("--host", default=DEFAULT_HOST, help="LDAP server host")
|
||||
@click.option("--port", default=DEFAULT_PORT, help="LDAP server port")
|
||||
@click.option("--use-ssl", is_flag=True, help="Use LDAPS")
|
||||
@click.option("--user", default=DEFAULT_ADMIN_DN, help="User DN")
|
||||
@click.option("--password", default=DEFAULT_ADMIN_PASSWORD, help="Password")
|
||||
def test_auth(host: str, port: int, use_ssl: bool, user: str, password: str):
|
||||
"""Test authentication with LDAP server."""
|
||||
if ldap3 is None:
|
||||
click.echo("Error: ldap3 library not installed", err=True)
|
||||
click.echo("Install it with: uv pip install ldap3", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
if use_ssl:
|
||||
port = DEFAULT_LDAPS_PORT
|
||||
url = f"ldaps://{host}:{port}"
|
||||
else:
|
||||
url = f"ldap://{host}:{port}"
|
||||
|
||||
click.echo(f"Testing authentication to {url}...")
|
||||
click.echo(f"User: {user}")
|
||||
|
||||
try:
|
||||
server = Server(url, get_info=ALL, use_ssl=use_ssl)
|
||||
conn = Connection(server, user=user, password=password, auto_bind=True)
|
||||
|
||||
click.echo("✅ Authentication successful")
|
||||
|
||||
# Try to perform a simple search
|
||||
conn.search(DEFAULT_BASE_DN, "(objectClass=*)", search_scope="BASE")
|
||||
if conn.entries:
|
||||
click.echo(f"✅ Base DN accessible: {DEFAULT_BASE_DN}")
|
||||
|
||||
conn.unbind()
|
||||
|
||||
except LDAPException as e:
|
||||
click.echo(f"❌ Authentication failed: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@test.command("users")
|
||||
@click.option("--host", default=DEFAULT_HOST, help="LDAP server host")
|
||||
@click.option("--port", default=DEFAULT_PORT, help="LDAP server port")
|
||||
@click.option("--use-ssl", is_flag=True, help="Use LDAPS")
|
||||
def test_users(host: str, port: int, use_ssl: bool):
|
||||
"""List all users in the LDAP directory."""
|
||||
if ldap3 is None:
|
||||
click.echo("Error: ldap3 library not installed", err=True)
|
||||
click.echo("Install it with: uv pip install ldap3", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
if use_ssl:
|
||||
port = DEFAULT_LDAPS_PORT
|
||||
url = f"ldaps://{host}:{port}"
|
||||
else:
|
||||
url = f"ldap://{host}:{port}"
|
||||
|
||||
try:
|
||||
server = Server(url, get_info=ALL, use_ssl=use_ssl)
|
||||
conn = Connection(
|
||||
server,
|
||||
user=DEFAULT_ADMIN_DN,
|
||||
password=DEFAULT_ADMIN_PASSWORD,
|
||||
auto_bind=True
|
||||
)
|
||||
|
||||
# Search for all users
|
||||
conn.search(
|
||||
DEFAULT_BASE_DN,
|
||||
"(objectClass=inetOrgPerson)",
|
||||
attributes=["uid", "cn", "mail", "uidNumber"]
|
||||
)
|
||||
|
||||
if conn.entries:
|
||||
click.echo(f"Found {len(conn.entries)} user(s):\n")
|
||||
for entry in conn.entries:
|
||||
click.echo(f" - {entry.cn}: {entry.uid} ({entry.mail})")
|
||||
else:
|
||||
click.echo("No users found")
|
||||
|
||||
conn.unbind()
|
||||
|
||||
except LDAPException as e:
|
||||
click.echo(f"❌ Query failed: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@cli.command("init")
|
||||
def init():
|
||||
"""Initialize the LDAP Docker environment."""
|
||||
click.echo("Initializing LDAP Docker environment...\n")
|
||||
|
||||
# Check Docker
|
||||
click.echo("1. Checking Docker...")
|
||||
check_docker()
|
||||
click.echo(" ✅ Docker is available\n")
|
||||
|
||||
# Check certificates
|
||||
click.echo("2. Checking SSL certificates...")
|
||||
if not check_certificates():
|
||||
if click.confirm("\nGenerate self-signed certificates now?", default=True):
|
||||
certs_generate.callback(force=False, hostname="ldap.testing.local")
|
||||
else:
|
||||
click.echo("\nℹ️ You can generate certificates later with: ldap-docker certs generate")
|
||||
click.echo(" Or copy your dev-ca certificates to the certs/ directory")
|
||||
else:
|
||||
click.echo(" ✅ Certificates are present\n")
|
||||
|
||||
# Start server
|
||||
click.echo("\n3. Starting LDAP server...")
|
||||
if click.confirm("Start the LDAP server now?", default=True):
|
||||
server_start.callback(detach=True, build=False)
|
||||
else:
|
||||
click.echo("\nℹ️ You can start the server later with: ldap-docker server start")
|
||||
|
||||
click.echo("\n✅ Initialization complete!")
|
||||
click.echo("\nUseful commands:")
|
||||
click.echo(" - View logs: ldap-docker server logs -f")
|
||||
click.echo(" - Test connection: ldap-docker test connection")
|
||||
click.echo(" - List users: ldap-docker test users")
|
||||
click.echo(" - Stop server: ldap-docker server stop")
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the CLI."""
|
||||
try:
|
||||
cli()
|
||||
except KeyboardInterrupt:
|
||||
click.echo("\n\nInterrupted by user")
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
click.echo(f"Error: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
299
scripts/generate_certs.py
Normal file
299
scripts/generate_certs.py
Normal file
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate self-signed SSL/TLS certificates for LDAP server testing.
|
||||
|
||||
This script creates a CA certificate and a server certificate for development use.
|
||||
For production, use proper certificates from your dev-ca or a trusted CA.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import ipaddress
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.x509.oid import ExtensionOID, NameOID
|
||||
except ImportError:
|
||||
print("Error: cryptography library not found.")
|
||||
print("Install it with: uv pip install cryptography")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def generate_private_key(key_size: int = 4096) -> rsa.RSAPrivateKey:
|
||||
"""Generate an RSA private key."""
|
||||
return rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=key_size,
|
||||
backend=default_backend(),
|
||||
)
|
||||
|
||||
|
||||
def generate_ca_certificate(
|
||||
private_key: rsa.RSAPrivateKey,
|
||||
common_name: str = "Testing CA",
|
||||
days_valid: int = 3650,
|
||||
) -> x509.Certificate:
|
||||
"""Generate a self-signed CA certificate."""
|
||||
subject = issuer = x509.Name(
|
||||
[
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
||||
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Development"),
|
||||
x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Testing Organization"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
||||
]
|
||||
)
|
||||
|
||||
certificate = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(issuer)
|
||||
.public_key(private_key.public_key())
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.utcnow())
|
||||
.not_valid_after(datetime.utcnow() + timedelta(days=days_valid))
|
||||
.add_extension(
|
||||
x509.BasicConstraints(ca=True, path_length=None),
|
||||
critical=True,
|
||||
)
|
||||
.add_extension(
|
||||
x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
key_cert_sign=True,
|
||||
crl_sign=True,
|
||||
key_encipherment=False,
|
||||
content_commitment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False,
|
||||
),
|
||||
critical=True,
|
||||
)
|
||||
.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||
critical=False,
|
||||
)
|
||||
.sign(private_key, hashes.SHA256(), default_backend())
|
||||
)
|
||||
|
||||
return certificate
|
||||
|
||||
|
||||
def generate_server_certificate(
|
||||
private_key: rsa.RSAPrivateKey,
|
||||
ca_cert: x509.Certificate,
|
||||
ca_key: rsa.RSAPrivateKey,
|
||||
hostname: str = "ldap.testing.local",
|
||||
san_list: list[str] = None,
|
||||
days_valid: int = 365,
|
||||
) -> x509.Certificate:
|
||||
"""Generate a server certificate signed by the CA."""
|
||||
if san_list is None:
|
||||
san_list = [
|
||||
"ldap.testing.local",
|
||||
"localhost",
|
||||
]
|
||||
|
||||
subject = x509.Name(
|
||||
[
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
||||
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Development"),
|
||||
x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Testing Organization"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
|
||||
]
|
||||
)
|
||||
|
||||
# Build Subject Alternative Names
|
||||
san_entries = []
|
||||
for name in san_list:
|
||||
san_entries.append(x509.DNSName(name))
|
||||
# Add IP addresses
|
||||
san_entries.append(x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")))
|
||||
|
||||
certificate = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(ca_cert.subject)
|
||||
.public_key(private_key.public_key())
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.utcnow())
|
||||
.not_valid_after(datetime.utcnow() + timedelta(days=days_valid))
|
||||
.add_extension(
|
||||
x509.SubjectAlternativeName(san_entries),
|
||||
critical=False,
|
||||
)
|
||||
.add_extension(
|
||||
x509.BasicConstraints(ca=False, path_length=None),
|
||||
critical=True,
|
||||
)
|
||||
.add_extension(
|
||||
x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
key_encipherment=True,
|
||||
key_cert_sign=False,
|
||||
crl_sign=False,
|
||||
content_commitment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False,
|
||||
),
|
||||
critical=True,
|
||||
)
|
||||
.add_extension(
|
||||
x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]),
|
||||
critical=False,
|
||||
)
|
||||
.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||
critical=False,
|
||||
)
|
||||
.add_extension(
|
||||
x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
|
||||
critical=False,
|
||||
)
|
||||
.sign(ca_key, hashes.SHA256(), default_backend())
|
||||
)
|
||||
|
||||
return certificate
|
||||
|
||||
|
||||
def save_private_key(key: rsa.RSAPrivateKey, filepath: Path) -> None:
|
||||
"""Save a private key to a file."""
|
||||
pem = key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
filepath.write_bytes(pem)
|
||||
filepath.chmod(0o600) # Set restrictive permissions
|
||||
print(f"✓ Private key saved: {filepath}")
|
||||
|
||||
|
||||
def save_certificate(cert: x509.Certificate, filepath: Path) -> None:
|
||||
"""Save a certificate to a file."""
|
||||
pem = cert.public_bytes(serialization.Encoding.PEM)
|
||||
filepath.write_bytes(pem)
|
||||
filepath.chmod(0o644)
|
||||
print(f"✓ Certificate saved: {filepath}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate SSL/TLS certificates for LDAP development server"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=Path(__file__).parent.parent / "certs",
|
||||
help="Output directory for certificates (default: ./certs)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hostname",
|
||||
default="ldap.testing.local",
|
||||
help="Server hostname (default: ldap.testing.local)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--san",
|
||||
action="append",
|
||||
help="Additional Subject Alternative Names (can be used multiple times)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Overwrite existing certificates",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ca-days",
|
||||
type=int,
|
||||
default=3650,
|
||||
help="CA certificate validity in days (default: 3650)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--server-days",
|
||||
type=int,
|
||||
default=365,
|
||||
help="Server certificate validity in days (default: 365)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Ensure output directory exists
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Define file paths
|
||||
ca_key_path = args.output_dir / "ca.key"
|
||||
ca_cert_path = args.output_dir / "ca.crt"
|
||||
server_key_path = args.output_dir / "server.key"
|
||||
server_cert_path = args.output_dir / "server.crt"
|
||||
|
||||
# Check if certificates already exist
|
||||
if not args.force:
|
||||
existing = [
|
||||
p for p in [ca_cert_path, server_cert_path, server_key_path] if p.exists()
|
||||
]
|
||||
if existing:
|
||||
print("Error: The following certificate files already exist:")
|
||||
for p in existing:
|
||||
print(f" - {p}")
|
||||
print("\nUse --force to overwrite existing certificates")
|
||||
sys.exit(1)
|
||||
|
||||
print("Generating certificates for LDAP server...")
|
||||
print(f"Hostname: {args.hostname}")
|
||||
print(f"Output directory: {args.output_dir}")
|
||||
print()
|
||||
|
||||
# Generate CA
|
||||
print("Step 1: Generating CA certificate...")
|
||||
ca_key = generate_private_key()
|
||||
ca_cert = generate_ca_certificate(ca_key, days_valid=args.ca_days)
|
||||
save_private_key(ca_key, ca_key_path)
|
||||
save_certificate(ca_cert, ca_cert_path)
|
||||
print()
|
||||
|
||||
# Generate Server Certificate
|
||||
print("Step 2: Generating server certificate...")
|
||||
server_key = generate_private_key()
|
||||
|
||||
san_list = [args.hostname, "localhost"]
|
||||
if args.san:
|
||||
san_list.extend(args.san)
|
||||
|
||||
server_cert = generate_server_certificate(
|
||||
server_key,
|
||||
ca_cert,
|
||||
ca_key,
|
||||
hostname=args.hostname,
|
||||
san_list=san_list,
|
||||
days_valid=args.server_days,
|
||||
)
|
||||
save_private_key(server_key, server_key_path)
|
||||
save_certificate(server_cert, server_cert_path)
|
||||
print()
|
||||
|
||||
print("✅ Certificate generation complete!")
|
||||
print()
|
||||
print("Generated files:")
|
||||
print(f" - CA Certificate: {ca_cert_path}")
|
||||
print(f" - CA Private Key: {ca_key_path} (keep secure!)")
|
||||
print(f" - Server Certificate: {server_cert_path}")
|
||||
print(f" - Server Private Key: {server_key_path} (keep secure!)")
|
||||
print()
|
||||
print("Next steps:")
|
||||
print(" 1. Keep the CA private key secure (you can delete it if not needed)")
|
||||
print(" 2. Start the LDAP server: docker-compose up -d")
|
||||
print(" 3. Test the connection: ldapsearch -H ldaps://localhost:636 -x")
|
||||
print()
|
||||
print("Note: These certificates are for DEVELOPMENT ONLY!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user