Refactor VM lifecycle management and enhance validation
- Introduced a new method `installed_kernel` in `ImageStore` to handle kernel verification and checksum validation. - Updated `Installer` to include additional packages: `nftables` and `conntrack`. - Refined `LifecycleService` to improve error handling and state management during VM creation, starting, stopping, and deletion. - Enhanced port management with new methods for setting and syncing ports, including validation for port rules. - Improved error handling in `StateStore` to raise specific `NotFoundError` and `ConflictError` exceptions. - Updated network management to ensure proper TAP device handling and firewall integration. - Enhanced API responses in `vms.py` to include policy status for better client-side handling. - Added validation for VM names and ports in `validation.py` to enforce stricter rules and prevent conflicts.
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
`uvm` is a small, local Firecracker microVM command-line manager for Ubuntu.
|
||||
It creates one Firecracker process per VM, attaches it to a host TAP device,
|
||||
and persists enough state to list, stop, and destroy the VM later.
|
||||
and keeps a private disk and local registry for named VMs. The CLI supports
|
||||
create, start, stop, restart, delete, inspection, SSH, logs, diagnostics, and
|
||||
enforced private-IPv4 TCP port allowlists.
|
||||
|
||||
This directory is a self-contained project. All UVM code, tests, packaging
|
||||
metadata, launcher scripts, and documentation live here.
|
||||
@@ -13,14 +15,17 @@ workflows, see [DEVELOPER.md](DEVELOPER.md).
|
||||
For end-to-end operator scenarios, CLI examples, and HTTP API calls, see
|
||||
[USAGE.md](USAGE.md).
|
||||
|
||||
For the original firewall proposal and backend evaluation, see
|
||||
[FIREWALL.md](FIREWALL.md). That proposal is design background, not the current
|
||||
feature contract; these guides describe the implementation in the workspace.
|
||||
|
||||
## Quick Start
|
||||
|
||||
From the parent repository directory, run commands from this project root:
|
||||
Run commands from this project root:
|
||||
|
||||
```sh
|
||||
cd uvm
|
||||
./uvm.py --help
|
||||
python3 -m uvm --help
|
||||
python3 -B -m uvm --help
|
||||
```
|
||||
|
||||
The `pyproject.toml` file also defines an installable `uvm` console command.
|
||||
@@ -32,12 +37,16 @@ The project is an application, not a reusable Python SDK.
|
||||
- Python 3.12 or newer
|
||||
- Root access for UVM commands that read or mutate the protected VM registry
|
||||
- KVM enabled and accessible at `/dev/kvm`
|
||||
- Linux 5.3 or newer with native bridge conntrack and safe pidfd support
|
||||
- An x86_64 host for the default guest assets
|
||||
- Working host networking, `iproute2`, `iptables`, `e2fsprogs`, `openssl`, and
|
||||
`openssh-client`
|
||||
- Working host networking, `iproute2`, `iptables`, `nftables`, `conntrack`,
|
||||
`e2fsprogs`, `util-linux`, `openssl`, and `openssh-client`
|
||||
- An STP-disabled software bridge; no offload path that bypasses bridge hooks
|
||||
|
||||
`uvm install` installs the required host packages. It validates `/dev/kvm`
|
||||
before downloading or launching a microVM.
|
||||
before downloading VM artifacts. Boot operations additionally check that the
|
||||
native nftables bridge policy is supported; installing packages alone is not
|
||||
a connectivity or isolation test.
|
||||
|
||||
## Trusted Installation
|
||||
|
||||
@@ -45,8 +54,6 @@ Installation requires trusted SHA-256 digests by default. Supply digests from
|
||||
an independently trusted source before running the privileged installer:
|
||||
|
||||
```sh
|
||||
cd uvm
|
||||
|
||||
sudo env \
|
||||
UVM_FIRECRACKER_SHA256='<sha256-of-firecracker-archive>' \
|
||||
UVM_KERNEL_SHA256='<sha256-of-vmlinux>' \
|
||||
@@ -72,59 +79,134 @@ available through an explicit opt-out:
|
||||
sudo env UVM_ALLOW_UNVERIFIED_DOWNLOADS=1 ./uvm.py install
|
||||
```
|
||||
|
||||
An unverified install is recorded as such and cannot satisfy a later strict
|
||||
An unverified manifest is not accepted as publisher trust in a later strict
|
||||
run. Reinstall with trusted digests to establish a verified local manifest.
|
||||
To create VMs from an unverified local-development install, retain
|
||||
`UVM_ALLOW_UNVERIFIED_DOWNLOADS=1` on later `create` commands and on the API
|
||||
server process.
|
||||
To boot VMs from an unverified local-development install, retain
|
||||
`UVM_ALLOW_UNVERIFIED_DOWNLOADS=1` on later `create`, `start`, and `restart`
|
||||
commands and on the API server process.
|
||||
|
||||
## Commands
|
||||
|
||||
These are separate operation examples, not a script to run from top to bottom.
|
||||
|
||||
```sh
|
||||
# Install after supplying the trusted checksum variables shown above.
|
||||
# A bare `install` command fails under the default strict integrity policy.
|
||||
|
||||
# Create and boot a VM with the defaults: 1 CPU, 512 MiB RAM, and root/root.
|
||||
sudo ./uvm.py create
|
||||
# Create a named VM: 1 CPU, 512 MiB RAM, root/root, and private TCP/22.
|
||||
# Use a unique non-default password for anything beyond a throwaway VM.
|
||||
sudo ./uvm.py create --name web
|
||||
|
||||
# Request CPU and RAM explicitly. RAM defaults to MiB; B/K/M/G suffixes work.
|
||||
sudo ./uvm.py create --cpu 1 --ram 1G
|
||||
# Choose resources, a private guest IP, and additional service ports.
|
||||
sudo ./uvm.py create --name app --cpu 1 --ram 1G \
|
||||
--guest-ip 10.42.0.10 --ports 80:HTTP,443:HTTPS
|
||||
|
||||
# Set credentials for an account that already exists in the guest image.
|
||||
sudo ./uvm.py create --username root --password '<guest-password>'
|
||||
# Read a password from one line of stdin, rather than a --password argument.
|
||||
# USAGE.md includes a non-echoing Bash prompt example.
|
||||
sudo ./uvm.py create --name worker --password-stdin
|
||||
|
||||
# Select the guest IP. Despite the legacy flag name, this is the guest IP.
|
||||
sudo ./uvm.py create --host-ip 10.42.0.10
|
||||
# Explicitly allow no inbound service ports, including SSH.
|
||||
sudo ./uvm.py create --name closed --no-default-ports
|
||||
|
||||
# Show persisted VM state and observed process status.
|
||||
# Inspect persisted state and observed process/policy status.
|
||||
sudo ./uvm.py list
|
||||
sudo ./uvm.py list --status running --json
|
||||
sudo ./uvm.py list -q
|
||||
sudo ./uvm.py inspect web --json
|
||||
sudo ./uvm.py logs web --tail 50 --follow
|
||||
sudo ./uvm.py doctor --json
|
||||
|
||||
# Connect over SSH by VM ID or guest IP.
|
||||
sudo ./uvm.py ssh vm-<id>
|
||||
# Resolve VMs by exact ID, name, or guest IP.
|
||||
sudo ./uvm.py ssh web
|
||||
sudo ./uvm.py ssh app --port 22 -- uname -a
|
||||
ssh root@10.42.0.10
|
||||
|
||||
# Stop or remove a VM.
|
||||
sudo ./uvm.py stop vm-<id>
|
||||
sudo ./uvm.py destroy vm-<id>
|
||||
# Stop and boot the same private disk, or restart selected VMs.
|
||||
sudo ./uvm.py stop web --timeout 5
|
||||
sudo ./uvm.py start web
|
||||
sudo ./uvm.py restart web app --timeout 10
|
||||
|
||||
# Preview bulk operations before confirming them.
|
||||
sudo ./uvm.py stop --all --dry-run --json
|
||||
sudo ./uvm.py stop --all -y
|
||||
sudo ./uvm.py start --all -y
|
||||
sudo ./uvm.py restart --all -y
|
||||
sudo ./uvm.py delete --all --dry-run
|
||||
sudo ./uvm.py delete web -y
|
||||
# destroy is an alias for delete, with the same flags and destructive effect.
|
||||
```
|
||||
|
||||
`start`, `stop`, `restart`, and `delete` accept one or more VM identifiers or
|
||||
`--all`, plus `--json`, `--dry-run`, and `--yes`/`-y`. Deletion and mutating
|
||||
`--all` selections require confirmation; non-interactive callers must use
|
||||
`--yes`. `--timeout` is available on stop/restart/delete (and `destroy`), not
|
||||
start. It is a finite non-negative SIGTERM wait in seconds, default `5`, not
|
||||
a total command deadline or a guest shutdown timeout.
|
||||
|
||||
Stop sends SIGTERM and, if needed, SIGKILL to the Firecracker VMM. It is **not
|
||||
a guest-coordinated shutdown**. Start boots the retained disk without resetting
|
||||
guest data, SSH host keys, or credentials; it does not restore guest memory.
|
||||
Delete permanently removes the disk and releases the VM's name and IP after
|
||||
cleanup. See [USAGE.md](USAGE.md) for the complete flag and recovery reference.
|
||||
|
||||
Names are unique local identifiers, not DNS names. `--host-ip` remains an
|
||||
alias for `--guest-ip`; both select the guest's private IPv4 address. JSON
|
||||
output omits passwords, with warnings and host-command traces on stderr.
|
||||
|
||||
SSH uses `StrictHostKeyChecking=accept-new` and a VM-ID-specific host-key
|
||||
alias by default. `--insecure-host-key` restores the old no-verification
|
||||
behavior for a single connection.
|
||||
alias by default. `--user`, `--key`, and `--port` customize the SSH client;
|
||||
append a remote command after a literal `--`. `--insecure-host-key` disables
|
||||
verification for one connection. SSH does not automatically supply the stored
|
||||
password or open a closed port.
|
||||
|
||||
### Private Ports
|
||||
|
||||
```sh
|
||||
sudo ./uvm.py ports list web --json
|
||||
sudo ./uvm.py ports list --all
|
||||
sudo ./uvm.py ports open web --ports 80:HTTP --ports 443:HTTPS,8080:TCP
|
||||
sudo ./uvm.py ports close web --ports 80
|
||||
sudo ./uvm.py ports set web --ports 22,443:HTTPS --dry-run --json
|
||||
sudo ./uvm.py ports set web --ports 22,443:HTTPS -y
|
||||
sudo ./uvm.py ports close web --all -y
|
||||
sudo ./uvm.py ports set web --none -y
|
||||
sudo ./uvm.py ports sync web
|
||||
sudo ./uvm.py ports sync --all --dry-run
|
||||
sudo ./uvm.py ports sync --all -y
|
||||
```
|
||||
|
||||
Rules use `PORT[:TYPE]`, for example `22`, `80:HTTP`, `443:HTTPS`, or
|
||||
`8080:TCP`, repeated or comma-separated. HTTP and HTTPS are **TCP labels**,
|
||||
not proxies or TLS configuration. Create adds `22:TCP` even when other ports
|
||||
are supplied, unless `--no-default-ports` is set. Later port edits do not
|
||||
automatically add SSH back. `ports close VM --all` closes ports on that one
|
||||
VM; only `ports list --all` and `ports sync --all` select all VMs.
|
||||
|
||||
Port mutations support `--dry-run` and `--json`; close/set/sync also support
|
||||
`--yes`/`-y`. Set, close-all, and removal of TCP/22 require confirmation
|
||||
(an already empty managed policy can be a no-op). Closing a service blocks
|
||||
subsequent packets on existing service connections in **both directions**, as
|
||||
well as new connections. This is packet dropping, not a guaranteed TCP RST or
|
||||
immediate application disconnect.
|
||||
|
||||
These rules govern access to the **guest private IP**. They do not start a
|
||||
guest service or provide public connectivity, host-port mappings, DNS,
|
||||
reverse proxies, or guest-service TLS. UDP and IPv6 service policies are not
|
||||
yet supported. Guest-initiated IPv4 TCP/UDP egress, its replies, and required
|
||||
ARP/ICMP traffic remain allowed; managed TAPs block IPv6.
|
||||
|
||||
## Management API
|
||||
|
||||
Install the project dependencies before serving the API:
|
||||
|
||||
```sh
|
||||
cd uvm
|
||||
python3 -m pip install .
|
||||
```
|
||||
|
||||
Launch the FastAPI management server with the requested CLI form:
|
||||
Launch the FastAPI management server using the environment where the project
|
||||
dependencies were installed (use the full console-script path if needed):
|
||||
|
||||
```sh
|
||||
cd uvm
|
||||
export UVM_API_TOKEN='replace-with-a-long-random-secret'
|
||||
sudo env UVM_API_TOKEN="$UVM_API_TOKEN" \
|
||||
uvm --serve --host 127.0.0.1 --port 8000
|
||||
@@ -144,13 +226,19 @@ The server exposes these routes:
|
||||
| `GET` | `/health` | Unauthenticated liveness response |
|
||||
| `POST` | `/install` | Install or refresh host assets; body: `{ "force": false }` |
|
||||
| `GET` | `/vms` | List VM records and observed status |
|
||||
| `POST` | `/vms` | Create a VM; body supports `cpu`, `ram`, `guest_ip`, `username`, and `password` |
|
||||
| `GET` | `/vms/{id-or-ip}` | Retrieve one VM by ID or guest IP |
|
||||
| `POST` | `/vms/{id}/stop` | Stop a VM while retaining its private disk |
|
||||
| `DELETE` | `/vms/{id}` | Destroy a VM and release its resources |
|
||||
| `POST` | `/vms` | Create; supports `cpu`, `ram`, `guest_ip`, `username`, `password`, `name`, `ports`, `no_default_ports` |
|
||||
| `GET` | `/vms/{identifier}` | Inspect by exact ID, name, or guest IP |
|
||||
| `POST` | `/vms/{identifier}/stop` | Stop a VM while retaining its private disk |
|
||||
| `DELETE` | `/vms/{identifier}` | Delete a VM and release its resources |
|
||||
|
||||
The API intentionally does not proxy interactive SSH. Use the returned guest
|
||||
IP with `uvm ssh` or a normal SSH client.
|
||||
API port rules are objects such as `{"port": 80, "protocol": "HTTP"}`;
|
||||
`no_default_ports` is a boolean, default `false`. Responses include `name`,
|
||||
`ports`, `policy_status`, and `operation`, but never passwords. See
|
||||
[USAGE.md](USAGE.md) for request examples and strict schema details.
|
||||
|
||||
The endpoint set is unchanged: there are **no start/restart, port-management,
|
||||
or bulk HTTP endpoints**. SSH, logs, and doctor are also CLI-only. Use the
|
||||
local CLI for those operations; the API is not a background reconciler.
|
||||
|
||||
### API Security
|
||||
|
||||
@@ -219,6 +307,24 @@ it, enables IPv4 forwarding, adds an egress masquerade rule, and adds matching
|
||||
forward rules for return traffic. Guest IPs, gateway, bridge name, and guest
|
||||
boot netmask derive from the configured CIDR.
|
||||
|
||||
The existing `iptables` egress NAT/forwarding behavior is unchanged. Private
|
||||
port enforcement uses separate per-VM native nftables `bridge` tables, not
|
||||
DNAT or host listeners. Policy is installed and read back before an owned TAP
|
||||
is activated and before the guest starts. The software bridge must have STP
|
||||
disabled; each TAP's bridge-slave `group_fwd_mask` is set to `0x4000` so LLDP
|
||||
cannot bypass the early identity filter. Native bridge conntrack requires
|
||||
Linux >= 5.3; no `br_netfilter` sysctl setup is required by this policy layer.
|
||||
|
||||
Host-to-VM and VM-to-VM traffic crossing these hooks is filtered. An allowed
|
||||
port is not a reachability guarantee, and a private IP alone is not protection
|
||||
against administrator-supplied external routing or NAT. Integration with UFW,
|
||||
firewalld, Docker, or other external firewall managers remains deferred.
|
||||
Do not offload traffic around the software bridge hooks.
|
||||
|
||||
There is no autostart service or daemon reconciliation. After externally lost
|
||||
or changed UVM rules, inspect policy status and explicitly run `ports sync`.
|
||||
Read-only commands do not repair rules; sync does not boot stopped VMs.
|
||||
|
||||
## Persistent State
|
||||
|
||||
The default data directory is `/var/lib/uvm`:
|
||||
@@ -232,9 +338,9 @@ The default data directory is `/var/lib/uvm`:
|
||||
└── state.json # Root-only VM inventory and guest credentials
|
||||
```
|
||||
|
||||
State writes are locked and atomic. Lifecycle operations share an additional
|
||||
operation lock so concurrent `create`, `stop`, `destroy`, and `install`
|
||||
commands cannot overwrite each other's state or binaries.
|
||||
The default registry is `/var/lib/uvm/state.json`, schema version `2`. Writes
|
||||
are locked and atomic. Lifecycle, port-policy, and installation mutations
|
||||
share an operation lock; bulk operations use a fixed snapshot of VM IDs.
|
||||
|
||||
The registry stores guest usernames and passwords in plaintext, so `state.json`
|
||||
is mode `0600`. Treat it as a secret, do not include it in bug reports, and
|
||||
@@ -243,14 +349,28 @@ replace the default `root` password immediately.
|
||||
Credential provisioning applies only to VMs created by this version. Existing
|
||||
VM disks are not modified automatically.
|
||||
|
||||
Older registries are read and migrated on the next write. A missing/null
|
||||
`ports` policy remains `None`/`unmanaged`, not an empty allowlist or an
|
||||
implicitly enforced default. Migrate a legacy VM explicitly with `stop`,
|
||||
`ports set VM --ports 22,80:HTTP -y` (choose the actual required ports), then
|
||||
`start`. Resume requires a complete, safe private disk, a verified kernel and
|
||||
VMM, and compatible network settings. Removing the original rootfs template
|
||||
does not block start of a retained private disk.
|
||||
|
||||
Process identity includes PID, Linux process start time, and host boot ID.
|
||||
A running legacy PID missing the boot ID is adopted only when its saved start
|
||||
time, running binary, exact API-socket command line, and protected paths all
|
||||
match. Missing or ambiguous evidence fails closed: UVM will not guess which
|
||||
process to signal or release a disk while process exit is unconfirmed.
|
||||
|
||||
## Configuration
|
||||
|
||||
All optional configuration is supplied through environment variables:
|
||||
Host and API settings can be overridden through environment variables:
|
||||
|
||||
| Variable | Purpose |
|
||||
|---|---|
|
||||
| `UVM_BASE` | Data directory; defaults to `/var/lib/uvm` |
|
||||
| `UVM_NETWORK` | Guest IPv4 CIDR; defaults to `10.42.0.0/24` |
|
||||
| `UVM_NETWORK` | Guest private IPv4 CIDR; defaults to `10.42.0.0/24` |
|
||||
| `UVM_GATEWAY` | Bridge/guest gateway; defaults to `10.42.0.1` |
|
||||
| `UVM_BRIDGE` | Linux bridge name; defaults to `uvm0` |
|
||||
| `UVM_KERNEL_URL` | Guest kernel download URL |
|
||||
@@ -267,52 +387,57 @@ All optional configuration is supplied through environment variables:
|
||||
## Project Layout
|
||||
|
||||
```text
|
||||
uvm/
|
||||
├── README.md
|
||||
./
|
||||
├── README.md, USAGE.md, DEVELOPER.md
|
||||
├── pyproject.toml
|
||||
├── uvm.py # Executable compatibility launcher
|
||||
├── uvm/ # Private application implementation
|
||||
│ ├── cli.py # argparse, terminal output, SSH execution
|
||||
│ ├── app.py # Application composition root
|
||||
│ ├── api_models.py # FastAPI request and response models
|
||||
│ ├── config.py # Settings and filesystem layout
|
||||
│ ├── domain.py # Typed VM and state models
|
||||
│ ├── errors.py # Expected application failures
|
||||
│ ├── integrity.py # Checksums and manifest provenance
|
||||
│ ├── install.py # Host/artifact installation
|
||||
│ ├── images.py # Guest templates and per-VM disks
|
||||
│ ├── lifecycle.py # Create/list/stop/destroy orchestration
|
||||
│ ├── lifecycle.py # Lifecycle, bulk operations, recovery, port-policy intent
|
||||
│ ├── network.py # IP/MAC/TAP/bridge/NAT management
|
||||
│ ├── routers/ # Health, installation, and VM HTTP routers
|
||||
│ ├── server.py # FastAPI application factory and Uvicorn launcher
|
||||
│ ├── state.py # Locked atomic JSON persistence
|
||||
│ ├── system.py # Host command and KVM adapters
|
||||
│ ├── firewall.py # Owned nftables bridge policies and offline conntrack cleanup
|
||||
│ └── firecracker/ # Firecracker config, API, and process adapters
|
||||
└── tests/ # Unit tests with mocked host boundaries
|
||||
└── tests/ # Unit tests and opt-in isolated namespace packet tests
|
||||
```
|
||||
|
||||
See [DEVELOPER.md](DEVELOPER.md) for the complete module map and ownership rules.
|
||||
|
||||
## Testing
|
||||
|
||||
From the parent repository directory, run the unit suite from this project root:
|
||||
Run the unit suite from this project root:
|
||||
|
||||
```sh
|
||||
cd uvm
|
||||
python3 -m pip install '.[test]'
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 -B -m unittest discover -s tests -v
|
||||
python3 -B -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
The unit tests do not need root, KVM, Firecracker, or host network changes.
|
||||
They cover validation, state migration and locking, integrity manifests,
|
||||
network command rendering, Firecracker request ordering, process safety, and
|
||||
lifecycle rollback. API route tests run automatically when the FastAPI
|
||||
and `httpx` test dependencies are installed.
|
||||
network/firewall rendering, Firecracker request ordering, process safety,
|
||||
retained-disk resume, bulk operations, and lifecycle rollback. API route tests
|
||||
run when the FastAPI and `httpx` test dependencies are installed.
|
||||
|
||||
Opt-in packet checks run **only in disposable isolated user/network
|
||||
namespaces**, which may require privileges and the installed network tools:
|
||||
|
||||
```sh
|
||||
UVM_FIREWALL_INTEGRATION=1 python3 -B -m unittest discover -s tests/integration -v
|
||||
```
|
||||
|
||||
The packet test was skipped in this workspace because namespace creation was
|
||||
denied (`Operation not permitted`). A skip is not packet-path or real-host
|
||||
validation. The harness uses veths in place of TAPs and does not boot a real VM;
|
||||
validate KVM, TAP, NAT, SSH, and guest connectivity on an isolated Linux host.
|
||||
|
||||
## Current Boundaries
|
||||
|
||||
- Fractional CPU requests still round Firecracker vCPUs up and are advisory.
|
||||
- `stop` retains the private VM disk; the current command surface does not yet
|
||||
include a separate `start` command.
|
||||
- No quotas or cgroup CPU, memory, I/O, or process limits are applied.
|
||||
- Firecracker is launched directly by this local CLI. Jailer/cgroup hardening
|
||||
is not implemented by the current command set.
|
||||
- No VM autostart, background reconciliation, guest readiness probing,
|
||||
snapshots, DHCP, public-port publishing, or multi-host scheduling.
|
||||
- The API uses one shared token, not users, roles, or tenants. UVM is not a
|
||||
hardened multi-tenant platform.
|
||||
- Run real KVM, TAP, and guest-connectivity checks on an isolated Linux host
|
||||
before relying on UVM for workloads.
|
||||
|
||||
Reference in New Issue
Block a user