Enhance security by ensuring unique SSH host keys for VMs and protecting registry credentials. Update documentation to reflect changes in SSH key management and improve state file permissions.

This commit is contained in:
kstyagi@brahmai.in
2026-09-04 21:31:54 +00:00
parent 1022f24c34
commit a8dbc704e9
7 changed files with 147 additions and 16 deletions
+69 -4
View File
@@ -24,6 +24,11 @@ _FIRECRACKER_DEMO_PUBLIC_KEY = (
)
_MODE_PATTERN = re.compile(r"Mode:\s+0*([0-7]+)")
_OWNER_PATTERN = re.compile(r"User:\s+(\d+)\s+Group:\s+(\d+)")
_HOST_KEY_TYPES = (
("rsa", "3072"),
("ecdsa", "256"),
("ed25519", None),
)
@dataclass(frozen=True, slots=True)
@@ -144,6 +149,7 @@ class ImageStore:
self._write_guest_file(disk, "/etc/shadow", shadow)
self._write_guest_file(disk, "/etc/ssh/sshd_config", sshd_config)
self._replace_guest_host_keys(disk, temporary)
if authorized_keys is not None:
self._remove_firecracker_demo_key(disk, authorized_keys)
except (OSError, UnicodeError) as error:
@@ -179,9 +185,12 @@ class ImageStore:
mode_match = _MODE_PATTERN.search(stat_result.stdout)
owner_match = _OWNER_PATTERN.search(stat_result.stdout)
if mode_match is None or owner_match is None:
if required:
output = f"{stat_result.stdout}\n{stat_result.stderr}".lower()
if not required and "file not found" in output:
return None
if required and "file not found" in output:
raise UvmError(f"guest rootfs is missing required file: {guest_path}")
return None
raise UvmError(f"could not inspect guest rootfs file: {guest_path}")
self._runner.run(
("debugfs", "-R", f"dump {guest_path} {destination}", disk),
@@ -229,6 +238,43 @@ class ImageStore:
):
raise UvmError(f"could not verify updated guest rootfs file: {guest_path}")
def _replace_guest_host_keys(self, disk: Path, temporary: Path) -> None:
for key_type, bits in _HOST_KEY_TYPES:
name = f"ssh_host_{key_type}_key"
private_path = temporary / name
command: list[str | Path] = [
"ssh-keygen",
"-q",
"-t",
key_type,
"-N",
"",
"-C",
"",
"-f",
private_path,
]
if bits is not None:
command[4:4] = ["-b", bits]
self._runner.run(command, capture=True)
for suffix, default_mode in (("", 0o600), (".pub", 0o644)):
guest_path = f"/etc/ssh/{name}{suffix}"
existing = self._read_guest_file(
disk,
guest_path,
temporary / f"existing-{name}{suffix}",
required=False,
)
generated = private_path.with_name(f"{name}{suffix}")
source = _GuestFile(
path=generated,
mode=existing.mode if existing is not None else default_mode,
uid=existing.uid if existing is not None else 0,
gid=existing.gid if existing is not None else 0,
)
self._write_guest_file(disk, guest_path, source)
def _remove_firecracker_demo_key(self, disk: Path, authorized_keys: _GuestFile) -> None:
contents = authorized_keys.path.read_text(encoding="utf-8")
updated = _without_firecracker_demo_key(contents)
@@ -269,10 +315,29 @@ def _set_shadow_password(contents: str, username: str, password_hash: str) -> st
def _enable_ssh_password_authentication(contents: str, username: str) -> str:
directives = ["PasswordAuthentication yes"]
managed_directives = {"hostkey", "passwordauthentication", "permitrootlogin"}
unmanaged_lines = []
for line in contents.splitlines():
stripped = line.lstrip()
directive = stripped.split(None, 1)[0].lower() if stripped else ""
if stripped.startswith("#") or directive not in managed_directives:
unmanaged_lines.append(line)
directives = [
"HostKey /etc/ssh/ssh_host_rsa_key",
"HostKey /etc/ssh/ssh_host_ecdsa_key",
"HostKey /etc/ssh/ssh_host_ed25519_key",
"PasswordAuthentication no",
]
if username == "root":
directives.append("PermitRootLogin yes")
return "# Managed by uvm\n" + "\n".join(directives) + "\n" + contents
return (
"# Managed by uvm\n"
+ "\n".join(directives)
+ f"\nMatch User {username}\n PasswordAuthentication yes\nMatch all\n"
+ "\n".join(unmanaged_lines).rstrip()
+ "\n"
)
def _without_firecracker_demo_key(contents: str) -> str | None:
+2
View File
@@ -94,6 +94,8 @@ class StateStore:
if not path.exists():
return State()
try:
if path.stat().st_mode & 0o077:
os.chmod(path, 0o600)
with path.open(encoding="utf-8") as state_file:
raw = json.load(state_file)
except (OSError, json.JSONDecodeError) as error: