diff --git a/CHANGELOG.md b/CHANGELOG.md index f4d6c5756..11a1ba48d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - sdk: `TlsKeyOptions.path` in the JavaScript SDK. `GetTlsKeyArgs` has no such field and `getTlsKey` never read it, so a caller who set it was silently ignored. Breaking at the type level only, and only for code whose value was already being discarded. `deriveKey`'s `path` is a real, deprecated Tappd-era parameter and stays; the Python, Rust and Go v0 TLS-key options never carried one - guest-agent: the `EmitEvent` RPC no longer records anything -- runtime RTMR3 events are system-owned in 0.6.0, so an app can no longer extend the measurement chain. The method itself stays on the unversioned path and always fails with an error naming the removal, rather than being deleted outright: a deleted method answers HTTP 404 `Service not found: EmitEvent`, which tells a 0.5.x caller nothing about why its events stopped being recorded, while the kept stub fails with a message naming the removal and pointing at `report_data`. **Breaking:** any app extending RTMR3 at runtime must stop; bind app data through `report_data` instead, which is what most callers wanted anyway +- gateway: `core.debug.insecure_skip_attestation`. It turned off both checks that make a gateway cluster a trust boundary: the node stopped asking its guest agent for its own app id, and every WaveKV sync and push, plus `ensure_from_gateway`, stopped checking the peer's. With it set, anything that could reach the sync routes could insert entries that replicated to every gateway in the cluster. It existed only because the integration suites could not run without it; they now run against a guest agent simulator and verify quotes through the production path, so nothing sets it. `docs/security/security-model.md` argues dstack's development switches are acceptable because they are visible in attestation measurements or public contract state -- that argument cannot cover the switch deciding whether attestation happens at all, which is why this one is deleted rather than documented. There is no replacement. **Breaking:** the config struct does not use `deny_unknown_fields`, so a leftover line is ignored rather than rejected, and what follows depends on why it was set. On a TDX host with a guest agent the gateway starts normally and peers that cannot present a verifiable app id stop being accepted. On a host with no guest agent -- the usual reason to have set it -- the gateway no longer starts at all, failing with `Failed to get app info`, because `my_app_id` is what every peer check compares against and a node that cannot learn its own identity must not come up without one. Remove the line and make sure every node can attest ## [0.5.5] - 2025-10-20 diff --git a/docs/security/security-model.md b/docs/security/security-model.md index 2fc39d683..03e48be76 100644 --- a/docs/security/security-model.md +++ b/docs/security/security-model.md @@ -329,10 +329,12 @@ The one case dstack does not leave to downstream is a genuinely invalid TCB: `dc ### Development modes are auditable, not production-safe -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. +dstack keeps several development switches as runtime or on-chain configuration rather than Cargo feature flags. Examples include KMS `attest_rpc_cert = false`, 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. 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. +This argument has a limit, and it is worth stating because it is what keeps the list short. A switch qualifies only if the trust decision still happens and is merely recorded as a measured setting. A switch that decides *whether* attestation happens at all does not qualify: there is then no measurement to audit, because the thing that would have produced it was skipped. Gateway `core.debug.insecure_skip_attestation` was such a switch -- it turned off both the peer identity check on WaveKV sync and the gateway's own app id lookup -- and it was removed rather than documented. + Production verifiers should reject deployments that use these development settings. Operators should treat them the same way they treat debug-mode TEE quotes: useful for testing, invalid for production trust. ### KMS mTLS is route-enforced for sensitive operations diff --git a/dstack/gateway/docs/cluster-deployment.md b/dstack/gateway/docs/cluster-deployment.md index 1bfbea124..3d248a048 100644 --- a/dstack/gateway/docs/cluster-deployment.md +++ b/dstack/gateway/docs/cluster-deployment.md @@ -241,7 +241,6 @@ address = "0.0.0.0" [core.debug] insecure_enable_debug_rpc = true -insecure_skip_attestation = false port = 9015 address = "0.0.0.0" diff --git a/dstack/gateway/gateway.toml b/dstack/gateway/gateway.toml index 4f963b4ff..07be1c145 100644 --- a/dstack/gateway/gateway.toml +++ b/dstack/gateway/gateway.toml @@ -60,7 +60,6 @@ insecure_enable_debug_rpc = false # arbitrary custom domains, so enabling this lets any DNS zone owner reach the # gateway's own loopback on a port of their choosing, bypassing port_policy. insecure_localhost_backend = false -insecure_skip_attestation = false address = "127.0.0.1:8012" [core.wg] diff --git a/dstack/gateway/src/config.rs b/dstack/gateway/src/config.rs index fa587bc28..7dec16a7b 100644 --- a/dstack/gateway/src/config.rs +++ b/dstack/gateway/src/config.rs @@ -639,8 +639,6 @@ pub struct DebugConfig { /// Enable debug server #[serde(default)] pub insecure_enable_debug_rpc: bool, - #[serde(default)] - pub insecure_skip_attestation: bool, /// Let the app-address `localhost` resolve to 127.0.0.1, so a hostname can /// be routed to a service on the gateway host itself. /// diff --git a/dstack/gateway/src/kv/https_client.rs b/dstack/gateway/src/kv/https_client.rs index 871c3e861..a3854cfb8 100644 --- a/dstack/gateway/src/kv/https_client.rs +++ b/dstack/gateway/src/kv/https_client.rs @@ -53,14 +53,19 @@ pub trait CertValidator: Debug + Send + Sync + 'static { fn validate(&self, cert_der: &[u8]) -> Result<(), String>; } -/// TLS configuration for mTLS with optional custom certificate validation +/// TLS configuration for mTLS with custom certificate validation #[derive(Clone)] pub struct HttpsClientConfig { pub cert_path: String, pub key_path: String, pub ca_cert_path: String, - /// Optional custom certificate validator (checked during TLS handshake) - pub cert_validator: Option>, + /// Custom certificate validator, checked against the peer's certificate + /// during the handshake. + /// + /// Not optional: this is the only thing that distinguishes a gateway in our own + /// cluster from any other holder of a certificate the shared CA signed, and CA-path + /// validation alone does not make that distinction. + pub cert_validator: Arc, } /// Wrapper that adapts a CertValidator to rustls ServerCertVerifier @@ -147,10 +152,10 @@ impl ServerCertVerifier for CustomCertVerifier { type HyperClient = Client, Full>; -/// HTTPS client with mTLS and optional custom certificate validation. +/// HTTPS client with mTLS and peer identity validation. /// -/// When a `cert_validator` is set in `TlsConfig`, the client runs the validator -/// during the TLS handshake, before any application data is sent. +/// The `cert_validator` runs during the TLS handshake, before any application data is +/// sent, on top of the chain verification against the configured CA. #[derive(Clone)] pub struct HttpsClient { client: HyperClient, @@ -221,22 +226,17 @@ impl HttpsClient { .next() .context("no CA certificate found")?; - // Build rustls config with custom verifier if validator is provided - let tls_config_builder = rustls::ClientConfig::builder(); - - let tls_config = if let Some(ref validator) = tls.cert_validator { - let verifier = CustomCertVerifier::new(validator.clone(), ca_cert)?; - tls_config_builder - .dangerous() - .with_custom_certificate_verifier(Arc::new(verifier)) - } else { - // Standard verification without custom validator - let mut root_store = rustls::RootCertStore::empty(); - root_store.add(ca_cert).context("failed to add CA cert")?; - tls_config_builder.with_root_certificates(root_store) - } - .with_client_auth_cert(certs, key) - .context("failed to set client auth cert")?; + // `CustomCertVerifier` runs the validator *after* rustls has verified the chain + // against `ca_cert`, so this is the CA path plus an identity check, never a + // replacement for it. There is deliberately no validator-less branch: it would + // accept any certificate the CA signed, which is every gateway in every cluster + // that shares the CA. + let verifier = CustomCertVerifier::new(tls.cert_validator.clone(), ca_cert)?; + let tls_config = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier)) + .with_client_auth_cert(certs, key) + .context("failed to set client auth cert")?; let https = HttpsConnectorBuilder::new() .with_tls_config(tls_config) @@ -433,39 +433,13 @@ mod transport_tests { use tokio::net::TcpListener; use tokio_rustls::TlsAcceptor; + /// The app id the HTTP-level tests below run under. They are not about identity, + /// but the validator is not optional, so their server and client agree on one. + const TEST_APP_ID: &[u8] = b"app-id-of-this-cluster"; + /// A CA plus a leaf valid for 127.0.0.1, written where `HttpsClient::new` expects. fn tls_material(dir: &std::path::Path) -> (HttpsClientConfig, Vec, Vec) { - use ra_tls::rcgen::{BasicConstraints, CertificateParams, IsCa, KeyPair}; - - let ca_key = KeyPair::generate().expect("ca key"); - let mut ca_params = CertificateParams::new(vec![]).expect("ca params"); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - let ca_cert = ca_params.self_signed(&ca_key).expect("ca cert"); - - let leaf_key = KeyPair::generate().expect("leaf key"); - let leaf_params = - CertificateParams::new(vec!["127.0.0.1".to_string()]).expect("leaf params"); - let leaf_cert = leaf_params - .signed_by(&leaf_key, &ca_cert, &ca_key) - .expect("leaf cert"); - - let cert_path = dir.join("node.crt"); - let key_path = dir.join("node.key"); - let ca_path = dir.join("ca.crt"); - std::fs::write(&cert_path, leaf_cert.pem()).expect("write cert"); - std::fs::write(&key_path, leaf_key.serialize_pem()).expect("write key"); - std::fs::write(&ca_path, ca_cert.pem()).expect("write ca"); - - ( - HttpsClientConfig { - cert_path: cert_path.to_string_lossy().into_owned(), - key_path: key_path.to_string_lossy().into_owned(), - ca_cert_path: ca_path.to_string_lossy().into_owned(), - cert_validator: None, - }, - leaf_cert.der().to_vec(), - leaf_key.serialize_der(), - ) + app_id_server_cert(dir, TEST_APP_ID) } /// Serve one fixed response over TLS and return the URL to reach it. @@ -593,46 +567,32 @@ mod transport_tests { } /// A server certificate carrying an app id, signed by the same test CA. + /// + /// The client config that comes back validates against that same app id, which is + /// what the HTTP-level tests want. `a_peer_from_another_app_cannot_complete_the_handshake` + /// overrides `cert_validator` to make the two disagree. fn app_id_server_cert( dir: &std::path::Path, app_id: &[u8], ) -> (HttpsClientConfig, Vec, Vec) { - use ra_tls::cert::CertRequest; - use ra_tls::rcgen::{BasicConstraints, CertificateParams, IsCa, KeyPair}; - - let ca_key = KeyPair::generate().expect("ca key"); - let mut ca_params = CertificateParams::new(vec![]).expect("ca params"); - ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); - let ca_cert = ca_params.self_signed(&ca_key).expect("ca cert"); - - let leaf_key = KeyPair::generate().expect("leaf key"); - let alt_names = vec!["127.0.0.1".to_string()]; - let leaf_cert = CertRequest::builder() - .key(&leaf_key) - .subject("peer.test") - .alt_names(&alt_names) - .app_id(app_id) - .usage_server_auth(true) - .build() - .signed_by(&ca_cert, &ca_key) - .expect("leaf cert"); - - let cert_path = dir.join("node.crt"); - let key_path = dir.join("node.key"); - let ca_path = dir.join("ca.crt"); - std::fs::write(&cert_path, leaf_cert.pem()).expect("write cert"); - std::fs::write(&key_path, leaf_key.serialize_pem()).expect("write key"); - std::fs::write(&ca_path, ca_cert.pem()).expect("write ca"); + let pki = ra_tls::test_pki::write_mtls_pki( + dir, + ra_tls::test_pki::TestCert::new("peer.test") + .alt_name("127.0.0.1") + .app_id(app_id) + .server_auth(true), + ) + .expect("write test PKI"); ( HttpsClientConfig { - cert_path: cert_path.to_string_lossy().into_owned(), - key_path: key_path.to_string_lossy().into_owned(), - ca_cert_path: ca_path.to_string_lossy().into_owned(), - cert_validator: None, + cert_path: pki.cert_path.to_string_lossy().into_owned(), + key_path: pki.key_path.to_string_lossy().into_owned(), + ca_cert_path: pki.ca_cert_path.to_string_lossy().into_owned(), + cert_validator: Arc::new(AppIdValidator::new(app_id.to_vec())), }, - leaf_cert.der().to_vec(), - leaf_key.serialize_der(), + pki.leaf.cert_der(), + pki.leaf.key_der(), ) } @@ -652,7 +612,7 @@ mod transport_tests { { let dir = tempfile::tempdir().expect("tempdir"); let (mut config, cert, key) = app_id_server_cert(dir.path(), &server_app_id); - config.cert_validator = Some(Arc::new(AppIdValidator::new(ours.clone()))); + config.cert_validator = Arc::new(AppIdValidator::new(ours.clone())); let url = serve(StatusCode::OK, gzip(b"response"), cert, key).await; let got = HttpsClient::new(&config) diff --git a/dstack/gateway/src/main.rs b/dstack/gateway/src/main.rs index e7e549291..6d9a97cac 100644 --- a/dstack/gateway/src/main.rs +++ b/dstack/gateway/src/main.rs @@ -166,16 +166,15 @@ async fn main() -> Result<()> { set_max_ulimit()?; } - let my_app_id = if config.debug.insecure_skip_attestation { - None - } else { - let dstack_client = dstack_agent().context("Failed to create dstack client")?; - let info = dstack_client - .info() - .await - .context("Failed to get app info")?; - Some(info.app_id) - }; + // Required, not best-effort: `my_app_id` is what every peer check compares against, + // so a gateway that cannot learn its own identity must not start rather than start + // without one. A host with no guest agent fails here. + let dstack_client = dstack_agent().context("Failed to create dstack client")?; + let my_app_id = dstack_client + .info() + .await + .context("Failed to get app info")? + .app_id; let proxy_config = config.proxy.clone(); let attestation_verifier = Arc::new( AttestationVerifier::load(&config.attestation) diff --git a/dstack/gateway/src/main_service.rs b/dstack/gateway/src/main_service.rs index 678117b91..503bcd203 100644 --- a/dstack/gateway/src/main_service.rs +++ b/dstack/gateway/src/main_service.rs @@ -69,7 +69,7 @@ pub struct ProxyInner { pub(crate) config: Arc, /// Multi-domain certbot (from KvStore DNS credentials and domain configs) pub(crate) certbot: Arc, - my_app_id: Option>, + my_app_id: Vec, state: Mutex, pub(crate) notify_state_updated: Notify, auth_client: AuthClient, @@ -136,7 +136,12 @@ pub(crate) struct ProxyState { /// Options for creating a Proxy instance pub struct ProxyOptions { pub config: Config, - pub my_app_id: Option>, + /// This gateway's own app id, from the guest agent. + /// + /// Not optional: it is the only thing `authorize_peer` and `AppIdValidator` + /// compare a peer against, so a gateway that does not know it cannot decide + /// who belongs in the cluster. `main` fails to start rather than construct one. + pub my_app_id: Vec, /// TLS configuration (from Rocket's tls config) pub tls_config: TlsConfig, } @@ -372,9 +377,7 @@ impl ProxyInner { // Build HttpsClientConfig for mTLS communication let https_config = { let tls = &tls_config; - let cert_validator = my_app_id - .clone() - .map(|app_id| Arc::new(AppIdValidator::new(app_id)) as _); + let cert_validator = Arc::new(AppIdValidator::new(my_app_id.clone())); HttpsClientConfig { cert_path: tls.certs.clone(), key_path: tls.key.clone(), @@ -552,8 +555,8 @@ impl ProxyInner { &self.kv_store } - pub(crate) fn my_app_id(&self) -> Option<&[u8]> { - self.my_app_id.as_deref() + pub(crate) fn my_app_id(&self) -> &[u8] { + &self.my_app_id } } @@ -2668,13 +2671,10 @@ pub struct RpcHandler { impl RpcHandler { fn ensure_from_gateway(&self) -> Result<()> { - if self.state.config.debug.insecure_skip_attestation { - return Ok(()); - } if self.remote_app_id.is_none() { bail!("Client authentication is required"); } - if self.state.my_app_id != self.remote_app_id { + if Some(&self.state.my_app_id) != self.remote_app_id.as_ref() { bail!("Remote app id is not from dstack-gateway"); } Ok(()) diff --git a/dstack/gateway/src/main_service/tests.rs b/dstack/gateway/src/main_service/tests.rs index 8d60abcc7..8cf0dd52e 100644 --- a/dstack/gateway/src/main_service/tests.rs +++ b/dstack/gateway/src/main_service/tests.rs @@ -43,7 +43,7 @@ async fn create_test_state_with(tweak: impl FnOnce(&mut Config)) -> TestState { tweak(&mut config); let options = ProxyOptions { config, - my_app_id: None, + my_app_id: b"test-app-id".to_vec(), tls_config: TlsConfig { certs: "".to_string(), key: "".to_string(), diff --git a/dstack/gateway/src/web_routes/wavekv_sync.rs b/dstack/gateway/src/web_routes/wavekv_sync.rs index 7c7860884..362748da3 100644 --- a/dstack/gateway/src/web_routes/wavekv_sync.rs +++ b/dstack/gateway/src/web_routes/wavekv_sync.rs @@ -90,11 +90,6 @@ fn decode_envelope(compressed: &[u8]) -> Result { /// Verify that the request is from a gateway with the same app_id (mTLS verification) fn verify_gateway_peer(state: &Proxy, cert: Option>) -> Result<(), Status> { - // Skip verification if not running in dstack (test mode) - if state.config.debug.insecure_skip_attestation { - return Ok(()); - } - let Some(cert) = cert else { warn!("WaveKV sync: client certificate required but not provided"); return Err(Status::Unauthorized); @@ -105,9 +100,9 @@ fn verify_gateway_peer(state: &Proxy, cert: Option>) -> Result<( /// Decide whether a certificate's app identity is one we accept. /// -/// Split out from `verify_gateway_peer` because that function's other half — the -/// attestation bypass and Rocket's certificate guard — cannot be exercised from a test, -/// which left this decision, the actual authorization rule, uncovered. +/// Split out from `verify_gateway_peer` because that function's other half — Rocket's +/// certificate guard — cannot be exercised from a test, which left this decision, the +/// actual authorization rule, uncovered. /// /// The app-id extension is the only identity accepted. There used to be a fallback to /// the app-info extension here, and it existed because certificates issued through a @@ -120,7 +115,7 @@ fn verify_gateway_peer(state: &Proxy, cert: Option>) -> Result<( /// rule across all three is worth more than a second path that was untested, disagreed /// with its neighbours, and read an issuer claim the cluster does not otherwise /// authorize on. -fn authorize_peer(cert: &impl CertExt, my_app_id: Option<&[u8]>) -> Result<(), Status> { +fn authorize_peer(cert: &impl CertExt, my_app_id: &[u8]) -> Result<(), Status> { let remote_app_id = cert.get_app_id().map_err(|e| { warn!("WaveKV sync: failed to extract app_id from certificate: {e}"); Status::Unauthorized @@ -131,7 +126,7 @@ fn authorize_peer(cert: &impl CertExt, my_app_id: Option<&[u8]>) -> Result<(), S return Err(Status::Unauthorized); }; - if my_app_id != Some(remote_app_id.as_slice()) { + if my_app_id != remote_app_id.as_slice() { warn!("WaveKV sync: app_id mismatch, expected {my_app_id:?}, got {remote_app_id:?}"); return Err(Status::Forbidden); } @@ -268,9 +263,9 @@ mod tests { /// from disk to build its rustls client config, and the root store only accepts a /// trust anchor with `CA:TRUE` — so a lone self-signed leaf is not enough. /// - /// The leaf carries an app_id because no test here turns the peer check off, so a - /// certificate without one is refused before any of them reach what they are about. - /// It is also what a real peer's certificate carries. + /// The leaf carries an app_id because the peer check is unconditional, so a + /// certificate without one is refused before any of these tests reach what they are + /// about. It is also what a real peer's certificate carries. fn write_tls_material(dir: &std::path::Path) -> TlsConfig { let pki = ra_tls::test_pki::write_mtls_pki( dir, @@ -291,9 +286,9 @@ mod tests { /// /// They are about everything below the peer check — the gzip framing, the store /// split, the uuid check, the removed-sender refusal — and they call `handle_sync` - /// and `handle_push` rather than going through the routes, because the peer check runs - /// on every request these tests make and Rocket's local client speaks no TLS, so a - /// request through the route can never get past it. `enforcing_gateway` covers the check itself. + /// and `handle_push` rather than going through the routes, because the peer check is + /// now unconditional and Rocket's local client speaks no TLS, so a request through + /// the route can never get past it. `enforcing_gateway` covers the check itself. async fn serving_gateway(sync_enabled: bool) -> (Proxy, TempDir) { let (_client, proxy, tmp) = serving_gateway_with(sync_enabled).await; (proxy, tmp) @@ -343,7 +338,7 @@ mod tests { let tls_config = write_tls_material(temp_dir.path()); let proxy = Proxy::new(ProxyOptions { config, - my_app_id: Some(TEST_APP_ID.to_vec()), + my_app_id: TEST_APP_ID.to_vec(), tls_config, }) .await @@ -358,9 +353,8 @@ mod tests { /// The sync routes are the cluster's write surface: anything that reaches them can /// insert entries that replicate to every gateway. `verify_gateway_peer` is the only - /// thing standing in front of them, and no test had ever executed it: every test - /// here reached the body below by turning the check off. Replacing the whole - /// function body with `Ok(())` did not turn the suite red. + /// thing standing in front of them, and no test had ever executed it. Replacing the + /// whole function body with `Ok(())` did not turn the suite red. /// /// Rocket's local client speaks no TLS and so presents no certificate, which is /// exactly the case that must be refused. @@ -397,7 +391,7 @@ mod tests { .expect("self-signed cert") } - fn authorize(der: &[u8], my_app_id: Option<&[u8]>) -> Result<(), Status> { + fn authorize(der: &[u8], my_app_id: &[u8]) -> Result<(), Status> { use rocket::mtls::x509::{FromDer, X509Certificate}; let (_, parsed) = X509Certificate::from_der(der).expect("parse cert"); authorize_peer(&RocketCert(parsed.extensions()), my_app_id) @@ -412,10 +406,10 @@ mod tests { fn a_peer_is_authorized_only_when_its_app_id_matches_ours() { let ours = b"app-id-of-this-cluster".to_vec(); - assert_eq!(authorize(&cert_with_app_id(&ours), Some(&ours)), Ok(())); + assert_eq!(authorize(&cert_with_app_id(&ours), &ours), Ok(())); assert_eq!( - authorize(&cert_with_app_id(b"a-different-app"), Some(&ours)), + authorize(&cert_with_app_id(b"a-different-app"), &ours), Err(Status::Forbidden), "a valid certificate from another app must not reach the sync routes" ); @@ -441,7 +435,7 @@ mod tests { .expect("self-signed cert"); assert_eq!( - authorize(&cert, Some(&ours)), + authorize(&cert, &ours), Err(Status::Unauthorized), "the app-id extension is the only identity the sync routes accept" ); @@ -452,21 +446,11 @@ mod tests { #[test] fn a_certificate_without_an_app_id_is_refused() { assert_eq!( - authorize(&cert_without_app_id(), Some(b"app-id-of-this-cluster")), + authorize(&cert_without_app_id(), b"app-id-of-this-cluster"), Err(Status::Unauthorized) ); } - /// A gateway that does not know its own app id cannot authorize anyone. Comparing - /// `None` against a present remote id must reject, never match. - #[test] - fn a_gateway_without_an_app_id_authorizes_nobody() { - assert_eq!( - authorize(&cert_with_app_id(b"anything"), None), - Err(Status::Forbidden) - ); - } - /// The adapter must match the app-id extension by OID and no other. Returning some /// other extension's bytes would hand `authorize_peer` a value it would happily /// compare. @@ -621,7 +605,9 @@ mod tests { cert_path: tls.certs.clone(), key_path: tls.key.clone(), ca_cert_path: tls.mutual.ca_certs.clone(), - cert_validator: None, + cert_validator: std::sync::Arc::new(crate::kv::AppIdValidator::new( + TEST_APP_ID.to_vec(), + )), }) .expect("HTTPS client"); let base = format!("https://127.0.0.1:{port}"); diff --git a/dstack/gateway/test-run/TESTING.md b/dstack/gateway/test-run/TESTING.md index 212870b67..945d13ec5 100644 --- a/dstack/gateway/test-run/TESTING.md +++ b/dstack/gateway/test-run/TESTING.md @@ -19,7 +19,7 @@ down. ## Attestation is on -No suite turns the checks off. The gateways obtain their +There is no switch to turn the checks off. The gateways obtain their RPC certificates from the guest agent simulator, which signs quotes under trust anchors derived from `attestation/tee-simulator.json`; the mock collateral service reconstructs the matching public roots from the same seed, and the diff --git a/dstack/gateway/test-run/e2e/configs/gateway-1.toml b/dstack/gateway/test-run/e2e/configs/gateway-1.toml index dec4e23ce..fa7079a2c 100644 --- a/dstack/gateway/test-run/e2e/configs/gateway-1.toml +++ b/dstack/gateway/test-run/e2e/configs/gateway-1.toml @@ -24,7 +24,6 @@ auth_token = "e2e-admin-token" [core.debug] # TEST ONLY - do not use in production; enables debug RPC insecure_enable_debug_rpc = true -insecure_skip_attestation = false port = 9015 address = "0.0.0.0" @@ -32,7 +31,7 @@ address = "0.0.0.0" # derives from the simulator's seed. Verification itself runs the normal # production path -- this only says the anchor may come from outside the vendor # set, which is what makes attestation testable off TDX hardware. It does not -# switch any check off. +# switch any check off; there is no longer a switch that does. [core.attestation] insecure_allow_external_trust_anchors = true diff --git a/dstack/gateway/test-run/e2e/configs/gateway-2.toml b/dstack/gateway/test-run/e2e/configs/gateway-2.toml index 975e0ab2e..0beae8420 100644 --- a/dstack/gateway/test-run/e2e/configs/gateway-2.toml +++ b/dstack/gateway/test-run/e2e/configs/gateway-2.toml @@ -24,7 +24,6 @@ auth_token = "e2e-admin-token" [core.debug] # TEST ONLY - do not use in production; enables debug RPC insecure_enable_debug_rpc = true -insecure_skip_attestation = false port = 9015 address = "0.0.0.0" @@ -32,7 +31,7 @@ address = "0.0.0.0" # derives from the simulator's seed. Verification itself runs the normal # production path -- this only says the anchor may come from outside the vendor # set, which is what makes attestation testable off TDX hardware. It does not -# switch any check off. +# switch any check off; there is no longer a switch that does. [core.attestation] insecure_allow_external_trust_anchors = true diff --git a/dstack/gateway/test-run/e2e/configs/gateway-3.toml b/dstack/gateway/test-run/e2e/configs/gateway-3.toml index a29b1de90..a590cad68 100644 --- a/dstack/gateway/test-run/e2e/configs/gateway-3.toml +++ b/dstack/gateway/test-run/e2e/configs/gateway-3.toml @@ -24,7 +24,6 @@ auth_token = "e2e-admin-token" [core.debug] # TEST ONLY - do not use in production; enables debug RPC insecure_enable_debug_rpc = true -insecure_skip_attestation = false port = 9015 address = "0.0.0.0" @@ -32,7 +31,7 @@ address = "0.0.0.0" # derives from the simulator's seed. Verification itself runs the normal # production path -- this only says the anchor may come from outside the vendor # set, which is what makes attestation testable off TDX hardware. It does not -# switch any check off. +# switch any check off; there is no longer a switch that does. [core.attestation] insecure_allow_external_trust_anchors = true diff --git a/test-suites/full-stack-compose/scripts/runner.sh b/test-suites/full-stack-compose/scripts/runner.sh index 635703a76..3160c5762 100755 --- a/test-suites/full-stack-compose/scripts/runner.sh +++ b/test-suites/full-stack-compose/scripts/runner.sh @@ -820,7 +820,7 @@ stop_network_probe() { assert_no_insecure_shortcuts() { log "auditing rendered manifests for production trust settings" local manifest - local forbidden='quote_enabled[[:space:]]*=[[:space:]]*false|attest_rpc_cert[[:space:]]*=[[:space:]]*false|insecure_skip_attestation[[:space:]]*=[[:space:]]*true|insecure_allow_external_trust_anchors[[:space:]]*=[[:space:]]*true|enforce_self_authorization[[:space:]]*=[[:space:]]*false|verify[[:space:]]*=[[:space:]]*false' + local forbidden='quote_enabled[[:space:]]*=[[:space:]]*false|attest_rpc_cert[[:space:]]*=[[:space:]]*false|insecure_allow_external_trust_anchors[[:space:]]*=[[:space:]]*true|enforce_self_authorization[[:space:]]*=[[:space:]]*false|verify[[:space:]]*=[[:space:]]*false' if grep -R -E "$forbidden" "$WORK_DIR"/*.app-compose.json; then die "rendered app manifest contains a forbidden development trust setting" fi