Control-plane agent for Confidential Virtual Machines (CVMs). Exposes an HTTPS API for bootstrapping and managing a CVM — provisioning configuration and secrets, setting up LUKS2-encrypted EBS volumes, rotating volume passphrases, and initiating graceful shutdown or reboot. The current implementation targets AWS EC2 instances with AMD SEV-SNP and NitroTPM (e.g. r6a, m6a instance families); volume device resolution is AWS-specific (see the cloud provider note below).
go build -o cvm-control .
./cvm-controlThe server listens on 0.0.0.0:9443 by default with a self-signed TLS certificate generated at startup.
A Nix flake is provided for reproducible builds. The flake exposes:
packages.<system>.cvm-control— the statically linked binary (CGO_ENABLED=0)overlays.default— nixpkgs overlay addingcvm-control
nix build # build the binary → ./result/bin/cvm-control
nix flake check # validate flake structure
nix flake show # list all outputsInputs are pinned to specific commit hashes for supply-chain security.
pkg/api/v1/ Public API request/response types (no framework deps)
internal/
config.go, logging.go, tlsutil.go Configuration, logging, TLS utilities
server/
server.go Server struct, lifecycle, exported state accessors
operation.go Async operation tracking
state.go Bootstrap record persistence and recovery
errors.go BootstrapError type
mount.go Mount-check helper
v1/
routes.go Route table (RegisterRoutes)
handlers/ HTTP handler methods (bootstrap, passphrase, shutdown, status)
util/ Shared utilities (volume ops, device helpers, env provisioning)
cmd/
root.go CLI entry point, wires server + v1 routes
Every setting can be configured via CLI flags or environment variables (with the CVM_CONTROL_ prefix). CLI flags take precedence over environment variables.
| Flag | Environment variable | Default | Description |
|---|---|---|---|
--address |
CVM_CONTROL_ADDRESS |
0.0.0.0 |
Listen address |
--port |
CVM_CONTROL_PORT |
9443 |
Listen port |
--tls-cn |
CVM_CONTROL_TLS_CN |
cvm-control |
Common name for the self-signed TLS certificate |
--log-level |
CVM_CONTROL_LOG_LEVEL |
info |
Log level (debug, info, warn, error) |
--log-format |
CVM_CONTROL_LOG_FORMAT |
json |
Log format (json, text) |
--run-dir |
CVM_CONTROL_RUN_DIR |
/run/cvm |
Directory for runtime state (env files, bootstrap record) |
Note (cloud provider): Volume device resolution is AWS-specific: it follows the NVMe symlink at
/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_*created by the AWS NVMe driver. Supporting other public or private cloud providers would require abstracting this step inResolveEBSDevice(internal/server/v1/util/device.go).
Note (TLS): The self-signed certificate is an MVP stub. The planned replacement is SPIRE SVID retrieval: the server will present a short-lived, workload-attested X.509 SVID and all callers will be authenticated via SPIFFE-based mutual TLS (mTLS), eliminating the need for static certificates entirely.
Only one long-running operation (bootstrap or passphrase rotation) can be active at a time. Any new request while one is in progress returns 409 Conflict, unless it is an identical retry of the in-progress request (idempotent retry → 202 Accepted).
Returns the current server status.
{"status": "bootstrap"}Status is "bootstrap" at startup, transitions to "ready" after a successful bootstrap, and to "shutdown" once a shutdown or reboot has been initiated. In the "shutdown" state, bootstrap and passphrase operations are rejected with 409 Conflict.
Bootstraps the CVM: provisions env/secret files and opens/mounts encrypted EBS volumes. Volumes are processed in parallel — each volume runs to completion independently so that a failure in one volume never cancels or corrupts an in-flight operation on another.
Request:
{
"user_id": "user-abc",
"env": {
"app.env": { "APP_MODE": "production" }
},
"secret": {
"credentials.env": { "DB_PASSWORD": "s3cret" }
},
"volumes": {
"data": {
"volume_id": "vol-abc123",
"passphrase": "secret",
"mount_point": "/data"
},
"logs": {
"volume_id": "vol-def456",
"passphrase": "secret2",
"mount_point": "/var/log/app"
}
}
}Top-level fields:
user_id(required) — identifier for the bootstrapping userenv(optional) — map of filename → key-value pairs, written as systemd-compatible env files under$RUN_DIR/env/(mode 0644)secret(optional) — same format asenv, written under$RUN_DIR/secret/(mode 0600)volumes(required) — map of dm-mapper names to volume specs
A user_id.env file is also written to $RUN_DIR/user_id.env (mode 0644) containing CVM_USER_ID=<user_id>.
Each volume has:
volume_id(required) — EBS volume IDpassphrase(required) — LUKS passphrasemount_point(optional) — defaults to/<dm_name>
Response (202 Accepted):
The handler validates the request and starts provisioning asynchronously. The HTTP connection is released immediately.
{"status": "in_progress"}Poll GET /api/v1/bootstrap to track progress and retrieve the final result.
Per-volume bootstrap flow:
- Check if already mounted — skip if so
- Check if
/dev/mapper/<dm_name>exists — skip LUKS setup if so - Resolve EBS volume symlink via
/dev/disk/by-id/ - Format with LUKS2 (aes-xts-plain64, argon2id) if not already formatted
- Open the LUKS volume (automatically uses full device size if the underlying EBS volume was grown)
- If the volume was already LUKS-formatted, remove any keyslots that don't match the bootstrap passphrase (best-effort cleanup)
- Create ext4 filesystem if none exists; if one already exists, run
e2fsck -fyto check and repair it (handles unclean shutdowns), thenresize2fsto expand it if the device has grown - Mount at the configured mount point
Note: Integrity protection (dm-integrity) is omitted in this release for two reasons. (1) Built-in LUKS2 integrity (
--integrity hmac-sha256) is incompatible with EBS volume resizing — cryptsetup does not support online or offline resize of LUKS2 volumes with built-in integrity enabled. (2) The alternative — a separate dm-integrity layer beneath LUKS2, which avoids the resize limitation — introduces its own MVP-blocking challenges: if configured with HMAC it requires managing separate integrity HMAC keys alongside LUKS passphrases, and dm-integrity imposes per-sector metadata and write-journal overhead causing significant write amplification. Adding a separate dm-integrity layer beneath LUKS2 is planned for a future release.
The endpoint is idempotent — retrying with the same configuration after success returns {"status": "ready"} without re-running any operations. Retrying the same request while the operation is still in progress returns 202 Accepted. A request with a different configuration returns 409 Conflict.
Returns the current bootstrap operation status.
Response (idle — no bootstrap attempted):
{"status": "idle"}Response (in progress):
{"status": "in_progress"}Response (completed):
{"status": "completed"}Response (failed):
When one or more volumes fail, the response includes per-volume results. Successful volumes still complete normally.
{
"status": "failed",
"error": "one or more volumes failed",
"volumes": {
"data": {"status": "ok"},
"logs": {"status": "error", "error": "luksFormat failed", "detail": "..."}
}
}Rotates LUKS passphrases for one or more volumes. Requires the server to be in "ready" state (i.e., bootstrapped). Volumes are processed in parallel — each volume runs to completion independently.
Request:
{
"volumes": {
"data": {
"old_passphrase": "current-secret",
"new_passphrase": "rotated-secret"
},
"logs": {
"old_passphrase": "current-secret2",
"new_passphrase": "rotated-secret2"
}
}
}Volume keys are dm-mapper names matching those used in bootstrap. Each volume requires:
old_passphrase(required) — current LUKS passphrase (used for authentication)new_passphrase(required) — new passphrase to add as a keyslot
Response (202 Accepted):
The handler validates the request and starts passphrase rotation asynchronously. The HTTP connection is released immediately.
{"status": "in_progress"}Poll GET /api/v1/passphrase to track progress and retrieve the final result.
The endpoint is idempotent — if the new passphrase already unlocks the volume, or if old and new are identical, the volume reports "ok" without making changes. Retrying the same request while the operation is still in progress returns 202 Accepted. Returns 401 if the old passphrase is invalid, 409 if the server is not ready or another operation is in progress.
Note:
luksAddKeyadds the new passphrase as a new keyslot; the old keyslot is not removed. Both the old and new passphrases will unlock the volume after a successful rotation. To remove the old keyslot, run another passphrase rotation with the roles swapped (old ↔ new is a no-op), or usecryptsetup luksKillSlotdirectly.
During bootstrap, pre-existing LUKS volumes have stale keyslots (those not matching the bootstrap passphrase) automatically removed after opening.
Returns the current passphrase change operation status.
Response (idle — no passphrase change attempted):
{"status": "idle"}Response (in progress):
{"status": "in_progress"}Response (completed):
{
"status": "completed",
"volumes": {
"data": {"status": "ok"},
"logs": {"status": "ok"}
}
}Response (failed):
{
"status": "failed",
"error": "one or more volumes failed",
"volumes": {
"data": {"status": "ok"},
"logs": {"status": "error", "error": "luksAddKey failed", "detail": "..."}
}
}Initiates a system shutdown or reboot. Cannot be called while a bootstrap or passphrase change operation is in progress (returns 409 Conflict).
When the server is in "ready" state (bootstrapped), the request body is required and must include a user_id that matches the one used during bootstrap. Returns 401 Unauthorized if user_id is missing or does not match. When the server has not been bootstrapped yet, no authentication is required.
Request:
{"user_id": "user-abc", "reboot": true}user_id(required when bootstrapped) — must match the bootstrapuser_idreboot(optional) — iftrue, reboots instead of powering off (default:false)
Response (success):
{"status": "ok"}Response (error):
{"error": "systemctl poweroff failed with exit code 1", "detail": "..."}On successful bootstrap a record is saved to $RUN_DIR/bootstrap.json (containing user_id and volume metadata — no passphrases). On startup the server loads this record and verifies that all volumes are still mounted; if so it recovers to "ready" state automatically, surviving restarts without requiring a new bootstrap request.