Skip to content
Guide · Cuckoo defense

Cloud-permissive without falling to vTPM replay

The cuckoo attack: an attacker provisions a cloud VM, attests, kills the VM, and replays the attestation against your service from a different VM (or no VM at all). You wanted to accept cloud workloads, but you don't want every accepted attestation to be a clone of someone else's.

The threat in one paragraph

A virtual TPM in EC2 / Azure / GCP is a real cryptographic primitive: it signs with a key that is, at the moment of signing, in a specific hypervisor instance. But the EK certificate that proves "this is an AWS NitroTPM" doesn't prove whichAWS account is using it, and the vTPM's key material is ephemeral-ish: when the instance terminates, that vTPM goes away. An attacker who captures one valid attestation document can replay it against your service from anywhere, because nothing in the document binds it to a still-living instance owned by the original requester.

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 at http://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 dumb client collector bundles the cloud-native document into the opaque evidence blob alongside the vTPM attestation, then hands the whole blob 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.

1

Use a cloud-cross-validated policy

policyjson
{
  "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.

2

Have the client collect cloud evidence, appraise server→server

client collector (runs in the cloud instance)ts
import fetch from "node-fetch";

// The dumb collector gathers the cloud-native instance-identity document and
// folds it into the evidence blob alongside the vTPM attestation. No keys,
// no Root Herald contact — it just posts the blob to your own server.
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.
your server — appraise server→serverts
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.

3

What the verifier does

The verifier's cross-validation logic, in plain English:

  1. Validates the vTPM EK certificate chains to the AWS / Azure / GCP TPM CA.
  2. Validates the cloud-native document signature against the cloud's own root.
  3. 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.
  4. Confirms the two instance ids match exactly.
  5. Confirms the quote is bound to the per-request challenge nonce your server minted (the ~5-minute nonce is the replay floor).
  6. 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.

What this doesn't defend against

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.