Skip to content

EternisAI/cvm-control

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cvm-control

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).

Build & run

go build -o cvm-control .
./cvm-control

The server listens on 0.0.0.0:9443 by default with a self-signed TLS certificate generated at startup.

Nix

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 adding cvm-control
nix build            # build the binary → ./result/bin/cvm-control
nix flake check      # validate flake structure
nix flake show       # list all outputs

Inputs are pinned to specific commit hashes for supply-chain security.

Project structure

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

Configuration

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 in ResolveEBSDevice (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.

API

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).

GET /api/v1/status

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.

POST /api/v1/bootstrap

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 user
  • env (optional) — map of filename → key-value pairs, written as systemd-compatible env files under $RUN_DIR/env/ (mode 0644)
  • secret (optional) — same format as env, 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 ID
  • passphrase (required) — LUKS passphrase
  • mount_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:

  1. Check if already mounted — skip if so
  2. Check if /dev/mapper/<dm_name> exists — skip LUKS setup if so
  3. Resolve EBS volume symlink via /dev/disk/by-id/
  4. Format with LUKS2 (aes-xts-plain64, argon2id) if not already formatted
  5. Open the LUKS volume (automatically uses full device size if the underlying EBS volume was grown)
  6. If the volume was already LUKS-formatted, remove any keyslots that don't match the bootstrap passphrase (best-effort cleanup)
  7. Create ext4 filesystem if none exists; if one already exists, run e2fsck -fy to check and repair it (handles unclean shutdowns), then resize2fs to expand it if the device has grown
  8. 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.

GET /api/v1/bootstrap

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": "..."}
  }
}

PUT /api/v1/passphrase

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: luksAddKey adds 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 use cryptsetup luksKillSlot directly.

During bootstrap, pre-existing LUKS volumes have stale keyslots (those not matching the bootstrap passphrase) automatically removed after opening.

GET /api/v1/passphrase

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": "..."}
  }
}

POST /api/v1/shutdown

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 bootstrap user_id
  • reboot (optional) — if true, reboots instead of powering off (default: false)

Response (success):

{"status": "ok"}

Response (error):

{"error": "systemctl poweroff failed with exit code 1", "detail": "..."}

State persistence

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.

About

Control-plane agent for Confidential Virtual Machines

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors