Apply platform SSH policies through a recoverable port handover

This commit is contained in:
deytenit committed 2026-11-08 12:39:49 +03:00
1 parent ba9ef92051
commit 702857ec0a
6 files changed
+488 -86

No files matched your search

+94 -48
View File
@@ -1,42 +1,39 @@
"""Apply the image's public-key SSH policy to adam after a verified key login."""
"""Hand SSH access over to fresh, verified adam sessions on port 2222."""
import ipaddress
import json
import os
import platform
import pwd
import re
import time
import uuid
from pathlib import Path
from engine.descriptor import Meta
from engine.descriptor import Arg, Meta, Opt
METADATA = Meta(summary="Require SSH public-key authentication for adam from an independent administrator or verified adam key session.")
CONFIG = "/etc/ssh/sshd_config"
POLICY = """
# APEX operator SSH policy
Match User adam
AuthenticationMethods publickey
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
X11Forwarding no
"""
METADATA = Meta(summary="Manage the protected SSH handover to port 2222.", args=[
Arg("operation", "begin (default), finalize, commit, rollback, or status", required=False),
Opt("--original-port", "Current SSH port when beginning from a console"),
Opt("--client-address", "SSH client IP when beginning from a console"),
])
STATE = "/var/lib/apex-init/access/manual-state.json"
BUNDLES = Path("/usr/local/lib/apex-provisioner")
def _ancestors():
ancestors = {}
pid = os.getppid()
ticks = os.sysconf("SC_CLK_TCK")
# proc starttime includes suspend time, while journald monotonic timestamps do not.
suspend_us = (time.clock_gettime_ns(time.CLOCK_BOOTTIME) - time.monotonic_ns()) // 1000
while pid > 1:
# comm can contain spaces and parentheses; the remaining fields start at state.
fields = Path(f"/proc/{pid}/stat").read_text().rsplit(")", 1)[1].split()
ancestors[pid] = int(fields[19]) * 1_000_000 // ticks
ancestors[pid] = int(fields[19]) * 1_000_000 // ticks - max(0, suspend_us)
pid = int(fields[1])
return ancestors
def _connection(ctx):
if pwd.getpwuid(os.getuid()).pw_name != "adam":
raise RuntimeError("Run config/hardening inside an SSH public-key session as adam.")
ancestors = _ancestors()
def _socket(ctx, ancestors):
sockets = ctx.sys.sudo(["ss", "-Hntp", "state", "established"], capture=True).stdout
connections = set()
for line in sockets.splitlines():
@@ -44,6 +41,8 @@ def _connection(ctx):
if len(fields) == 5 and any(int(pid) in ancestors for pid in
re.findall(r'"sshd(?:-session)?",pid=(\d+),', fields[4])):
connections.add((fields[2], fields[3]))
if not connections:
return None
if len(connections) != 1:
raise RuntimeError("Cannot identify a unique established SSH socket for this process.")
local, peer = connections.pop()
@@ -53,6 +52,19 @@ def _connection(ctx):
ip = ipaddress.ip_address(value.strip("[]"))
return str(getattr(ip, "ipv4_mapped", None) or ip)
peer, local = address(peer), address(local)
return local, int(local_port), peer, int(peer_port)
def _connection(ctx, *, after=None):
if pwd.getpwuid(os.getuid()).pw_name != "adam":
raise RuntimeError("Run this step inside a fresh SSH public-key session as adam.")
ancestors = _ancestors()
endpoint = _socket(ctx, ancestors)
if not endpoint:
raise RuntimeError("Cannot identify the SSH connection for this process.")
local, local_port, peer, peer_port = endpoint
if after is not None and local_port != 2222:
raise RuntimeError("Open a fresh adam SSH connection on port 2222.")
record = rf"^Accepted publickey for adam from {re.escape(peer)} port {int(peer_port)} ssh2:"
result = ctx.sys.sudo(["journalctl", "-b", "_UID=0", "_EXE=/usr/sbin/sshd",
"_EXE=/usr/lib/openssh/sshd-session", "_EXE=/usr/lib/openssh/sshd-auth",
@@ -62,10 +74,11 @@ def _connection(ctx):
entry = json.loads(line)
pid = int(entry["_PID"])
if (pid in ancestors and int(entry["__MONOTONIC_TIMESTAMP"]) >= ancestors[pid]
and (after is None or ancestors[pid] > after)
and re.match(record, entry["MESSAGE"])):
ctx.sys.run(["sudo", "-n", "true"])
return f"user=adam,host={peer},addr={peer},laddr={local},lport={int(local_port)}"
raise RuntimeError("Cannot verify public-key login for this SSH process in the root sshd journal.")
raise RuntimeError("Cannot verify a fresh public-key login for this SSH process in the root sshd journal.")
def _operator_access(ctx):
@@ -75,7 +88,7 @@ def _operator_access(ctx):
if pwd.getpwuid(os.getuid()).pw_name == "adam":
return _connection(ctx)
loginuid = int(Path("/proc/self/loginuid").read_text())
if loginuid in (account.pw_uid, 4294967295) or loginuid < 0:
if loginuid == account.pw_uid or (loginuid == 4294967295 and os.getuid() != 0) or loginuid < 0:
raise RuntimeError("Use an independent administrator login or an SSH public-key session as adam.")
if os.getuid() not in (0, loginuid):
raise RuntimeError("Run from the independent administrator's own login.")
@@ -112,32 +125,65 @@ def _effective(system, connection):
return dict(line.split(" ", 1) for line in result.stdout.splitlines() if " " in line)
def _state(system):
if system.sudo(["test", "-f", STATE], check=False).returncode:
return {}
return json.loads(system.sudo(["cat", STATE], capture=True).stdout)
def _original_endpoint(ctx, args):
endpoint = _socket(ctx, _ancestors())
port = getattr(args, "original_port", None)
address = getattr(args, "client_address", None)
if endpoint:
if port or address:
raise ValueError("SSH sessions detect the peer address and port automatically.")
return endpoint[1], endpoint[2]
if not port or not address or not 1 <= int(port) <= 65535:
raise ValueError("From a console, supply --original-port and --client-address for the SSH client.")
return int(port), str(ipaddress.ip_address(address))
def run(ctx, args):
connection = _operator_access(ctx)
system = ctx.sys
system.sudo(["/usr/sbin/sshd", "-t"])
effective = _effective(system, connection)
if effective.get("authenticationmethods") not in ("any", "publickey"):
raise RuntimeError("Preserve the existing SSH authentication chain; reconcile it explicitly.")
required = {"authenticationmethods": "publickey", "pubkeyauthentication": "yes",
"passwordauthentication": "no", "kbdinteractiveauthentication": "no",
"permitemptypasswords": "no", "x11forwarding": "no"}
if all(effective.get(key) == value for key, value in required.items()):
operation = getattr(args, "operation", None) or "begin"
if operation not in ("begin", "finalize", "commit", "rollback", "status"):
raise ValueError("Choose begin, finalize, commit, rollback, or status.")
if operation == "begin":
_operator_access(ctx)
state = _state(ctx.sys)
phase = state.get("status", "absent")
if operation == "status" and phase == "absent":
print("absent")
return
if Path(CONFIG).is_symlink():
raise RuntimeError(f"Refusing redirected SSH configuration: {CONFIG}")
original = system.sudo(["cat", CONFIG], capture=True).stdout
if "# APEX operator SSH policy" in original:
raise RuntimeError("The managed SSH policy is ineffective; reconcile earlier directives.")
ctx.host.write_file(CONFIG, original + "\n" + POLICY, sudo=True)
try:
system.sudo(["/usr/sbin/sshd", "-t"])
effective = _effective(system, connection)
if any(effective.get(key) != value for key, value in required.items()):
raise RuntimeError("The effective SSH policy differs; inspect existing Match blocks.")
system.reload("ssh")
except BaseException:
ctx.host.write_file(CONFIG, original, backup=False, sudo=True)
system.sudo(["/usr/sbin/sshd", "-t"])
system.reload("ssh")
raise
if operation == "begin" and phase in ("transition", "final", "recovery_pending"):
raise RuntimeError(f"Handover is {phase}; inspect status, continue from a fresh SSH login, or roll back.")
if operation in ("finalize", "commit"):
required = "transition" if operation == "finalize" else "final"
if phase != required:
raise RuntimeError(f"{operation} requires phase {required}; current phase is {phase}.")
_connection(ctx, after=state["authenticated_after_us"])
profile = state.get("profile") if operation != "begin" else None
if not profile:
if Path("/usr/bin/pveversion").is_file():
profile = "proxmox"
elif platform.freedesktop_os_release().get("ID") == "debian":
profile = "debian"
else:
raise RuntimeError("SSH hardening supports Debian and Proxmox VE.")
options = ["--profile", profile]
if operation == "begin":
port, address = _original_endpoint(ctx, args)
options.extend(["--original-port", str(port), "--client-address", address, "--token", uuid.uuid4().hex])
installer = Path(__file__).resolve().parents[2] / "ci/scripts/init-node/provisioning/manual_bundle.py"
bundle = ctx.sys.sudo(["/usr/bin/python3", "-I", "-B", str(installer)], capture=True).stdout.strip()
else:
if not state:
raise RuntimeError("No SSH handover has started.")
bundle = state["bundle"]
options.extend(["--token", state["token"]])
if not re.fullmatch(re.escape(str(BUNDLES)) + "/[0-9a-f]{64}", bundle):
raise RuntimeError("Invalid installed rollback bundle location.")
command = ["/usr/bin/python3", "-I", "-c",
"import sys; sys.path.insert(0, sys.argv.pop(1)); from provisioning.access import main; sys.exit(main())",
bundle, "tick" if operation == "status" else operation, *options]
ctx.sys.sudo(command)