Accept real cloud users without the replay loophole
If you accept sign-ups from cloud servers, an attacker can rent one server, pass the check, shut it down, and reuse that one 'pass' for thousands of fake accounts. This guide shows how to accept real cloud users without that.
What goes wrong, in one paragraph
A virtual TPM in EC2 / Azure / GCP is a genuine signing chip — but a software one that lives only as long as the cloud instance does. Its certificate proves "this is an AWS NitroTPM," but it doesn't prove whichAWS account is using it, and once the instance shuts down that virtual chip is gone. An attacker who captures one valid attestation can replay it against your service from anywhere, because nothing in it ties the attestation to a still-running instance owned by the original requester. That replay is the "cuckoo" attack: one real nest, many impostor eggs.
Why you can't just reject cloud workloads
The most paranoid policy (strict-hardware) rejects every cloud vTPM outright. That works for consumer-facing flows: bots don't need to bring their own real human, they just lease compute, and rejecting all leased compute is fine. But many B2B and developer-tool flows have legitimate cloud users:
- API key issuance for a developer SaaS, where devs sign up from their EC2 dev box.
- Device check before issuing CI/CD credentials to a build runner.
- Bot framework registration for legitimate automation (e.g. customer-owned scrapers).
For those, you need to accept cloud vTPMs but bind them to a still-living, still-owned cloud account.
The fix: cross-validate the vTPM with provider-issued instance identity
Each major cloud signs its own instance-identity document with its own root key, and that document is queryable from inside the instance:
- AWS: Nitro Attestation Document via
/dev/nsm(Nitro Enclaves) or instance-identity document athttp://169.254.169.254/latest/dynamic/instance-identity/document. - Azure: Managed Service Identity (MSI) token from
http://169.254.169.254/metadata/identity/oauth2/token. - GCP: Attested instance identity token from
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=…&format=full.
The keyless collector bundles the cloud-native document into the sealed evidence alongside the vTPM attestation, then hands the whole bundle to your server. Your server submits it with rh.verify()and our verifier cross-validates: the cloud document's instance id, the vTPM EK cert's subject, and the freshness of both have to agree.
Use a cloud-cross-validated policy
{
"name": "cloud-cross-validated",
"acceptedClassGroups": ["hardware", "firmware-tpm", "mobile-hardware"],
"acceptedSpecificClasses": [
"cloud-vtpm-aws-nitro",
"cloud-vtpm-azure",
"cloud-vtpm-gcp"
],
"unacceptedBehavior": "reject",
"requireCloudCrossValidation": true,
"requireSignedQuote": true,
"minimumEarStatus": "warning",
"mode": "permissive"
}Same recipe as the policy-recipes page; reproduced here for the full walkthrough.
Collect cloud evidence, appraise server→server
The keyless native collector (RootHeraldClient_CollectEvidence) is functional on Windows only today — its Linux and macOS entry points return ROOTHERALD_ERR_INTERNAL until the per-platform collectors land, and @rootherald/browser is browser-only. There is no npm / go get collector that runs on a Linux server yet. The snippet below is illustrative: it shows the AWS instance-identity half you would fold into the evidence blob. The server-side rh.verify() half is real and works today.
import fetch from "node-fetch";
// Illustrative: gather the cloud-native instance-identity document to fold into
// the evidence blob alongside a vTPM attestation. On a Linux server today the
// keyless native collector is not yet shipped — this shows the IMDSv2 half only.
async function awsInstanceIdentity() {
// Token-based IMDSv2 — required for any modern EC2 hardening profile.
const tokenRes = await fetch("http://169.254.169.254/latest/api/token", {
method: "PUT",
headers: { "X-aws-ec2-metadata-token-ttl-seconds": "60" },
});
const imdsToken = await tokenRes.text();
const docRes = await fetch(
"http://169.254.169.254/latest/dynamic/instance-identity/document",
{ headers: { "X-aws-ec2-metadata-token": imdsToken } },
);
return docRes.json();
}
// ...bundle awsInstanceIdentity() into the evidence blob, POST to your server.import { RootHerald } from "@rootherald/node";
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! }); // rh_sk_…
// Earlier: rh.issueChallenge() -> relay nonce to the collector.
// The collector posts back { challengeId, evidence } (evidence carries the
// vTPM attestation + the cloud instance-identity document).
const verdict = await rh.verify(body.evidence, {
challengeId: body.challengeId,
policy: "cloud-permissive", // requireCloudCrossValidation toggled on — see Step 1
});
if (verdict.device.verdict !== "pass") {
return res.status(403).json({ error: "device_rejected" });
}
// verdict.device.ueid is the stable per-tenant device id.cloud-cross-validated is not a built-in; the five built-ins are strict-hardware, strict-hardware-permissive-mobile, cloud-permissive, enterprise-managed-only, and dev. Cross-validation is a toggle enabled within cloud-permissive (or supply the custom policy from Step 1 by its id). Either way, set requireCloudCrossValidation: true to get the cuckoo defense described here.
What the verifier does
The verifier's cross-validation logic, in plain English:
- Validates the vTPM EK certificate chains to the AWS / Azure / GCP TPM CA.
- Validates the cloud-native document signature against the cloud's own root.
- Extracts the instance id from bothdocuments. AWS Nitro: from the EK cert subject + the instance-identity document. Azure: from the MSI token + the vTPM quote's subjectAltName. GCP: from the attested identity token.
- Confirms the two instance ids match exactly.
- Confirms the quote is bound to the per-request challenge nonce your server minted (the ~5-minute nonce is the replay floor).
- Issues the verdict.
A replayed vTPM document (from a now-dead instance) fails at step 4 if the attacker replays from a different instance, and fails the nonce binding at step 5 if they replay a stale capture.
A determined attacker who keeps the original instance alive and proxies the attestation requests through it can still abuse the original valid attestation. The defense is economic: each persistent EC2 / Azure VM costs ~$30-100/month, so spinning up hundreds-of-thousands of accounts requires hundreds-of-thousands of dollars in cloud spend. The math no longer favors the attacker.