Skip to content

Commit b0478b6

Browse files
committed
trusted-workload-launcher: default to repo-owned tee-launch.sh
Make INSTALL_CMD and RUN_CMD optional and add a recommended default mode: when neither is set the launcher runs 'bash tee-launch.sh' from the pinned commit (under REPO_SUBDIR if set, else repo root). The script's bytes are covered by source provenance of COMMIT_SHA, so the trust- bearing config in default mode collapses to REPO_URL + COMMIT_SHA. The existing RUN_CMD path is preserved as an advanced mode for repos that cannot host their own entry script; INSTALL_CMD remains optional and must accompany RUN_CMD. Aligned changes: - bin/trusted-workload-launcher: validate INSTALL_CMD requires RUN_CMD, drop "INSTALL_CMD must be set" and "RUN_CMD required" gates, add the default-mode branch that requires ./tee-launch.sh in target. No exec bit required (we invoke 'bash tee-launch.sh'). Refreshed top comment and parse_config comment to reflect the new model. - tests/run-tests.sh: extended fixture repo with c3 (adds sub/tee-launch.sh, intentionally non-executable), added default_mode_happy, default_mode_missing_script_fails, and install_cmd_without_run_cmd_fails. - README.md, examples/web-app.conf: lead with default mode; advanced mode explicitly scoped to "workload repo cannot host its own entry script"; clarified that no executable bit is required. - VERIFY.md: quick path shortened to 4 steps for default mode, with an explicit one-line note that advanced mode adds the command audit; step 4 covers tee-launch.sh under source provenance and step 5 shows the default-mode log lines. Updated the recommended-path Mermaid diagram. Smoke transcript annotated that the prior production run used advanced mode because Hello-World ships no tee-launch.sh, and that the compose-hash binding it demonstrates is identical. - Root README: Details-table description compressed to a true one-liner ("Run a pinned Git commit in a TEE.").
1 parent 68cb658 commit b0478b6

6 files changed

Lines changed: 294 additions & 138 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ Implementation details and infrastructure patterns.
231231
| Example | Description |
232232
|---------|-------------|
233233
| [launcher](./launcher) | Generic launcher pattern for Docker Compose apps (auto-update) |
234-
| [trusted-workload-launcher](./trusted-workload-launcher) | Auditable launcher that pins a workload to a full upstream Git commit SHA (no auto-update). |
234+
| [trusted-workload-launcher](./trusted-workload-launcher) | Run a pinned Git commit in a TEE. |
235235
| [prelaunch-script](./prelaunch-script) | Pre-launch script patterns (Phala Cloud) |
236236
| [private-docker-image-deployment](./private-docker-image-deployment) | Using private Docker registries |
237237
| [attestation/rtmr3-based](./attestation/rtmr3-based) | RTMR3-based attestation (legacy) |

trusted-workload-launcher/README.md

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# trusted-workload-launcher
22

3-
A minimal, auditable launcher image for dstack: given a config file that
3+
A minimal, auditable launcher image for dstack. Given a config file that
44
names an upstream Git repo and a full commit SHA, the launcher fetches that
5-
exact commit, verifies `HEAD` after checkout, and `exec`s the configured run
6-
command — with no fallback to branches, tags, or short SHAs.
5+
exact commit, verifies `HEAD` after checkout, and runs the workload's own
6+
entry point script — with no fallback to branches, tags, or short SHAs.
77

88
"Trusted" in the name refers to what a dstack deployment using this image
99
can produce — a *trusted workload deployment* — not to any intrinsic
@@ -13,6 +13,14 @@ auditable image digest and an attested config that names the workload
1313
commit. Whether the workload at that commit is itself trustworthy is up to
1414
the auditor.
1515

16+
By convention, **the workload repo provides its own bash entry point at the
17+
fixed path `tee-launch.sh`** (default mode). This keeps install/build/run
18+
logic inside the workload repo, where it is covered by source provenance of
19+
the pinned `COMMIT_SHA` and is **not** a trust-bearing field in the
20+
launcher config. A verifier therefore only audits two things: the launcher
21+
image's identity, and the `REPO_URL` + `COMMIT_SHA` pair in the attested
22+
config.
23+
1624
The launcher image is **generic**: its digest attests the launcher's
1725
implementation, not the workload. The workload identity comes from the
1826
config file, which must be attested separately (see [Trust model](#trust-model)).
@@ -56,10 +64,11 @@ launcher image digest ──► launcher implementation identity
5664
5765
launcher config file ──► workload pin
5866
(REPO_URL + full COMMIT_SHA U; selects which
59-
upstream commit gets fetched and exec()d)
67+
upstream commit gets fetched and run)
6068
6169
──► workload running inside the TEE
62-
= workload repo at commit U
70+
= workload repo at commit U,
71+
starting from its tee-launch.sh
6372
```
6473

6574
The published launcher image is a **generic** runner: the same image digest
@@ -103,17 +112,18 @@ trusted-workload-launcher <config-file>
103112
```
104113

105114
The launcher is a single bash script (`bin/trusted-workload-launcher`). It
106-
depends only on `bash`, `git`, and POSIX coreutils. It is **not** sourced and
107-
**does not source** the config; the only values executed as shell are
108-
`INSTALL_CMD` and `RUN_CMD`, which are documented to be intentionally
109-
executed.
115+
depends only on `bash`, `git`, and POSIX coreutils. It is **not** sourced
116+
and **does not source** the config. In default mode, the only bytes it
117+
executes are those of the workload repo's `tee-launch.sh` at the pinned
118+
`COMMIT_SHA`. In advanced mode (see below), it additionally executes the
119+
configured `INSTALL_CMD` / `RUN_CMD` via `bash -c`.
110120

111121
## Config contract
112122

113123
An env-file with `KEY=VALUE` lines. Comments start with `#`. Surrounding
114124
matching single or double quotes are stripped (one layer). Unknown keys are
115-
rejected. The config is parsed, not sourced — no command substitution and no
116-
shell expansion in the parse step.
125+
rejected. The config is parsed, not sourced — no command substitution and
126+
no shell expansion in the parse step.
117127

118128
### Required
119129

@@ -122,32 +132,39 @@ shell expansion in the parse step.
122132
| `REPO_URL` | Git URL of the upstream workload repo (`https://…` or `git@…`). |
123133
| `COMMIT_SHA` | **Full** 40-hex SHA-1 or 64-hex SHA-256. Branches, tags, and short SHAs are rejected. |
124134
| `WORK_DIR` | Local directory used as the checkout. Created if missing. Reused on subsequent runs as long as the existing clone's `origin` URL matches `REPO_URL`. |
125-
| `INSTALL_CMD` | Shell command run inside the checkout (in `REPO_SUBDIR` if set). Pass `INSTALL_CMD=` to explicitly skip the install step. The key must still be present. |
126-
| `RUN_CMD` | Shell command `exec`d after the install step. Because `exec` is used, signals reach the child program directly. |
127135

128136
### Optional
129137

130138
| Key | Meaning |
131139
| --- | --- |
132-
| `REPO_SUBDIR` | Relative directory inside the repo to `cd` into before `INSTALL_CMD` and `RUN_CMD`. Must not be absolute and must not contain `..`. |
133-
| `CHILD_ENV_FILE` | Path to a separate env file. Each `KEY=VALUE` line is `export`ed into the environment seen by `INSTALL_CMD` and `RUN_CMD`. The file is parsed line-by-line just like the main config (not sourced). |
134-
135-
### Multi-line install or run logic
136-
137-
`INSTALL_CMD` and `RUN_CMD` are single-line shell strings — the config is
138-
line-oriented and the launcher does not implement multi-line value parsing.
139-
This is intentional: a config that "almost" parses a multi-line script is a
140-
trust hazard. If you need more than one command, put a script in the
141-
workload repo at the pinned commit (e.g. `scripts/install.sh`,
142-
`scripts/run.sh`) and call it:
143-
144-
```
145-
INSTALL_CMD=./scripts/install.sh
146-
RUN_CMD=./scripts/run.sh
147-
```
148-
149-
The script then lives at the same pinned `COMMIT_SHA` as the workload, so
150-
its bytes are covered by the same audit.
140+
| `REPO_SUBDIR` | Relative directory inside the repo to `cd` into before running the entry point or `RUN_CMD`. Must not be absolute and must not contain `..`. |
141+
| `CHILD_ENV_FILE` | Path to a separate env file. Each `KEY=VALUE` line is `export`ed into the environment seen by `tee-launch.sh` / `INSTALL_CMD` / `RUN_CMD`. The file is parsed line-by-line just like the main config (not sourced). |
142+
| `RUN_CMD` | **Advanced.** Shell command to exec instead of the default `tee-launch.sh`. Use only when the workload repo cannot host its own entry script. |
143+
| `INSTALL_CMD` | **Advanced.** Shell command to run before `RUN_CMD`. Only valid alongside `RUN_CMD`. |
144+
145+
### Default mode: `tee-launch.sh` in the workload repo
146+
147+
Recommended for every workload you control. The workload repo provides a
148+
bash script at the fixed path `tee-launch.sh` (at the repo root, or at
149+
`REPO_SUBDIR/tee-launch.sh` if `REPO_SUBDIR` is set). The launcher runs it
150+
with `bash tee-launch.sh` after checkout — **no executable bit is
151+
required**. All install/build/run logic lives in that script; the launcher
152+
config carries only `REPO_URL` + `COMMIT_SHA` (+ local `WORK_DIR`).
153+
154+
Because the script's bytes are pinned by `COMMIT_SHA` and stored in the
155+
workload repo, they are covered by source provenance of the pinned commit.
156+
The verifier does not need to extract or audit any command string out of
157+
the launcher config.
158+
159+
### Advanced mode: explicit `RUN_CMD` / `INSTALL_CMD`
160+
161+
Use this when the workload repo cannot be modified to add a
162+
`tee-launch.sh` (e.g. you are pinning a third-party repo unchanged).
163+
Setting `RUN_CMD` switches the launcher into advanced mode; if you need
164+
more than one command, set `INSTALL_CMD` to run before `RUN_CMD`. Each is
165+
a single-line shell string and the launcher does not implement multi-line
166+
parsing. In this mode both values are trust-bearing config and must be
167+
audited alongside `COMMIT_SHA`.
151168

152169
### What the launcher will and will not do
153170

@@ -159,21 +176,24 @@ its bytes are covered by the same audit.
159176
A missing commit is a hard failure.
160177
* Will not: accept short SHAs. A truncated SHA could resolve ambiguously if
161178
the upstream history changes.
162-
* Will not: source the config or `eval` anything beyond `INSTALL_CMD` /
163-
`RUN_CMD`, which are executed via `bash -c`.
179+
* Will not: source the config or `eval` config values. In default mode the
180+
launcher executes `bash tee-launch.sh` from the pinned commit; in advanced
181+
mode it executes `INSTALL_CMD` / `RUN_CMD` via `bash -c`. Nothing else
182+
from the config reaches a shell.
164183

165184
## Example
166185

167186
See [`examples/web-app.conf`](./examples/web-app.conf). Adapt `REPO_URL`,
168-
`COMMIT_SHA`, `INSTALL_CMD`, and `RUN_CMD` for your workload.
187+
`COMMIT_SHA`, and (if you need it) `REPO_SUBDIR` for your workload, and
188+
make sure the workload repo has a `tee-launch.sh` at the pinned commit.
169189

170190
```sh
171191
./bin/trusted-workload-launcher ./examples/web-app.conf
172192
```
173193

174-
The launcher logs the resolved repo, commit, workdir, and commands at
175-
startup, then logs the verified `HEAD` after checkout, before invoking the
176-
install and run steps.
194+
The launcher logs the resolved repo, commit, workdir, and selected mode at
195+
startup, then logs the verified `HEAD` after checkout, before handing
196+
control to `tee-launch.sh` (or `INSTALL_CMD` / `RUN_CMD` in advanced mode).
177197

178198
## Deploying with dstack
179199

@@ -227,8 +247,6 @@ configs:
227247
REPO_URL=https://github.com/example-org/example-web-app.git
228248
COMMIT_SHA=<full-40-or-64-hex-sha>
229249
WORK_DIR=/var/lib/trusted-workload-launcher/example-web-app
230-
INSTALL_CMD=npm ci --omit=dev
231-
RUN_CMD=node server.js
232250
233251
volumes:
234252
workload-checkout:

trusted-workload-launcher/VERIFY.md

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,47 @@ How a relying party verifies that a dstack CVM is running
44
`trusted-workload-launcher` and that the workload commit executed inside the
55
TEE is the one they audited.
66

7-
## Quick path (5 steps)
7+
## Quick path (default mode, 4 steps)
88

9-
For a verifier who already trusts dstack's attestation tooling, the whole
10-
chain comes down to:
9+
In default mode the workload repo provides its own `tee-launch.sh` at the
10+
pinned commit, so the trust-bearing config is just `REPO_URL + COMMIT_SHA`
11+
and the install/run command chain disappears from the verifier's checklist.
12+
The whole chain is:
1113

1214
```mermaid
1315
flowchart LR
14-
A[dstack attestation] --> B[image digest<br/>+ compose hash]
15-
B --> C[Sigstore attestation<br/>for launcher image]
16-
C --> D[GitHub workflow<br/>+ repo + commit]
17-
B --> E[config bytes<br/>+ COMMIT_SHA]
18-
E --> F[upstream repo<br/>at COMMIT_SHA]
16+
A[dstack attestation] --> B[launcher image digest<br/>+ REPO_URL<br/>+ COMMIT_SHA]
17+
B --> C[Sigstore attestation<br/>for launcher image digest<br/>= dstack-examples@ref/SHA]
18+
B --> D[upstream repo at COMMIT_SHA<br/>incl. tee-launch.sh]
1919
```
2020

21-
1. **Pull the dstack attestation** for the CVM with
22-
`phala cvms attestation --cvm-id <id> --json`, and verify the TDX quote
23-
with the dstack verifier (or trust Phala Cloud's verifier for the lite
24-
path).
25-
2. **Read the attested compose** out of the quote. The launcher image
26-
digest and the inline `configs:` block containing `REPO_URL` and
27-
`COMMIT_SHA` both live there; both are covered by the dstack
28-
`compose-hash` measurement.
29-
3. **Verify launcher image provenance.** Check the Sigstore attestation on
30-
the image digest: it must be signed by the
31-
`Dstack-TEE/dstack-examples` GitHub Actions workflow that produced it,
32-
from a known repo, ref, and commit.
33-
4. **Confirm the pinned workload commit** by checking out the upstream
34-
repo at `COMMIT_SHA` and reviewing it.
35-
5. **Spot-check runtime logs**`phala logs --cvm-id <id>` should show
36-
`HEAD verified: <COMMIT_SHA>`. Logs are corroborating only; the trust
37-
root is steps 1–4.
38-
39-
If all five line up, the bytes executing in the TEE are exactly the
21+
1. **Verify the dstack attestation.**
22+
`phala cvms attestation --cvm-id <id> --json` and feed the TDX quote
23+
into the dstack verifier (or trust the Phala Cloud verifier as a lite
24+
path). Read out the deployed launcher image digest and the attested
25+
`REPO_URL` + `COMMIT_SHA`.
26+
2. **Verify launcher image provenance via Sigstore.** Confirm the image
27+
digest from step 1 carries a build-provenance attestation signed by
28+
the expected `Dstack-TEE/dstack-examples` GitHub Actions workflow at
29+
the ref / commit you audited.
30+
3. **Audit the upstream commit.** Check out the workload repo at
31+
`COMMIT_SHA` and review it. In default mode this single review covers
32+
the workload code *and* its entry point `tee-launch.sh`; no separate
33+
install/run command audit is needed.
34+
4. **Spot-check runtime logs.** `phala logs --cvm-id <id>` should show
35+
`HEAD verified: <COMMIT_SHA>` and `exec in <dir>: bash tee-launch.sh`.
36+
Logs are corroborating only; the trust root is steps 1–3.
37+
38+
If all four line up, the bytes executing in the TEE are exactly the
4039
upstream commit you audited, produced by an audited launcher.
4140

41+
> **Advanced mode adds one step.** If the launcher config sets `RUN_CMD`
42+
> (and optionally `INSTALL_CMD`) instead of relying on `tee-launch.sh`,
43+
> those strings are trust-bearing config: read them from the attested
44+
> compose in step 1 and audit them as if they were source code at the
45+
> pinned commit. The simplification of the default mode is exactly that
46+
> this extra step does not exist.
47+
4248
The rest of this document explains how the chain works and what to do at
4349
each step.
4450

@@ -56,16 +62,18 @@ downstream image; the image digest then covers both launcher and pin.
5662
```mermaid
5763
flowchart LR
5864
L[launcher image<br/>@sha256:&lt;L&gt;] --> CMP
59-
P[config bytes<br/>REPO_URL<br/>COMMIT_SHA<br/>RUN_CMD] -.inline configs:.-> CMP
65+
P[config bytes<br/>REPO_URL<br/>COMMIT_SHA] -.inline configs:.-> CMP
6066
CMP[docker-compose.yml] --> CH[compose-hash<br/>= sha256 app_compose]
6167
CH --> Q[dstack attestation<br/>TDX quote]
6268
```
6369

6470
The compose YAML references the generic launcher image by digest and
6571
provides the launcher's config via a compose `configs:` block (with
66-
`content:` inline). dstack measures the resulting `app_compose` JSON into
67-
the quote as the `compose-hash` event, so changing either the image
68-
reference or the config bytes changes the attestation.
72+
`content:` inline). In default mode the config is just `REPO_URL` +
73+
`COMMIT_SHA` + `WORK_DIR`; in advanced mode it also carries `RUN_CMD`
74+
(and optionally `INSTALL_CMD`). Either way dstack measures the resulting
75+
`app_compose` JSON into the quote as the `compose-hash` event, so changing
76+
either the image reference or the config bytes changes the attestation.
6977

7078
This is also the surface that dstack KMS policy governs: a CVM can only
7179
unwrap KMS-protected secrets while running a compose whose hash matches
@@ -167,42 +175,53 @@ evidence of tampering.
167175

168176
### 4. Extract and audit the workload pin
169177

170-
Parse the `configs:` content from step 2 and read `REPO_URL`,
171-
`COMMIT_SHA`, `INSTALL_CMD`, `RUN_CMD` (and optional `REPO_SUBDIR` /
172-
`CHILD_ENV_FILE`). Then:
178+
Parse the `configs:` content from step 2 and read `REPO_URL` and
179+
`COMMIT_SHA` (plus optional `REPO_SUBDIR` / `CHILD_ENV_FILE`). In default
180+
mode there are no `INSTALL_CMD` / `RUN_CMD` strings to audit — the entry
181+
point is the fixed-path `tee-launch.sh` in the workload repo, which is
182+
covered by source provenance of the pinned commit. In advanced mode
183+
(`RUN_CMD` present) also read `RUN_CMD` and any `INSTALL_CMD` and treat
184+
them as trust-bearing config.
173185

174186
```sh
175187
git -C <workload-checkout> rev-parse --verify <COMMIT_SHA>
176188
```
177189

178190
Confirm the upstream repo at `REPO_URL` contains `COMMIT_SHA`, and review
179-
the workload at that commit. This is the code that actually serves
180-
traffic.
191+
the workload at that commit, including `tee-launch.sh` in default mode.
192+
This is the code that actually serves traffic.
181193

182194
### 5. Spot-check runtime logs
183195

184196
```sh
185197
phala logs --cvm-id <id> -n 200
186198
```
187199

188-
Expected:
200+
Default mode expected:
189201

190202
```
191203
[trusted-workload-launcher] checking out <COMMIT_SHA>
192204
[trusted-workload-launcher] HEAD verified: <COMMIT_SHA>
193-
[trusted-workload-launcher] exec in <WORK_DIR>[/<REPO_SUBDIR>]: <RUN_CMD>
205+
[trusted-workload-launcher] mode: default (workload repo tee-launch.sh)
206+
[trusted-workload-launcher] exec in <WORK_DIR>[/<REPO_SUBDIR>]: bash tee-launch.sh
194207
```
195208

196-
These show the launcher reached the post-checkout state. They are not
197-
signed, so they don't replace steps 1–4 — they corroborate.
209+
Advanced mode shows the explicit `RUN_CMD` instead of `bash tee-launch.sh`
210+
on the last line. Either way these lines show the launcher reached the
211+
post-checkout state. They are not signed, so they don't replace
212+
steps 1–4 — they corroborate.
198213

199214
A workload that needs signed runtime evidence should produce its own
200215
attested output (see [Limitations](#limitations)).
201216

202217
## Reference: production smoke transcript
203218

204219
A real verification of this example was exercised against production
205-
Phala on 2026-05-11 using the recommended compose-mounted-config path:
220+
Phala on 2026-05-11 using the recommended compose-mounted-config path.
221+
The pinned upstream (`octocat/Hello-World`) does not host a
222+
`tee-launch.sh`, so the smoke used **advanced mode** to set `RUN_CMD`
223+
inline; default-mode behavior is covered by the launcher's own test
224+
suite. The compose-hash binding it demonstrates is identical:
206225

207226
| Field | Value |
208227
| --- | --- |

0 commit comments

Comments
 (0)