Fleet HA Sidecar
The fleet HA sidecar is the Mero TEE half of hardware-attested fleet
admission. It is a Bash script
(mero-tee/ansible/roles/merotee/templates/fleet-sidecar.sh.j2) baked into the
ReadOnly TEE node image and run as a systemd service alongside merod. Its
job is to keep this node’s Calimero namespace membership in sync with what
the fleet control plane (MDMA) currently assigns to it: join the namespaces
MDMA wants it in, and leave the ones it withdraws.
The admission handshake itself — quote generation, TeeAttestationAnnounce,
MRTD-gated admission — lives in Calimero Core, not here. The sidecar is only
the trigger: it drives merod through meroctl and reports back to MDMA. See
Calimero Core → TEE Attestation & Fleet Admission
for the protocol it drives.
What it reads at startup
Section titled “What it reads at startup”Before it can poll, the sidecar has to know two things about itself, plus where to talk to MDMA. Configuration is baked at image build time via Ansible (so it is measured into the MRTD), not read from instance metadata; TLS to MDMA is always verified.
| Value | Source | Notes |
|---|---|---|
MDMA_URL |
Ansible build-time var (fleet_mdma_url) |
Sidecar exits if unset. |
FLEET_TOKEN |
Ansible build-time var (fleet_auth_token, optional) |
Sent as the X-Fleet-Token header when present. |
| PeerId | /mnt/data/calimero/default/config.toml → [identity].peer_id |
Read after merod is ready; retried once after a short sleep. |
| MRTD | /sys/class/misc/tdx_guest/measurements/mrtd:sha384 |
See below. |
Discovering the PeerId
Section titled “Discovering the PeerId”get_peer_id() reads the node’s PeerId straight from its config file at
/mnt/data/calimero/default/config.toml, under [identity].peer_id (via
tomllib, falling back to a grep/sed parse). It does not shell out to
meroctl for this — an earlier version that tried meroctl peers list failed
because peers takes no arguments and returns only a count.
Discovering its own MRTD
Section titled “Discovering its own MRTD”get_mrtd() reads this TDX guest’s own MRTD (the SHA-384 measurement of the
initial VM image) directly from the kernel-exposed sysfs attribute:
/sys/class/misc/tdx_guest/measurements/mrtd:sha384This is the same 48-byte value the Intel Quoting Enclave writes into a TDX quote
(quote.mrtd()), but the kernel surfaces it read-only, so no quote generation
or configfs-tsm round-trip is required. The script hex-encodes it to 96
lowercase hex characters with no 0x prefix — exactly the shape MDMA’s MRTD
allowlist compares against. MRTD is computed once at startup because it is
fixed for the life of the immutable node image.
The reconcile loop
Section titled “The reconcile loop”After a one-time wait_for_merod() (which blocks until
meroctl --output-format json peers exits 0) and the PeerId/MRTD reads above,
the sidecar enters an infinite loop with a one-second poll interval
(POLL_INTERVAL=1). Each cycle:
- Poll MDMA.
POST {MDMA_URL}/api/fleet/should-joinwith body{"peer_id": "...", "mrtd": "..."}(curl -sf --max-time 10, plus theX-Fleet-Tokenheader if a token is configured). - Gate on poll success. The response must be HTTP 200 and parse as an
object with an
assignmentsarray. Any curl error, non-2xx, timeout, or unparseable/wrong-shape 200 body aborts the cycle before any state change — the loop sleeps andcontinues, leaving the confirmed set untouched (see the safety gate below). - Compute
desired. The sorted set ofgroup_ids inassignments— every namespace MDMA currently assigns this node to. - Join toward
desired. For each assigned namespace not already in the local confirmed set, runjoin_group(see below). On admission,POST{MDMA_URL}/api/fleet/confirmwith{"peer_id": "...", "group_id": "..."}. A namespace is added tofleet-confirmed.jsononly when both local admission and the MDMA confirm succeed. - Leave dropped namespaces. Compute
to_leave = confirmed − desiredandleave_groupeach one (see below). This runs before the prune, so the diff is taken whileconfirmedstill holds the dropped entries. - Prune the confirmed set. Set
confirmed = confirmed ∩ desiredand persist it, so a future re-add is recognized as new. - Sleep one second and repeat.
The confirmed set is persisted at /var/lib/calimero/fleet-confirmed.json
(a deliberately separate path from any pre-upgrade legacy state file, and a
different semantics: it records “admitted and confirmed”, not merely
“attempted”). Activity is logged to /var/log/fleet-sidecar.log.
Joining a namespace
Section titled “Joining a namespace”join_group() runs:
meroctl --home /mnt/data/calimero --node default \ --output-format json tee fleet-join <GROUP_ID>The group id is passed positionally (an earlier --group-id flag form
failed clap parsing; the positional form matches the GROUP_ID argument in the
baked core release — see the CHANGELOG for the exact core version).
Under the hood meroctl tee fleet-join generates a TDX quote, broadcasts
TeeAttestationAnnounce on the namespace topic, and polls for admission for up
to roughly 30 seconds. Critically, meroctl exits 0 whether or not the node
was admitted — process success is not admission success. So the sidecar parses
the JSON response and only treats admitted: true as success (it is tolerant of
both a flat {"admitted": ...} shape and a {"data": {...}} wrapper). If
fleet-join completes without admission, the namespace is left unconfirmed and
retried next cycle — fleet-join is idempotent in Core (an already-admitted
group returns admitted: true immediately), so retrying is cheap. This retry
convergence is what an earlier “try once, mark done” design lacked: if the group
owner was offline during the first ~30s window, the old code marked the
assignment tried and never retried.
Leaving a namespace (disable path)
Section titled “Leaving a namespace (disable path)”When a namespace the node previously confirmed is no longer in desired — HA
disabled, the slot reclaimed, or the node’s MRTD no longer trusted — the sidecar
self-leaves it:
meroctl --home /mnt/data/calimero --node default \ --output-format json namespace leave <HEX_NAMESPACE_ID>This publishes MemberLeft at the namespace root, which cascades through every
descendant subgroup where this node has a direct row. leave_group is
idempotent and non-fatal: leaving a namespace the node already left (or was
never a direct member of) returns a benign error (nothing to leave /
not a direct member) that is logged, and the loop continues — it never aborts
the sidecar.
The poll-success safety gate
Section titled “The poll-success safety gate”The step-2 gate is safety-critical, not a nicety. Because a missing assignment
now triggers an irreversible namespace leave + key purge, a transient MDMA
outage must never be mistaken for “all namespaces disabled” — that would shred
keys on every healthy replica at once.
So poll_mdma captures curl’s body and exit status separately and returns:
0+ a validated body only on HTTP 200 whose body parses as an{"assignments": [...]}object; or1with no stdout on any curl error, non-2xx, timeout, or unparseable/wrong-shape body.
On a 1, the main loop skips the entire reconcile — no desired computed, no
join, no leave, no prune — and preserves fleet-confirmed.json untouched.
Preserving confirmed across a failed poll is itself load-bearing: if a failed
poll pruned confirmed, a genuine disable on the next good poll would no longer
appear in the confirmed − desired diff, and the node would never leave.
Symmetrically, confirm_assignment returns curl’s real exit code (rather than
swallowing errors), so a transient blip on the confirm step leaves the namespace
out of confirmed and is simply retried next cycle instead of being lost.
MDMA endpoints and paths at a glance
Section titled “MDMA endpoints and paths at a glance”| Item | Value |
|---|---|
| Poll (should-join) | POST {MDMA_URL}/api/fleet/should-join — body {"peer_id", "mrtd"} |
| Confirm | POST {MDMA_URL}/api/fleet/confirm — body {"peer_id", "group_id"} |
| Join command | meroctl … tee fleet-join <GROUP_ID> (positional) |
| Leave command | meroctl … namespace leave <HEX_NAMESPACE_ID> |
| Readiness probe | meroctl --output-format json peers |
| PeerId source | /mnt/data/calimero/default/config.toml → [identity].peer_id |
| MRTD source | /sys/class/misc/tdx_guest/measurements/mrtd:sha384 |
| Confirmed state | /var/lib/calimero/fleet-confirmed.json |
| Log | /var/log/fleet-sidecar.log |
| Poll interval | 1 second |
| Auth header | X-Fleet-Token (when FLEET_TOKEN is set) |
Where to go next
Section titled “Where to go next”- System overview — the two trust flows, and where the sidecar sits in fleet admission.
- Key release flow — the other key mechanism (KMS storage key), kept deliberately separate from fleet admission.
- Trust model — what MRTD and the other measurements pin.
- Glossary — MRTD, MDMA, namespace,
ReadOnlyTee, and the rest of the vocabulary. - Operate: runbooks and release pipeline — how the node image carrying this sidecar is built and rolled out.