authentik.crypto.tasks
Crypto tasks
1"""Crypto tasks""" 2 3from glob import glob 4from pathlib import Path 5from sys import platform 6 7from cryptography.hazmat.backends import default_backend 8from cryptography.hazmat.primitives.serialization import load_pem_private_key 9from cryptography.x509.base import load_pem_x509_certificate 10from django.conf import settings 11from django.utils.translation import gettext_lazy as _ 12from dramatiq.actor import actor 13from dramatiq.middleware import Middleware 14from structlog.stdlib import get_logger 15from watchdog.events import ( 16 FileCreatedEvent, 17 FileModifiedEvent, 18 FileSystemEvent, 19 FileSystemEventHandler, 20) 21from watchdog.observers import Observer 22 23from authentik.crypto.models import CertificateKeyPair 24from authentik.lib.config import CONFIG 25from authentik.tasks.middleware import CurrentTask 26from authentik.tasks.schedules.models import Schedule 27from authentik.tenants.models import Tenant 28 29LOGGER = get_logger() 30 31MANAGED_DISCOVERED = "goauthentik.io/crypto/discovered/%s" 32 33 34def ensure_private_key_valid(body: str): 35 """Attempt loading of a PEM Private key without password""" 36 load_pem_private_key( 37 str.encode("\n".join([x.strip() for x in body.split("\n")])), 38 password=None, 39 backend=default_backend(), 40 ) 41 return body 42 43 44def ensure_certificate_valid(body: str): 45 """Attempt loading of a PEM-encoded certificate""" 46 load_pem_x509_certificate(body.encode("utf-8"), default_backend()) 47 return body 48 49 50class CertificateWatcherMiddleware(Middleware): 51 """Middleware to start certificate file watcher""" 52 53 def start_certificate_watcher(self): 54 """Start certificate file watcher""" 55 observer = Observer() 56 kwargs = {} 57 if platform.startswith("linux"): 58 kwargs["event_filter"] = (FileCreatedEvent, FileModifiedEvent) 59 observer.schedule( 60 CertificateEventHandler(), 61 CONFIG.get("cert_discovery_dir"), 62 recursive=True, 63 **kwargs, 64 ) 65 observer.start() 66 67 def after_worker_boot(self, broker, worker): 68 if not settings.TEST: 69 self.start_certificate_watcher() 70 71 72class CertificateEventHandler(FileSystemEventHandler): 73 """Event handler for certificate file events""" 74 75 # We only ever get creation and modification events. 76 # See the creation of the Observer instance above for the event filtering. 77 78 # Even though we filter to only get file events, we might still get 79 # directory events as some implementations such as inotify do not support 80 # filtering on file/directory. 81 82 def dispatch(self, event: FileSystemEvent) -> None: 83 """Call specific event handler method. Ignores directory changes.""" 84 if event.is_directory: 85 return None 86 return super().dispatch(event) 87 88 def on_created(self, event: FileSystemEvent): 89 """Process certificate file creation""" 90 LOGGER.debug( 91 "Certificate file created, triggering discovery", 92 file=event.src_path, 93 ) 94 for tenant in Tenant.objects.filter(ready=True): 95 with tenant: 96 Schedule.dispatch_by_actor(certificate_discovery) 97 98 def on_modified(self, event: FileSystemEvent): 99 """Process certificate file modification""" 100 LOGGER.debug( 101 "Certificate file modified, triggering discovery", 102 file=event.src_path, 103 ) 104 for tenant in Tenant.objects.filter(ready=True): 105 with tenant: 106 Schedule.dispatch_by_actor(certificate_discovery) 107 108 109@actor(description=_("Discover, import and update certificates from the filesystem.")) 110def certificate_discovery(): 111 self = CurrentTask.get_task() 112 certs = {} 113 private_keys = {} 114 discovered = 0 115 for file in glob(CONFIG.get("cert_discovery_dir") + "/**", recursive=True): 116 path = Path(file) 117 if not path.exists() or path.is_dir(): 118 continue 119 # For certbot setups, we want to ignore archive. 120 if "archive" in file: 121 continue 122 # Handle additionalOutputFormats from cert-manager gracefully 123 if path.name in ["ca.crt", "tls-combined.pem", "key.der"]: 124 continue 125 # Support certbot & kubernetes.io/tls directory structure 126 if path.name in ["fullchain.pem", "privkey.pem", "tls.crt", "tls.key"]: 127 cert_name = path.parent.name 128 else: 129 cert_name = path.name.replace(path.suffix, "") 130 try: 131 with open(path, encoding="utf-8") as _file: 132 body = _file.read() 133 if "PRIVATE KEY" in body: 134 private_keys[cert_name] = ensure_private_key_valid(body) 135 else: 136 certs[cert_name] = ensure_certificate_valid(body) 137 discovered += 1 138 except (OSError, ValueError) as exc: 139 LOGGER.warning("Failed to open file or invalid format", exc=exc, file=path) 140 for name, cert_data in certs.items(): 141 # First, try to find by filename-based managed field 142 cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first() 143 144 # If not found by filename and we have a private key, check for existing key match 145 if not cert and name in private_keys: 146 existing_with_key = ( 147 CertificateKeyPair.objects.filter( 148 managed__startswith="goauthentik.io/crypto/discovered/", 149 key_data=private_keys[name], 150 ) 151 .exclude(key_data="") 152 .first() 153 ) 154 if existing_with_key: 155 cert = existing_with_key 156 # Update name and managed field to reflect the new filename 157 if cert.name != name: 158 cert.name = name 159 cert.managed = MANAGED_DISCOVERED % name 160 cert.save() 161 162 # Create new certificate if not found 163 if not cert: 164 cert = CertificateKeyPair( 165 name=name, 166 managed=MANAGED_DISCOVERED % name, 167 ) 168 169 # Update certificate data if changed 170 dirty = False 171 if cert.certificate_data != cert_data: 172 cert.certificate_data = cert_data 173 dirty = True 174 if name in private_keys: 175 if cert.key_data != private_keys[name]: 176 cert.key_data = private_keys[name] 177 dirty = True 178 if dirty: 179 cert.save() 180 self.info(f"Successfully imported {discovered} files.")
LOGGER =
<BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
MANAGED_DISCOVERED =
'goauthentik.io/crypto/discovered/%s'
def
ensure_private_key_valid(body: str):
35def ensure_private_key_valid(body: str): 36 """Attempt loading of a PEM Private key without password""" 37 load_pem_private_key( 38 str.encode("\n".join([x.strip() for x in body.split("\n")])), 39 password=None, 40 backend=default_backend(), 41 ) 42 return body
Attempt loading of a PEM Private key without password
def
ensure_certificate_valid(body: str):
45def ensure_certificate_valid(body: str): 46 """Attempt loading of a PEM-encoded certificate""" 47 load_pem_x509_certificate(body.encode("utf-8"), default_backend()) 48 return body
Attempt loading of a PEM-encoded certificate
class
CertificateWatcherMiddleware(dramatiq.middleware.middleware.Middleware):
51class CertificateWatcherMiddleware(Middleware): 52 """Middleware to start certificate file watcher""" 53 54 def start_certificate_watcher(self): 55 """Start certificate file watcher""" 56 observer = Observer() 57 kwargs = {} 58 if platform.startswith("linux"): 59 kwargs["event_filter"] = (FileCreatedEvent, FileModifiedEvent) 60 observer.schedule( 61 CertificateEventHandler(), 62 CONFIG.get("cert_discovery_dir"), 63 recursive=True, 64 **kwargs, 65 ) 66 observer.start() 67 68 def after_worker_boot(self, broker, worker): 69 if not settings.TEST: 70 self.start_certificate_watcher()
Middleware to start certificate file watcher
def
start_certificate_watcher(self):
54 def start_certificate_watcher(self): 55 """Start certificate file watcher""" 56 observer = Observer() 57 kwargs = {} 58 if platform.startswith("linux"): 59 kwargs["event_filter"] = (FileCreatedEvent, FileModifiedEvent) 60 observer.schedule( 61 CertificateEventHandler(), 62 CONFIG.get("cert_discovery_dir"), 63 recursive=True, 64 **kwargs, 65 ) 66 observer.start()
Start certificate file watcher
class
CertificateEventHandler(watchdog.events.FileSystemEventHandler):
73class CertificateEventHandler(FileSystemEventHandler): 74 """Event handler for certificate file events""" 75 76 # We only ever get creation and modification events. 77 # See the creation of the Observer instance above for the event filtering. 78 79 # Even though we filter to only get file events, we might still get 80 # directory events as some implementations such as inotify do not support 81 # filtering on file/directory. 82 83 def dispatch(self, event: FileSystemEvent) -> None: 84 """Call specific event handler method. Ignores directory changes.""" 85 if event.is_directory: 86 return None 87 return super().dispatch(event) 88 89 def on_created(self, event: FileSystemEvent): 90 """Process certificate file creation""" 91 LOGGER.debug( 92 "Certificate file created, triggering discovery", 93 file=event.src_path, 94 ) 95 for tenant in Tenant.objects.filter(ready=True): 96 with tenant: 97 Schedule.dispatch_by_actor(certificate_discovery) 98 99 def on_modified(self, event: FileSystemEvent): 100 """Process certificate file modification""" 101 LOGGER.debug( 102 "Certificate file modified, triggering discovery", 103 file=event.src_path, 104 ) 105 for tenant in Tenant.objects.filter(ready=True): 106 with tenant: 107 Schedule.dispatch_by_actor(certificate_discovery)
Event handler for certificate file events
def
dispatch(self, event: watchdog.events.FileSystemEvent) -> None:
83 def dispatch(self, event: FileSystemEvent) -> None: 84 """Call specific event handler method. Ignores directory changes.""" 85 if event.is_directory: 86 return None 87 return super().dispatch(event)
Call specific event handler method. Ignores directory changes.
def
on_created(self, event: watchdog.events.FileSystemEvent):
89 def on_created(self, event: FileSystemEvent): 90 """Process certificate file creation""" 91 LOGGER.debug( 92 "Certificate file created, triggering discovery", 93 file=event.src_path, 94 ) 95 for tenant in Tenant.objects.filter(ready=True): 96 with tenant: 97 Schedule.dispatch_by_actor(certificate_discovery)
Process certificate file creation
def
on_modified(self, event: watchdog.events.FileSystemEvent):
99 def on_modified(self, event: FileSystemEvent): 100 """Process certificate file modification""" 101 LOGGER.debug( 102 "Certificate file modified, triggering discovery", 103 file=event.src_path, 104 ) 105 for tenant in Tenant.objects.filter(ready=True): 106 with tenant: 107 Schedule.dispatch_by_actor(certificate_discovery)
Process certificate file modification