Skip to content

Commit 9b077ff

Browse files
committed
fix(cli): respect gateway name for mTLS lookup
Signed-off-by: Alex Lewontin <alex.lewontin@canonical.com>
1 parent 188b355 commit 9b077ff

2 files changed

Lines changed: 192 additions & 27 deletions

File tree

crates/openshell-cli/src/run.rs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -688,24 +688,10 @@ fn is_loopback_gateway_endpoint(endpoint: &str) -> bool {
688688
}
689689
}
690690

691-
/// Check whether mTLS client certs exist on disk for the gateway that
692-
/// would serve this endpoint.
693-
///
694-
/// Loopback endpoints (`localhost`, `127.0.0.1`, `::1`) resolve to the
695-
/// `"openshell"` gateway name, matching the convention used by local
696-
/// `openshell-gateway generate-certs` and the TLS cert resolver in `tls.rs`.
697-
fn mtls_certs_exist_for_endpoint(name: &str, endpoint: &str) -> bool {
698-
let cert_name = if is_loopback_gateway_endpoint(endpoint) {
699-
"openshell"
700-
} else {
701-
name
702-
};
691+
/// Check whether mTLS client certs exist on disk for a gateway name.
692+
fn mtls_certs_exist_for_gateway(name: &str) -> bool {
703693
openshell_core::paths::xdg_config_dir().is_ok_and(|d| {
704-
let mtls = d
705-
.join("openshell")
706-
.join("gateways")
707-
.join(cert_name)
708-
.join("mtls");
694+
let mtls = d.join("openshell").join("gateways").join(name).join("mtls");
709695
mtls.join("ca.crt").is_file()
710696
&& mtls.join("tls.crt").is_file()
711697
&& mtls.join("tls.key").is_file()
@@ -1030,7 +1016,7 @@ pub async fn gateway_add(
10301016
if endpoint.starts_with("http://") {
10311017
// Warn if mTLS certs exist for this gateway — the user likely
10321018
// meant to use https:// instead of http://.
1033-
let has_mtls_certs = mtls_certs_exist_for_endpoint(name, &endpoint);
1019+
let has_mtls_certs = mtls_certs_exist_for_gateway(name);
10341020

10351021
if has_mtls_certs {
10361022
let https_endpoint = endpoint.replacen("http://", "https://", 1);
@@ -1084,8 +1070,7 @@ pub async fn gateway_add(
10841070
} else {
10851071
None
10861072
};
1087-
let certs_on_disk =
1088-
imported_mtls_dir.is_some() || mtls_certs_exist_for_endpoint(name, &endpoint);
1073+
let certs_on_disk = imported_mtls_dir.is_some() || mtls_certs_exist_for_gateway(name);
10891074
if !certs_on_disk {
10901075
return Err(miette::miette!(
10911076
"mTLS certificates for gateway '{name}' were not found.\n\
@@ -6986,12 +6971,12 @@ mod tests {
69866971
gateway_auth_label, gateway_env_override_warning, gateway_select_with, gateway_type_label,
69876972
git_sync_files, http_health_check, image_requests_gpu, import_local_package_mtls_bundle,
69886973
inferred_provider_type, local_upload_path_exists, local_upload_path_is_symlink,
6989-
package_managed_tls_dirs, parse_cli_setting_value, parse_credential_expiry_cli_value,
6990-
parse_credential_expiry_pairs, parse_credential_pairs, plaintext_gateway_is_remote,
6991-
progress_step_from_metadata, provider_profile_allows_refresh_bootstrap,
6992-
provisioning_timeout_message, ready_false_condition_message, refresh_status_header,
6993-
refresh_status_row, resolve_from, sandbox_should_persist, service_expose_status_error,
6994-
service_url_for_gateway,
6974+
mtls_certs_exist_for_gateway, package_managed_tls_dirs, parse_cli_setting_value,
6975+
parse_credential_expiry_cli_value, parse_credential_expiry_pairs, parse_credential_pairs,
6976+
plaintext_gateway_is_remote, progress_step_from_metadata,
6977+
provider_profile_allows_refresh_bootstrap, provisioning_timeout_message,
6978+
ready_false_condition_message, refresh_status_header, refresh_status_row, resolve_from,
6979+
sandbox_should_persist, service_expose_status_error, service_url_for_gateway,
69956980
};
69966981
use crate::TEST_ENV_LOCK;
69976982
use hyper::StatusCode;
@@ -7993,6 +7978,21 @@ mod tests {
79937978
});
79947979
}
79957980

7981+
#[test]
7982+
fn mtls_certs_exist_for_gateway_uses_explicit_name_for_loopback_endpoint() {
7983+
let tmpdir = tempfile::tempdir().expect("create tmpdir");
7984+
let mtls = tmpdir.path().join("openshell/gateways/k8s/mtls");
7985+
fs::create_dir_all(&mtls).expect("create mtls dir");
7986+
fs::write(mtls.join("ca.crt"), "ca").expect("write ca");
7987+
fs::write(mtls.join("tls.crt"), "client cert").expect("write cert");
7988+
fs::write(mtls.join("tls.key"), "client key").expect("write key");
7989+
7990+
with_tmp_xdg(tmpdir.path(), || {
7991+
assert!(mtls_certs_exist_for_gateway("k8s"));
7992+
assert!(!mtls_certs_exist_for_gateway("openshell"));
7993+
});
7994+
}
7995+
79967996
#[test]
79977997
fn plaintext_gateway_locality_infers_loopback_endpoints_as_local() {
79987998
assert!(!plaintext_gateway_is_remote(

crates/openshell-cli/tests/mtls_integration.rs

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ mod helpers;
66
use helpers::{
77
EnvVarGuard, build_ca, build_client_cert, build_server_cert, install_rustls_provider,
88
};
9-
use openshell_cli::tls::{TlsOptions, grpc_client};
9+
use openshell_bootstrap::{get_gateway_metadata, load_active_gateway};
10+
use openshell_cli::{
11+
run,
12+
tls::{TlsOptions, grpc_client},
13+
};
1014
use openshell_core::proto::{
1115
CreateProviderRequest, CreateSshSessionRequest, CreateSshSessionResponse,
1216
DeleteProviderRequest, DeleteProviderResponse, ExecSandboxEvent, ExecSandboxInput,
@@ -494,6 +498,167 @@ async fn run_server(
494498
addr
495499
}
496500

501+
fn write_gateway_mtls_bundle(
502+
config_dir: &std::path::Path,
503+
gateway_name: &str,
504+
ca_cert: &str,
505+
client_cert: &str,
506+
client_key: &str,
507+
) {
508+
let mtls = config_dir
509+
.join("openshell")
510+
.join("gateways")
511+
.join(gateway_name)
512+
.join("mtls");
513+
std::fs::create_dir_all(&mtls).unwrap();
514+
std::fs::write(mtls.join("ca.crt"), ca_cert).unwrap();
515+
std::fs::write(mtls.join("tls.crt"), client_cert).unwrap();
516+
std::fs::write(mtls.join("tls.key"), client_key).unwrap();
517+
}
518+
519+
fn isolated_gateway_add_env(
520+
config_dir: &std::path::Path,
521+
state_dir: &std::path::Path,
522+
) -> EnvVarGuard {
523+
let xdg_config = config_dir.to_string_lossy().into_owned();
524+
let xdg_state = state_dir.to_string_lossy().into_owned();
525+
let local_tls_dir = state_dir.join("no-package-managed-tls");
526+
let local_tls = local_tls_dir.to_string_lossy().into_owned();
527+
528+
EnvVarGuard::set(&[
529+
("XDG_CONFIG_HOME", xdg_config.as_str()),
530+
("XDG_STATE_HOME", xdg_state.as_str()),
531+
("HOME", xdg_state.as_str()),
532+
("OPENSHELL_LOCAL_TLS_DIR", local_tls.as_str()),
533+
("OPENSHELL_GATEWAY", "unused-by-named-gateway-add"),
534+
])
535+
}
536+
537+
#[tokio::test]
538+
async fn gateway_add_mtls_loopback_uses_explicit_gateway_name() {
539+
install_rustls_provider();
540+
541+
let (ca, ca_key) = build_ca();
542+
let (server_cert, server_key) = build_server_cert(&ca, &ca_key);
543+
let (client_cert, client_key) = build_client_cert(&ca, &ca_key);
544+
let ca_cert = ca.pem();
545+
let addr = run_server(server_cert, server_key, ca_cert.clone()).await;
546+
547+
let config_dir = tempdir().unwrap();
548+
let state_dir = tempdir().unwrap();
549+
write_gateway_mtls_bundle(
550+
config_dir.path(),
551+
"k8s",
552+
&ca_cert,
553+
&client_cert,
554+
&client_key,
555+
);
556+
let _env = isolated_gateway_add_env(config_dir.path(), state_dir.path());
557+
558+
let endpoint = format!("https://localhost:{}", addr.port());
559+
run::gateway_add(
560+
&endpoint,
561+
Some("k8s"),
562+
None,
563+
true,
564+
None,
565+
"openshell-cli",
566+
None,
567+
None,
568+
false,
569+
)
570+
.await
571+
.unwrap();
572+
573+
let metadata = get_gateway_metadata("k8s").unwrap();
574+
assert_eq!(metadata.name, "k8s");
575+
assert_eq!(metadata.gateway_endpoint, endpoint);
576+
assert_eq!(metadata.auth_mode.as_deref(), Some("mtls"));
577+
assert_eq!(load_active_gateway().as_deref(), Some("k8s"));
578+
assert!(get_gateway_metadata("openshell").is_none());
579+
}
580+
581+
#[tokio::test]
582+
async fn gateway_add_mtls_loopback_without_name_uses_openshell_default() {
583+
install_rustls_provider();
584+
585+
let (ca, ca_key) = build_ca();
586+
let (server_cert, server_key) = build_server_cert(&ca, &ca_key);
587+
let (client_cert, client_key) = build_client_cert(&ca, &ca_key);
588+
let ca_cert = ca.pem();
589+
let addr = run_server(server_cert, server_key, ca_cert.clone()).await;
590+
591+
let config_dir = tempdir().unwrap();
592+
let state_dir = tempdir().unwrap();
593+
write_gateway_mtls_bundle(
594+
config_dir.path(),
595+
"openshell",
596+
&ca_cert,
597+
&client_cert,
598+
&client_key,
599+
);
600+
let _env = isolated_gateway_add_env(config_dir.path(), state_dir.path());
601+
602+
let endpoint = format!("https://localhost:{}", addr.port());
603+
run::gateway_add(
604+
&endpoint,
605+
None,
606+
None,
607+
true,
608+
None,
609+
"openshell-cli",
610+
None,
611+
None,
612+
false,
613+
)
614+
.await
615+
.unwrap();
616+
617+
let metadata = get_gateway_metadata("openshell").unwrap();
618+
assert_eq!(metadata.name, "openshell");
619+
assert_eq!(metadata.gateway_endpoint, endpoint);
620+
assert_eq!(metadata.auth_mode.as_deref(), Some("mtls"));
621+
assert_eq!(load_active_gateway().as_deref(), Some("openshell"));
622+
}
623+
624+
#[tokio::test]
625+
async fn gateway_add_mtls_loopback_explicit_name_does_not_fallback_to_openshell_certs() {
626+
install_rustls_provider();
627+
628+
let (ca, ca_key) = build_ca();
629+
let (client_cert, client_key) = build_client_cert(&ca, &ca_key);
630+
let ca_cert = ca.pem();
631+
632+
let config_dir = tempdir().unwrap();
633+
let state_dir = tempdir().unwrap();
634+
write_gateway_mtls_bundle(
635+
config_dir.path(),
636+
"openshell",
637+
&ca_cert,
638+
&client_cert,
639+
&client_key,
640+
);
641+
let _env = isolated_gateway_add_env(config_dir.path(), state_dir.path());
642+
643+
let err = run::gateway_add(
644+
"https://localhost:1",
645+
Some("k8s"),
646+
None,
647+
true,
648+
None,
649+
"openshell-cli",
650+
None,
651+
None,
652+
false,
653+
)
654+
.await
655+
.expect_err("explicit name should require matching named mTLS material");
656+
657+
assert!(err.to_string().contains("gateway 'k8s'"));
658+
assert!(get_gateway_metadata("k8s").is_none());
659+
assert!(load_active_gateway().is_none());
660+
}
661+
497662
#[tokio::test]
498663
async fn cli_connects_with_client_cert() {
499664
install_rustls_provider();

0 commit comments

Comments
 (0)