diff --git a/.github/buildomat/jobs/deploy.sh b/.github/buildomat/jobs/deploy.sh index ce200422bcc..f564b790027 100644 --- a/.github/buildomat/jobs/deploy.sh +++ b/.github/buildomat/jobs/deploy.sh @@ -132,6 +132,55 @@ done ptime -m pfexec ./tools/create_virtual_hardware.sh +# +# Generate a self-signed certificate to use as the initial TLS certificate for +# the recovery Silo. Its DNS name is determined by the silo name and the +# delegated external DNS name, both of which are in the RSS config file. In a +# real system, the certificate would come from the customer during initial rack +# setup on the technician port. +# +tar xf out/omicron-sled-agent.tar pkg/config-rss.toml +SILO_NAME="$(sed -n 's/silo_name = "\(.*\)"/\1/p' pkg/config-rss.toml)" +EXTERNAL_DNS_DOMAIN="$(sed -n 's/external_dns_zone_name = "\(.*\)"/\1/p' pkg/config-rss.toml)" +rm -f pkg/config-rss.toml + +# +# By default, OpenSSL creates self-signed certificates with "CA:true". The TLS +# implementation used by reqwest rejects endpoint certificates that are also CA +# certificates. So in order to use the certificate, we need one without +# "CA:true". There doesn't seem to be a way to do this on the command line. +# Instead, we must override the system configuration with our own configuration +# file. There's virtually nothing in it. +# +TLS_NAME="$SILO_NAME.sys.$EXTERNAL_DNS_DOMAIN" +openssl req \ + -newkey rsa:4096 \ + -x509 \ + -sha256 \ + -days 3 \ + -nodes \ + -out "pkg/initial-tls-cert.pem" \ + -keyout "pkg/initial-tls-key.pem" \ + -subj "/CN=$TLS_NAME" \ + -addext "subjectAltName=DNS:$TLS_NAME" \ + -addext "basicConstraints=critical,CA:FALSE" \ + -config /dev/stdin < Result { - build_authenticated_client().await -} - fn rss_config() -> Result { toml::from_str(RSS_CONFIG_STR) .with_context(|| format!("parsing {:?} as TOML", RSS_CONFIG_PATH)) } -pub async fn nexus_addr() -> Result { +fn nexus_external_dns_name(config: &SetupServiceConfig) -> String { + format!( + "{}.sys.{}", + config.recovery_silo.silo_name.as_str(), + config.external_dns_zone_name + ) +} + +fn external_dns_addr(config: &SetupServiceConfig) -> Result { + // From the RSS config, grab the first address from the configured services + // IP pool as the DNS server's IP address. + let dns_ip = config + .internal_services_ip_pool_ranges + .iter() + .flat_map(|range| range.iter()) + .next() + .ok_or_else(|| { + anyhow!( + "failed to get first IP from internal service \ + pool in RSS configuration" + ) + })?; + Ok(SocketAddr::from((dns_ip, 53))) +} + +pub async fn nexus_addr() -> Result { // Check $OXIDE_HOST first. if let Ok(host) = std::env::var("OXIDE_HOST").map_err(anyhow::Error::from).and_then(|s| { Ok(Url::parse(&s)? .host_str() .context("no host in OXIDE_HOST url")? - .parse()?) + .parse::()? + .ip()) }) { return Ok(host); @@ -92,87 +121,70 @@ pub async fn nexus_addr() -> Result { // Otherwise, use the RSS configuration to find the DNS server, silo name, // and delegated DNS zone name. Use this to look up Nexus's IP in the - // external DNS server. - // - // First, load the RSS configuration file. + // external DNS server. This could take a few seconds, since it's + // asynchronous with the rack initialization request. let config = rss_config()?; + let dns_addr = external_dns_addr(&config)?; + let dns_name = nexus_external_dns_name(&config); + let resolver = CustomDnsResolver::new(dns_addr)?; + resolver + .wait_for_records( + &dns_name, + Duration::from_secs(1), + Duration::from_secs(300), + ) + .await +} - // From config-rss.toml, grab the first address from the configured services - // IP pool as the DNS server's IP address. - let dns_ip = config - .internal_services_ip_pool_ranges - .iter() - .flat_map(|range| range.iter()) - .next() - .ok_or_else(|| { - anyhow!( - "failed to get first IP from internal service \ - pool in {}", - RSS_CONFIG_PATH, - ) - })?; - let dns_addr = SocketAddr::from((dns_ip, 53)); - - // Resolve the DNS name of the recovery Silo that ought to have been created - // already. This could take a few seconds, since it's asynchronous with the - // rack initialization request. - let silo_name = &config.recovery_silo.silo_name; - let dns_name = format!( - "{}.sys.{}", - silo_name.as_str(), - &config.external_dns_zone_name - ); +pub async fn build_client() -> Result { + // Make a reqwest client that we can use to make the initial login request. + // To do this, we need to find the IP of the external DNS server in the RSS + // configuration and then set up a custom resolver to use this DNS server. + let config = rss_config()?; + let dns_addr = external_dns_addr(&config)?; + let dns_name = nexus_external_dns_name(&config); + let resolver = Arc::new(CustomDnsResolver::new(dns_addr)?); - let mut resolver_config = ResolverConfig::new(); - resolver_config.add_name_server(NameServerConfig { - socket_addr: dns_addr, - protocol: Protocol::Udp, - tls_dns_name: None, - trust_nx_responses: false, - bind_addr: None, - }); + // Do not have reqwest follow redirects. That's because our login response + // includes both a redirect and the session cookie header. If reqwest + // follows the redirect, we won't have a chance to get the cookie. + let mut builder = reqwest::ClientBuilder::new() + .connect_timeout(Duration::from_secs(15)) + .redirect(reqwest::redirect::Policy::none()) + .dns_resolver(resolver.clone()) + .timeout(Duration::from_secs(60)); - let resolver = - TokioAsyncResolver::tokio(resolver_config, ResolverOpts::default()) - .context("failed to create resolver")?; + // If we were provided with a path to a certificate in the environment, add + // it as a trusted one. + let (proto, extra_root_cert) = match std::env::var(E2E_TLS_CERT_ENV) { + Err(_) => ("http", None), + Ok(path) => { + let cert_bytes = std::fs::read(&path).with_context(|| { + format!("reading certificate from {:?}", &path) + })?; + let cert = reqwest::tls::Certificate::from_pem(&cert_bytes) + .with_context(|| { + format!("parsing certificate from {:?}", &path) + })?; + ("https", Some(cert)) + } + }; - wait_for_condition::<_, anyhow::Error, _, _>( - || async { - let addr = resolver - .lookup_ip(&dns_name) - .await - .map_err(|e| match e.kind() { - ResolveErrorKind::NoRecordsFound { .. } - | ResolveErrorKind::Timeout => CondCheckError::NotYet, - _ => CondCheckError::Failed(anyhow::Error::new(e).context( - format!("resolving {:?} from {}", dns_name, dns_addr), - )), - })? - .iter() - .next() - .ok_or(CondCheckError::NotYet)?; - Ok(SocketAddr::from((addr, 80))) - }, - &Duration::from_secs(1), - &Duration::from_secs(300), - ) - .await - .context("failed to get Nexus addr") -} + if let Some(cert) = &extra_root_cert { + builder = builder.add_root_certificate(cert.clone()); + } -async fn get_base_url() -> Result { - Ok(format!("http://{}", nexus_addr().await?)) -} + let reqwest_login_client = builder.build()?; -async fn build_authenticated_client() -> Result { - let config = rss_config()?; - let base_url = get_base_url().await?; + // Prepare to make a login request. + let base_url = format!("{}://{}", proto, dns_name); let silo_name = config.recovery_silo.silo_name.as_str(); + let login_url = format!("{}/login/{}/local", base_url, silo_name); let username: oxide_client::types::UserId = config.recovery_silo.user_name.as_str().parse().map_err(|s| { anyhow!("parsing configured recovery user name: {:?}", s) })?; - // See the comment in the config file. + // See the comment in the config file about this password. let password: oxide_client::types::Password = "oxide".parse().unwrap(); let login_request_body = serde_json::to_string(&UsernamePasswordCredentials { @@ -181,35 +193,23 @@ async fn build_authenticated_client() -> Result { }) .context("serializing login request body")?; - // Do not have reqwest follow redirects. That's because our login response - // includes both a redirect and the session cookie header. If reqwest - // follows the redirect, we won't have a chance to get the cookie. - let reqwest_login_client = reqwest::ClientBuilder::new() - .connect_timeout(Duration::from_secs(15)) - .redirect(reqwest::redirect::Policy::none()) - .timeout(Duration::from_secs(60)) - .build()?; - let login_url = format!("{}/login/{}/local", base_url, silo_name); - - // By the time we get here, we generally would have successfully resolved - // Nexus's external IP address from the external DNS server. So we'd - // expect Nexus to be up. But that's not necessarily true: external DNS can - // be set up during rack initialization, before Nexus has opened its - // external listening socket. This is arguably a bug, advertising a service - // before it's ready, but a pretty niche corner case (rack initialization) - // and anyway DNS is always best-effort. The point is: let's retry a little - // while if we can't immediately connect. + // By the time we get here, Nexus might not be up yet. It may not have + // published its names to external DNS, and even if it has, it may not have + // opened its external listening socket. So we have to retry a bit until we + // succeed. let response = wait_for_condition( || async { // Use a raw reqwest client because it's not clear that Progenitor // is intended to support endpoints that return 300-level response // codes. See progenitor#451. + eprintln!("{}: attempting to log into API", Utc::now()); reqwest_login_client .post(&login_url) .body(login_request_body.clone()) .send() .await .map_err(|e| { + eprintln!("{}: login failed: {:#}", Utc::now(), e); if e.is_connect() { CondCheckError::NotYet } else { @@ -220,11 +220,12 @@ async fn build_authenticated_client() -> Result { }) }, &Duration::from_secs(1), - &Duration::from_secs(30), + &Duration::from_secs(300), ) .await .context("logging in")?; + eprintln!("{}: login succeeded", Utc::now()); let session_cookie = response .headers() .get(http::header::SET_COOKIE) @@ -241,10 +242,100 @@ async fn build_authenticated_client() -> Result { HeaderValue::from_str(session_token).unwrap(), ); - let reqwest_client = reqwest::ClientBuilder::new() + let mut builder = reqwest::ClientBuilder::new() .default_headers(headers) .connect_timeout(Duration::from_secs(15)) - .timeout(Duration::from_secs(60)) - .build()?; + .dns_resolver(resolver) + .timeout(Duration::from_secs(60)); + + if let Some(cert) = extra_root_cert { + builder = builder.add_root_certificate(cert); + } + + let reqwest_client = builder.build()?; Ok(Client::new_with_client(&base_url, reqwest_client)) } + +/// Wrapper around a `TokioAsyncResolver` so that we can impl +/// `reqwest::dns::Resolve` for it. +struct CustomDnsResolver { + dns_addr: SocketAddr, + // The lifetime constraints on the `Resolve` trait make it hard to avoid an + // Arc here. + resolver: Arc, +} + +impl CustomDnsResolver { + fn new(dns_addr: SocketAddr) -> Result { + let mut resolver_config = ResolverConfig::new(); + resolver_config.add_name_server(NameServerConfig { + socket_addr: dns_addr, + protocol: Protocol::Udp, + tls_dns_name: None, + trust_nx_responses: false, + bind_addr: None, + }); + + let resolver = Arc::new( + TokioAsyncResolver::tokio(resolver_config, ResolverOpts::default()) + .context("failed to create resolver")?, + ); + Ok(CustomDnsResolver { dns_addr, resolver }) + } + + async fn wait_for_records( + &self, + dns_name: &str, + check_period: Duration, + max: Duration, + ) -> Result { + wait_for_condition::<_, anyhow::Error, _, _>( + || async { + self.resolver + .lookup_ip(dns_name) + .await + .map_err(|e| match e.kind() { + ResolveErrorKind::NoRecordsFound { .. } + | ResolveErrorKind::Timeout => CondCheckError::NotYet, + _ => CondCheckError::Failed( + anyhow::Error::new(e).context(format!( + "resolving {:?} from {}", + dns_name, self.dns_addr + )), + ), + })? + .iter() + .next() + .ok_or(CondCheckError::NotYet) + }, + &check_period, + &max, + ) + .await + .with_context(|| { + format!( + "failed to resolve {:?} from {:?} within {:?}", + dns_name, self.dns_addr, max + ) + }) + } +} + +impl reqwest::dns::Resolve for CustomDnsResolver { + fn resolve( + &self, + name: hyper::client::connect::dns::Name, + ) -> reqwest::dns::Resolving { + let resolver = self.resolver.clone(); + async move { + let list = resolver.lookup_ip(name.as_str()).await?; + Ok(Box::new(list.into_iter().map(|s| { + // reqwest does not appear to use the port number here. + // (See the docs for `ClientBuilder::resolve()`, which isn't + // the same thing, but is related.) + SocketAddr::from((s, 0)) + })) as Box + Send>) + } + .boxed() + } +} diff --git a/end-to-end-tests/src/helpers/mod.rs b/end-to-end-tests/src/helpers/mod.rs index 5030fe58975..2f514cdb129 100644 --- a/end-to-end-tests/src/helpers/mod.rs +++ b/end-to-end-tests/src/helpers/mod.rs @@ -18,7 +18,7 @@ pub fn generate_name(prefix: &str) -> Result { /// the DHCP range is 100-249, and in the buildomat lab environment the network /// is currently private.) pub async fn get_system_ip_pool() -> Result<(Ipv4Addr, Ipv4Addr)> { - let nexus_addr = match nexus_addr().await?.ip() { + let nexus_addr = match nexus_addr().await? { IpAddr::V4(addr) => addr.octets(), IpAddr::V6(_) => bail!("not sure what to do about IPv6 here"), }; diff --git a/openapi/bootstrap-agent.json b/openapi/bootstrap-agent.json index cf556564ed6..cd8845023a8 100644 --- a/openapi/bootstrap-agent.json +++ b/openapi/bootstrap-agent.json @@ -156,6 +156,31 @@ } ] }, + "Certificate": { + "type": "object", + "properties": { + "cert": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "key": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "required": [ + "cert", + "key" + ] + }, "Component": { "type": "object", "properties": { @@ -274,6 +299,13 @@ "type": "string" } }, + "external_certificates": { + "description": "initial TLS certificates for the external API", + "type": "array", + "items": { + "$ref": "#/components/schemas/Certificate" + } + }, "external_dns_zone_name": { "description": "DNS name for the DNS zone delegated to the rack for external DNS", "type": "string" @@ -314,6 +346,7 @@ "required": [ "bootstrap_discovery", "dns_servers", + "external_certificates", "external_dns_zone_name", "internal_services_ip_pool_ranges", "ntp_servers", diff --git a/sled-agent/Cargo.toml b/sled-agent/Cargo.toml index 15180aeeeb5..753fc981003 100644 --- a/sled-agent/Cargo.toml +++ b/sled-agent/Cargo.toml @@ -80,6 +80,7 @@ omicron-test-utils.workspace = true openapi-lint.workspace = true openapiv3.workspace = true pretty_assertions.workspace = true +rcgen.workspace = true serial_test.workspace = true subprocess.workspace = true slog-async.workspace = true diff --git a/sled-agent/src/bootstrap/params.rs b/sled-agent/src/bootstrap/params.rs index 56507af589d..1afca8dfe70 100644 --- a/sled-agent/src/bootstrap/params.rs +++ b/sled-agent/src/bootstrap/params.rs @@ -55,10 +55,14 @@ pub struct RackInitializeRequest { /// DNS name for the DNS zone delegated to the rack for external DNS pub external_dns_zone_name: String, + /// initial TLS certificates for the external API + pub external_certificates: Vec, + /// Configuration of the Recovery Silo (the initial Silo) pub recovery_silo: RecoverySiloConfig, } +pub type Certificate = nexus_client::types::Certificate; pub type RecoverySiloConfig = nexus_client::types::RecoverySiloConfig; /// Configuration information for launching a Sled Agent. diff --git a/sled-agent/src/config.rs b/sled-agent/src/config.rs index e98f9f9a71e..e9a4d9848b6 100644 --- a/sled-agent/src/config.rs +++ b/sled-agent/src/config.rs @@ -79,6 +79,8 @@ pub enum ConfigError { #[source] err: toml::de::Error, }, + #[error("Loading certificate: {0}")] + Certificate(#[source] anyhow::Error), #[error("Could not determine if host is a Gimlet: {0}")] SystemDetection(#[source] anyhow::Error), #[error("Could not enumerate physical links")] diff --git a/sled-agent/src/rack_setup/config.rs b/sled-agent/src/rack_setup/config.rs index 8a1873afbb8..4816bb5f2cd 100644 --- a/sled-agent/src/rack_setup/config.rs +++ b/sled-agent/src/rack_setup/config.rs @@ -10,6 +10,7 @@ use omicron_common::address::{ get_64_subnet, Ipv6Subnet, AZ_PREFIX, RACK_PREFIX, SLED_PREFIX, }; +use crate::bootstrap::params::Certificate; pub use crate::bootstrap::params::RackInitializeRequest as SetupServiceConfig; impl SetupServiceConfig { @@ -17,8 +18,55 @@ impl SetupServiceConfig { let path = path.as_ref(); let contents = std::fs::read_to_string(&path) .map_err(|err| ConfigError::Io { path: path.into(), err })?; - toml::from_str(&contents) - .map_err(|err| ConfigError::Parse { path: path.into(), err }) + let mut raw_config: SetupServiceConfig = toml::from_str(&contents) + .map_err(|err| ConfigError::Parse { path: path.into(), err })?; + + // In the same way that sled-agent itself (our caller) discovers the + // optional config-rss.toml in a well-known path relative to its config + // file, we look for a pair of well-known paths adjacent to + // config-rss.toml that specify an extra TLS certificate and private + // key. This is used by the end-to-end tests. Any developer can also + // use this to inject a TLS certificate into their setup. + // (config-rss.toml is only used for dev/test, not production + // deployments, which will always get their RSS configuration from + // Wicket.) + if let Some(parent) = path.parent() { + let cert_path = parent.join("initial-tls-cert.pem"); + let key_path = parent.join("initial-tls-key.pem"); + let cert_bytes = std::fs::read(&cert_path); + let key_bytes = std::fs::read(&key_path); + match (cert_bytes, key_bytes) { + (Ok(cert), Ok(key)) => { + raw_config + .external_certificates + .push(Certificate { key, cert }); + } + (Err(cert_error), Err(key_error)) + if cert_error.kind() == std::io::ErrorKind::NotFound + && key_error.kind() == std::io::ErrorKind::NotFound => + { + // Fine. No extra cert was provided. + } + (Err(cert_error), _) => { + return Err(ConfigError::Certificate( + anyhow::Error::new(cert_error).context(format!( + "loading certificate from {:?}", + cert_path + )), + )); + } + (_, Err(key_error)) => { + return Err(ConfigError::Certificate( + anyhow::Error::new(key_error).context(format!( + "loading private key from {:?}", + key_path + )), + )); + } + }; + } + + Ok(raw_config) } pub fn az_subnet(&self) -> Ipv6Subnet { @@ -41,6 +89,8 @@ mod test { use super::*; use crate::bootstrap::params::BootstrapAddressDiscovery; use crate::bootstrap::params::RecoverySiloConfig; + use anyhow::Context; + use camino::Utf8PathBuf; use omicron_common::address::IpRange; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -56,6 +106,7 @@ mod test { internal_services_ip_pool_ranges: vec![IpRange::from(IpAddr::V4( Ipv4Addr::new(129, 168, 1, 20), ))], + external_certificates: vec![], recovery_silo: RecoverySiloConfig { silo_name: "test-silo".parse().unwrap(), user_name: "dummy".parse().unwrap(), @@ -110,4 +161,69 @@ mod test { cfg.sled_subnet(255) ); } + + #[test] + fn test_extra_certs() { + // The stock non-Gimlet config has no TLS certificates. + let path = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../smf/sled-agent/non-gimlet/config-rss.toml"); + let cfg = SetupServiceConfig::from_file(&path) + .unwrap_or_else(|e| panic!("failed to parse {:?}: {}", &path, e)); + assert!(cfg.external_certificates.is_empty()); + + // Now let's create a configuration that does have an adjacent + // certificate and key. + let tempdir = + camino_tempfile::tempdir().expect("creating temporary directory"); + println!("using temp path: {:?}", tempdir); + + // Generate the certificate. + let domain = format!( + "{}.sys.{}", + cfg.external_dns_zone_name, + cfg.recovery_silo.silo_name.as_str(), + ); + let cert = rcgen::generate_simple_self_signed(vec![domain.clone()]) + .unwrap_or_else(|error| { + panic!( + "generating certificate for domain {:?}: {}", + domain, error + ) + }); + + // Write the configuration file. + let cfg_path = tempdir.path().join("config-rss.toml"); + let _ = std::fs::copy(&path, &cfg_path) + .with_context(|| { + format!("failed to copy file {:?} to {:?}", &path, &cfg_path) + }) + .unwrap(); + + // Write the certificate. + let cert_bytes = cert + .serialize_pem() + .expect("serializing generated certificate") + .into_bytes(); + let cert_path = tempdir.path().join("initial-tls-cert.pem"); + std::fs::write(&cert_path, &cert_bytes) + .with_context(|| format!("failed to write to {:?}", &cert_path)) + .unwrap(); + + // Write the private key. + let key_path = tempdir.path().join("initial-tls-key.pem"); + let key_bytes = cert.serialize_private_key_pem().into_bytes(); + std::fs::write(&key_path, &key_bytes) + .with_context(|| format!("failed to write to {:?}", &key_path)) + .unwrap(); + + // Now try to load it all. + let read_cfg = SetupServiceConfig::from_file(&cfg_path) + .expect("failed to read generated config with certificate"); + assert_eq!(read_cfg.external_certificates.len(), 1); + let cert = read_cfg.external_certificates.iter().next().unwrap(); + let key_pem = std::str::from_utf8(&cert.key) + .expect("generated PEM was not UTF-8"); + let _ = rcgen::KeyPair::from_pem(&key_pem) + .expect("generated PEM did not parse as KeyPair"); + } } diff --git a/sled-agent/src/rack_setup/service.rs b/sled-agent/src/rack_setup/service.rs index 1ed3d6176c1..d8b4edbf34e 100644 --- a/sled-agent/src/rack_setup/service.rs +++ b/sled-agent/src/rack_setup/service.rs @@ -729,13 +729,7 @@ impl ServiceInner { services, datasets, internal_services_ip_pool_ranges, - // TODO(https://github.com/oxidecomputer/omicron/issues/1959): Plumb - // these paths through RSS's API. - // - // These certificates CAN be updated through Nexus' HTTP API, but - // should be bootstrapped during the rack setup process to avoid - // the need for unencrypted communication. - certs: vec![], + certs: config.external_certificates.clone(), internal_dns_zone_config: d2n_params(&service_plan.dns_config), external_dns_zone_name: config.external_dns_zone_name.clone(), recovery_silo: config.recovery_silo.clone(), diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index d3a864b3906..ddfdf2e2f92 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -1316,7 +1316,11 @@ impl ServiceManager { IpAddr::V6(*internal_ip), NEXUS_INTERNAL_PORT, ), - request_body_max_bytes: 1048576, + // This has to be large enough to support, among + // other things, the initial list of TLS + // certificates provided by the customer during rack + // setup. + request_body_max_bytes: 10 * 1024 * 1024, ..Default::default() }, subnet: Ipv6Subnet::::new( diff --git a/smf/sled-agent/gimlet-standalone/config-rss.toml b/smf/sled-agent/gimlet-standalone/config-rss.toml index facea8837b8..62ca9683283 100644 --- a/smf/sled-agent/gimlet-standalone/config-rss.toml +++ b/smf/sled-agent/gimlet-standalone/config-rss.toml @@ -1,4 +1,8 @@ # RSS (Rack Setup Service) "stand-in" configuration. +# +# This file conforms to the schema for "RackInitializeRequest" in the Bootstrap +# Agent API. See the `RackInitializeRequest` type in bootstrap-agent or its +# OpenAPI spec (in openapi/bootstrap-agent.json in the root of this workspace). # The /56 subnet for the rack. # Also implies the /48 AZ subnet. @@ -20,6 +24,15 @@ dns_servers = [ "1.1.1.1", "9.9.9.9" ] # Delegated external DNS zone name external_dns_zone_name = "oxide.test" +# Initial TLS certificates for the external API +# +# For the structure of these certificates, see `Certificate` in the Nexus +# internal API. In practice, it can be unwieldy to put them here. You can also +# specify a certificate by including the certificate chain and private key in +# PEM-format files called "initial-tls-cert.pem" and "initial-tls-key.pem", +# respectively, in the same place as this configuration file. +external_certificates = [] + # The IP ranges configured as part of the services IP Pool. # e.g., Nexus will be configured to use an address from this # pool as its external IP. diff --git a/smf/sled-agent/non-gimlet/config-rss.toml b/smf/sled-agent/non-gimlet/config-rss.toml index b81663b4371..091293d677b 100644 --- a/smf/sled-agent/non-gimlet/config-rss.toml +++ b/smf/sled-agent/non-gimlet/config-rss.toml @@ -1,4 +1,8 @@ # RSS (Rack Setup Service) "stand-in" configuration. +# +# This file conforms to the schema for "RackInitializeRequest" in the Bootstrap +# Agent API. See the `RackInitializeRequest` type in bootstrap-agent or its +# OpenAPI spec (in openapi/bootstrap-agent.json in the root of this workspace). # The /56 subnet for the rack. # Also implies the /48 AZ subnet. @@ -20,6 +24,15 @@ dns_servers = [ "1.1.1.1", "9.9.9.9" ] # Delegated external DNS zone name external_dns_zone_name = "oxide.test" +# Initial TLS certificates for the external API +# +# For the structure of these certificates, see `Certificate` in the Nexus +# internal API. In practice, it can be unwieldy to put them here. You can also +# specify a certificate by including the certificate chain and private key in +# PEM-format files called "initial-tls-cert.pem" and "initial-tls-key.pem", +# respectively, in the same place as this configuration file. +external_certificates = [] + # The IP ranges configured as part of the services IP Pool. # e.g., Nexus will be configured to use an address from this # pool as its external IP.