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:
+69
-4
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user