TDX confidential VM images for Seismic nodes.
Fork of flashbots/flashbots-images, extended with a modules/seismic/ module that bundles the Seismic node stack into a reproducible mkosi-built image deployable to Azure and GCP Confidential VMs.
Requires Lima on macOS/Linux. For alternatives (native Nix, etc.), see DEVELOPMENT.md.
make build IMAGE=seismicProduces in build/:
| File | Purpose |
|---|---|
seismic_{VERSION}.efi |
Unified Kernel Image — the boot artifact, shared across clouds |
seismic_{VERSION}.vhd |
Azure Confidential VM disk image |
seismic_{VERSION}.tar.gz |
GCP Confidential VM custom-image tarball |
seismic_{VERSION}.manifest |
JSON SBOM of every Debian package in the image |
seismic_{VERSION}.{initrd,vmlinuz} |
Debug extracts of the UKI's contents |
{VERSION} is {commit-date}.{commit-hash}[-dirty] — see mkosi.version.
Generate expected measurement values (RTMRs, MRTD) for the built UKI so a verifier can attest that a running node booted from exactly this image:
make measure # Azure-style measurements -> build/measurements.json
make measure-gcp # GCP-style measurements -> build/gcp_measurements.jsonThe two formats differ because Azure and GCP expose TDX quotes through different mechanisms — Azure via tpm2-tools + the Microsoft attestation service, GCP via the dstack toolchain. The deploy tooling (and the Seismic enclave's attestation path) consume these files to verify that deployed nodes match a known-good image.
make measure also stamps a measurement_id into the file: the versioned artifact filename (seismic_{VERSION}.vhd) these PCRs measure — the same name make push-azure uploads as the blob. Consumers of the measurements read which image they bind to from the file itself, with no out-of-band identifier to pass around (or get wrong).
| Component | Purpose | Source |
|---|---|---|
tdx-init |
First-boot LUKS provisioning; writes /persistent/conf/node.json |
SeismicSystems/enclave/bin/tdx-init |
seismic-custodian-service |
Owns root_key; serves key derivation and root-key wrapping over a local Unix socket only |
SeismicSystems/enclave/bin/custodian-service |
seismic-attestation-service |
Attestation evidence + peer root-key bootstrap; network-facing on :7878 |
SeismicSystems/enclave/bin/attestation-service |
seismic-reth |
Execution client | SeismicSystems/seismic-reth |
summit |
Consensus client | SeismicSystems/summit |
nginx + certbot |
HTTPS termination with Let's Encrypt for public RPC/WS/metrics | Debian |
Source-built pins are in modules/seismic/mkosi.build.
nginx terminates TLS (Let's Encrypt) and reverse-proxies the following paths to in-TEE services. See modules/seismic/mkosi.extra/etc/nginx/node-template.conf.
| Route | Backend | Purpose | Public? |
|---|---|---|---|
/rpc |
reth :8545 |
Ethereum JSON-RPC (shielded tx support via TxSeismic) | ✅ intended public |
/ws |
reth :8546 |
Ethereum WebSocket RPC | ✅ intended public |
/summit |
summit :3030 |
Consensus REST API, incl. /summit/get_deposit_signature/... used by the staking UI |
|
/attestation |
attestation-service :7878 |
Attestation API (tx-io attestation evidence, health/LUKS status, reth's purpose-key fetch, peer root-key bootstrap) | |
/metrics/reth |
reth :9001 |
Prometheus metrics | |
/metrics/summit |
summit :9002 |
Prometheus metrics |
Everything above listens on the single public :443. The intended long-term fix is to split nginx into two tiers — a public server block with only the endpoints that should reach the open internet (/rpc, /ws, a narrowed /summit/get_deposit_signature, attestation-quote queries on /attestation), and an internal server block on a separate port with the rest (/metrics/*, the full /summit/* query surface, operator-facing /attestation/*). The deploy tooling would then configure cloud firewall rules (Azure NSG / GCP firewall) to allow the public port from 0.0.0.0/0 and restrict the internal port to the VPC CIDR. Until that split lands, the concrete issues are:
/summit/*is a blanket proxy. Summit exposes a JSON-RPC surface (viajsonrpsee) with ~20 methods: mostly read-only state queries (getCheckpoint,getValidatorBalance,getDeposit, etc. — analogous toeth_*reads and safe to expose), plusgetDepositSignaturewhich causes a BLS signature in the enclave, plussendGenesison the genesis-setup API which must never be public at runtime. Narrowing requires JSON-RPC-method-level filtering (all calls arePOST /, so you can't gate by URL path alone).getDepositSignatureadditionally has no rate limiting — trivially DoS-able — and should gainlimit_reqregardless of network controls./attestation/*is a blanket proxy. Only endpoints designed to be public should be reachable (attestation quotes, health/status queries).getPurposeKeysreturns secret key material and is meant for reth over localhost only. Current config doesn't enforce this, and the attestation service has no caller auth of its own — anyone who can reach the port can callgetPurposeKeys. Pending the public/internal port split, or narrowing via an explicit allowlist — andgetPurposeKeysretires entirely once reth fetches its keys from the custodian socket./metrics/*is unauthenticated. Operationally safe on a locked-down network (the cloud firewall rule above is the right fix), but anyone who can reach the port can scrape sync status, peer info, and resource usage. Do not expose the internal port to the open internet without adding basic auth or an IP allowlist on top.
You can track diff with upstream by comparing the seismic branch with origin/main (which is pinned to the upstream commit we most recently rebased on).
- DEVELOPMENT.md — generic mkosi module/kernel-config/reproducibility guidance (from upstream)
docs/azure-measurements.md— what a node's boot measures on Azure TDX: the register inventory, what each PCR covers, and the(pcr4, pcr9, pcr11)guest identity admission usesmodules/seismic/mkosi.conf— Debian packages in the imagemodules/seismic/mkosi.build— pinned commits forreth/enclave/summit/tdx-initmodules/seismic/mkosi.postinst— systemd services enabled on bootmodules/seismic/mkosi.extra/— per-service unit files and configs- Deploy tooling lives in a separate repo.
Boot a built image under QEMU for smoke testing (without TDX). TDX-enabled invocation and troubleshooting are in the upstream README.
Uses the Lima VM you already run for builds. Nested virt isn't available, so boot goes through software emulation (TCG) — slow (~10 min to reach systemd) but works without any extra setup on the host.
Enter the Lima VM:
limactl shell <your-lima-vm> # e.g. tee-builder-<hash>
cd ~/mnt # where the repo is mountedEverything below runs inside that shell.
One-time setup:
sudo apt-get install -y qemu-system-x86 ovmf
cp /usr/share/OVMF/OVMF_VARS.fd /tmp/OVMF_VARS.fd # writable NVRAMBoot:
qemu-system-x86_64 \
-accel tcg -machine type=q35,smm=on -m 1024M -nographic \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE.secboot.fd \
-drive if=pflash,format=raw,file=/tmp/OVMF_VARS.fd \
-kernel build/latest.efi \
-netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::8080-:8080 \
-device virtio-net-pci,netdev=net0Exit QEMU with Ctrl-a x.
TODO: not validated in this repo. The upstream-inherited command below assumes bare-metal Linux with KVM access and should boot in seconds, but hasn't been tested in our fork — please update this section if you run it successfully.
qemu-system-x86_64 \
-enable-kvm -machine type=q35,smm=on -m 16384M -nographic \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE.secboot.fd \
-drive if=pflash,format=raw,file=/usr/share/OVMF/OVMF_VARS.fd \
-kernel build/latest.efi \
-netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::8080-:8080 \
-device virtio-net-pci,netdev=net0Built on flashbots/flashbots-images. Thanks to the Flashbots team for the mkosi-based TDX image tooling that this repo forks.