Skip to content

Commit 6178b2e

Browse files
committed
Clarify mesh attestation tracking
1 parent 76cda6c commit 6178b2e

8 files changed

Lines changed: 181 additions & 19 deletions

File tree

consul-postgres-ha/ATTESTATION.md

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ current dstack guest agents: `quote`, `event_log`, and `vm_config`.
2828
coordinator admission broker.
2929
4. The worker builds a binding statement containing the claimed
3030
identity, peer id, dstack app id, instance id, and compose hash.
31-
5. The worker calls dstack `GetQuote(SHA-512(binding || nonce))` and
31+
5. The worker calls dstack `GetQuote(SHA-256(binding || nonce))` and
3232
sends `{identity, binding, nonce, quote, event_log, vm_config}` to
3333
the broker.
3434
6. The broker verifies the quote through `dstack-verifier`, checks
@@ -38,6 +38,80 @@ current dstack guest agents: `quote`, `event_log`, and `vm_config`.
3838
agents receive a node-identity token; service workloads receive
3939
service-identity tokens plus any declared Consul permissions.
4040

41+
## Report Data
42+
43+
`report_data` is an unstructured SHA-256 digest. The structured
44+
admission data stays outside the quote in the binding statement:
45+
46+
```text
47+
report_data = SHA-256(binding_statement_json || broker_nonce)
48+
```
49+
50+
The broker nonce is 32 random bytes encoded as hex. The binding
51+
statement is JSON generated by the workload sidecar:
52+
53+
```json
54+
{
55+
"version": 1,
56+
"identity": "spiffe://demo/postgres",
57+
"cluster": "demo",
58+
"peer_id": "worker-3",
59+
"app_id": "<dstack app id>",
60+
"instance_id": "<dstack instance id>",
61+
"compose_hash": "<dstack compose hash>",
62+
"issued_at": "2026-05-18T00:00:00Z"
63+
}
64+
```
65+
66+
The client checks that dstack `GetQuote()` returns the same
67+
`report_data` it requested. The broker independently recomputes the
68+
digest from the submitted binding statement and nonce, then compares
69+
that digest with the verifier-returned `report_data`.
70+
71+
## Peer IDs
72+
73+
`peer_id` is the mesh node label, not a secret and not a hardware
74+
identity. In this example it is derived from the local role and
75+
ordinal, such as `worker-3`, and reused as the Consul node name.
76+
77+
The current binding is:
78+
79+
```text
80+
peer_id -> binding statement -> SHA-256 report_data -> TDX quote
81+
```
82+
83+
The broker also requires the `peer_id` to match the Terraform policy
84+
row before it issues a node token or a service token scoped to that
85+
node. This prevents one attested workload row from directly claiming
86+
another row's Consul node identity.
87+
88+
For stronger production binding, the policy should also pin a
89+
platform-stable workload instance field, such as a Phala-provided
90+
instance id or app id when that can be supplied without introducing
91+
a deployment cycle. Until then, `peer_id` is best understood as an
92+
authorization namespace declared by Terraform and quoted by the CVM.
93+
94+
## Evidence Policy
95+
96+
The broker treats `dstack-verifier` as the authority for quote and
97+
event-log semantics. The broker should require verifier outputs such
98+
as:
99+
100+
- `quote_verified == true`
101+
- `event_log_verified == true`
102+
- `report_data` present and equal to the broker-computed digest
103+
- `app_info.compose_hash` present and policy-allowed
104+
105+
KMS identity should be policy-controlled because it defines the key
106+
lifecycle. A production policy should pin the long-term KMS public
107+
key or an equivalent well-known KMS identity that is hard-coded in
108+
Terraform and checked against verified dstack evidence.
109+
110+
For production profiles, OS image identity should also be checked.
111+
The expected OS image hash should be both a Terraform launch setting
112+
and an admission policy field, so a workload cannot satisfy the
113+
compose policy while running on an unexpected base image.
114+
41115
## Runtime Checks
42116

43117
From a coordinator CVM, inspect Consul membership:

consul-postgres-ha/PROGRESS.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Project Progress
2+
3+
This tracks the service-mesh project as a whole, not only
4+
attestation. Keep it current when landing changes that alter the
5+
deployment model, trust model, or operational status.
6+
7+
## Current Status
8+
9+
- Terraform deploys a 3-coordinator / 3-worker real Phala Cloud
10+
cluster with public provider `phala-network/phala v0.2.0-beta.4`.
11+
- `mesh-conn` direct connectivity is enabled by default; relay-only
12+
configuration was removed.
13+
- Consul server quorum, Consul clients, Envoy Connect sidecars, and
14+
Patroni HA Postgres are running through the mesh.
15+
- Attestation admission gates Consul node and service token issuance
16+
through real dstack quotes and `dstack-verifier`.
17+
- Worker preflight compose hashes are generated by Terraform and used
18+
as revision evidence in the admission policy.
19+
20+
## Recently Validated
21+
22+
- Full real-cluster Terraform apply with preflight-backed admission
23+
policy.
24+
- Public Terraform provider path, with no local provider override.
25+
- Real attestation path on all workers for node, webdemo, and
26+
postgres identities.
27+
- Negative admission test: a valid quote claiming the wrong `peer_id`
28+
is rejected by policy.
29+
- Worker sidecar restart: node token, webdemo token, and postgres
30+
token are re-issued through attestation before the worker rejoins.
31+
- Consul health after tests: zero critical checks, all coordinators
32+
and workers alive.
33+
34+
## Open Work
35+
36+
- KMS policy: pin the long-term KMS public key or equivalent KMS
37+
identity in Terraform and require the verifier-proven KMS evidence
38+
to match it.
39+
- Production OS policy: when a production profile is enabled, include
40+
the expected OS image hash in both Phala launch config and admission
41+
policy.
42+
- Peer identity hardening: keep `peer_id` as the Terraform-declared
43+
Consul node namespace, but bind it to a platform-stable instance
44+
identifier when Phala exposes one without creating a deployment
45+
cycle.
46+
- Token lifecycle: keep non-expiring tokens for the first integrated
47+
version; next step is one active token per attested workload
48+
instance, then bounded TTL plus renewal once Consul, Envoy, and
49+
Patroni reload paths are explicit.
50+
- Upgrade policy: replace static compose-hash allowlists with a
51+
policy server or on-chain bridge that admits new revisions under
52+
signed policy epochs.
53+
- Secret lifecycle: move remaining cluster-wide-identical secrets
54+
out of Terraform env into TEE-rooted generation/distribution.
55+
- Long soak: run the real cluster for at least one hour with zero ACL
56+
authorization failures, zero attestation rejections, and stable
57+
Patroni leader/replica health.
58+
59+
## Decisions
60+
61+
- Workload identity is the Terraform-declared identity; compose hash
62+
is revision evidence, not the workload key.
63+
- `report_data` is `SHA-256(binding_statement_json || nonce)`.
64+
- The broker relies on `dstack-verifier` for quote and event-log
65+
semantics. It does not parse raw RTMR event-log internals itself.
66+
- `peer_id` is the logical mesh/Consul node label, for example
67+
`worker-3`. It is not a secret. It is quoted in `report_data` and
68+
matched against Terraform policy before node-scoped tokens are
69+
issued.
70+
- Production image policy should prefer digest-pinned Compose image
71+
references.

consul-postgres-ha/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ external derivation needed.
3434
For the full topology and layering walkthrough, see
3535
[`ARCHITECTURE.md`](ARCHITECTURE.md).
3636

37+
For current project status and remaining work across Terraform,
38+
mesh transport, Consul, Patroni, and attestation, see
39+
[`PROGRESS.md`](PROGRESS.md).
40+
3741
### The three address spaces (you'll see these everywhere)
3842

3943
Apps and operators run into three loopback ranges in this example. Knowing
@@ -110,6 +114,7 @@ consul-postgres-ha/
110114
├── README.md you are here
111115
├── ARCHITECTURE.md the three-layer stack, peer topology, port plan
112116
├── FAILOVER.md soft-kill / hard-kill / disk-loss recipes + measured RTO
117+
├── PROGRESS.md project status, recently validated flows, open work
113118
├── PUBLISHING.md CI publish flow, manual ttl.sh shortcuts, hot-patch
114119
├── ROBUSTNESS.md where each layer breaks + mitigations
115120

consul-postgres-ha/admission-broker/main.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"context"
66
"crypto/rand"
77
"crypto/sha256"
8-
"crypto/sha512"
98
"encoding/hex"
109
"encoding/json"
1110
"errors"
@@ -342,7 +341,7 @@ func reportDataHex(bindingHex, nonceHex string) (string, error) {
342341
if len(nonce) != 32 {
343342
return "", fmt.Errorf("nonce must be 32 bytes, got %d", len(nonce))
344343
}
345-
h := sha512.New()
344+
h := sha256.New()
346345
h.Write(binding)
347346
h.Write(nonce)
348347
return hex.EncodeToString(h.Sum(nil)), nil

consul-postgres-ha/admission-broker/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package main
33
import (
44
"bytes"
55
"context"
6-
"crypto/sha512"
6+
"crypto/sha256"
77
"encoding/hex"
88
"encoding/json"
99
"net/http"
@@ -120,7 +120,7 @@ func TestReportDataHexBindsStatementAndNonce(t *testing.T) {
120120
}
121121
bindingBytes, _ := hex.DecodeString(binding)
122122
nonceBytes, _ := hex.DecodeString(nonce)
123-
h := sha512.New()
123+
h := sha256.New()
124124
h.Write(bindingBytes)
125125
h.Write(nonceBytes)
126126
want := hex.EncodeToString(h.Sum(nil))

consul-postgres-ha/admission-client/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package main
33
import (
44
"bytes"
55
"context"
6-
"crypto/sha512"
6+
"crypto/sha256"
77
"encoding/hex"
88
"encoding/json"
99
"errors"
@@ -237,7 +237,7 @@ func reportData(statementBytes []byte, nonceHex string) ([]byte, error) {
237237
if len(nonce) != 32 {
238238
return nil, fmt.Errorf("nonce must be 32 bytes, got %d", len(nonce))
239239
}
240-
h := sha512.New()
240+
h := sha256.New()
241241
h.Write(statementBytes)
242242
h.Write(nonce)
243243
return h.Sum(nil), nil

consul-postgres-ha/admission-client/main_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package main
33
import (
44
"bytes"
55
"context"
6-
"crypto/sha512"
6+
"crypto/sha256"
77
"encoding/hex"
88
"encoding/json"
99
"net/http"
@@ -23,7 +23,7 @@ func TestReportDataBindsStatementAndNonce(t *testing.T) {
2323
t.Fatal(err)
2424
}
2525
nonceBytes, _ := hex.DecodeString(nonce)
26-
h := sha512.New()
26+
h := sha256.New()
2727
h.Write(statement)
2828
h.Write(nonceBytes)
2929
if !bytes.Equal(got, h.Sum(nil)) {

consul-postgres-ha/design/attestation-admission.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,15 @@ The broker validates:
141141
1. **Quote chain**: TDX → Intel PCS root, via a sibling
142142
`dstack-verifier` HTTP service.
143143
2. **`report_data` binding**: must equal
144-
`SHA-512(binding_statement || nonce)`. Anything else means the
144+
`SHA-256(binding_statement || nonce)`. Anything else means the
145145
quote was generated for a different handshake; reject. The broker
146146
also verifies that the statement identity equals the claimed
147147
identity.
148148
3. **Nonce freshness**: must be a nonce the broker issued
149149
recently and hasn't been consumed.
150-
4. **RTMR replay**: use the Go SDK's `ReplayRTMRs()` helper to
151-
confirm the event log matches the quote's RTMRs. Otherwise the
152-
event log could be forged.
150+
4. **Verifier event-log result**: require `event_log_verified` from
151+
`dstack-verifier`. The broker forwards the event log to the
152+
verifier but does not reimplement RTMR replay semantics itself.
153153
5. **Policy check**: evaluate the claimed `identity` against the
154154
generated admission policy. Phase 1 accepts a quote when the
155155
identity names a known Terraform workload and the quote's
@@ -258,11 +258,24 @@ single ordinary apply: Terraform already knows the workload
258258
definition, preflight yields the measured revision hash, and the
259259
broker receives a policy document derived from those same inputs.
260260

261-
`app_id`, KMS details, TCB fields, and `app-compose.json` remain
262-
valuable evidence and audit material, but they are not the primary
263-
policy key in Phase 1. In particular, `app-compose.json` is useful
264-
for human review and transparency logs because it is the exact byte
265-
payload dstack measures and it embeds the Docker Compose string.
261+
`app_id`, instance id, KMS details, OS image hash, TCB fields, and
262+
`app-compose.json` remain valuable evidence and audit material, but
263+
they are not the primary policy key in Phase 1. In particular,
264+
`app-compose.json` is useful for human review and transparency logs
265+
because it is the exact byte payload dstack measures and it embeds
266+
the Docker Compose string.
267+
268+
KMS identity should be promoted into the admission predicate. KMS
269+
public keys are long-term, so Terraform can carry a well-known KMS
270+
public key or equivalent KMS identity and the broker can compare it
271+
with verified dstack evidence. This ties service-mesh admission to
272+
the key lifecycle domain that protects workload secrets.
273+
274+
OS image identity should be enforced when the Terraform deployment
275+
uses a production profile. The expected OS image hash should appear
276+
both in the real Phala CVM launch config and in the admission policy,
277+
so a valid compose hash cannot be admitted on an unexpected base
278+
image.
266279

267280
## Upgradeability policy server
268281

@@ -315,7 +328,7 @@ client agent itself, then for each declared service identity.
315328
316329
3. POST /v1/admission/challenge to broker. Receive nonce.
317330
318-
4. report_data = SHA-512(binding_statement || nonce)
331+
4. report_data = SHA-256(binding_statement || nonce)
319332
Call dstack GetQuote(report_data). Receive quote, event_log,
320333
vm_config, and report_data.
321334

0 commit comments

Comments
 (0)