Components
Mero TEE is three components plus a release pipeline that ties them together. This page is the map: what each part does and where it lives.
mero-kms — the KMS
Section titled “mero-kms — the KMS”mero-kms/ · Rust + Axum · runs as a frozen GCP TDX image
(merotee-kms-<profile>-<version>, built by mero-tee/playbook-kms.yml), one
cluster per release.
The KMS validates a node’s TDX attestation and releases a deterministic storage
encryption key, HKDF’d from a random root that exists only in the memory of the
cluster’s replicas. A cluster is five replicas across three zones in at least two
regions, behind one VPC-internal URL. Source of truth: mero-kms/src/; design:
docs/design/gcp-tdx-kms.md.
Endpoints
Section titled “Endpoints”Registered in mero-kms/src/handlers/mod.rs:
| Method · Path | Request | Response |
|---|---|---|
GET /health |
— | { status: "alive", service: "mero-kms", clusterRootReady } (always 200) |
POST /challenge |
{ peerId } |
{ challengeId, nonceB64, expiresAt } |
POST /get-key |
{ challengeId, quoteB64, peerId, peerPublicKeyB64, signatureB64, sealToB64 } |
{ sealedKeyB64, sealNonceB64 } |
POST /attest |
{ nonceB64, bindingB64?, transportKey?, eventLog? } |
{ quoteB64, reportDataHex, transportPublicKeyB64?, eventLogB64? } |
POST /cluster/nonce |
— | a nonce for a joining replica |
POST /cluster/join |
the joiner’s quote and one-time public key | the root sealed to that key, plus the giver’s own quote |
The key release flow walks the /challenge → /get-key
exchange step by step; trust model covers the checks.
Key modules
Section titled “Key modules”| File | Responsibility |
|---|---|
src/main.rs |
Bootstraps config, CORS, the backend, and the Axum server. |
src/config/mod.rs, config/env.rs |
Parse and validate all environment variables into Config. |
src/backend.rs |
configfs-tsm quotes, the in-memory root, and HKDF key derivation. |
src/cluster.rs |
Bootstrap and the attested join protocol (“same as me”, debug TDs refused, the root sealed between one-time X25519 keys). |
src/policy.rs |
AttestationPolicy model and fail-closed startup validation. |
src/measurement.rs, src/util.rs |
The 48-byte SHA-384 HexMeasurement newtype, the debug-TD check, and hex helpers. |
src/stateless_challenge.rs |
Stateless challenges: nonces HMAC’d from the challenge ID and peer ID with a root-derived key, plus a per-replica set of spent challenges until expiry. |
src/sealed.rs |
Sealing released keys to the node’s one-time key. |
src/handlers/ |
challenge.rs, get_key.rs, attest.rs, and the errors.rs ServiceError enum. |
Configuration & deployment
Section titled “Configuration & deployment”Everything that decides what the KMS releases keys to — the node allowlist, the
profile, the listen port — is baked into the image (/etc/mero-kms/kms.env) and
so into its measurements. There is no runtime policy fetch. The only inputs from
outside are two instance-metadata keys, kms-bootstrap and kms-peers, which
decide only whether a replica generates the root or joins, and from whom. The full
variable table is in the config reference.
MDMA deploys a cluster per release: the bootstrap replica first, then the others joining it, then one internal URL. Replicas never restart in place; a dead one is replaced by a new VM that joins. See the runbooks.
Node image build
Section titled “Node image build”mero-tee/ · mkosi + Ansible roles · builds GCP TDX Confidential VM images
containing merod. The same build makes the KMS image (ROLE=kms,
playbook-kms.yml: the same base and lockdown, none of the node’s services) and
the private agent image (ROLE=agent, playbook-agent.yml).
mero-tee/mkosi-image/build.sh renders the role’s playbook into a file tree
(scripts/image/render_role_tree.py), drops the empty directories no role made
on purpose (vector’s /etc/vector/data stays: vector does not start without
it), and runs mkosi on Ubuntu 26.04 LTS (Resolute
Raccoon) from a pinned archive snapshot. Kernel 6.17+ is required for RTMR3
sysfs support. The result is a UKI on the ESP and an EROFS root behind dm-verity,
and two builds of one commit give the same image. The build runs on a CI runner,
with no build VM; the image runs on TDX-capable hardware (e.g. c3-standard-4).
Output image family: merotee-ubuntu-questing-<profile>, a frozen identifier
that mdma’s dispatcher matches on, not a claim about the base release.
What the image bakes
Section titled “What the image bakes”Driven by playbook.yml and mero-tee/versions.json:
merod,meroctl, andmero-authat a pinned core tag (via thecalimero-corerole).- Traefik reverse proxy, plus
node-exporter,vmagent, andvectorfor metrics/logs. - The fleet HA sidecar systemd service
(
ansible/roles/merotee/templates/fleet-sidecar.sh.j2). - A profile marker at
/etc/calimero/image-profile.calimero.role,calimero.profileand the dm-verity root hash (roothash=) are on the UKI’s kernel command line, so RTMR2 covers every byte of the root and binary substitution changes the measurements.
Profiles
Section titled “Profiles”| Profile | Posture |
|---|---|
debug |
Development. SSH and read-write filesystem. |
debug-read-only |
Integration / pre-production. Read-only root, SSH retained. |
locked-read-only |
Production. The merod-lockdown and merotee-conformance roles run only for this profile, so its content — and therefore its MRTD/RTMR — differs from the others. |
Each profile produces a distinct RTMR3 via calimero-init, giving the
cryptographic cohort separation the trust model
relies on.
Building
Section titled “Building”sudo ROLE=node PROFILE=locked-read-only mero-tee/mkosi-image/build.sh OUTVersions come from versions.json. Release builds go through
scripts/release/mkosi/build-image.sh, which also checks the command line and
computes RTMR1–3 before the image boots. See
Build the images with mkosi.
Attestation verifier
Section titled “Attestation verifier”attestation-verifier/ · React (Vite) SPA + Vercel serverless API.
A public tool for independent verification: anyone can confirm a KMS or a
merod node is running genuine TEE code, without being part of the node ⇄ KMS
trust relationship. It routes quotes through Intel Trust Authority (ITA).
Serverless API
Section titled “Serverless API”| Route | Purpose |
|---|---|
POST /api/verify |
Accepts { node_url }, or a pasted { attestation, nonce_b64? } (the only way for a KMS, which listens only inside its VPC); submits the quote to ITA, verifies the returned JWT, and checks the nonce binding. |
GET /api/policy |
Proxies kms-attestation-policy.{profile}.json from a release (avoids CORS). |
GET /api/node-policy |
Proxies published-mrtds.json ({ profiles: {…} }) from a node release. |
GET /api/compat-map |
Proxies kms-compatibility-map.json (KMS release → node release, per-profile images and policy URLs) from a KMS release. |
api/verify.js fetches POST /admin-api/tee/attest (with { nonce } hex) from a
node, or takes a pasted KMS /attest response, then verifies the ITA appraisal JWT against Intel’s JWKS
(https://portal.trustauthority.intel.com/certs, issuer
https://portal.trustauthority.intel.com).
Security & configuration
Section titled “Security & configuration”Deployed on Vercel. Environment: ITA_API_KEY, ITA_APPRAISAL_URL (defaults to
the ITA v2 appraisal endpoint), and NODE_ALLOWED_HOSTS, a regex allowlist a
node URL must match before any outbound request is made (SSRF protection); the
address must also be public, unless NODE_ALLOW_PRIVATE=1 for local development. The
verifier never fetches from a KMS. A node request generates a fresh 32-byte nonce,
a pasted attestation is checked against the nonce_b64 supplied with it, and a
quote whose report_data does not carry the nonce is rejected. See
verification.
Release pipeline
Section titled “Release pipeline”Each version ships two artifact families from GitHub releases:
mero-kms-vX.Y.Z— per-profile attestation policies (kms-attestation-policy.<profile>.json), the compatibility map, the SBOM, checksums and Sigstore signatures. The KMS images (merotee-kms-<profile>-X-Y-Z) are kept in the GCP images project.mero-tee-vX.Y.Z—published-mrtds.json, release provenance, SBOM, checksums, Sigstore signatures.
Release assets are keyless-signed with Sigstore (GitHub OIDC) — no signing
key is stored in the repo (SECURITY.md). After a release, the
update-compatibility-catalog workflow refreshes the repo-root
compatibility-catalog.json, which maps each version to its KMS/node tags and
policy URLs:
{ "schema_version": 1, "releases": [ { "version": "2.3.x", "kms_tag": "mero-kms-v2.3.x", "node_image_tag": "mero-tee-v2.3.x", "kms_policy_url": "https://github.com/.../kms-attestation-policy.json", "node_policy_url": "https://github.com/.../published-mrtds.json" } ]}CI/CD lives in .github/workflows/ (release, staging probes, post-release e2e,
release auditor, security audit, version-sync guard). The
release pipeline and
policy management pages cover it in depth.