Improve security and error handling for VM state and guest file operations; clarify permissions requirements in documentation.
This commit is contained in:
+59
-3
@@ -24,6 +24,8 @@ _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+)")
|
||||
_FLAGS_PATTERN = re.compile(r"Flags:\s+0x([0-9a-fA-F]+)")
|
||||
_LINKS_PATTERN = re.compile(r"Links:\s+(\d+)")
|
||||
_HOST_KEY_TYPES = (
|
||||
("rsa", "3072"),
|
||||
("ecdsa", "256"),
|
||||
@@ -43,6 +45,9 @@ class _GuestFile:
|
||||
mode: int
|
||||
uid: int
|
||||
gid: int
|
||||
flags: int | None = None
|
||||
links: int = 1
|
||||
has_extended_attributes: bool = False
|
||||
|
||||
|
||||
class ImageStore:
|
||||
@@ -184,7 +189,14 @@ 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:
|
||||
flags_match = _FLAGS_PATTERN.search(stat_result.stdout)
|
||||
links_match = _LINKS_PATTERN.search(stat_result.stdout)
|
||||
if (
|
||||
mode_match is None
|
||||
or owner_match is None
|
||||
or flags_match is None
|
||||
or links_match is None
|
||||
):
|
||||
output = f"{stat_result.stdout}\n{stat_result.stderr}".lower()
|
||||
if not required and "file not found" in output:
|
||||
return None
|
||||
@@ -198,14 +210,35 @@ class ImageStore:
|
||||
)
|
||||
if not destination.is_file():
|
||||
raise UvmError(f"could not read guest rootfs file: {guest_path}")
|
||||
attributes = self._runner.run(
|
||||
("debugfs", "-R", f"ea_list {guest_path}", disk),
|
||||
check=False,
|
||||
capture=True,
|
||||
)
|
||||
diagnostics = [
|
||||
line
|
||||
for line in attributes.stderr.splitlines()
|
||||
if line and not line.startswith("debugfs ")
|
||||
]
|
||||
if attributes.returncode != 0 or diagnostics:
|
||||
raise UvmError(f"could not inspect guest rootfs metadata: {guest_path}")
|
||||
return _GuestFile(
|
||||
path=destination,
|
||||
mode=int(mode_match.group(1), 8),
|
||||
uid=int(owner_match.group(1)),
|
||||
gid=int(owner_match.group(2)),
|
||||
flags=int(flags_match.group(1), 16),
|
||||
links=int(links_match.group(1)),
|
||||
has_extended_attributes=bool(attributes.stdout.strip()),
|
||||
)
|
||||
|
||||
def _write_guest_file(self, disk: Path, guest_path: str, source: _GuestFile) -> None:
|
||||
if source.links != 1:
|
||||
raise UvmError(f"cannot safely replace hard-linked guest file: {guest_path}")
|
||||
if source.has_extended_attributes:
|
||||
raise UvmError(
|
||||
f"cannot safely replace guest file with extended attributes: {guest_path}"
|
||||
)
|
||||
self._runner.run(("debugfs", "-w", "-R", f"rm {guest_path}", disk), capture=True)
|
||||
self._runner.run(
|
||||
("debugfs", "-w", "-R", f"write {source.path} {guest_path}", disk),
|
||||
@@ -226,6 +259,17 @@ class ImageStore:
|
||||
),
|
||||
capture=True,
|
||||
)
|
||||
if source.flags is not None:
|
||||
self._runner.run(
|
||||
(
|
||||
"debugfs",
|
||||
"-w",
|
||||
"-R",
|
||||
f"set_inode_field {guest_path} flags 0x{source.flags:x}",
|
||||
disk,
|
||||
),
|
||||
capture=True,
|
||||
)
|
||||
|
||||
verification = source.path.with_name(f"{source.path.name}.verify")
|
||||
written = self._read_guest_file(disk, guest_path, verification)
|
||||
@@ -235,6 +279,7 @@ class ImageStore:
|
||||
or written.mode != source.mode
|
||||
or written.uid != source.uid
|
||||
or written.gid != source.gid
|
||||
or (source.flags is not None and written.flags != source.flags)
|
||||
):
|
||||
raise UvmError(f"could not verify updated guest rootfs file: {guest_path}")
|
||||
|
||||
@@ -315,7 +360,14 @@ def _set_shadow_password(contents: str, username: str, password_hash: str) -> st
|
||||
|
||||
|
||||
def _enable_ssh_password_authentication(contents: str, username: str) -> str:
|
||||
managed_directives = {"hostkey", "passwordauthentication", "permitrootlogin"}
|
||||
managed_directives = {
|
||||
"challengeresponseauthentication",
|
||||
"hostkey",
|
||||
"kbdinteractiveauthentication",
|
||||
"passwordauthentication",
|
||||
}
|
||||
if username == "root":
|
||||
managed_directives.add("permitrootlogin")
|
||||
unmanaged_lines = []
|
||||
for line in contents.splitlines():
|
||||
stripped = line.lstrip()
|
||||
@@ -328,13 +380,17 @@ def _enable_ssh_password_authentication(contents: str, username: str) -> str:
|
||||
"HostKey /etc/ssh/ssh_host_ecdsa_key",
|
||||
"HostKey /etc/ssh/ssh_host_ed25519_key",
|
||||
"PasswordAuthentication no",
|
||||
"KbdInteractiveAuthentication no",
|
||||
"ChallengeResponseAuthentication no",
|
||||
]
|
||||
if username == "root":
|
||||
directives.append("PermitRootLogin yes")
|
||||
return (
|
||||
"# Managed by uvm\n"
|
||||
+ "\n".join(directives)
|
||||
+ f"\nMatch User {username}\n PasswordAuthentication yes\nMatch all\n"
|
||||
+ f"\nMatch User {username}\n"
|
||||
+ " PasswordAuthentication yes\n"
|
||||
+ "Match all\n"
|
||||
+ "\n".join(unmanaged_lines).rstrip()
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
@@ -98,6 +98,10 @@ class StateStore:
|
||||
os.chmod(path, 0o600)
|
||||
with path.open(encoding="utf-8") as state_file:
|
||||
raw = json.load(state_file)
|
||||
except PermissionError as error:
|
||||
raise StateError(
|
||||
f"cannot access protected VM registry {path}. Run this command with sudo."
|
||||
) from error
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise StateError(f"cannot read {path}: {error}") from error
|
||||
return State.from_dict(raw)
|
||||
|
||||
Reference in New Issue
Block a user