carlos / platform Public

harness: donors captured over SSM and terminated, IMDS stand-in (step 4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev c5069bd905d054be4942085bb874f2fe42a89e71 parent 1abefd0
4 files changed, +263 −0
  • harness/RESOURCES.md +4 −0
  • harness/RUNLOG.md +49 −0
  • harness/donor-user-data.sh +7 −0
  • harness/stand-ins/imds.py +203 −0
diff --git a/harness/RESOURCES.md b/harness/RESOURCES.md
index 98db93995..96cb866d9 100644
--- a/harness/RESOURCES.md
+++ b/harness/RESOURCES.md
@@ -10,3 +10,7 @@ Appended as each is created. Teardown commands: harness/TEARDOWN.md.
| 2026-10-20T00:40:41Z | iam instance profile | carlos-harness | holds role carlos-harness |
| 2026-10-20T00:40:50Z | security group | sg-0b9cf4c2a508b7cec (carlos-harness) | vpc-02212d67a9fca3b20; no inbound; egress tcp 80, 443 only |
| 2026-10-20T00:41:14Z | ec2 instance (host) | i-06a1ec144aa5226fb | t4g.xlarge, ami-071babd89cea53fa3, subnet-0975dda9ac0e65347, profile carlos-harness, 60 GiB gp3 (DeleteOnTermination), terminate on shutdown, shutdown -h 12:40 UTC |
+| 2026-10-20T00:50:42Z | ec2 instance (donor-1) | i-0d9175215cfcf63b8 | t4g.nano, profile carlos-harness, terminate on shutdown, shutdown -h +60; to be terminated once captured |
+| 2026-10-20T00:50:44Z | ec2 instance (donor-2) | i-019177034b088459f | t4g.nano, profile carlos-harness, terminate on shutdown, shutdown -h +60; to be terminated once captured |
+| 2026-10-20T00:51:50Z | (terminated) donor-1 | i-0d9175215cfcf63b8 | documents in s3://carlos-harness-640012953448/donors/donor-1/ |
+| 2026-10-20T00:51:52Z | (terminated) donor-2 | i-019177034b088459f | documents in s3://carlos-harness-640012953448/donors/donor-2/ |
diff --git a/harness/RUNLOG.md b/harness/RUNLOG.md
index 880841b9d..2a441675a 100644
--- a/harness/RUNLOG.md
+++ b/harness/RUNLOG.md
@@ -164,3 +164,52 @@ entry keys: accessKey api deploymentid event remotehost requestHeader requestHos
Check: conditional PUT answers 412 where S3 does (all rows); If-Match on DELETE recorded
(differs, above); one audit line per request with `If-*` present. **Step 3: GREEN** with the
DELETE fidelity hole recorded (2026-10-20T00:49:53Z).
+
+## Step 4: donors and the IMDS stand-in
+
+Donors (B5): two t4g.nano, `harness/donor-user-data.sh` (root runner's SSM-agent user-data
+plus `shutdown -h +60`), profile `carlos-harness`, SG `sg-0b9cf4c2a508b7cec`, tagged.
+donor-1 `i-0d9175215cfcf63b8`, donor-2 `i-019177034b088459f`, launched 00:50 UTC.
+Captured over `ssm send-command` (token PUT, then `dynamic/instance-identity/{document,
+signature,pkcs7,rsa2048}`, base64 over stdout), checked by tags before every call.
+
+```
+$ openssl dgst -sha256 -verify <(pubkey of internal/boxid/certs/eu-west-1.pem) -signature sig donor-N/document
+donor-1: Verified OK
+donor-2: Verified OK
+tampered document: Verification failure # negative control
+```
+
+Stored at `s3://carlos-harness-640012953448/donors/donor-{1,2}/`; both donors terminated
+00:52 UTC (`ec2 wait instance-terminated`: both `terminated`). Donor ids are in the harness
+bucket and on the host only.
+
+IMDS stand-in: `harness/stand-ins/imds.py` -> host `/opt/harness/imds.py`, unit
+`harness-imds.service`, bound 203.0.113.1:8169 (DNAT target). Config `/etc/harness/imds.json`:
+leo 203.0.113.21 = donor-1, role `carlos-dev-host-b`, MinIO `harness-leo`; sophie .22 =
+donor-2, `carlos-dev-host`, `harness-sophie`; dennis .23 = no document,
+`carlos-dev-host-uk`; signer .30 = no document, `carlos-claim-signer`. IMDSv2 tokens bound
+to the asking address; `X-Forwarded-For` refused; log `/var/log/harness/imds.jsonl`.
+Also serves `mac` and `network/interfaces/macs/<mac>/vpc-ipv4-cidr-blocks` (203.0.113.0/24):
+**deviation from the review**, which found no reader, but `internal/host/awsaddr.go:51-63`
+(region HA's edges-file address source) reads both.
+
+Check (scratch container on leo's address 203.0.113.21, `$TMPDIR/harness-build/check4.sh`):
+
+```
+GET without a token: 401
+token PUT with X-Forwarded-For: 403
+token PUT: 56 chars
+meta-data/instance-id = i-0d9175215cfcf63b8 [200]
+meta-data/placement/region = eu-west-1 [200]
+meta-data/public-ipv4 = 203.0.113.21 [200]
+meta-data/local-ipv4 = 203.0.113.21 [200]
+meta-data/mac = 06:00:cb:00:71:15 [200]
+meta-data/iam/security-credentials/ = carlos-dev-host-b [200]
+role leaf: {"Code": "Success", "Type": "AWS-HMAC", "AccessKeyId": "harness-leo", "SecretAccessKey": "<40 chars>", "Token": "", ...}
+document sha256 e206e461... = donor-1's (byte for byte); signature sha256 31bb4971...
+document: i-0d9175215cfcf63b8 eu-west-1 640012953448
+openssl under internal/boxid/certs/eu-west-1.pem: Verified OK
+```
+
+**Step 4: GREEN** (2026-10-20T00:53:26Z).
diff --git a/harness/donor-user-data.sh b/harness/donor-user-data.sh
new file mode 100644
index 000000000..bab5f38e6
--- /dev/null
+++ b/harness/donor-user-data.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+# carlos harness donor: SSM agent only (root runner's user-data), and a 60-minute dead man's switch.
+set -e
+shutdown -h +60 "carlos harness donor TTL"
+curl -fsSL -o /tmp/ssm.deb https://s3.eu-west-1.amazonaws.com/amazon-ssm-eu-west-1/latest/debian_arm64/amazon-ssm-agent.deb
+dpkg -i /tmp/ssm.deb
+systemctl enable --now amazon-ssm-agent
diff --git a/harness/stand-ins/imds.py b/harness/stand-ins/imds.py
new file mode 100644
index 000000000..47d7796bc
--- /dev/null
+++ b/harness/stand-ins/imds.py
@@ -0,0 +1,203 @@
+#!/usr/bin/env python3
+"""IMDS stand-in for harness boxes (spec §3.5). Python 3 standard library only.
+
+Binds 203.0.113.1:8169; the host's nftables DNATs 169.254.169.254:80 from the
+bridge here, so the source address is the box's own bridge address, and every
+answer is keyed by it. IMDSv2 only: a GET needs a token from PUT
+/latest/api/token, and a token is bound to the address that asked for it.
+
+Config /etc/harness/imds.json:
+ {"203.0.113.21": {"box": "leo", "donor": "/etc/harness/donors/donor-1",
+ "role": "carlos-dev-host-b", "user": "leo"}, ...}
+A donor directory holds AWS's `document` and `signature`, served byte for byte.
+`user` names an entry in /root/harness/minio-users.json, whose keys are
+served as the role's credentials. Every request is logged as one JSON line to
+/var/log/harness/imds.jsonl (paths and status; never a key).
+"""
+import datetime, http.server, json, os, secrets, sys, threading, time
+
+CONFIG = os.environ.get("IMDS_CONFIG", "/etc/harness/imds.json")
+USERS = os.environ.get("IMDS_USERS", "/root/harness/minio-users.json")
+LOG = os.environ.get("IMDS_LOG", "/var/log/harness/imds.jsonl")
+BIND = (os.environ.get("IMDS_ADDR", "203.0.113.1"), int(os.environ.get("IMDS_PORT", "8169")))
+VPC_CIDR = "203.0.113.0/24"
+
+tokens = {} # token -> (source address, expiry)
+lock = threading.Lock()
+
+
+def load():
+ with open(CONFIG) as f:
+ cfg = json.load(f)
+ with open(USERS) as f:
+ users = json.load(f)
+ return cfg, users
+
+
+def mac_for(addr):
+ # A stable, locally administered MAC per box (the real one is the ENI's).
+ last = int(addr.rsplit(".", 1)[1])
+ return "06:00:cb:00:71:%02x" % last
+
+
+def leaves(addr, box, users):
+ """The metadata tree for one box, path (no leading /latest) -> body."""
+ out = {}
+ doc = sig = None
+ if box.get("donor"):
+ with open(os.path.join(box["donor"], "document"), "rb") as f:
+ doc = f.read()
+ with open(os.path.join(box["donor"], "signature"), "rb") as f:
+ sig = f.read()
+ d = json.loads(doc)
+ out["meta-data/instance-id"] = d["instanceId"]
+ out["meta-data/ami-id"] = d.get("imageId", "")
+ out["meta-data/instance-type"] = d.get("instanceType", "")
+ out["meta-data/placement/region"] = d["region"]
+ out["meta-data/placement/availability-zone"] = d.get("availabilityZone", "")
+ out["dynamic/instance-identity/document"] = doc
+ out["dynamic/instance-identity/signature"] = sig
+ else:
+ out["meta-data/placement/region"] = "eu-west-1"
+ mac = mac_for(addr)
+ out["meta-data/local-ipv4"] = addr
+ out["meta-data/public-ipv4"] = addr
+ out["meta-data/mac"] = mac
+ out[f"meta-data/network/interfaces/macs/{mac}/vpc-ipv4-cidr-blocks"] = VPC_CIDR
+ out["meta-data/hostname"] = box.get("box", "")
+ out["meta-data/local-hostname"] = box.get("box", "")
+ role = box.get("role")
+ if role and box.get("user") in users:
+ u = users[box["user"]]
+ now = datetime.datetime.now(datetime.timezone.utc)
+ out["meta-data/iam/security-credentials/"] = role
+ out[f"meta-data/iam/security-credentials/{role}"] = json.dumps({
+ "Code": "Success",
+ "LastUpdated": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "Type": "AWS-HMAC",
+ "AccessKeyId": u["access_key"],
+ "SecretAccessKey": u["secret_key"],
+ "Token": "",
+ "Expiration": (now + datetime.timedelta(hours=6)).strftime("%Y-%m-%dT%H:%M:%SZ"),
+ }, indent=2)
+ out["meta-data/iam/info"] = json.dumps({
+ "Code": "Success", "LastUpdated": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "InstanceProfileArn": f"arn:aws:iam::640012953448:instance-profile/{role}",
+ "InstanceProfileId": "AIPAHARNESS" + box.get("box", "").upper()})
+ return out
+
+
+def listing(tree, prefix):
+ """Directory listing in IMDS style: immediate children, dirs with a slash."""
+ kids = set()
+ for p in tree:
+ if p.startswith(prefix) and p != prefix:
+ rest = p[len(prefix):]
+ if not rest:
+ continue
+ head, sep, _ = rest.partition("/")
+ kids.add(head + ("/" if sep else ""))
+ return "\n".join(sorted(kids))
+
+
+class H(http.server.BaseHTTPRequestHandler):
+ protocol_version = "HTTP/1.1"
+ server_version = "EC2ws"
+
+ def log(self, status, note=""):
+ rec = {"t": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "src": self.client_address[0],
+ "method": self.command, "path": self.path, "status": status}
+ if note:
+ rec["note"] = note
+ try:
+ with open(LOG, "a") as f:
+ f.write(json.dumps(rec) + "\n")
+ except OSError:
+ pass
+
+ def reply(self, status, body=b"", ctype="text/plain"):
+ if isinstance(body, str):
+ body = body.encode()
+ self.send_response(status)
+ self.send_header("Content-Type", ctype)
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ if self.command != "HEAD":
+ self.wfile.write(body)
+ self.log(status)
+
+ def do_PUT(self):
+ n = int(self.headers.get("Content-Length") or 0)
+ if n:
+ self.rfile.read(n)
+ if self.path != "/latest/api/token":
+ return self.reply(404)
+ if self.headers.get("X-Forwarded-For"):
+ return self.reply(403)
+ try:
+ ttl = int(self.headers.get("X-aws-ec2-metadata-token-ttl-seconds", ""))
+ except ValueError:
+ return self.reply(400)
+ if not 1 <= ttl <= 21600:
+ return self.reply(400)
+ tok = secrets.token_urlsafe(42)
+ with lock:
+ now = time.time()
+ for k in [k for k, v in tokens.items() if v[1] < now]:
+ del tokens[k]
+ tokens[tok] = (self.client_address[0], now + ttl)
+ self.send_response(200)
+ self.send_header("X-aws-ec2-metadata-token-ttl-seconds", str(ttl))
+ self.send_header("Content-Type", "text/plain")
+ self.send_header("Content-Length", str(len(tok)))
+ self.end_headers()
+ self.wfile.write(tok.encode())
+ self.log(200)
+
+ def do_GET(self):
+ src = self.client_address[0]
+ tok = self.headers.get("X-aws-ec2-metadata-token", "")
+ with lock:
+ ent = tokens.get(tok)
+ if not ent or ent[0] != src or ent[1] < time.time():
+ return self.reply(401)
+ try:
+ cfg, users = load()
+ except (OSError, ValueError) as e:
+ self.log(500, f"config: {e}")
+ return self.reply(500)
+ box = cfg.get(src)
+ if box is None:
+ return self.reply(404)
+ path = self.path.split("?", 1)[0]
+ if not path.startswith("/latest/"):
+ return self.reply(404)
+ p = path[len("/latest/"):]
+ tree = leaves(src, box, users)
+ if p in tree:
+ body = tree[p]
+ ctype = "application/json" if p.endswith(("/document", "info")) or (p.startswith("meta-data/iam/security-credentials/") and not p.endswith("/")) else "text/plain"
+ return self.reply(200, body, ctype)
+ if p == "" or p.endswith("/") or any(k.startswith(p + "/") for k in tree):
+ pre = p if (p == "" or p.endswith("/")) else p + "/"
+ ls = listing(tree, pre)
+ if ls:
+ return self.reply(200, ls)
+ return self.reply(404)
+
+ do_HEAD = do_GET
+
+ def log_message(self, *a):
+ pass
+
+
+def main():
+ os.makedirs(os.path.dirname(LOG), exist_ok=True)
+ load() # fail at start, not at the first request
+ srv = http.server.ThreadingHTTPServer(BIND, H)
+ print(f"imds stand-in on {BIND[0]}:{BIND[1]}", flush=True)
+ srv.serve_forever()
+
+
+if __name__ == "__main__":
+ sys.exit(main())