Remote development VM tooling for Fly.io with attestation-gated access and token authentication.
fly-vault is a Rust workspace with five binaries:
fly-vault(client): runs on your laptop, verifies attestation, authenticates withaccess_token, opens console/port forwards.fly-vault-admin(deployment admin): runs on your laptop or CI, manages tenant machines invault-tenantsvia the Fly Machines API.init(server): runs as the Fly Machine entrypoint, serves QUIC, provisions rootfs to/data/rootfs, preserves/rootand/homeunder/data/persist, and boots the workload.vault-proxy(UDP multiplexer): routes client UDP packets to tenant machines by machine id.- shared
protocolcrate: stream tags, control/console framing, shared wire types.
The design is documented in DESIGN.md.
- Attestation authenticity: client verifies Fly OIDC JWT signature and checks
iss+audchannel binding +app_name. - Transport security: QUIC (TLS 1.3).
- Authentication: single
ACCESS_TOKENused for initial provisioning and reconnects. - Data at rest: the VM rootfs plus persistent
/rootand/homestate are stored on the Fly volume (/data/rootfsand/data/persist) without client-side disk encryption.
Important: because data is not encrypted client-side, Fly platform operators with host/platform access can read persisted VM data.
crates/protocol: stream tags, control/console framing, shared wire types.crates/init: VM-side server, setup manager, forwarding.crates/client: CLI client, attestation verification, console/forwarding.crates/admin: deployment admin CLI for tenant create/list/delete/update-image.crates/proxy: UDP proxy service for multi-tenant routing.docs/: protocol/API references used by the implementation.
cargo buildBuild static init binary for container image:
cargo zigbuild --release --target x86_64-unknown-linux-musl -p initcargo test
cargo test -p init --bin initConfig path:
~/.config/fly-vault/config.toml
Example:
[vault.my-dev]
address = "[fdaa:x:x::x]:8443"
org = "my-org"
app = "my-dev-vault"
#[vault.my-proxy]
#address = "proxy.fly.dev:443"
#org = "my-org"
#app = "my-proxy-vault"
#machine_id = "148e21ea7e46e8"
access_token = "secret-token-here"
forward = ["8080:localhost:8080"]
rootfs = "~/.config/fly-vault/rootfs/dev-env.tar.gz"
# Alternative: let init download a tar.gz directly
# rootfs_url = "https://example.com/dev-env.tar.gz"fly-vault connect <vault-name>
fly-vault connect <vault-name> --forward 3000:localhost:3000
fly-vault connect <vault-name> --reprovision
fly-vault exec <vault-name> -- ls -lash /
fly-vault build--reprovision sends a new rootfs tarball to a ready VM after successful token authentication. The reprovision replaces the extracted rootfs in place but preserves /root and /home.
For local development via Cargo:
cargo run -p fly-vault -- connect <vault-name>
cargo run -p fly-vault -- exec <vault-name> -- uname -aAuth/config:
--api-tokenorFLY_API_TOKEN(required)--apporFLY_APP(required)--api-baseorFLY_API_BASE(defaults tohttps://api.machines.dev)
Tenant template snippet:
version = 1
app = "vault-tenants"
machine_count = 1
[machine]
name = "tenant-{{tenant_id}}-{{index}}"
region = "ord"
[machine.config]
image = "registry.fly.io/vault-tenants/init:latest"
[[machine.config.mounts]]
path = "/data"
name = "tvol_{{tenant_id}}_{{index}}"
[machine.config.env]
ACCESS_TOKEN = "{{access_token}}"Supported template variables:
{{tenant_id}}{{index}}(1-based machine index){{access_token}}(if--access-tokenis supplied)- extra values from repeated
--var key=value
Commands:
fly-vault-admin tenant create acme --template ./tenant.template.toml --access-token secret
fly-vault-admin tenant create acme --template ./tenant.template.toml --dry-run
fly-vault-admin tenant list --wide
fly-vault-admin tenant list --json
fly-vault-admin tenant usage-facts --tenant acme
fly-vault-admin tenant usage-facts --listen 127.0.0.1:18083 --bearer-token secret
fly-vault-admin tenant delete acme --yes
fly-vault-admin tenant update-image --image registry.fly.io/vault-tenants/init:latest --all-tenantstenant usage-facts emits a flat array of machine usage facts, and
tenant usage-facts --listen ... serves the same payload at GET /usage-facts
with optional bearer auth plus a GET /healthz liveness route.
Each usage fact includes:
machine_id,state,region,instance_idstarted_at,stopped_at,deleted_atmetadatavolumes[].volume_idvolumes[].size_gib
This JSON shape is intended to be consumed by upstream metering adapters such as
unbox, rather than relying on the broader tenant list --json output.