Skip to content

Config & Secrets Injection

mvm supports injecting custom files onto the guest’s config and secrets drives at boot time. Files are written to the drive images before the VM starts.

Terminal window
mkdir -p /tmp/my-config /tmp/my-secrets
echo '{"gateway": {"port": 8080}}' > /tmp/my-config/app.json
echo 'API_KEY=sk-...' > /tmp/my-secrets/app.env
mvmctl up --template my-app \
--volume /tmp/my-config:/mnt/config \
--volume /tmp/my-secrets:/mnt/secrets

The --volume (-v for short) flag uses the format host_dir:/guest/path:

Guest pathDrivePermissionsPurpose
/mnt/config/dev/vdbRead-only (0444)Application configuration
/mnt/secrets/dev/vdcRead-only (0400)API keys, tokens, credentials

Every file in the host directory is written to the corresponding drive image. For persistent volumes with explicit size, use the 3-part format: --volume host:/guest/path:size.

The same functionality is available programmatically for library consumers:

use mvm_runtime::vm::microvm::{DriveFile, FlakeRunConfig};
let config = FlakeRunConfig {
config_files: vec![DriveFile {
name: "app.json".into(),
content: serde_json::to_string(&app_config)?,
mode: 0o444,
}],
secret_files: vec![DriveFile {
name: "app.env".into(),
content: format!("API_KEY={}", api_key),
mode: 0o400,
}],
..base_config
};

For AI agent workloads, use --secret to bind environment variable secrets to specific target domains. This provides domain-scoped secret injection — combine with --network-preset to prevent exfiltration:

Terminal window
mvmctl up --flake . \
--secret OPENAI_API_KEY:api.openai.com \
--secret ANTHROPIC_API_KEY:api.anthropic.com:x-api-key \
--network-preset dev

Binding syntax:

FormatMeaning
KEY:hostRead KEY from host env, bind to host (Authorization header)
KEY:host:headerCustom HTTP header name
KEY=value:hostExplicit value instead of env lookup
KEY=value:host:headerExplicit value + custom header

What happens at boot:

  1. Secret values are resolved (from host env or explicit) and written to the secrets drive (mode 0600)
  2. A secrets-manifest.json is written to the config drive (metadata only, no values)
  3. Placeholder env vars (mvm-managed:KEY) are set in the guest environment so tools pass existence checks
  4. Combined with network allowlists, the VM can only send traffic to the allowed domains

This is the “config-drive injection” approach. The secret values are on the guest’s secrets drive but are scoped to specific domains via network policy. A future upgrade will add MITM proxy-based injection where secrets never touch the guest filesystem.

The DriveFile type is content-agnostic — it’s just {name, content, mode}. It knows nothing about specific file formats or keys. This means:

  • Any file format works (JSON, TOML, YAML, env files, certificates, etc.)
  • Adding support for new applications doesn’t require code changes
  • NixOS EnvironmentFile can load .env files directly as systemd environment variables

The OpenClaw example demonstrates all of these features. It ships with a default config baked into the image, but you can override it by mounting host directories.

Terminal window
mvmctl template build openclaw
mvmctl up --template openclaw --name oc \
-v nix/examples/openclaw/config:/mnt/config \
-v nix/examples/openclaw/secrets:/mnt/secrets \
-p 3000:3000
mvmctl forward oc 3000:3000

The default config uses auth.mode: "none" — no token is required to access the Control UI. The gateway binds to loopback inside the VM with a TCP proxy forwarding external connections, so all connections appear local and are auto-approved by OpenClaw (no device pairing prompts). To enable token auth, set "auth": { "mode": "token" } in your config and OPENCLAW_GATEWAY_TOKEN in secrets/api-keys.env.

Terminal window
# Create config directory with OpenClaw gateway settings
mkdir -p /tmp/oc-config
cat > /tmp/oc-config/openclaw.json << 'EOF'
{
"gateway": {
"mode": "local",
"channelHealthCheckMinutes": 0,
"auth": { "mode": "none" },
"reload": { "mode": "off" },
"controlUi": {
"dangerouslyAllowHostHeaderOriginFallback": true
}
}
}
EOF
# Create secrets directory with API keys
mkdir -p /tmp/oc-secrets
cat > /tmp/oc-secrets/api-keys.env << 'EOF'
ANTHROPIC_API_KEY=sk-ant-...
EOF
mvmctl up --template openclaw --name oc \
-v /tmp/oc-config:/mnt/config \
-v /tmp/oc-secrets:/mnt/secrets \
-p 3000:3000

The OpenClaw service’s preStart script checks for /mnt/config/openclaw.json and uses it (with envsubst expansion) instead of the built-in default. The command script sources /mnt/config/env.sh and /mnt/secrets/api-keys.env if they exist, making environment variables available to the gateway process.

Build the template with --snapshot to capture a running VM state. Subsequent runs restore from the snapshot instead of cold-booting, reducing startup time from minutes to 1-2 seconds:

Terminal window
mvmctl template build openclaw --snapshot
mvmctl up --template openclaw --name oc \
-v nix/examples/openclaw/config:/mnt/config \
-v nix/examples/openclaw/secrets:/mnt/secrets \
-p 3000:3000

When restoring from a snapshot with -v mounts, the guest agent automatically remounts config/secrets drives and restarts services with the fresh data.

Snapshots + Dynamic Mounts = Instant Boots with Flexible Config

Section titled “Snapshots + Dynamic Mounts = Instant Boots with Flexible Config”

Key insight: The snapshot stores the OS and application state (memory, running processes, compiled code caches), but config and secrets drives are created fresh at runtime from your host directories. This means:

  • Same snapshot can serve multiple instances with different configs
  • Update configs without rebuilding — just change the files in your host directory
  • Instant boot + dynamic configuration — get both benefits simultaneously

Example: Run three OpenClaw instances from one snapshot with different API keys:

Terminal window
# Production gateway with prod Anthropic key
mvmctl up --template openclaw --name oc-prod \
-v ./prod/config:/mnt/config \
-v ./prod/secrets:/mnt/secrets \
-p 3000:3000
# Staging gateway with test key
mvmctl up --template openclaw --name oc-staging \
-v ./staging/config:/mnt/config \
-v ./staging/secrets:/mnt/secrets \
-p 3001:3000
# Dev gateway with no key (localhost-only testing)
mvmctl up --template openclaw --name oc-dev \
-v ./dev/config:/mnt/config \
-p 3002:3000

All three VMs restore from the same snapshot (1-2 second boot) but get different configs and secrets at runtime.

Check the OpenClaw gateway logs via the guest console:

Terminal window
mvmctl logs oc # view console output
mvmctl logs oc -f # follow in real time

See nix/examples/openclaw/ for the full example with sample config and secrets files.