|
| 1 | +# configfs-tsm shim (run unmodified TDX attestation binaries) |
| 2 | + |
| 3 | +Some programs request a TDX quote through the **standard Linux interfaces** — |
| 4 | +`configfs-tsm` (`/sys/kernel/config/tsm/report/*`, with `inblob`/`outblob`) and a |
| 5 | +`/dev/tdx-guest` device — rather than through the dstack SDK / guest-agent |
| 6 | +socket. On a stock dstack CVM those kernel interfaces aren't exposed to app |
| 7 | +containers, so such binaries fail out of the box. |
| 8 | + |
| 9 | +This example ships a small **sidecar** that bridges the gap. It re-exposes the |
| 10 | +guest-agent's `GetQuote` RPC under the configfs-tsm file ABI: your app writes |
| 11 | +`report_data` to `inblob` and reads the raw Intel DCAP TDX quote from `outblob`, |
| 12 | +exactly as it would against the kernel. |
| 13 | + |
| 14 | +- **No OS change** — pure userspace, runs in a normal container. |
| 15 | +- **No FUSE, no `CAP_SYS_ADMIN`, no privileged mode, no device passthrough.** |
| 16 | +- **No weaker attestation** — the quote is the genuine hardware quote and |
| 17 | + `report_data` is forwarded byte-for-byte. |
| 18 | + |
| 19 | +The shim image is built and published to GHCR by |
| 20 | +[`.github/workflows/build-tsm-shim.yml`](../.github/workflows/build-tsm-shim.yml): |
| 21 | +`ghcr.io/dstack-tee/dstack-tsm-shim`. |
| 22 | + |
| 23 | +## Try it |
| 24 | + |
| 25 | +```bash |
| 26 | +phala deploy -n tsm-shim-example -c docker-compose.yaml |
| 27 | +phala cvms logs <app_id> -c app |
| 28 | +``` |
| 29 | + |
| 30 | +Expected `app` log: |
| 31 | + |
| 32 | +``` |
| 33 | +quote length : 5010 bytes |
| 34 | +quote header : 0400 (a TDX v4 quote starts with 0400) |
| 35 | +report_data bound in quote: True |
| 36 | +PASS - unmodified configfs-tsm app got a real TDX quote via the shim |
| 37 | +``` |
| 38 | + |
| 39 | +## Adopt it in your app |
| 40 | + |
| 41 | +Add the `tsm-shim` service and two volumes, then add the four lines marked `(+)` |
| 42 | +to your existing service: |
| 43 | + |
| 44 | +```yaml |
| 45 | +services: |
| 46 | + tsm-shim: |
| 47 | + image: ghcr.io/dstack-tee/dstack-tsm-shim:latest |
| 48 | + restart: unless-stopped |
| 49 | + volumes: |
| 50 | + - /var/run/dstack.sock:/var/run/dstack.sock |
| 51 | + - tsm-report:/run/tsm/report |
| 52 | + |
| 53 | + my-app: |
| 54 | + image: your-app:latest |
| 55 | + # ... your existing config ... |
| 56 | + depends_on: |
| 57 | + tsm-shim: |
| 58 | + condition: service_healthy # (+) wait until the shim is ready |
| 59 | + environment: |
| 60 | + - TSM_REPORT_PATH=/run/tsm/report # (+) point your app at the shim dir |
| 61 | + volumes: |
| 62 | + - tsm-report:/run/tsm/report # (+) see the shim's inblob/outblob |
| 63 | + - tsm-devstub:/dev/tdx-guest # (+) satisfy /dev/tdx-guest checks |
| 64 | + |
| 65 | +volumes: |
| 66 | + tsm-report: {} |
| 67 | + tsm-devstub: {} |
| 68 | +``` |
| 69 | +
|
| 70 | +If your binary **hard-codes** `/sys/kernel/config/tsm/report`, mount the shared |
| 71 | +volume there instead of using `TSM_REPORT_PATH`: |
| 72 | + |
| 73 | +```yaml |
| 74 | + volumes: |
| 75 | + - tsm-report:/sys/kernel/config/tsm/report |
| 76 | +``` |
| 77 | + |
| 78 | +For production, pin the image by digest (e.g. |
| 79 | +`ghcr.io/dstack-tee/dstack-tsm-shim:latest@sha256:...`). |
| 80 | + |
| 81 | +## How it works |
| 82 | + |
| 83 | +`tsm-shim` exposes `inblob` and `outblob` as **named pipes (FIFOs)** in a shared |
| 84 | +volume. A read of `outblob` blocks until the quote for the most recent `inblob` |
| 85 | +write is ready — which matches configfs-tsm's write-then-read contract with no |
| 86 | +race and no privileges. When `inblob` is written, the shim calls |
| 87 | +`POST /GetQuote` on `/var/run/dstack.sock` and writes the returned quote to |
| 88 | +`outblob`. The image reports healthy only once both FIFOs exist, so the app can |
| 89 | +gate on `depends_on: { condition: service_healthy }`. |
| 90 | + |
| 91 | +The `/dev/tdx-guest` device can't be created inside another container from a |
| 92 | +sidecar, so an empty volume is mounted at that path — enough for the common |
| 93 | +"does the device exist?" check. (dstack permits mounting under `/dev`.) |
| 94 | + |
| 95 | +## Files |
| 96 | + |
| 97 | +| File | Purpose | |
| 98 | +|------|---------| |
| 99 | +| `tsm_shim.py` | the sidecar daemon (pure Python stdlib, no dependencies) | |
| 100 | +| `demo-app.py` | a stand-in unmodified configfs-tsm consumer (bundled self-test) | |
| 101 | +| `Dockerfile` | builds the published image | |
| 102 | +| `docker-compose.yaml` | the shim + demo app wired together | |
| 103 | + |
| 104 | +## Limitations |
| 105 | + |
| 106 | +- Covers the **configfs-tsm `inblob`/`outblob`** path (used by `go-configfs-tsm`, |
| 107 | + recent `libtdx-attest`, etc.). |
| 108 | +- Does **not** emulate the `/dev/tdx-guest` `TDX_CMD_GET_REPORT0` ioctl. That |
| 109 | + returns a raw, locally-MAC'd TDREPORT, which dstack does not expose and which |
| 110 | + can't be derived from a quote — so it isn't recoverable in userspace by any |
| 111 | + shim. Binaries that drive the device by ioctl rather than configfs are out of |
| 112 | + scope. |
| 113 | +- One quote at a time per shim instance (matches the kernel's single |
| 114 | + configfs-tsm entry). Run one shim per app. |
0 commit comments