Skip to content

Commit 4d68758

Browse files
authored
Merge pull request #830 from Dstack-TEE/fix/kms-unattested-rpc-cert
feat(kms): make RPC certificate attestation configurable
2 parents 80a5270 + 2d43948 commit 4d68758

9 files changed

Lines changed: 129 additions & 24 deletions

File tree

docs/security/public-security-reports.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ These issues were filed as concrete vulnerability reports, security audit findin
4949
| [#606](https://github.com/Dstack-TEE/dstack/issues/606) App keys and decrypted env files world-readable | Open | Valid hardening, open | Tightening secret-bearing file writes to owner-only permissions (`0600`) is valid defense-in-depth work with no expected compatibility cost |
5050
| [#607](https://github.com/Dstack-TEE/dstack/issues/607) `gateway_app_id = "any"` disables gateway identity pinning | Closed | Not a production vulnerability | `gateway_app_id` is KMS contract configuration and is publicly auditable. Production deployments must not use `"any"`. No code fix was applied |
5151
| [#608](https://github.com/Dstack-TEE/dstack/issues/608) `auth_api.type = "dev"` allows all authorization | Closed | Not a production vulnerability | Dev auth is measured runtime configuration, not a production mode. Production must use webhook/on-chain authorization. No code fix was applied |
52-
| [#609](https://github.com/Dstack-TEE/dstack/issues/609) `quote_enabled = false` bypasses attestation | Closed | Not a production vulnerability | The flag is measured in runtime configuration and should fail production attestation policy. No code fix was applied |
52+
| [#609](https://github.com/Dstack-TEE/dstack/issues/609) `quote_enabled = false` bypasses attestation | Closed | Not a production vulnerability | The flag was measured in runtime configuration and would fail production attestation policy, so no code fix was applied at the time. The setting has since been retired; current KMS uses `attest_rpc_cert`, which defaults to `true`, to control RPC certificate attestation |
5353
| [#610](https://github.com/Dstack-TEE/dstack/issues/610) Unauthenticated bootstrap endpoint can overwrite root keys | Closed | Not a production vulnerability | The bootstrap endpoint does not accept caller-supplied root key material. Root keys are generated server-side, and the operator chooses which result to publish. No code fix was applied |
5454
| [#611](https://github.com/Dstack-TEE/dstack/issues/611) Unauthenticated `/finish` endpoint can shut down KMS onboard service | Closed | Not a production vulnerability | The onboard service is a short-lived setup flow. Premature shutdown causes operator retry, not persistent compromise or data loss. No code fix was applied |
5555
| [#612](https://github.com/Dstack-TEE/dstack/issues/612) Gateway `register_cvm` prefers stale `app_info` over live attestation | Closed | Not a production vulnerability | Cert-embedded `app_info` is extracted from attestation and signed by KMS. Preferring it avoids redundant extraction and is not a trust bypass. No code fix was applied |

docs/security/security-best-practices.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Example app-compose.json:
8484

8585
Development settings are intentionally easy to audit, but they are not production-safe. A production deployment should satisfy all of the following:
8686

87-
- KMS quote verification remains enabled. Do not deploy production KMS with `quote_enabled = false`.
87+
- The KMS attests its own RPC certificate. Do not deploy production KMS with `attest_rpc_cert = false`.
8888
- KMS authorization uses webhook/on-chain policy. Do not use `auth_api.type = "dev"` with real key material.
8989
- The KMS contract pins a concrete gateway app id. Do not use `gateway_app_id = "any"` for production traffic.
9090
- TEE quotes are evaluated by deployment policy, including TCB status and expected OS/application measurements.

docs/security/security-model.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ The one case dstack does not leave to downstream is a genuinely invalid TCB: `dc
307307
308308
### Development modes are auditable, not production-safe
309309

310-
dstack keeps several development switches as runtime or on-chain configuration rather than Cargo feature flags. Examples include KMS `quote_enabled = false`, `auth_api.type = "dev"`, and KMS contract `gateway_app_id = "any"`. These settings exist for local development and integration tests, not for production deployments.
310+
dstack keeps several development switches as runtime or on-chain configuration rather than Cargo feature flags. Examples include KMS `attest_rpc_cert = false`, gateway `core.debug.insecure_skip_attestation = true`, KMS `auth_api.type = "dev"`, and KMS contract `gateway_app_id = "any"`. These settings exist for local development and integration tests, not for production deployments.
311311

312312
This is intentional. Runtime configuration that affects the trust boundary is visible in attestation measurements or public contract state. Cargo feature gates are not automatically more auditable because feature unification can enable a feature through a dependency graph, and the resulting runtime behavior is not represented as a measured deployment setting.
313313

dstack/kms/kms.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ site_name = ""
3131
# outside a TEE (e.g. local dev/testing) where the local guest agent socket
3232
# is unavailable.
3333
enforce_self_authorization = true
34+
# Whether the KMS embeds an attestation in its own RPC certificate. Set false
35+
# only for local dev/testing where the KMS runs outside a TEE. This narrows what
36+
# the KMS asserts about itself and relaxes no verification: quotes presented to
37+
# the KMS are still fully checked, a guest that accepts an unattested
38+
# certificate simply does not extend mr-kms, and another KMS refuses to onboard
39+
# from one.
40+
attest_rpc_cert = true
3441
# AMD SEV-SNP key/cert release remains disabled unless this local KMS gate is
3542
# explicitly enabled. External auth policy must still allow the verified
3643
# BootInfo before any sensitive material is returned.

dstack/kms/src/config.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ pub(crate) struct KmsConfig {
5555
pub aws_nitro_tpm_key_release: bool,
5656
#[serde(default)]
5757
pub site_name: String,
58+
/// Whether the KMS embeds an attestation in its own RPC certificate.
59+
/// Defaults to `true`. Set `false` only for local dev/testing where the KMS
60+
/// runs outside a TEE and cannot reach a guest agent socket.
61+
///
62+
/// This narrows what the KMS asserts about itself; it relaxes no
63+
/// verification anywhere. Quotes presented *to* the KMS are still fully
64+
/// checked, so key release stays gated, and relying parties keep their own
65+
/// policy: a guest accepts an unattested KMS certificate but then does not
66+
/// extend `mr-kms`, so a remote verifier can still tell, and another KMS
67+
/// refuses to onboard from one.
68+
#[serde(default = "default_true")]
69+
pub attest_rpc_cert: bool,
5870
/// Whether trusted RPCs require the KMS to first attest itself to its
5971
/// own auth API. Defaults to `true` (strict). Set `false` only for local
6072
/// dev/testing where the KMS runs outside a TEE and cannot reach a guest
@@ -183,6 +195,7 @@ pub(crate) struct OnboardConfig {
183195
#[cfg(test)]
184196
mod tests {
185197
use super::*;
198+
use rocket::figment::providers::{Format, Toml};
186199

187200
#[test]
188201
fn default_config_parses_with_admin_disabled_and_no_hash() {
@@ -198,4 +211,32 @@ mod tests {
198211
);
199212
assert!(!config.admin.insecure_no_auth);
200213
}
214+
215+
#[test]
216+
fn rpc_cert_is_attested_by_default() {
217+
let figment = load_config_figment(None);
218+
let config: KmsConfig = figment
219+
.focus("core")
220+
.extract()
221+
.expect("kms.toml must parse into KmsConfig");
222+
assert!(
223+
config.attest_rpc_cert,
224+
"the KMS must attest its own RPC certificate unless explicitly told not to"
225+
);
226+
}
227+
228+
#[test]
229+
fn omitting_the_key_keeps_the_attested_default() {
230+
// Configs written before this key existed must keep working, and must
231+
// land on the attested side.
232+
#[derive(Deserialize)]
233+
struct Probe {
234+
#[serde(default = "default_true")]
235+
attest_rpc_cert: bool,
236+
}
237+
let probe: Probe = Figment::from(Toml::string(""))
238+
.extract()
239+
.expect("an absent attest_rpc_cert must parse");
240+
assert!(probe.attest_rpc_cert);
241+
}
201242
}

dstack/kms/src/main.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ async fn main() -> Result<()> {
116116
let figment = config::load_config_figment(args.config.as_deref());
117117
let config: KmsConfig = figment.focus("core").extract()?;
118118

119+
if !config.attest_rpc_cert {
120+
warn!(
121+
"attest_rpc_cert = false; the KMS RPC certificate carries no attestation, so \
122+
guests cannot verify which KMS they are talking to and will not extend \
123+
mr-kms. Intended for local development only"
124+
);
125+
}
126+
119127
if config.onboard.enabled && !config.keys_exists() {
120128
info!("Onboarding");
121129
run_onboard_service(config.clone(), figment.clone()).await?;
@@ -126,6 +134,12 @@ async fn main() -> Result<()> {
126134

127135
info!("Updating certs");
128136
if let Err(err) = onboard_service::update_certs(&config).await {
137+
if config.attest_rpc_cert {
138+
return Err(err).context(
139+
"Failed to reissue the attested KMS RPC certificate; refusing to start with a \
140+
potentially unattested certificate",
141+
);
142+
}
129143
warn!("Failed to update certs: {err}");
130144
};
131145

dstack/kms/src/onboard_service.rs

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use ra_tls::{
2727
};
2828
use safe_write::safe_write;
2929
use sha2::Digest;
30+
use tracing::info;
3031

3132
use crate::{
3233
config::KmsConfig,
@@ -76,7 +77,7 @@ impl OnboardRpc for OnboardHandler {
7677
ensure_self_kms_allowed(&self.state.config, &self.state.attestation_verifier)
7778
.await
7879
.context("KMS is not allowed to bootstrap")?;
79-
let keys = Keys::generate(&request.domain)
80+
let keys = Keys::generate(&request.domain, self.state.config.attest_rpc_cert)
8081
.await
8182
.context("Failed to generate keys")?;
8283

@@ -355,12 +356,20 @@ struct Keys {
355356
}
356357

357358
impl Keys {
358-
async fn generate(domain: &str) -> Result<Self> {
359+
async fn generate(domain: &str, attest_rpc_cert: bool) -> Result<Self> {
359360
let tmp_ca_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
360361
let ca_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
361362
let rpc_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
362363
let k256_key = SigningKey::random(&mut rand::rngs::OsRng);
363-
Self::from_keys(tmp_ca_key, ca_key, rpc_key, k256_key, domain).await
364+
Self::from_keys(
365+
tmp_ca_key,
366+
ca_key,
367+
rpc_key,
368+
k256_key,
369+
domain,
370+
attest_rpc_cert,
371+
)
372+
.await
364373
}
365374

366375
async fn from_keys(
@@ -369,6 +378,7 @@ impl Keys {
369378
rpc_key: KeyPair,
370379
k256_key: SigningKey,
371380
domain: &str,
381+
attest_rpc_cert: bool,
372382
) -> Result<Self> {
373383
let tmp_ca_cert = CertRequest::builder()
374384
.org_name("Dstack")
@@ -386,20 +396,32 @@ impl Keys {
386396
.key(&ca_key)
387397
.build()
388398
.self_signed()?;
389-
let pubkey = rpc_key.public_key_der();
390-
let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey);
391-
let response = app_attest(report_data.to_vec())
392-
.await
393-
.context("Failed to get quote")?;
394-
let attestation = VersionedAttestation::from_bytes(&response.attestation)
395-
.context("Invalid attestation")?;
399+
// The only place the KMS embeds its own attestation. Skipping it lets
400+
// the KMS run outside a TEE for development; it does not affect the
401+
// verification of quotes presented *to* the KMS, which is a separate
402+
// path (main_service::ensure_app_attestation_allowed) and stays on.
403+
let attestation = if attest_rpc_cert {
404+
let pubkey = rpc_key.public_key_der();
405+
let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey);
406+
let response = app_attest(report_data.to_vec()).await.context(
407+
"failed to get a quote for the KMS RPC certificate. The KMS attests \
408+
itself through the dstack guest agent, so it must run inside a dstack \
409+
CVM. For local development set attest_rpc_cert = false",
410+
)?;
411+
Some(
412+
VersionedAttestation::from_bytes(&response.attestation)
413+
.context("Invalid attestation")?,
414+
)
415+
} else {
416+
None
417+
};
396418

397419
// Sign WWW server cert with KMS cert
398420
let rpc_cert = CertRequest::builder()
399421
.subject(domain)
400422
.alt_names(&[domain.to_string()])
401423
.special_usage("kms:rpc")
402-
.maybe_attestation(Some(&attestation))
424+
.maybe_attestation(attestation.as_ref())
403425
.key(&rpc_key)
404426
.build()
405427
.signed_by(&ca_cert, &ca_key)?;
@@ -483,7 +505,15 @@ impl Keys {
483505
KeyPair::from_pem(&tmp_ca_key_pem).context("Failed to parse tmp CA key")?;
484506
let ecdsa_key =
485507
SigningKey::from_slice(&root_k256_key).context("Failed to parse ECDSA key")?;
486-
Self::from_keys(tmp_ca_key, ca_key, rpc_key, ecdsa_key, domain).await
508+
Self::from_keys(
509+
tmp_ca_key,
510+
ca_key,
511+
rpc_key,
512+
ecdsa_key,
513+
domain,
514+
cfg.attest_rpc_cert,
515+
)
516+
.await
487517
}
488518

489519
fn store(&self, cfg: &KmsConfig) -> Result<()> {
@@ -527,12 +557,22 @@ pub(crate) async fn update_certs(cfg: &KmsConfig) -> Result<()> {
527557
let domain = domain.trim();
528558

529559
// Regenerate certificates using existing keys
530-
let keys = Keys::from_keys(tmp_ca_key, ca_key, rpc_key, k256_key, domain)
531-
.await
532-
.context("Failed to regenerate certificates")?;
533-
534-
// Write the new certificates to files
560+
let keys = Keys::from_keys(
561+
tmp_ca_key,
562+
ca_key,
563+
rpc_key,
564+
k256_key,
565+
domain,
566+
cfg.attest_rpc_cert,
567+
)
568+
.await
569+
.context("Failed to regenerate certificates")?;
570+
571+
// Write the new certificates to files. This runs on every start, so a
572+
// hand-placed certificate is replaced -- say so, because the old silence
573+
// made that look like the file had survived.
535574
keys.store_certs(cfg)?;
575+
info!("Reissued the KMS RPC certificate for {domain}");
536576

537577
Ok(())
538578
}
@@ -541,7 +581,7 @@ pub(crate) async fn bootstrap_keys(cfg: &KmsConfig, verifier: &AttestationVerifi
541581
ensure_self_kms_allowed(cfg, verifier)
542582
.await
543583
.context("KMS is not allowed to auto-bootstrap")?;
544-
let keys = Keys::generate(&cfg.onboard.auto_bootstrap_domain)
584+
let keys = Keys::generate(&cfg.onboard.auto_bootstrap_domain, cfg.attest_rpc_cert)
545585
.await
546586
.context("Failed to generate keys")?;
547587
keys.store(cfg)?;

test-suites/full-stack-compose/scripts/runner.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ stop_network_probe() {
697697
assert_no_insecure_shortcuts() {
698698
log "auditing rendered manifests for production trust settings"
699699
local manifest
700-
local forbidden='quote_enabled[[:space:]]*=[[:space:]]*false|enforce_self_authorization[[:space:]]*=[[:space:]]*false|verify[[:space:]]*=[[:space:]]*false'
700+
local forbidden='quote_enabled[[:space:]]*=[[:space:]]*false|attest_rpc_cert[[:space:]]*=[[:space:]]*false|insecure_skip_attestation[[:space:]]*=[[:space:]]*true|enforce_self_authorization[[:space:]]*=[[:space:]]*false|verify[[:space:]]*=[[:space:]]*false'
701701
if grep -R -E "$forbidden" "$WORK_DIR"/*.app-compose.json; then
702702
die "rendered app manifest contains a forbidden development trust setting"
703703
fi

tools/dev-stack.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ mandatory = false
181181
182182
[core]
183183
cert_dir = "$CERTS_DIR"
184+
attest_rpc_cert = false
185+
enforce_self_authorization = false
184186
185187
[core.gpu]
186188
enabled = $VMM_ENABLE_GPU
@@ -189,7 +191,6 @@ enabled = $VMM_ENABLE_GPU
189191
type = "dev"
190192
191193
[core.onboard]
192-
quote_enabled = false
193194
address = "127.0.0.1"
194195
port = $KMS_RPC_LISTEN_PORT
195196
auto_bootstrap_domain = "$KMS_DOMAIN"
@@ -215,7 +216,9 @@ mandatory = false
215216
[core]
216217
kms_url = "https://localhost:$KMS_RPC_LISTEN_PORT"
217218
rpc_domain = "$GATEWAY_DOMAIN"
218-
run_in_dstack = false
219+
220+
[core.debug]
221+
insecure_skip_attestation = true
219222
220223
[core.sync]
221224
enabled = false

0 commit comments

Comments
 (0)