This guide is for self-hosted deployments on your own TDX hardware. For cloud deployments, see Quickstart.
dstack-auth is the file-based KMS authorization webhook. It allowlists OS
images, KMS instances and apps in one JSON file. For on-chain governance, use
auth-eth instead
(see On-Chain Governance).
This guide covers running dstack-auth and managing apps and devices with it.
For initial deployment setup, see Deployment Guide.
The allowlist file whitelists:
- OS images - Which guest OS versions can boot
- KMS nodes - Which KMS instances can bootstrap, serve and onboard (
mrAggregated, devices) - Apps - Which applications can boot (appId, composeHash, devices)
- Platform state (opt-in) - Which TCB statuses and security advisories are acceptable
The file is re-read on every request, so changes take effect immediately without restart. A missing or invalid file denies every request.
dstackup installs dstack-auth as the dstack-auth.service systemd unit,
listening on 127.0.0.1:8001, and writes the allowlist to
auth-allowlist.json in its config directory. dstack run adds the compose
hash of each app it deploys. Edit that file to change the policy.
Build it from the repository:
cd dstack
cargo build --release -p dstack-auth
./target/release/dstack-auth --config /path/to/auth-config.json --port 3001| Option | Default | Description |
|---|---|---|
--config |
/var/lib/dstack/auth-allowlist.json |
Allowlist JSON file |
--address |
127.0.0.1 |
Bind address |
--port |
8001 |
Bind port |
A KMS CVM on QEMU user-mode networking reaches the host loopback at 10.0.2.2,
so the default address is enough for a KMS on the same host: point the KMS at
http://10.0.2.2:<port>. Bind --address 0.0.0.0 only when the KMS reaches the
host over a real network, and restrict access to that port.
Point the KMS at it in kms.toml:
[core.auth_api]
type = "webhook"
[core.auth_api.webhook]
url = "http://10.0.2.2:3001"{
"osImages": ["0x..."],
"gatewayAppId": "0x...",
"kmsContractAddr": "0x<kms-app-id>",
"kms": {
"mrAggregated": ["0x..."],
"devices": ["0x..."],
"allowAnyDevice": true
},
"apps": {
"0x<app-id>": {
"composeHashes": ["0x..."],
"devices": ["0x..."],
"allowAnyDevice": true
}
},
"rootEpochs": [{"caPubkey": "0x...", "k256Pubkey": "0x..."}],
"currentRootEpoch": 0
}Note:
osImagesis always required for apps. For KMS authorization, you must also populatekms.mrAggregated; if it is left empty, dstack-auth denies all KMS boots. AddgatewayAppIdafter deploying the Gateway. Addappsentries as you deploy applications.
Hex values are compared case-insensitively, with or without a 0x prefix.
| Field | Required | Description |
|---|---|---|
osImages |
Yes | Allowed OS image hashes (from digest.txt). Empty denies every app. |
gatewayAppId |
No | Gateway app ID (add after Gateway deployment). Several gateways: join their IDs with commas. |
kmsContractAddr |
For /authorize* |
The attested app ID of the KMS. Every operation except kms_bootstrap is denied while it is unset, and a caller is accepted only on the endpoint matching its app ID (/authorizeKms for this ID, /authorizeApp for any other). Reported by GET /. |
chainId |
No | Informational, reported by GET / (default 0). |
appImplementation |
No | Informational, reported by GET / as appImplementation and appAuthImplementation. |
allowedTcbStatuses |
No | When set, deny unless the verifier-derived tcbStatus is listed; [] denies everything. Absent: not checked. See TCB and advisory policy. |
allowedAdvisoryIds |
No | When set, deny any reported advisory ID that is not listed; [] denies any advisory. Absent: not checked. |
kms.mrAggregated |
Yes | Allowed KMS early aggregate MR values (boot-mr-done). Empty denies every KMS. |
kms.devices |
No | Allowed KMS device IDs. Empty denies all unless kms.allowAnyDevice is true. |
kms.allowAnyDevice |
No | If true, skip the device ID check for KMS |
apps.<appId>.composeHashes |
No | Allowed compose hashes for this app |
apps.<appId>.devices |
No | Allowed device IDs for this app. Empty denies all unless allowAnyDevice is true. |
apps.<appId>.allowAnyDevice |
No | If true, skip the device ID check for this app |
apps.<appId>.currentAppEpoch |
No | Current app key epoch (default 0), served by GET /app/<appId>. A non-zero epoch denies the legacy /bootAuth/app route. |
rootEpochs |
No | KMS root public keys of each root epoch, indexed by epoch, [{"caPubkey": "0x...", "k256Pubkey": "0x..."}]. Epoch 0 is the original roots. Append only. GET / reports the current epoch as rootEpoch with its chain hash as rootEpochHash; the last entry is reported as latestRootEpoch, and entries beyond currentRootEpoch are pending activation. |
currentRootEpoch |
No | Root epoch KMS instances serve (default 0). Key-issuing operations (kms_serve, app_get_key, app_get_cert) may request up to this epoch; KMS key handover (kms_export_keys, kms_import_keys) may request any registered epoch, so the other KMS nodes can onboard from the rotated one while the new epoch is pending. Raise it only once every KMS node holds that epoch. |
KMS (/authorizeKms, /bootAuth/kms):
tcbStatusmust be inallowedTcbStatuses, if set- Every
advisoryIdsentry must be inallowedAdvisoryIds, if set mrAggregatedmust be inkms.mrAggregateddeviceIdmust be inkms.devicesunlesskms.allowAnyDeviceis true
Apps (/authorizeApp, /bootAuth/app):
tcbStatusmust be inallowedTcbStatuses, if set- Every
advisoryIdsentry must be inallowedAdvisoryIds, if set osImageHashmust be inosImagesappIdmust exist inappscomposeHashmust be in the app'scomposeHashesdeviceIdmust be in the app'sdevicesunlessallowAnyDeviceis true
KMS requests are not matched against osImages: mrAggregated already pins
the OS image measurements. The /authorize* endpoints and legacy /bootAuth/*
routes also enforce root epochs, see the
KMS authorization protocol.
By default dstack-auth does not check the platform TCB status or security
advisories. In the single-operator model the operator controls and trusts the
host, and real TDX hosts routinely report a non-UpToDate TCB when microcode or
the TDX module lags behind, so a hard TCB gate there adds friction without
adding trust.
Deployments where the host operator is not the party relying on the policy (a KMS serving other users, AWS, SEV-SNP) should enforce it explicitly:
{
"allowedTcbStatuses": ["UpToDate"],
"allowedAdvisoryIds": [],
"osImages": ["0x..."],
"kms": { "mrAggregated": ["0x..."], "allowAnyDevice": true },
"apps": {}
}With this, any status other than UpToDate and any advisory ID denies. To
accept a known state temporarily, list it explicitly, for example
"allowedTcbStatuses": ["UpToDate", "SWHardeningNeeded"] or
"allowedAdvisoryIds": ["INTEL-SA-00837"].
AWS NitroTPM has no TDX/SNP-style TCB advisory surface, so the KMS normalizes a
verified NitroTPM attestation to tcbStatus: "UpToDate" before calling the
webhook; the policy above works unchanged there.
auth-simple (the Bun/TypeScript server in dstack/kms/auth-simple) has been
removed. dstack-auth reads the same config file format, so an existing
auth-config.json loads unchanged. To switch:
-
Keep auth-simple's strict platform defaults by adding them explicitly, unless your config already sets them:
"allowedTcbStatuses": ["UpToDate"], "allowedAdvisoryIds": []
Without these fields, dstack-auth accepts any TCB status and advisory.
-
Set
kmsContractAddrto the attested KMS app ID if it is not set. auth-simple defaulted it to the zero address and did not check it; dstack-auth denies every/authorize*operation exceptkms_bootstrapwithout it. -
Replace the start command.
PORTandAUTH_CONFIG_PATHbecome flags, and auth-simple listened on all interfaces while dstack-auth binds to loopback by default:# before PORT=3001 AUTH_CONFIG_PATH=/path/to/auth-config.json bun run start # after dstack-auth --config /path/to/auth-config.json --port 3001
Add
--address 0.0.0.0if the KMS reaches the webhook over a real network rather than10.0.2.2.
Other differences: KMS requests are no longer matched against osImages (see
Validation Logic), GET / no longer reports configPath,
and rejection reasons are lowercase (for example root epoch not registered).
App IDs are typically the contract address (for on-chain) or a unique identifier you choose.
For dstack-auth, you can use any unique hex string (40 characters / 20 bytes):
# Generate a random app ID
openssl rand -hex 20
# Output: 7a3b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5bThe compose hash is computed from the normalized docker-compose file. The VMM displays it when deploying:
Docker compose file:
...
Compose hash: 0x700a50336df7c07c82457b116e144f526c29f6d8...
Or query from a running CVM:
curl -s --unix-socket /var/run/dstack.sock http://localhost/Info | \
jq -r '"0x" + (.tcb_info | fromjson | .compose_hash)'Note: The VMM normalizes YAML to JSON before hashing. For exact hash, use the value shown during deployment.
Edit your allowlist file:
{
"apps": {
"0x7a3b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b": {
"composeHashes": [
"0x700a50336df7c07c82457b116e144f526c29f6d8..."
],
"devices": [],
"allowAnyDevice": true
}
}
}Use the App ID when deploying through VMM:
./vmm-cli.py deploy \
--app-id 7a3b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b \
--compose docker-compose.yaml \
...When you change your docker-compose.yaml, a new compose hash is generated.
Add the new hash to the composeHashes array:
{
"apps": {
"0x<app-id>": {
"composeHashes": [
"0x<old-hash>",
"0x<new-hash>"
]
}
}
}Remove the old hash to prevent old versions from booting:
{
"apps": {
"0x<app-id>": {
"composeHashes": [
"0x<new-hash>"
]
}
}
}Devices are identified by their TDX device ID (hardware-specific).
The deviceId is sent by the booting app/KMS in its auth request. dstack-auth
logs it with each decision (journalctl -u dstack-auth when installed by
dstackup):
authorize AppGetKey app=7a3b... compose=700a... mr=... os=0b32... device=e5a0... tcb=UpToDate advisories=[] -> authorized=true ()
Tip: Use
allowAnyDevice: trueinitially, then restrict to specific devices after capturing IDs from logs.
{
"apps": {
"0x<app-id>": {
"composeHashes": ["0x..."],
"devices": [
"0xe5a0c70bb6503de2d31c11d85914fe3776ed5b33a078ed856327c371a60fe0fd"
],
"allowAnyDevice": false
}
}
}For initial testing or when device restriction isn't needed:
{
"apps": {
"0x<app-id>": {
"allowAnyDevice": true
}
}
}Delete the app entry from the apps object. Running instances continue until
restarted; new key requests are rejected.
The gatewayAppId field is optional during initial KMS deployment. Add it after deploying the Gateway.
The Gateway is a special app that routes traffic to other apps. Once deployed, add its App ID to your config:
{
"gatewayAppId": "0x75537828f2ce51be7289709686A69CbFDbB714F1",
"apps": {
"0x75537828f2ce51be7289709686A69CbFDbB714F1": {
"composeHashes": ["0x..."],
"allowAnyDevice": true
}
}
}The gatewayAppId is returned in authorization responses and used by KMS for key derivation.
To trust several gateways, join their App IDs with commas ("0xaaa...,0xbbb..."). CVMs accept a gateway whose App ID is contained in this value.
To allow additional KMS nodes to onboard (receive root keys from the primary KMS), whitelist their mrAggregated value.
- Initial setup: capture the first KMS measurement with
Onboard.GetAttestationInfoand add it tokms.mrAggregatedbefore bootstrap. An empty array denies all KMS boots. - From dstack-auth logs: every KMS request is logged with its measurement:
authorize KmsBootstrap app=... compose=... mr=<mr-aggregated> os=... device=... -> authorized=false (kms mrAggregated not allowed)
{
"kms": {
"mrAggregated": ["0x<mr-aggregated-hash>"],
"allowAnyDevice": true
}
}Note: All KMS nodes using the same OS image and compose will have the same mrAggregated, so you only need to capture it once.
curl -s http://127.0.0.1:3001/ | jqReturns status, gatewayAppId, kmsContractAddr and, once rootEpochs is
configured, rootEpoch and rootEpochHash. An invalid rootEpochs
configuration returns HTTP 503.
curl -s -X POST http://127.0.0.1:3001/bootAuth/app \
-H "Content-Type: application/json" \
-d '{
"appId": "0x<app-id>",
"composeHash": "0x<compose-hash>",
"osImageHash": "0x<os-image-hash>",
"deviceId": "0x<device-id>",
"tcbStatus": "UpToDate",
"advisoryIds": []
}' | jqExpected responses:
{"isAllowed": true, ...}- App is authorized{"isAllowed": false, "reason": "app not registered", ...}- App ID not in config{"isAllowed": false, "reason": "compose hash not allowed", ...}- Hash not whitelisted{"isAllowed": false, "reason": "tcb status not allowed", ...}-tcbStatusnot inallowedTcbStatuses
| Endpoint | Description |
|---|---|
GET / |
Health check and info (kmsContractAddr, gatewayAppId, chainId, appImplementation, rootEpoch, rootEpochHash) |
POST /authorizeKms, POST /authorizeApp |
Epoch-aware authorization; return an AuthorizationResult. See the KMS authorization protocol. |
GET /app/<appId> |
{"currentAppEpoch": N}; 404 for an app that is not configured |
POST /bootAuth/kms, POST /bootAuth/app |
Legacy boot authorization for older KMS binaries; return {isAllowed, reason, gatewayAppId} |
- Deployment Guide - Initial setup
- On-Chain Governance - Smart contract-based alternative