Improve security and error handling for VM state and guest file operations; clarify permissions requirements in documentation.

This commit is contained in:
2026-09-04 21:51:04 +00:00
parent a8dbc704e9
commit 4ce14c3378
6 changed files with 117 additions and 7 deletions
+37
View File
@@ -10,6 +10,7 @@ from uvm.config import Settings
from uvm.errors import UvmError
from uvm.images import (
_FIRECRACKER_DEMO_PUBLIC_KEY,
_GuestFile,
_enable_ssh_password_authentication,
_set_shadow_password,
_without_firecracker_demo_key,
@@ -96,6 +97,8 @@ class ImageStoreTests(unittest.TestCase):
"HostKey /etc/ssh/ssh_host_ecdsa_key\n"
"HostKey /etc/ssh/ssh_host_ed25519_key\n"
"PasswordAuthentication no\n"
"KbdInteractiveAuthentication no\n"
"ChallengeResponseAuthentication no\n"
"PermitRootLogin yes\n"
"Match User root\n"
" PasswordAuthentication yes\n"
@@ -105,6 +108,16 @@ class ImageStoreTests(unittest.TestCase):
self.assertNotIn("/insecure/shared-key", updated)
self.assertNotIn("PermitRootLogin prohibit-password", updated)
def test_non_root_credentials_preserve_the_existing_root_login_policy(self) -> None:
updated = _enable_ssh_password_authentication(
"PermitRootLogin no\nChallengeResponseAuthentication yes\n",
"service",
)
self.assertIn("PermitRootLogin no", updated)
self.assertIn("Match User service\n PasswordAuthentication yes", updated)
self.assertNotIn("ChallengeResponseAuthentication yes", updated)
def test_removes_only_the_public_firecracker_demo_key(self) -> None:
own_key = "ssh-ed25519 AAAA-own-key developer@example"
@@ -150,3 +163,27 @@ class ImageStoreTests(unittest.TestCase):
Path(temporary_directory) / "authorized_keys",
required=False,
)
def test_refuses_to_replace_files_with_unpreserved_security_metadata(self) -> None:
runner = PasswordHashRunner()
store = ImageStore(Settings(), runner=runner) # type: ignore[arg-type]
for guest_file in (
_GuestFile(Path("/tmp/shadow"), 0o640, 0, 42, links=2),
_GuestFile(
Path("/tmp/shadow"),
0o640,
0,
42,
has_extended_attributes=True,
),
):
with self.subTest(guest_file=guest_file):
with self.assertRaises(UvmError):
store._write_guest_file(
Path("/tmp/rootfs.ext4"),
"/etc/shadow",
guest_file,
)
self.assertEqual(runner.calls, [])
+13
View File
@@ -6,9 +6,11 @@ import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
from unittest.mock import patch
from uvm.config import Settings
from uvm.domain import VmRecord
from uvm.errors import StateError
from uvm.state import StateStore
@@ -97,3 +99,14 @@ class StateStoreTests(unittest.TestCase):
self.store.load()
self.assertEqual(stat.S_IMODE(self.settings.state_path.stat().st_mode), 0o600)
def test_load_explains_when_registry_permissions_require_root(self) -> None:
self.base.mkdir(parents=True)
self.settings.state_path.write_text('{"vms": {}}', encoding="utf-8")
self.settings.state_path.chmod(0o644)
with (
patch("uvm.state.os.chmod", side_effect=PermissionError),
self.assertRaisesRegex(StateError, "Run this command with sudo"),
):
self.store.load()