#!/usr/bin/python3 from __future__ import annotations import hmac import ctypes import os import re import signal import socket import stat import struct import subprocess import sys import termios from typing import Callable, Sequence KEEPASSXC_CLI = "/usr/bin/keepassxc-cli" PYTHON3 = "/usr/bin/python3" SOCKET_NAME = "vault.sock" BASELINE_NAME = "baseline.kdbx" CANDIDATE_NAME = "candidate.kdbx" ENTRY_GROUP = "/Platform/Observability/Slack" ENTRY_TITLE = "Alertmanager webhook - desktop-infra-전체" ENTRY_PATH = ENTRY_GROUP + "/" + ENTRY_TITLE ENTRY_USERNAME = "desktop-infra" ENTRY_URL = "" MAX_WEBHOOK_BYTES = 4096 MAX_INPUT_BYTES = 4096 MAX_PROTOCOL_BYTES = 4096 MAX_CHILD_OUTPUT_BYTES = 64 * 1024 MAX_CHILD_STDERR_BYTES = 8192 DEFAULT_ACCEPT_TIMEOUT = 900.0 DEFAULT_IO_TIMEOUT = 15.0 DEFAULT_CHILD_TIMEOUT = 30.0 WEBHOOK_RE = re.compile( rb"https://hooks[.]slack[.]com/services/" rb"[A-Za-z0-9_-]+/[A-Za-z0-9_-]+/[A-Za-z0-9_-]+" ) class VaultContractError(RuntimeError): pass class VaultProtocolError(VaultContractError): pass class VaultSignal(BaseException): def __init__(self, signum: int) -> None: super().__init__(signum) self.signum = signum HANDLED_SIGNALS = (signal.SIGHUP, signal.SIGINT, signal.SIGTERM) def install_process_signal_handlers() -> None: def raise_first_signal(signum: int, _frame: object) -> None: for handled in HANDLED_SIGNALS: signal.signal(handled, signal.SIG_IGN) raise VaultSignal(signum) for signum in HANDLED_SIGNALS: signal.signal(signum, raise_first_signal) class KeepassResult: def __init__(self, returncode: int, stdout: bytes, stderr: bytes) -> None: self.returncode = returncode self.stdout = stdout self.stderr = stderr def wipe_mutable_buffer(buffer: bytearray) -> None: for index in range(len(buffer)): buffer[index] = 0 def _regular_metadata(fd: int, *, allow_unlinked: bool = False) -> tuple[int, ...]: info = os.fstat(fd) if not stat.S_ISREG(info.st_mode): raise VaultContractError("expected regular file") if info.st_uid != os.getuid(): raise VaultContractError("file owner mismatch") if stat.S_IMODE(info.st_mode) != 0o600: raise VaultContractError("file mode mismatch") if info.st_nlink != 1 and not (allow_unlinked and info.st_nlink == 0): raise VaultContractError("file link count mismatch") return ( info.st_dev, info.st_ino, info.st_uid, stat.S_IMODE(info.st_mode), info.st_nlink, info.st_size, ) class RetainedWebhook: def __init__(self, fd: int, metadata: tuple[int, ...], content: bytearray) -> None: self._fd = fd self._metadata = metadata self._content = bytearray(content) self._closed = False @property def fd(self) -> int: if self._closed: raise VaultContractError("webhook descriptor is closed") return self._fd def read_bytes(self) -> bytes: if self._closed: raise VaultContractError("webhook descriptor is closed") before = _regular_metadata(self._fd) if before != self._metadata: raise VaultContractError("webhook metadata drift") collected = bytearray() confirmation = bytearray() try: for target in (collected, confirmation): os.lseek(self._fd, 0, os.SEEK_SET) while len(target) <= MAX_WEBHOOK_BYTES: chunk = os.read( self._fd, min(1024, MAX_WEBHOOK_BYTES + 1 - len(target)), ) if not chunk: break target.extend(chunk) after = _regular_metadata(self._fd) if after != before: raise VaultContractError("webhook metadata drift") if len(collected) > MAX_WEBHOOK_BYTES or len(confirmation) > MAX_WEBHOOK_BYTES: raise VaultContractError("webhook content is oversized") if not hmac.compare_digest(bytes(collected), bytes(confirmation)) or not hmac.compare_digest( bytes(confirmation), self._content ): raise VaultContractError("webhook content drift") return bytes(collected) finally: wipe_mutable_buffer(collected) wipe_mutable_buffer(confirmation) def close(self) -> None: if not self._closed: try: os.close(self._fd) finally: wipe_mutable_buffer(self._content) self._closed = True def _validate_webhook_content(content: bytes) -> None: if not content or len(content) > MAX_WEBHOOK_BYTES: raise VaultContractError("invalid webhook content") if b"\r" in content or b"\n" in content or b"\0" in content: raise VaultContractError("invalid webhook content") if content.strip() != content: raise VaultContractError("invalid webhook content") if WEBHOOK_RE.fullmatch(content) is None: raise VaultContractError("invalid webhook content") def open_validated_webhook(path: str) -> RetainedWebhook: if not os.path.isabs(path): raise VaultContractError("absolute webhook path required") flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC fd = os.open(path, flags) try: before = _regular_metadata(fd) content_buffer = bytearray() confirmation_buffer = bytearray() try: for target in (content_buffer, confirmation_buffer): if _regular_metadata(fd) != before: raise VaultContractError("webhook metadata drift") os.lseek(fd, 0, os.SEEK_SET) while len(target) <= MAX_WEBHOOK_BYTES: chunk = os.read( fd, min(1024, MAX_WEBHOOK_BYTES + 1 - len(target)), ) if not chunk: break target.extend(chunk) if _regular_metadata(fd) != before: raise VaultContractError("webhook metadata drift") after = _regular_metadata(fd) if before != after: raise VaultContractError("webhook metadata drift") if not hmac.compare_digest( bytes(content_buffer), bytes(confirmation_buffer) ): raise VaultContractError("webhook content drift") _validate_webhook_content(confirmation_buffer) return RetainedWebhook(fd, before, confirmation_buffer) finally: wipe_mutable_buffer(content_buffer) wipe_mutable_buffer(confirmation_buffer) except BaseException: os.close(fd) raise def _read_line_fd(fd: int, limit: int) -> bytearray: value = bytearray() try: while len(value) <= limit: chunk = os.read(fd, 1) if not chunk: raise VaultContractError("operator input ended early") if chunk == b"\n": return value if chunk in {b"\r", b"\0"}: raise VaultContractError("invalid operator input") value.extend(chunk) raise VaultContractError("operator input is oversized") except BaseException: wipe_mutable_buffer(value) raise def _validate_app_name(raw: bytes) -> str: try: app = raw.decode("utf-8", "strict") except UnicodeDecodeError as exc: raise VaultContractError("invalid app name") from exc if not 1 <= len(app) <= 80 or app.strip() != app: raise VaultContractError("invalid app name") if any(character in ";=\\\r\n" or not character.isprintable() for character in app): raise VaultContractError("invalid app name") return app def read_operator_secrets( *, input_fd: int | None = None, output_fd: int | None = None ) -> tuple[str, bytearray]: owned_fd = False if input_fd is None: input_fd = os.open("/dev/tty", os.O_RDWR | os.O_CLOEXEC) output_fd = input_fd owned_fd = True if output_fd is None: raise VaultContractError("operator output descriptor required") master = bytearray() old_termios = None try: os.write(output_fd, b"Slack app name: ") raw_app = _read_line_fd(input_fd, MAX_INPUT_BYTES) try: app = _validate_app_name(bytes(raw_app)) finally: wipe_mutable_buffer(raw_app) if os.isatty(input_fd): previous_mask = signal.pthread_sigmask(signal.SIG_BLOCK, HANDLED_SIGNALS) try: old_termios = termios.tcgetattr(input_fd) new_termios = list(old_termios) new_termios[3] &= ~termios.ECHO termios.tcsetattr(input_fd, termios.TCSAFLUSH, new_termios) os.write(output_fd, b"KeePassXC master password: ") finally: signal.pthread_sigmask(signal.SIG_SETMASK, previous_mask) else: os.write(output_fd, b"KeePassXC master password: ") master = _read_line_fd(input_fd, MAX_INPUT_BYTES) if not master: raise VaultContractError("empty master password") if old_termios is not None: previous_mask = signal.pthread_sigmask(signal.SIG_BLOCK, HANDLED_SIGNALS) try: termios.tcsetattr(input_fd, termios.TCSAFLUSH, old_termios) old_termios = None os.write(output_fd, b"\n") finally: signal.pthread_sigmask(signal.SIG_SETMASK, previous_mask) return app, master except BaseException: wipe_mutable_buffer(master) raise finally: if old_termios is not None: previous_mask = signal.pthread_sigmask(signal.SIG_BLOCK, HANDLED_SIGNALS) try: termios.tcsetattr(input_fd, termios.TCSAFLUSH, old_termios) os.write(output_fd, b"\n") finally: signal.pthread_sigmask(signal.SIG_SETMASK, previous_mask) if owned_fd: os.close(input_fd) def _validate_child_stderr(stderr: bytes) -> None: if len(stderr) > MAX_CHILD_STDERR_BYTES or b"\0" in stderr or b"\r" in stderr: raise VaultContractError("invalid KeePass diagnostic") try: text = stderr.decode("utf-8", "strict") except UnicodeDecodeError as exc: raise VaultContractError("invalid KeePass diagnostic") from exc if any(character not in "\t\n" and not character.isprintable() for character in text): raise VaultContractError("invalid KeePass diagnostic") def run_keepass( arguments: Sequence[str], master_buffer: bytearray, private_home: str, *, webhook: RetainedWebhook | None = None, timeout: float = DEFAULT_CHILD_TIMEOUT, check: bool = True, ) -> KeepassResult: if not arguments or any(not isinstance(value, str) or "\0" in value for value in arguments): raise VaultContractError("invalid KeePass arguments") if not os.path.isabs(private_home): raise VaultContractError("private home must be absolute") child_input = bytearray(master_buffer) child_input.extend(b"\n") if webhook is not None: protected = bytearray(webhook.read_bytes()) try: child_input.extend(protected) child_input.extend(b"\n") finally: wipe_mutable_buffer(protected) environment = { "HOME": os.devnull, "XDG_CONFIG_HOME": os.devnull, "LC_ALL": "C.UTF-8", "PATH": "/usr/bin:/bin", } expected_parent_pid = os.getpid() def prepare_child() -> None: os.umask(0o077) arm_parent_death_signal(expected_parent_pid) process = subprocess.Popen( [KEEPASSXC_CLI, *arguments], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=environment, close_fds=True, start_new_session=True, preexec_fn=prepare_child, ) try: try: stdout, stderr = process.communicate(bytes(child_input), timeout=timeout) except subprocess.TimeoutExpired as exc: try: os.killpg(process.pid, 15) except ProcessLookupError: pass try: process.communicate(timeout=1.0) except subprocess.TimeoutExpired: try: os.killpg(process.pid, 9) except ProcessLookupError: pass process.communicate() raise VaultContractError("KeePass child timeout") from exc if len(stdout) > MAX_CHILD_OUTPUT_BYTES: raise VaultContractError("KeePass output is oversized") _validate_child_stderr(stderr) result = KeepassResult(process.returncode, stdout, stderr) if check and process.returncode != 0: raise VaultContractError("KeePass command failed") return result finally: wipe_mutable_buffer(child_input) def arm_parent_death_signal(expected_parent_pid: int) -> None: """Ensure a vault death cannot orphan a secret-bearing KeePass child.""" if expected_parent_pid <= 1: raise VaultContractError("invalid expected parent PID") libc = ctypes.CDLL(None, use_errno=True) prctl = libc.prctl rc = prctl(1, 9, 0, 0, 0) # PR_SET_PDEATHSIG, SIGKILL if rc != 0: err = ctypes.get_errno() raise OSError(err, os.strerror(err)) if os.getppid() != expected_parent_pid: os.kill(os.getpid(), 9) def expected_notes(app_name: str) -> str: _validate_app_name(app_name.encode("utf-8")) return ( "channel=desktop-infra-전체;" f"app={app_name};" "recovery=revoke-and-reissue-in-slack" ) def _entry_count( database: str, master: bytearray, private_home: str, child_timeout: float = DEFAULT_CHILD_TIMEOUT, ) -> tuple[int, set[str]]: result = run_keepass( ["ls", "-q", "-R", "-f", database], master, private_home, check=False, timeout=child_timeout, ) if result.returncode != 0: raise VaultContractError("KeePass database unlock failed") try: text = result.stdout.decode("utf-8", "strict") except UnicodeDecodeError as exc: raise VaultContractError("invalid KeePass listing") from exc lines = text.splitlines() exact = ENTRY_PATH.lstrip("/") return sum(line == exact for line in lines), set(lines) def _shown_attributes( database: str, master: bytearray, private_home: str, child_timeout: float = DEFAULT_CHILD_TIMEOUT, ) -> tuple[str, str, str, str]: result = run_keepass( [ "show", "-q", "-a", "Title", "-a", "UserName", "-a", "URL", "-a", "Notes", database, ENTRY_PATH, ], master, private_home, timeout=child_timeout, ) try: text = result.stdout.decode("utf-8", "strict") except UnicodeDecodeError as exc: raise VaultContractError("invalid KeePass attributes") from exc lines = text.split("\n") if len(lines) != 5 or lines[-1] != "" or any("\r" in line for line in lines[:-1]): raise VaultContractError("invalid KeePass attribute framing") return lines[0], lines[1], lines[2], lines[3] def protected_value_matches(producer_rc: int, shown: bytes, expected: bytes) -> bool: comparison = bytearray(expected) comparison.extend(b"\n") try: return producer_rc == 0 and hmac.compare_digest(shown, bytes(comparison)) finally: wipe_mutable_buffer(comparison) def _password_matches( database: str, master: bytearray, webhook: RetainedWebhook, private_home: str, child_timeout: float = DEFAULT_CHILD_TIMEOUT, ) -> bool: result = run_keepass( ["show", "-q", "-s", "-a", "Password", database, ENTRY_PATH], master, private_home, check=False, timeout=child_timeout, ) expected = bytearray(webhook.read_bytes()) shown = bytearray(result.stdout) try: return protected_value_matches(result.returncode, bytes(shown), bytes(expected)) finally: wipe_mutable_buffer(expected) wipe_mutable_buffer(shown) def classify_database( database: str, master_buffer: bytearray, webhook: RetainedWebhook, app_name: str, private_home: str, child_timeout: float = DEFAULT_CHILD_TIMEOUT, ) -> str: try: count, _ = _entry_count(database, master_buffer, private_home, child_timeout) if count == 0: return "absent" if count > 1: return "ambiguous" expected = (ENTRY_TITLE, ENTRY_USERNAME, ENTRY_URL, expected_notes(app_name)) if _shown_attributes(database, master_buffer, private_home, child_timeout) != expected: return "mismatch" if not _password_matches( database, master_buffer, webhook, private_home, child_timeout ): return "mismatch" return "exact-noop" except (OSError, VaultContractError): return "failed" def _copy_private_database(source: str, destination: str) -> None: source_fd = os.open(source, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC) destination_fd = -1 try: _regular_metadata(source_fd) destination_fd = os.open( destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, ) while True: chunk = os.read(source_fd, 1024 * 1024) if not chunk: break view = memoryview(chunk) while view: written = os.write(destination_fd, view) view = view[written:] os.fsync(destination_fd) _regular_metadata(destination_fd) finally: if destination_fd >= 0: os.close(destination_fd) os.close(source_fd) def _ensure_groups( database: str, master: bytearray, private_home: str, existing_lines: set[str], child_timeout: float = DEFAULT_CHILD_TIMEOUT, ) -> None: for group in ("/Platform", "/Platform/Observability", ENTRY_GROUP): flattened = group.lstrip("/") + "/" if flattened not in existing_lines: run_keepass( ["mkdir", "-q", database, group], master, private_home, timeout=child_timeout, ) existing_lines.add(flattened) def build_candidate( baseline: str, candidate: str, mode: str, master_buffer: bytearray, webhook: RetainedWebhook, app_name: str, private_home: str, child_timeout: float = DEFAULT_CHILD_TIMEOUT, ) -> None: if mode not in {"ADD", "UPDATE"}: raise VaultContractError("invalid candidate mode") classification = classify_database( baseline, master_buffer, webhook, app_name, private_home, child_timeout ) if (mode == "ADD" and classification != "absent") or ( mode == "UPDATE" and classification != "mismatch" ): raise VaultContractError("candidate mode does not match classification") _copy_private_database(baseline, candidate) try: _, existing_lines = _entry_count( candidate, master_buffer, private_home, child_timeout ) notes = expected_notes(app_name) if mode == "ADD": _ensure_groups( candidate, master_buffer, private_home, existing_lines, child_timeout ) arguments = [ "add", "-q", "-p", "-u", ENTRY_USERNAME, "--url", ENTRY_URL, "--notes", notes, candidate, ENTRY_PATH, ] else: arguments = [ "edit", "-q", "-p", "-t", ENTRY_TITLE, "-u", ENTRY_USERNAME, "--url", ENTRY_URL, "--notes", notes, candidate, ENTRY_PATH, ] run_keepass( arguments, master_buffer, private_home, webhook=webhook, timeout=child_timeout, ) if not verify_database( candidate, master_buffer, webhook, app_name, private_home, child_timeout ): raise VaultContractError("candidate verification failed") except BaseException: try: os.unlink(candidate) except OSError: pass raise def verify_database( database: str, master_buffer: bytearray, webhook: RetainedWebhook, app_name: str, private_home: str, child_timeout: float = DEFAULT_CHILD_TIMEOUT, ) -> bool: return classify_database( database, master_buffer, webhook, app_name, private_home, child_timeout ) == "exact-noop" def _attest_runtime_root(runtime_root: str) -> str: if not os.path.isabs(runtime_root): raise VaultContractError("absolute runtime root required") canonical = os.path.realpath(runtime_root) if canonical != os.path.abspath(runtime_root): raise VaultContractError("runtime root must be canonical") expected_parent = f"/run/user/{os.getuid()}" if os.path.commonpath((canonical, expected_parent)) != expected_parent or canonical == expected_parent: raise VaultContractError("runtime root is outside the private runtime") fd = os.open(canonical, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC) try: info = os.fstat(fd) if info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o700: raise VaultContractError("runtime root metadata mismatch") finally: os.close(fd) mount_type = None best_length = -1 with open("/proc/self/mountinfo", "r", encoding="utf-8") as stream: for line in stream: left, right = line.rstrip("\n").split(" - ", 1) mount_point = left.split()[4].replace("\\040", " ") if canonical == mount_point or canonical.startswith(mount_point.rstrip("/") + "/"): if len(mount_point) > best_length: mount_type = right.split()[0] best_length = len(mount_point) if mount_type != "tmpfs": raise VaultContractError("runtime root is not on tmpfs") return canonical def _retained_runtime_path(runtime_fd: int) -> str: info = os.fstat(runtime_fd) if not stat.S_ISDIR(info.st_mode): raise VaultContractError("retained runtime is not a directory") proc_path = f"/proc/{os.getpid()}/fd/{runtime_fd}" link_info = os.lstat(proc_path) if not stat.S_ISLNK(link_info.st_mode): raise VaultContractError("retained runtime proc anchor is unavailable") resolved = os.stat(proc_path) if (resolved.st_dev, resolved.st_ino) != (info.st_dev, info.st_ino): raise VaultContractError("retained runtime proc anchor mismatch") return proc_path def _open_snapshot_below_runtime( path: str, runtime_root: str, runtime_fd: int ) -> int: if not os.path.isabs(path) or os.path.normpath(path) != path: raise VaultProtocolError("snapshot must be canonical") relative = os.path.relpath(path, runtime_root) parts = relative.split(os.sep) if not parts or any( not part or part in {".", ".."} or os.sep in part for part in parts ): raise VaultProtocolError("snapshot is outside runtime root") current_fd = os.dup(runtime_fd) try: for part in parts[:-1]: next_fd = os.open( part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=current_fd, ) os.close(current_fd) current_fd = next_fd directory_info = os.fstat(current_fd) if ( directory_info.st_uid != os.getuid() or stat.S_IMODE(directory_info.st_mode) != 0o700 ): raise VaultProtocolError("snapshot directory metadata mismatch") return os.open( parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=current_fd, ) finally: os.close(current_fd) def _attest_snapshot( path: str, runtime_root: str, runtime_fd: int | None = None ) -> None: if runtime_fd is not None: fd = _open_attested_snapshot(path, runtime_root, runtime_fd) try: return finally: os.close(fd) if not os.path.isabs(path) or os.path.realpath(path) != os.path.abspath(path): raise VaultProtocolError("snapshot must be canonical") if os.path.commonpath((path, runtime_root)) != runtime_root or path == runtime_root: raise VaultProtocolError("snapshot is outside runtime root") fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC) try: _regular_metadata(fd) finally: os.close(fd) def _open_attested_snapshot(path: str, runtime_root: str, runtime_fd: int) -> int: fd = _open_snapshot_below_runtime(path, runtime_root, runtime_fd) try: _regular_metadata(fd) return fd except BaseException: os.close(fd) raise def read_protocol_line(connection: socket.socket, *, timeout: float) -> str: connection.settimeout(timeout) data = bytearray() try: while len(data) <= MAX_PROTOCOL_BYTES: chunk = connection.recv(MAX_PROTOCOL_BYTES + 2 - len(data)) if not chunk: raise VaultProtocolError("partial protocol EOF") data.extend(chunk) if b"\n" in data: break if len(data) > MAX_PROTOCOL_BYTES + 1: raise VaultProtocolError("protocol request is oversized") if not data.endswith(b"\n") or data.count(b"\n") != 1: raise VaultProtocolError("invalid protocol framing") line = data[:-1] if b"\r" in line or b"\0" in line: raise VaultProtocolError("invalid protocol bytes") try: return line.decode("utf-8", "strict") except UnicodeDecodeError as exc: raise VaultProtocolError("invalid protocol UTF-8") from exc finally: wipe_mutable_buffer(data) def parse_protocol_command(line: str) -> tuple[str, str | None]: if line in {"PREPARE", "VERIFY_CANDIDATE", "SHUTDOWN"}: return line, None if line in {"BUILD ADD", "BUILD UPDATE"}: return "BUILD", line.split(" ", 1)[1] prefix = "VERIFY_COMMITTED " if line.startswith(prefix): path = line[len(prefix):] if not path or " " in path or not os.path.isabs(path): raise VaultProtocolError("invalid committed snapshot command") return "VERIFY_COMMITTED", path raise VaultProtocolError("unknown protocol command") def peer_uid(connection: socket.socket) -> int: credentials = connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12) _, uid, _ = struct.unpack("3i", credentials) return uid def _write_reply(connection: socket.socket, reply: str, timeout: float) -> None: payload = reply.encode("ascii") + b"\n" connection.settimeout(timeout) connection.sendall(payload) def serve_private_socket( runtime_root: str, webhook_file: str, *, input_fd: int | None = None, output_fd: int | None = None, accept_timeout: float = DEFAULT_ACCEPT_TIMEOUT, io_timeout: float = DEFAULT_IO_TIMEOUT, child_timeout: float = DEFAULT_CHILD_TIMEOUT, wipe_hook: Callable[[bytearray], None] | None = None, ) -> None: if child_timeout <= 0: raise VaultContractError("invalid child timeout") runtime = "" runtime_fd = -1 runtime_access = "" webhook = None master = bytearray() listener = None socket_identity = None try: runtime = _attest_runtime_root(runtime_root) runtime_fd = os.open( runtime, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC ) runtime_access = _retained_runtime_path(runtime_fd) webhook = open_validated_webhook(webhook_file) listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) socket_path = os.path.join(runtime_access, SOCKET_NAME) private_home = os.devnull baseline = os.path.join(runtime_access, BASELINE_NAME) candidate = os.path.join(runtime_access, CANDIDATE_NAME) app_name, master = read_operator_secrets(input_fd=input_fd, output_fd=output_fd) previous_mask = signal.pthread_sigmask(signal.SIG_BLOCK, HANDLED_SIGNALS) try: listener.bind(socket_path) os.chmod(SOCKET_NAME, 0o600, dir_fd=runtime_fd, follow_symlinks=False) socket_info = os.stat( SOCKET_NAME, dir_fd=runtime_fd, follow_symlinks=False ) socket_identity = (socket_info.st_dev, socket_info.st_ino) listener.listen(1) finally: signal.pthread_sigmask(signal.SIG_SETMASK, previous_mask) listener.settimeout(accept_timeout) connection, _ = listener.accept() listener.close() if peer_uid(connection) != os.getuid(): connection.close() raise VaultContractError("socket peer UID mismatch") state = "initial" classification = None with connection: while True: try: command, argument = parse_protocol_command( read_protocol_line(connection, timeout=io_timeout) ) if command == "PREPARE": if state != "initial": raise VaultProtocolError("PREPARE out of state") classification = classify_database( baseline, master, webhook, app_name, private_home, child_timeout, ) state = "prepared" _write_reply(connection, classification, io_timeout) elif command == "BUILD": if state != "prepared" or ( argument == "ADD" and classification != "absent" ) or ( argument == "UPDATE" and classification != "mismatch" ): raise VaultProtocolError("BUILD out of state") try: build_candidate( baseline, candidate, argument or "", master, webhook, app_name, private_home, child_timeout, ) except (OSError, VaultContractError): _write_reply(connection, "failed", io_timeout) continue state = "candidate-ready" _write_reply(connection, "candidate-ready", io_timeout) elif command == "VERIFY_CANDIDATE": if state != "candidate-ready": raise VaultProtocolError("VERIFY_CANDIDATE out of state") verified = verify_database( candidate, master, webhook, app_name, private_home, child_timeout, ) if verified: state = "candidate-verified" _write_reply(connection, "verified" if verified else "failed", io_timeout) elif command == "VERIFY_COMMITTED": if state != "candidate-verified" or argument is None: raise VaultProtocolError("VERIFY_COMMITTED out of state") snapshot_fd = _open_attested_snapshot( argument, runtime, runtime_fd ) try: verified = verify_database( f"/proc/{os.getpid()}/fd/{snapshot_fd}", master, webhook, app_name, private_home, child_timeout, ) finally: os.close(snapshot_fd) _write_reply(connection, "verified" if verified else "failed", io_timeout) elif command == "SHUTDOWN": _write_reply(connection, "stopped", io_timeout) break except VaultProtocolError: try: _write_reply(connection, "failed", io_timeout) except OSError: pass raise finally: try: if listener is not None: listener.close() finally: try: if webhook is not None: webhook.close() finally: try: wipe_mutable_buffer(master) if wipe_hook is not None: wipe_hook(master) finally: try: if socket_identity is not None: try: current = os.stat( SOCKET_NAME, dir_fd=runtime_fd, follow_symlinks=False, ) if (current.st_dev, current.st_ino) == socket_identity: os.unlink(SOCKET_NAME, dir_fd=runtime_fd) except FileNotFoundError: pass finally: if runtime_fd >= 0: os.close(runtime_fd) def _main(argv: Sequence[str]) -> int: if len(argv) != 5 or argv[0] != "--serve" or argv[1] != "--runtime-root" or argv[3] != "--webhook-file": return 2 runtime_root, webhook_file = argv[2], argv[4] if not os.path.isabs(runtime_root) or not os.path.isabs(webhook_file): return 2 install_process_signal_handlers() try: serve_private_socket(runtime_root, webhook_file) return 0 except VaultSignal as exc: return 128 + exc.signum except (OSError, VaultContractError): os.write(2, b"vault helper failed\n") return 1 if __name__ == "__main__": raise SystemExit(_main(sys.argv[1:]))