Files
aza/AzA march 2026/aza_tls.py

176 lines
5.6 KiB
Python
Raw Normal View History

2026-03-25 22:03:39 +01:00
# -*- coding: utf-8 -*-
"""
AZA Zentrale TLS-Konfiguration für alle Backend-Services.
Alle TLS-Parameter werden über Umgebungsvariablen gesteuert:
AZA_TLS_CERTFILE Pfad zum Zertifikat (PEM)
AZA_TLS_KEYFILE Pfad zum Private Key (PEM)
AZA_TLS_MIN_VERSION Minimale TLS-Version ("1.2" oder "1.3", Default: "1.2")
AZA_TLS_REQUIRE "1" = Server startet nicht ohne Zertifikat (Default: "1")
DEV-Modus (Self-Signed):
python aza_tls.py Erzeugt dev-cert.pem + dev-key.pem
AZA_TLS_CERTFILE=dev-cert.pem AZA_TLS_KEYFILE=dev-key.pem python backend_main.py
"""
import os
import ssl
import sys
import datetime
AZA_TLS_CERTFILE = os.getenv("AZA_TLS_CERTFILE", "").strip()
AZA_TLS_KEYFILE = os.getenv("AZA_TLS_KEYFILE", "").strip()
AZA_TLS_MIN_VERSION = os.getenv("AZA_TLS_MIN_VERSION", "1.2").strip()
AZA_TLS_REQUIRE = os.getenv("AZA_TLS_REQUIRE", "1").strip()
_TLS_VERSIONS = {
"1.2": ssl.TLSVersion.TLSv1_2,
"1.3": ssl.TLSVersion.TLSv1_3,
}
_STRONG_CIPHERS = ":".join([
"ECDHE+AESGCM",
"ECDHE+CHACHA20",
"DHE+AESGCM",
"DHE+CHACHA20",
"!aNULL", "!eNULL", "!MD5", "!DSS", "!RC4", "!3DES",
])
def tls_required() -> bool:
return AZA_TLS_REQUIRE == "1"
def has_tls_config() -> bool:
return bool(AZA_TLS_CERTFILE and AZA_TLS_KEYFILE)
def check_tls_or_exit():
"""Beendet den Prozess wenn TLS erforderlich aber nicht konfiguriert ist."""
if tls_required() and not has_tls_config():
print(
"FEHLER: TLS ist erforderlich (AZA_TLS_REQUIRE=1), aber "
"AZA_TLS_CERTFILE und/oder AZA_TLS_KEYFILE sind nicht gesetzt.\n"
"Setzen Sie die Umgebungsvariablen oder deaktivieren Sie die "
"Pflicht mit AZA_TLS_REQUIRE=0 (nur für Entwicklung).",
file=sys.stderr,
)
sys.exit(1)
if has_tls_config():
if not os.path.isfile(AZA_TLS_CERTFILE):
print(f"FEHLER: Zertifikatsdatei nicht gefunden: {AZA_TLS_CERTFILE}", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(AZA_TLS_KEYFILE):
print(f"FEHLER: Key-Datei nicht gefunden: {AZA_TLS_KEYFILE}", file=sys.stderr)
sys.exit(1)
def create_ssl_context() -> ssl.SSLContext:
"""Erstellt einen gehärteten SSL-Kontext für Server."""
if not has_tls_config():
raise RuntimeError("TLS nicht konfiguriert (AZA_TLS_CERTFILE / AZA_TLS_KEYFILE fehlen)")
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
min_ver = _TLS_VERSIONS.get(AZA_TLS_MIN_VERSION)
if min_ver is None:
raise ValueError(f"Ungültige TLS-Version: {AZA_TLS_MIN_VERSION} (erlaubt: 1.2, 1.3)")
ctx.minimum_version = min_ver
ctx.maximum_version = ssl.TLSVersion.TLSv1_3
ctx.set_ciphers(_STRONG_CIPHERS)
ctx.load_cert_chain(certfile=AZA_TLS_CERTFILE, keyfile=AZA_TLS_KEYFILE)
return ctx
def get_uvicorn_ssl_kwargs() -> dict:
"""Gibt die SSL-Parameter für uvicorn.run() zurück."""
if not has_tls_config():
return {}
return {
"ssl_certfile": AZA_TLS_CERTFILE,
"ssl_keyfile": AZA_TLS_KEYFILE,
"ssl_ciphers": _STRONG_CIPHERS,
}
def generate_dev_cert(base_dir: str = None):
"""Erzeugt ein Self-Signed-Zertifikat für Entwicklung."""
try:
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
except ImportError:
print("FEHLER: 'cryptography' Paket wird benötigt: pip install cryptography", file=sys.stderr)
sys.exit(1)
if base_dir is None:
base_dir = os.path.dirname(os.path.abspath(__file__))
cert_path = os.path.join(base_dir, "dev-cert.pem")
key_path = os.path.join(base_dir, "dev-key.pem")
key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "AZA MedWork DEV"),
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
])
now = datetime.datetime.now(datetime.timezone.utc)
cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=365))
.add_extension(
x509.SubjectAlternativeName([
x509.DNSName("localhost"),
x509.DNSName("127.0.0.1"),
x509.IPAddress(ipaddress_from_str("127.0.0.1")),
]),
critical=False,
)
.sign(key, hashes.SHA256())
)
with open(key_path, "wb") as f:
f.write(key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
))
with open(cert_path, "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))
print(f"DEV-Zertifikat erstellt:")
print(f" Zertifikat: {cert_path}")
print(f" Key: {key_path}")
print(f" Gültigkeit: 365 Tage")
print(f" Schlüssel: RSA 4096-bit")
print(f" Signatur: SHA-256")
print()
print(f"Verwendung:")
print(f' set AZA_TLS_CERTFILE={cert_path}')
print(f' set AZA_TLS_KEYFILE={key_path}')
return cert_path, key_path
def ipaddress_from_str(addr: str):
"""Hilfsfunktion für IP-Adresse in SAN."""
import ipaddress
return ipaddress.IPv4Address(addr)
if __name__ == "__main__":
generate_dev_cert()