Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 164 additions & 9 deletions src/reconcile/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,14 +520,13 @@ async fn reconcile_cert_manager_tls(
let secret_resource_version = secret.metadata.resource_version.clone();

let san_dns_names = san_validation_dns_names(&san_budget, config, &entry);
if config.require_san_match
&& let Err(failure) = validate_tls_secret_san_match_with_budget(
&secret_name,
&cert_bytes,
&san_dns_names,
&mut runtime_budget,
)
{
if let Err(failure) = validate_configured_tls_secret_san_match(
config,
&secret_name,
&cert_bytes,
&san_dns_names,
&mut runtime_budget,
) {
return tls_validation_blocked(ctx, tenant, config, failure).await;
}

Expand Down Expand Up @@ -2452,6 +2451,25 @@ fn validate_tls_secret_san_match_with_budget(
}
}

fn validate_configured_tls_secret_san_match(
config: &TlsConfig,
secret_name: &str,
cert_bytes: &[u8],
expected_dns_names: &[String],
runtime_budget: &mut TlsCertificateRuntimeBudget,
) -> Result<(), TlsValidationFailure> {
if !config.require_san_match {
return Ok(());
}

validate_tls_secret_san_match_with_budget(
secret_name,
cert_bytes,
expected_dns_names,
runtime_budget,
)
}

#[cfg(test)]
fn validate_tls_secret_san_match(
secret_name: &str,
Expand Down Expand Up @@ -2890,10 +2908,17 @@ mod tests {
use crate::types::v1alpha1::tls::{
CaTrustConfig, CertManagerPrivateKeyConfig, CertManagerTlsConfig, TlsCertificateConfig,
};
use http::{Method, Request, Response, StatusCode};
use k8s_openapi::ByteString;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
use kube::CustomResourceExt;
use kube::{Client, CustomResourceExt, client::Body};
use std::collections::BTreeMap;
use std::convert::Infallible;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use tower::service_fn;

const PUBLIC_CERT_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\nMIIDCTCCAfGgAwIBAgIUD4D7ObFcJ5PEZwq2t/cmrTbzcU0wDQYJKoZIhvcNAQEL\nBQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTExMDA3NDQwNVoXDTI2MTEx\nMDA3NDQwNVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAsnrreaQGztdaTppY7p1ExoDU7FpYjk8MalWs9xIioHTe\ndpDlZmEWak0Q80qTvc+x6GT8VD/pLYqg6B2mot8I+Uv44GUmpPD/+WDxVbjvwL2b\nfvcNGEniqKJUOy2za98WcmI8EoILwbmYy7cZslf6b3D0xuDsmovYJgtjNeziV6ie\nLQfbWWXhAipYhUwaBAdUSQS+BWPPdYFG4LEE/8+BqmYdGU7ujIFlqSU89ZMfpZS4\npVRoEy16fs5O0UkbP1l63Q0qBLrLXjWw874dV8wC2p9iuVwofpDZRGhfYFaviZHb\nMHdUBRUughU4vvTknAGwMzbrIH+eTp7aKrGKWb7ozQIDAQABo1MwUTAdBgNVHQ4E\nFgQUGSE2L3XLbuxlA1Q0iX65aVGKzl4wHwYDVR0jBBgwFoAUGSE2L3XLbuxlA1Q0\niX65aVGKzl4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAGHwM\nSYFN1/9ZlriVaJEpSvGlfeDvN5ipXqf0s1Ykux9rsTYchn7tcA6zhWqZUimwy/jO\nI7jLfBNa3r5HT1uX3/RlMs6dMIO4h3vkSWjQ3QaGiuXh6U+erbkaeETtrw9b40ta\nDsj2rruE3Z11JV0y5fGcvXjXMFV7XsFQjNXF5TlXu4OUvfMeo9h4IbPmNQtq+g+t\nnx0ZBloqo+punQVjHjovoQUWlrOOL5ZRZl1vLqqhHfw54a9weCXY8XJNnxWN0l0C\nKzht0TgbidDlWKBsk/CMTY8zpYrfVyPhnjNCeFGFG0DzrsehCgpEiEZ6vlylei7c\nRfKUdp4DXmUZBDzeQw==\n-----END CERTIFICATE-----\n";
const CERT_WITH_PEER_SANS_PEM: &[u8] = br#"-----BEGIN CERTIFICATE-----
Expand All @@ -2920,6 +2945,15 @@ S2+cuFyHX+xgTPNxiG9zUDrgtXds/63ePISjIADAUvsmI97k96E6jdcgB9MmWdJj
-----END CERTIFICATE-----
"#;

fn kube_response(status: StatusCode, body: Value) -> Response<Body> {
Response::builder()
.status(status)
.body(Body::from(
serde_json::to_vec(&body).expect("response should serialize"),
))
.expect("response should build")
}

#[test]
fn tenant_crd_schema_types_cert_manager_private_key() {
let crd = serde_json::to_value(Tenant::crd()).expect("tenant CRD serializes to JSON");
Expand Down Expand Up @@ -3429,6 +3463,127 @@ S2+cuFyHX+xgTPNxiG9zUDrgtXds/63ePISjIADAUvsmI97k96E6jdcgB9MmWdJj
);
}

#[tokio::test]
async fn require_san_match_blocks_public_tls_during_reconcile_without_internode_https() {
let mut tenant = crate::tests::create_test_tenant(None, None);
tenant.spec.tls = Some(TlsConfig {
mode: TlsMode::CertManager,
enable_internode_https: false,
require_san_match: true,
cert_manager: Some(CertManagerTlsConfig {
secret_name: Some("server-tls".to_string()),
dns_names: vec!["s3.example.com".to_string()],
include_generated_dns_names: Some(false),
..Default::default()
}),
..Default::default()
});

let mut server_secret = tls_secret(
"server-tls",
"7",
Some(KUBERNETES_TLS_SECRET_TYPE),
true,
true,
None,
);
server_secret.metadata.namespace = Some("default".to_string());
server_secret
.data
.as_mut()
.expect("test TLS Secret should contain data")
.insert(
TLS_CERT_KEY.to_string(),
ByteString(CERT_WITH_PEER_SANS_PEM.to_vec()),
);

let request_count = Arc::new(AtomicUsize::new(0));
let service = service_fn({
let request_count = Arc::clone(&request_count);
let server_secret = server_secret.clone();
let tenant = tenant.clone();
move |request: Request<Body>| {
let request_number = request_count.fetch_add(1, Ordering::SeqCst);
let server_secret = server_secret.clone();
let tenant = tenant.clone();
async move {
let response = match request_number {
0 => {
assert_eq!(request.method(), Method::GET);
assert_eq!(
request.uri().path(),
"/api/v1/namespaces/default/secrets/server-tls"
);
kube_response(
StatusCode::OK,
serde_json::to_value(server_secret)
.expect("Secret should serialize"),
)
}
1 => {
assert_eq!(request.method(), Method::PATCH);
assert_eq!(
request.uri().path(),
"/apis/rustfs.com/v1alpha1/namespaces/default/tenants/test-tenant/status"
);
let patch: Value = serde_json::from_slice(
&request
.into_body()
.collect_bytes()
.await
.expect("status patch body should be readable"),
)
.expect("status patch should be JSON");
assert_eq!(patch["status"]["currentState"], "Blocked");
assert_eq!(
patch["status"]["certificates"]["tls"]["lastErrorReason"],
Reason::CertificateSanMismatch.as_str()
);
assert!(
patch["status"]["certificates"]["tls"]["lastErrorMessage"]
.as_str()
.is_some_and(|message| message.contains("s3.example.com"))
);
kube_response(
StatusCode::OK,
serde_json::to_value(tenant).expect("Tenant should serialize"),
)
}
2 => {
assert_eq!(request.method(), Method::POST);
assert!(request.uri().path().contains("/events"));
kube_response(
StatusCode::INTERNAL_SERVER_ERROR,
serde_json::json!({
"apiVersion": "v1",
"kind": "Status",
"status": "Failure",
"reason": "InternalError",
"code": 500
}),
)
}
_ => panic!("unexpected Kubernetes request: {request:?}"),
};
Ok::<_, Infallible>(response)
}
}
});
let ctx = Context::new(Client::new(service, "default"));

let error = reconcile_tls(&ctx, &tenant, "default")
.await
.expect_err("public TLS SAN mismatch must block reconcile without internode HTTPS");

assert!(matches!(
error,
Error::TlsBlocked { reason, message }
if reason == Reason::CertificateSanMismatch.as_str()
&& message.contains("s3.example.com")
));
assert_eq!(request_count.load(Ordering::SeqCst), 3);
}

#[test]
fn tls_status_records_explicit_ca_and_client_ca_resource_versions() {
let config = TlsConfig {
Expand Down
Loading