diff --git a/USAGE.md b/USAGE.md index 55fdef6..f79bc77 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1067,7 +1067,6 @@ sudo env \ UVM_BRIDGE='uvm50' \ UVM_FIRECRACKER_SHA256='' \ UVM_KERNEL_SHA256='' \ - UVM_ROOTFS_SHA256='' \ ./uvm.py install ``` @@ -1086,10 +1085,10 @@ automatically detects every conflict with host routes or other managers. | `UVM_BRIDGE` | Linux bridge name; default `uvm0` | | `UVM_KNOWN_HOSTS_FILES` | Override deletion's local SSH cache files; colon-separated absolute paths | | `UVM_KERNEL_URL` | Override the host-architecture default kernel URL | -| `UVM_ROOTFS_URL` | Override the host-architecture default rootfs URL | +| `UVM_ROOTFS_URL` | Optional prebuilt ext4 URL; unset to build Ubuntu 22.04 locally | | `UVM_FIRECRACKER_SHA256` | Strict Firecracker archive checksum | | `UVM_KERNEL_SHA256` | Strict kernel checksum | -| `UVM_ROOTFS_SHA256` | Strict rootfs checksum | +| `UVM_ROOTFS_SHA256` | Strict prebuilt rootfs checksum, used with `UVM_ROOTFS_URL` | | `UVM_FIRECRACKER_BINARY_SHA256` | Optional local binary checksum | | `UVM_ALLOW_UNVERIFIED_DOWNLOADS` | Set to `1` only for local development | | `UVM_API_TOKEN` | Required API token | diff --git a/tests/test_images.py b/tests/test_images.py index c89e0ac..3df063e 100644 --- a/tests/test_images.py +++ b/tests/test_images.py @@ -143,6 +143,31 @@ class ImageStoreTests(unittest.TestCase): self.assertLess(updated.index("Match User codemate-user"), updated.index("Match Group restricted")) self.assertIn("Match Group restricted\n X11Forwarding no", updated) + @unittest.skipUnless( + shutil.which("sshd") and shutil.which("ssh-keygen") and Path("/run/sshd").is_dir(), + "OpenSSH server tools and the privilege-separation directory are required", + ) + def test_openssh_accepts_the_full_config_and_limits_password_auth_to_the_guest_user(self) -> None: + runner = CommandRunner(emit=None) + with tempfile.TemporaryDirectory() as temporary: + key = Path(temporary) / "host_key" + runner.run(("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", key), capture=True) + contents = _enable_ssh_password_authentication( + "UsePAM yes\nSubsystem sftp internal-sftp\nPermitRootLogin no\n", + "codemate-user", + ) + for key_type in ("rsa", "ecdsa", "ed25519"): + contents = contents.replace(f"/etc/ssh/ssh_host_{key_type}_key", str(key)) + config = Path(temporary) / "sshd_config" + config.write_text(contents, encoding="utf-8") + for username, expected in (("codemate-user", "yes"), ("root", "no")): + with self.subTest(username=username): + result = runner.run(( + shutil.which("sshd"), "-T", "-f", config, + "-C", f"user={username},host=localhost,addr=127.0.0.1", + ), capture=True) + self.assertIn(f"passwordauthentication {expected}\n", result.stdout) + def test_removes_only_the_public_firecracker_demo_key(self) -> None: own_key = "ssh-ed25519 AAAA-own-key developer@example" @@ -325,6 +350,11 @@ class OfflineGuestCredentialsTests(unittest.TestCase): self.assertIn("Defaults:codemate-user !rootpw, !runaspw, !targetpw\n", self.contents("/etc/sudoers")) self.assert_metadata("/etc/sudoers", 0o440, 0, 0) self.assert_metadata("/usr/bin/sudo", 0o4755, 0, 0) + if shutil.which("visudo"): + sudoers = self.base / "sudoers" + sudoers.write_text(self.contents("/etc/sudoers"), encoding="utf-8") + sudoers.chmod(0o440) + self.runner.run(("visudo", "-c", "-f", sudoers), capture=True) for guest_path in ("/etc/passwd", "/etc/group"): self.assert_metadata(guest_path, 0o644, 0, 0) for guest_path in ("/etc/shadow", "/etc/gshadow"): diff --git a/uvm/dashboard/static/index.html b/uvm/dashboard/static/index.html index 23d444f..9c093ef 100644 --- a/uvm/dashboard/static/index.html +++ b/uvm/dashboard/static/index.html @@ -193,7 +193,7 @@

01A name and a little space

Give your new machine a shape.
Start with a size
-

02A way to connect

Private networking and guest access.

Missing users are created with a home directory. Credentials are applied to the machine’s private disk.

+

02A way to connect

Private networking and guest access.

Missing users are created with a home directory and sudo access using their guest password. Credentials are applied to the machine’s private disk.

03Open the right doors

Choose which private TCP services are allowed.
@@ -202,7 +202,7 @@ diff --git a/uvm/images.py b/uvm/images.py index bc39aff..2bcfa81 100644 --- a/uvm/images.py +++ b/uvm/images.py @@ -253,8 +253,10 @@ class ImageStore: owner = _OWNER_PATTERN.search(sudo.stdout) if ( sudoers is None + or sudoers.uid != 0 or sudoers.mode & 0o022 or re.search(r"Type:\s+regular\b", sudo.stdout) is None or mode is None or not int(mode.group(1), 8) & 0o4000 + or not int(mode.group(1), 8) & 0o111 or owner is None or int(owner.group(1)) != 0 ): raise UvmError( diff --git a/uvm/install.py b/uvm/install.py index 0be99ea..ded2449 100644 --- a/uvm/install.py +++ b/uvm/install.py @@ -204,6 +204,13 @@ class Installer: label="guest kernel", environment_name="UVM_KERNEL_SHA256", ) + verify_file( + kernel, + self._settings.kernel_sha256, + "guest kernel", + "UVM_KERNEL_SHA256", + allow_unverified=self._settings.allow_unverified_downloads, + ) if not self._settings.rootfs_url: self._install_ubuntu_rootfs(rootfs, force=force) elif force or not rootfs.exists(): @@ -214,13 +221,6 @@ class Installer: label="guest root filesystem", environment_name="UVM_ROOTFS_SHA256", ) - verify_file( - kernel, - self._settings.kernel_sha256, - "guest kernel", - "UVM_KERNEL_SHA256", - allow_unverified=self._settings.allow_unverified_downloads, - ) if self._settings.rootfs_url: verify_file( rootfs,