Skip to content

Commit

Permalink
[nexus] Allow silo admins to upload new certs (#4669)
Browse files Browse the repository at this point in the history
The additional cert validation added in #4100 broke the ability for silo
admins to upload new certs, because it introduced a call to fetch the
rack DNS configuration (in order to assemble the FQDNs for the silo to
check that the cert is valid for them). This PR fixes that by using an
elevated internal privilege for that DNS config lookup.

Fixes #4532.
  • Loading branch information
jgallagher authored Dec 12, 2023
1 parent 76a35f2 commit 343835c
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 2 deletions.
18 changes: 16 additions & 2 deletions nexus/src/app/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,22 @@ impl super::Nexus {
.silo_required()
.internal_context("creating a Certificate")?;

let silo_fq_dns_names =
self.silo_fq_dns_names(opctx, authz_silo.id()).await?;
// The `opctx` we received is going to be checked for permission to
// create a cert below in `db_datastore.certificate_create`, but first
// we need to look up this silo's fully-qualified domain names in order
// to check that the cert we've been given is valid for this silo.
// Looking up DNS names requires reading the DNS configuration of the
// _rack_, which this user may not be able to do (even if they have
// permission to upload new certs, which almost certainly implies a
// silo-level admin). We'll use our `opctx_external_authn()` context,
// which is the same context used to create a silo. This is a higher
// privilege than the current user may have, but we believe it does not
// leak any information that a silo admin doesn't already know (the
// external DNS name(s) of the rack, which leads to their silo's DNS
// name(s)).
let silo_fq_dns_names = self
.silo_fq_dns_names(self.opctx_external_authn(), authz_silo.id())
.await?;

let kind = params.service;
let new_certificate = db::model::Certificate::new(
Expand Down
82 changes: 82 additions & 0 deletions nexus/tests/integration_tests/silos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::integration_tests::saml::SAML_IDP_DESCRIPTOR;
use dropshot::ResultsPage;
use nexus_db_queries::authn::silos::{
AuthenticatedSubject, IdentityProviderType,
};
Expand All @@ -19,6 +20,7 @@ use nexus_test_utils::resource_helpers::{
objects_list_page_authz, projects_list,
};
use nexus_test_utils_macros::nexus_test;
use nexus_types::external_api::views::Certificate;
use nexus_types::external_api::views::{
self, IdentityProvider, Project, SamlIdentityProvider, Silo,
};
Expand All @@ -27,6 +29,7 @@ use omicron_common::api::external::ObjectIdentity;
use omicron_common::api::external::{
IdentityMetadataCreateParams, LookupType, Name,
};
use omicron_test_utils::certificates::CertificateChain;
use omicron_test_utils::dev::poll::{wait_for_condition, CondCheckError};

use std::collections::{BTreeMap, BTreeSet, HashSet};
Expand Down Expand Up @@ -2437,3 +2440,82 @@ async fn check_fleet_privileges(
.unwrap();
}
}

// Test that a silo admin can create new certificates for their silo
//
// Internally, the certificate validation check requires the `authz::DNS_CONFIG`
// resource (to check that the certificate is valid for
// `{silo_name}.{external_dns_zone_name}`), which silo admins may not have. We
// have to use an alternate, elevated context to perform that check, and this
// test confirms we do so.
#[nexus_test]
async fn test_silo_admin_can_create_certs(cptestctx: &ControlPlaneTestContext) {
let client = &cptestctx.external_client;
let certs_url = "/v1/certificates";

// Create a silo with an admin user
let silo = create_silo(
client,
"silo-name",
true,
shared::SiloIdentityMode::LocalOnly,
)
.await;

let new_silo_user_id = create_local_user(
client,
&silo,
&"admin".parse().unwrap(),
params::UserPassword::LoginDisallowed,
)
.await
.id;

grant_iam(
client,
"/v1/system/silos/silo-name",
SiloRole::Admin,
new_silo_user_id,
AuthnMode::PrivilegedUser,
)
.await;

// The user should be able to create certs for this silo
let chain = CertificateChain::new(cptestctx.wildcard_silo_dns_name());
let (cert, key) =
(chain.cert_chain_as_pem(), chain.end_cert_private_key_as_pem());

let cert: Certificate = NexusRequest::objects_post(
client,
certs_url,
&params::CertificateCreate {
identity: IdentityMetadataCreateParams {
name: "test-cert".parse().unwrap(),
description: "the test cert".to_string(),
},
cert,
key,
service: shared::ServiceUsingCertificate::ExternalApi,
},
)
.authn_as(AuthnMode::SiloUser(new_silo_user_id))
.execute()
.await
.expect("failed to create certificate")
.parsed_body()
.unwrap();

// The cert should exist when listing the silo's certs as the silo admin
let silo_certs =
NexusRequest::object_get(client, &format!("{certs_url}?limit=10"))
.authn_as(AuthnMode::SiloUser(new_silo_user_id))
.execute()
.await
.expect("failed to list certificates")
.parsed_body::<ResultsPage<Certificate>>()
.expect("failed to parse body as ResultsPage<Certificate>")
.items;

assert_eq!(silo_certs.len(), 1);
assert_eq!(silo_certs[0].identity.id, cert.identity.id);
}

0 comments on commit 343835c

Please sign in to comment.