#!/usr/bin/python3 """Constrain production denylist reload commands to the colocated E2E Caddy.""" import os from pathlib import Path import re import stat import subprocess import sys ENV_FILE = Path("/etc/truf-edge/edge.env") AUDIT_PATH = Path("/var/lib/truf-edge/edge-e2e-reload.audit") VALIDATION_ERROR_PATH = Path("/var/lib/truf-edge/edge-e2e-validation.error") COMPOSE_PREFIX = ( "compose", "--ansi", "never", "--env-file", str(ENV_FILE), "--project-directory", "/opt/truf", "--file", "/opt/truf/compose.yaml", "--file", "/opt/truf/compose.edge.yaml", ) VALIDATE_COMMAND = COMPOSE_PREFIX + ( "exec", "-T", "edge", "caddy", "validate", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile", ) RELOAD_COMMAND = COMPOSE_PREFIX + ("kill", "--signal", "SIGUSR1", "edge") REQUIRED_ENV = { "TRUF_EDGE_HOST", "TRUF_EDGE_TLS_INCLUDE", "TRUF_ADMIN_PREFIX", "TRUF_ADMIN_USER", "TRUF_ADMIN_PASSWORD_HASH", "TRUF_ADMIN_EDGE_MARKER", } def classify_command(arguments): command = tuple(arguments) if command == VALIDATE_COMMAND: return "validate" if command == RELOAD_COMMAND: return "reload" raise ValueError("unsupported command") def audit(operation, result): payload = f"{operation}:{result}\n".encode("ascii") flags = ( os.O_WRONLY | os.O_APPEND | os.O_CREAT | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_BINARY", 0) ) descriptor = os.open(AUDIT_PATH, flags, 0o600) try: details = os.fstat(descriptor) if not stat.S_ISREG(details.st_mode) or details.st_size + len(payload) > 4096: raise ValueError("invalid audit file") os.write(descriptor, payload) finally: os.close(descriptor) def record_validation_error(content, environment): if len(content) > 65536: content = b"caddy validation error exceeded evidence bound\n" text = content.decode("utf-8", errors="replace") for value in environment.values(): if value: text = text.replace(value, "[redacted]") text = re.sub(r"\$2[aby]\$(?:0[4-9]|[12][0-9]|3[01])\$[./A-Za-z0-9]{53}", "[redacted]", text) text = re.sub(r"(? 8192: raise ValueError("invalid environment file") values = {} for raw_line in path.read_text(encoding="ascii").splitlines(): if not raw_line or raw_line.startswith("#"): continue name, separator, value = raw_line.partition("=") if not separator or name not in REQUIRED_ENV or name in values or "\x00" in value: raise ValueError("invalid environment entry") values[name] = value if set(values) != REQUIRED_ENV: raise ValueError("incomplete environment") if ( values["TRUF_EDGE_HOST"] != "localhost" or values["TRUF_EDGE_TLS_INCLUDE"] != "/etc/caddy/tls/static-tls.caddy" or not re.fullmatch(r"[0-9a-f]{64}", values["TRUF_ADMIN_PREFIX"]) or not re.fullmatch(r"[A-Za-z0-9_.-]{1,64}", values["TRUF_ADMIN_USER"]) or not re.fullmatch(r"\$2[aby]\$(?:0[4-9]|[12][0-9]|3[01])\$[./A-Za-z0-9]{53}", values["TRUF_ADMIN_PASSWORD_HASH"]) or not re.fullmatch(r"[0-9a-f]{64}", values["TRUF_ADMIN_EDGE_MARKER"]) ): raise ValueError("unsupported environment") return { **values, "HOME": "/tmp", "LANG": "C.UTF-8", "LC_ALL": "C.UTF-8", "PATH": "/usr/bin:/bin", } def validate(): try: environment = load_environment() except Exception: audit("environment", 64) raise try: completed = subprocess.run( ( "/usr/bin/caddy", "validate", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile", ), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, env=environment, timeout=30, check=False, ) except Exception: audit("caddy-exec", 64) raise if completed.returncode: record_validation_error(completed.stderr, environment) return completed.returncode def reload_caddy(): try: command = Path("/proc/1/cmdline").read_bytes() except Exception: audit("reload-proc", 64) raise if ( len(command) > 4096 or command.rstrip(b"\0").split(b"\0") not in ( [b"caddy", b"run", b"--config", b"/etc/caddy/Caddyfile", b"--adapter", b"caddyfile"], [b"/usr/bin/caddy", b"run", b"--config", b"/etc/caddy/Caddyfile", b"--adapter", b"caddyfile"], ) ): audit("reload-identity", 64) raise ValueError("unexpected pid namespace") completed = subprocess.run( ( "/usr/bin/caddy", "reload", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile", "--address", "127.0.0.1:2019", ), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=load_environment(), timeout=30, check=False, ) return completed.returncode def main(argv=None): try: operation = classify_command((argv or sys.argv)[1:]) result = validate() if operation == "validate" else reload_caddy() except Exception: if "operation" in locals(): try: audit(operation, 64) except Exception: pass return 64 try: audit(operation, result) except Exception: return 64 return result if __name__ == "__main__": raise SystemExit(main())