From 6793737b19a4900a5539c420337e29d6e551c07d Mon Sep 17 00:00:00 2001 From: Seth Jennings Date: Tue, 12 May 2026 20:35:49 -0500 Subject: [PATCH] feat(server): separate HTTPS from mTLS authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make --tls-client-ca optional and make client certificates always optional when a CA is configured. This decouples HTTPS encryption from mTLS authentication, allowing mTLS and OIDC bearer tokens to coexist as parallel authentication mechanisms. When --tls-client-ca is provided, client certificates are validated against the CA when presented but never required. Clients may connect with or without a certificate — authentication is handled at the application layer (e.g. OIDC). Two TLS modes are now supported: - HTTPS with optional mTLS (--tls-client-ca provided) - HTTPS-only (--tls-client-ca omitted) The --disable-gateway-auth flag is preserved for backward compatibility but is now a no-op. The allow_unauthenticated field has been removed from TlsConfig. The Helm chart conditionally includes the client-ca volume and env var based on whether clientCaSecretName is configured. --- crates/openshell-core/src/config.rs | 26 +-- crates/openshell-server/src/cli.rs | 32 +-- crates/openshell-server/src/compute/vm.rs | 6 +- crates/openshell-server/src/lib.rs | 3 +- .../openshell-server/src/service_routing.rs | 3 +- crates/openshell-server/src/tls.rs | 62 +++--- .../tests/edge_tunnel_auth.rs | 193 +++++++----------- .../tests/multiplex_tls_integration.rs | 34 ++- .../helm/openshell/templates/statefulset.yaml | 6 + deploy/helm/openshell/values.yaml | 8 +- 10 files changed, 160 insertions(+), 213 deletions(-) diff --git a/crates/openshell-core/src/config.rs b/crates/openshell-core/src/config.rs index 1ec0926fc..32d52b155 100644 --- a/crates/openshell-core/src/config.rs +++ b/crates/openshell-core/src/config.rs @@ -326,10 +326,15 @@ pub struct ServiceRoutingConfig { /// TLS configuration. /// -/// By default mTLS is enforced — all clients must present a certificate -/// signed by the given CA. When `allow_unauthenticated` is `true`, the -/// TLS handshake also accepts connections without a client certificate -/// (needed for reverse-proxy deployments like Cloudflare Tunnel). +/// Two modes are supported: +/// - **HTTPS with optional mTLS** (`client_ca_path = Some`): +/// Client certificates are validated against the given CA when presented, +/// but never required. Clients may connect with or without a certificate. +/// - **HTTPS-only** (`client_ca_path = None`): +/// Server-side TLS only; no client certificates are requested. +/// +/// In both modes, authentication is handled at the application layer +/// (e.g. OIDC bearer tokens). mTLS is an additional, optional mechanism. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TlsConfig { /// Path to the TLS certificate file. @@ -338,16 +343,11 @@ pub struct TlsConfig { /// Path to the TLS private key file. pub key_path: PathBuf, - /// Path to the CA certificate file for client certificate verification (mTLS). - /// The server requires all clients to present a valid certificate signed by - /// this CA. - pub client_ca_path: PathBuf, - - /// When `true`, the TLS handshake succeeds even without a client - /// certificate. Application-layer middleware must then enforce auth - /// (e.g. via a CF JWT header). + /// Path to the CA certificate file for client certificate verification. + /// When `Some`, client certs signed by this CA are accepted but not + /// required. When `None`, the server does not request client certs. #[serde(default)] - pub allow_unauthenticated: bool, + pub client_ca_path: Option, } /// OIDC (`OpenID` Connect) configuration for JWT-based authentication. diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index 3cd9e8c79..805ae0fc7 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -12,7 +12,7 @@ use openshell_core::config::{ }; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; -use tracing::info; +use tracing::{info, warn}; use tracing_subscriber::EnvFilter; use crate::certgen; @@ -248,10 +248,10 @@ struct RunArgs { #[arg(long, env = "OPENSHELL_DISABLE_TLS")] disable_tls: bool, - /// Disable gateway authentication (mTLS client certificate requirement). - /// When set, the TLS handshake accepts connections without a client - /// certificate. Ignored when --disable-tls is set. - #[arg(long, env = "OPENSHELL_DISABLE_GATEWAY_AUTH")] + /// Deprecated: client certificates are now always optional when + /// --tls-client-ca is provided. Kept for backward compatibility. + #[arg(long, env = "OPENSHELL_DISABLE_GATEWAY_AUTH", hide = true)] + #[allow(dead_code)] disable_gateway_auth: bool, /// OIDC issuer URL for JWT-based authentication. @@ -345,6 +345,7 @@ async fn run_from_args(args: RunArgs) -> Result<()> { let bind = SocketAddr::new(args.bind_address, args.port); + let has_client_ca = args.tls_client_ca.is_some(); let tls = if args.disable_tls { None } else { @@ -356,16 +357,10 @@ async fn run_from_args(args: RunArgs) -> Result<()> { let key_path = args.tls_key.ok_or_else(|| { miette::miette!("--tls-key is required when TLS is enabled (use --disable-tls to skip)") })?; - let client_ca_path = args.tls_client_ca.ok_or_else(|| { - miette::miette!( - "--tls-client-ca is required when TLS is enabled (use --disable-tls to skip)" - ) - })?; Some(openshell_core::TlsConfig { cert_path, key_path, - client_ca_path, - allow_unauthenticated: args.disable_gateway_auth, + client_ca_path: args.tls_client_ca, }) }; @@ -477,8 +472,17 @@ async fn run_from_args(args: RunArgs) -> Result<()> { if args.disable_tls { info!("TLS disabled — listening on plaintext HTTP"); - } else if args.disable_gateway_auth { - info!("Gateway auth disabled — accepting connections without client certificates"); + } else if has_client_ca { + info!("mTLS enabled — client certificates accepted but not required"); + } else { + info!("HTTPS enabled (no mTLS) — client certificates not requested"); + } + + if !args.disable_tls && !has_client_ca && config.oidc.is_none() { + warn!( + "Neither mTLS (--tls-client-ca) nor OIDC (--oidc-issuer) is configured — \ + the gateway has no authentication mechanism" + ); } info!(bind = %config.bind_address, "Starting OpenShell server"); diff --git a/crates/openshell-server/src/compute/vm.rs b/crates/openshell-server/src/compute/vm.rs index 1e62d4942..9892bfa7d 100644 --- a/crates/openshell-server/src/compute/vm.rs +++ b/crates/openshell-server/src/compute/vm.rs @@ -610,8 +610,7 @@ mod tests { let config = Config::new(Some(TlsConfig { cert_path: server_cert, key_path: server_key, - client_ca_path: server_ca, - allow_unauthenticated: false, + client_ca_path: Some(server_ca), })) .with_grpc_endpoint("https://gateway.internal:8443"); @@ -646,8 +645,7 @@ mod tests { let config = Config::new(Some(TlsConfig { cert_path: server_cert.clone(), key_path: server_key.clone(), - client_ca_path: server_ca, - allow_unauthenticated: false, + client_ca_path: Some(server_ca), })) .with_grpc_endpoint("https://gateway.internal:8443"); let vm_config = VmComputeConfig { diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 93ccdc9dc..6ad5cc1d5 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -289,8 +289,7 @@ pub async fn run_server( Some(TlsAcceptor::from_files( &tls.cert_path, &tls.key_path, - &tls.client_ca_path, - tls.allow_unauthenticated, + tls.client_ca_path.as_deref(), )?) } else { info!("TLS disabled — accepting plaintext connections"); diff --git a/crates/openshell-server/src/service_routing.rs b/crates/openshell-server/src/service_routing.rs index 194f10417..b2dcdeae8 100644 --- a/crates/openshell-server/src/service_routing.rs +++ b/crates/openshell-server/src/service_routing.rs @@ -826,8 +826,7 @@ mod tests { openshell_core::TlsConfig { cert_path: "server.crt".into(), key_path: "server.key".into(), - client_ca_path: "ca.crt".into(), - allow_unauthenticated: false, + client_ca_path: Some("ca.crt".into()), } } diff --git a/crates/openshell-server/src/tls.rs b/crates/openshell-server/src/tls.rs index 95c18608f..710406b27 100644 --- a/crates/openshell-server/src/tls.rs +++ b/crates/openshell-server/src/tls.rs @@ -19,17 +19,15 @@ pub struct TlsAcceptor { } impl TlsAcceptor { - /// Create a new TLS acceptor from certificate, key, and client CA files. + /// Create a new TLS acceptor from certificate and key files. /// - /// When `allow_unauthenticated` is `false` (the default), the server - /// enforces mTLS — all clients must present a valid certificate signed - /// by the given CA. + /// When `client_ca_path` is `Some`, client certificates are validated + /// against the given CA but never required. Clients may connect with + /// or without a certificate; presented certs from an unknown CA are + /// still rejected. /// - /// When `allow_unauthenticated` is `true`, the TLS handshake succeeds - /// even without a client certificate. This is required when the server - /// sits behind a reverse proxy (e.g. Cloudflare Tunnel) that terminates - /// TLS and cannot forward client certificates. Application-layer - /// middleware must then enforce authentication (e.g. via a JWT header). + /// When `client_ca_path` is `None`, the server does not request client + /// certificates at all (HTTPS-only). /// /// # Errors /// @@ -37,33 +35,35 @@ impl TlsAcceptor { pub fn from_files( cert_path: &Path, key_path: &Path, - client_ca_path: &Path, - allow_unauthenticated: bool, + client_ca_path: Option<&Path>, ) -> Result { let certs = load_certs(cert_path)?; let key = load_key(key_path)?; - let ca_certs = load_certs(client_ca_path)?; - let mut root_store = rustls::RootCertStore::empty(); - for cert in ca_certs { - root_store - .add(cert) - .map_err(|e| Error::tls(format!("failed to add CA certificate: {e}")))?; - } - - let verifier_builder = WebPkiClientVerifier::builder(Arc::new(root_store)); - let verifier = if allow_unauthenticated { - verifier_builder.allow_unauthenticated() + let mut config = if let Some(ca_path) = client_ca_path { + let ca_certs = load_certs(ca_path)?; + let mut root_store = rustls::RootCertStore::empty(); + for cert in ca_certs { + root_store + .add(cert) + .map_err(|e| Error::tls(format!("failed to add CA certificate: {e}")))?; + } + + let verifier = WebPkiClientVerifier::builder(Arc::new(root_store)) + .allow_unauthenticated() + .build() + .map_err(|e| Error::tls(format!("failed to build client verifier: {e}")))?; + + ServerConfig::builder() + .with_client_cert_verifier(verifier) + .with_single_cert(certs, key) + .map_err(|e| Error::tls(format!("failed to create TLS config: {e}")))? } else { - verifier_builder - } - .build() - .map_err(|e| Error::tls(format!("failed to build client verifier: {e}")))?; - - let mut config = ServerConfig::builder() - .with_client_cert_verifier(verifier) - .with_single_cert(certs, key) - .map_err(|e| Error::tls(format!("failed to create TLS config: {e}")))?; + ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key) + .map_err(|e| Error::tls(format!("failed to create TLS config: {e}")))? + }; config .alpn_protocols diff --git a/crates/openshell-server/tests/edge_tunnel_auth.rs b/crates/openshell-server/tests/edge_tunnel_auth.rs index a4676232e..fc02c9a23 100644 --- a/crates/openshell-server/tests/edge_tunnel_auth.rs +++ b/crates/openshell-server/tests/edge_tunnel_auth.rs @@ -12,18 +12,17 @@ //! //! Test matrix: //! -//! | `allow_unauthenticated` | client cert | bearer auth header | expected | -//! |-----------------------|-------------|--------------------|----------| -//! | false | valid | — | OK | -//! | false | none | — | rejected | -//! | true | valid | — | OK | -//! | true | none | present | OK (*) | -//! | true | none | absent | OK (**) | +//! | `client_ca` | client cert | bearer header | expected | +//! |-------------|-------------|---------------|---------------------------| +//! | Some | valid | — | OK (cert validated) | +//! | Some | none | — | OK (cert optional) | +//! | Some | none | present | OK (bearer auth) | +//! | Some | rogue CA | — | rejected (bad cert) | +//! | None | none | — | OK (HTTPS-only) | //! -//! (*) Simulates the edge tunnel path: no client cert but a JWT header. -//! (**) TLS handshake succeeds, but in production the auth middleware (not yet -//! implemented) would reject. This test proves the TLS layer alone does -//! not block unauthenticated connections when the flag is set. +//! Client certificates are always optional when a CA is configured. They are +//! validated when present (rogue-CA certs are rejected) but never required. +//! Authentication is handled at the application layer (OIDC bearer tokens). use bytes::Bytes; use http_body_util::Empty; @@ -675,17 +674,16 @@ fn https_client_no_cert( // Tests // =========================================================================== -/// Baseline: with `allow_unauthenticated=false` (default), mTLS connections work. +/// Valid client cert is accepted when a CA is configured. #[tokio::test] -async fn baseline_mtls_works_with_mandatory_client_certs() { +async fn mtls_valid_client_cert_accepted() { install_rustls_provider(); let (temp, pki) = generate_pki(); let tls_acceptor = TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - false, // mandatory mTLS + Some(temp.path().join("ca.pem").as_path()), ) .unwrap(); @@ -715,102 +713,17 @@ async fn baseline_mtls_works_with_mandatory_client_certs() { server.abort(); } -/// Baseline: with `allow_unauthenticated=false`, no-client-cert connections are -/// rejected at the TLS layer. +/// No client cert is accepted when a CA is configured — client certs are +/// always optional. Auth is deferred to the application layer. #[tokio::test] -async fn baseline_no_cert_rejected_with_mandatory_mtls() { +async fn no_client_cert_accepted_with_ca_configured() { install_rustls_provider(); let (temp, pki) = generate_pki(); let tls_acceptor = TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - false, // mandatory mTLS - ) - .unwrap(); - - let (addr, server) = start_test_server(tls_acceptor).await; - - let ca_cert = tonic::transport::Certificate::from_pem(pki.ca_cert_pem.clone()); - let tls = ClientTlsConfig::new() - .ca_certificate(ca_cert) - .domain_name("localhost"); - let endpoint = Endpoint::from_shared(format!("https://localhost:{}", addr.port())) - .expect("invalid endpoint") - .tls_config(tls) - .expect("failed to set tls"); - - let result = endpoint.connect().await; - if let Ok(channel) = result { - let mut client = OpenShellClient::new(channel); - let rpc_result = client.health(HealthRequest {}).await; - assert!( - rpc_result.is_err(), - "expected RPC to fail without client cert when mTLS is mandatory" - ); - } - // If connect() itself failed, that's also correct — TLS handshake rejected. - - server.abort(); -} - -/// With `allow_unauthenticated=true`, mTLS connections still work (dual-auth). -#[tokio::test] -async fn dual_auth_mtls_still_accepted() { - install_rustls_provider(); - let (temp, pki) = generate_pki(); - - let tls_acceptor = TlsAcceptor::from_files( - &temp.path().join("server-cert.pem"), - &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - true, // allow unauthenticated (tunnel mode) - ) - .unwrap(); - - let (addr, server) = start_test_server(tls_acceptor).await; - - // gRPC with mTLS should still work - let mut grpc = grpc_client_mtls( - addr, - pki.ca_cert_pem.clone(), - pki.client_cert_pem.clone(), - pki.client_key_pem.clone(), - ) - .await; - let resp = grpc.health(HealthRequest {}).await.unwrap(); - assert_eq!(resp.get_ref().status, ServiceStatus::Healthy as i32); - - // HTTP with mTLS should still work - let client = https_client_mtls(&pki); - let req = Request::builder() - .method("GET") - .uri(format!("https://localhost:{}/healthz", addr.port())) - .body(Empty::::new()) - .unwrap(); - let resp = client.request(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - - server.abort(); -} - -/// With `allow_unauthenticated=true`, no-client-cert connections pass the TLS -/// handshake. This simulates Cloudflare Tunnel re-originating a connection. -/// -/// The gRPC health check succeeds because there is no auth middleware yet — -/// this proves the TLS layer is no longer the gate. When auth middleware is -/// added, the test should be updated to expect 401 without a valid JWT. -#[tokio::test] -async fn tunnel_mode_no_cert_passes_tls_handshake() { - install_rustls_provider(); - let (temp, pki) = generate_pki(); - - let tls_acceptor = TlsAcceptor::from_files( - &temp.path().join("server-cert.pem"), - &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - true, // allow unauthenticated (tunnel mode) + Some(temp.path().join("ca.pem").as_path()), ) .unwrap(); @@ -822,7 +735,7 @@ async fn tunnel_mode_no_cert_passes_tls_handshake() { assert_eq!( resp.get_ref().status, ServiceStatus::Healthy as i32, - "gRPC health check should succeed without client cert in tunnel mode" + "gRPC health check should succeed without client cert" ); // HTTP without client cert @@ -836,28 +749,23 @@ async fn tunnel_mode_no_cert_passes_tls_handshake() { assert_eq!( resp.status(), StatusCode::OK, - "HTTP health check should succeed without client cert in tunnel mode" + "HTTP health check should succeed without client cert" ); server.abort(); } -/// Simulate the steady-state Cloudflare tunnel flow: no client cert, but the -/// `cf-authorization` header carries a token. At the TLS level this must -/// succeed; the header is passed through to the gRPC handler. -/// -/// Note: We use a dummy token value here. When real JWT verification middleware -/// is added, this test should use a properly-signed test JWT. +/// Bearer auth header passes through to the gRPC handler when no client +/// cert is presented. #[tokio::test] -async fn tunnel_mode_cf_authorization_header_reaches_server() { +async fn bearer_header_reaches_server_without_client_cert() { install_rustls_provider(); let (temp, pki) = generate_pki(); let tls_acceptor = TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - true, + Some(temp.path().join("ca.pem").as_path()), ) .unwrap(); @@ -871,24 +779,23 @@ async fn tunnel_mode_cf_authorization_header_reaches_server() { assert_eq!( resp.get_ref().status, ServiceStatus::Healthy as i32, - "gRPC with cf-authorization header should succeed in tunnel mode" + "gRPC with bearer header should succeed without client cert" ); server.abort(); } -/// With `allow_unauthenticated=true`, a client cert from a rogue CA is still -/// rejected by the TLS layer — the verifier still validates presented certs. +/// A client cert from a rogue CA is rejected at the TLS layer even though +/// client certs are optional — presented certs are still validated. #[tokio::test] -async fn tunnel_mode_rogue_cert_still_rejected() { +async fn rogue_cert_rejected() { install_rustls_provider(); let (temp, pki) = generate_pki(); let tls_acceptor = TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - true, + Some(temp.path().join("ca.pem").as_path()), ) .unwrap(); @@ -936,10 +843,52 @@ async fn tunnel_mode_rogue_cert_still_rejected() { let rpc_result = client.health(HealthRequest {}).await; assert!( rpc_result.is_err(), - "expected RPC to fail with rogue client cert even in tunnel mode" + "expected RPC to fail with rogue client cert" ); } // If connect() itself failed, that's also correct. server.abort(); } + +/// HTTPS-only mode: no client CA configured, so the server never requests +/// client certificates. Clients connect with server-only TLS. +#[tokio::test] +async fn https_only_no_client_cert_required() { + install_rustls_provider(); + let (temp, pki) = generate_pki(); + + let tls_acceptor = TlsAcceptor::from_files( + &temp.path().join("server-cert.pem"), + &temp.path().join("server-key.pem"), + None, + ) + .unwrap(); + + let (addr, server) = start_test_server(tls_acceptor).await; + + // gRPC without client cert — should succeed (no client certs requested) + let mut grpc = grpc_client_no_cert(addr, pki.ca_cert_pem.clone()).await; + let resp = grpc.health(HealthRequest {}).await.unwrap(); + assert_eq!( + resp.get_ref().status, + ServiceStatus::Healthy as i32, + "gRPC health check should succeed in HTTPS-only mode" + ); + + // HTTP without client cert + let client = https_client_no_cert(&pki.ca_cert_pem); + let req = Request::builder() + .method("GET") + .uri(format!("https://localhost:{}/healthz", addr.port())) + .body(Empty::::new()) + .unwrap(); + let resp = client.request(req).await.unwrap(); + assert_eq!( + resp.status(), + StatusCode::OK, + "HTTP health check should succeed in HTTPS-only mode" + ); + + server.abort(); +} diff --git a/crates/openshell-server/tests/multiplex_tls_integration.rs b/crates/openshell-server/tests/multiplex_tls_integration.rs index 289f608f1..bd272819f 100644 --- a/crates/openshell-server/tests/multiplex_tls_integration.rs +++ b/crates/openshell-server/tests/multiplex_tls_integration.rs @@ -586,8 +586,7 @@ async fn serves_grpc_and_http_over_tls_on_same_port() { let tls_acceptor = TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - false, + Some(temp.path().join("ca.pem").as_path()), ) .unwrap(); @@ -625,8 +624,7 @@ async fn mtls_valid_client_cert_accepted() { let tls_acceptor = TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - false, + Some(temp.path().join("ca.pem").as_path()), ) .unwrap(); @@ -646,21 +644,20 @@ async fn mtls_valid_client_cert_accepted() { } #[tokio::test] -async fn mtls_no_client_cert_rejected() { +async fn no_client_cert_accepted_with_ca() { install_rustls_provider(); let (temp, pki) = generate_pki(); let tls_acceptor = TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - false, + Some(temp.path().join("ca.pem").as_path()), ) .unwrap(); let (addr, server) = start_test_server(tls_acceptor).await; - // Connect with CA trust but no client cert -- should be rejected. + // Connect with CA trust but no client cert — should succeed (certs are optional). let ca_cert = tonic::transport::Certificate::from_pem(pki.ca_cert_pem.clone()); let tls = ClientTlsConfig::new() .ca_certificate(ca_cert) @@ -670,17 +667,13 @@ async fn mtls_no_client_cert_rejected() { .tls_config(tls) .expect("failed to set tls"); - let result = endpoint.connect().await; - // Connection should fail at the TLS handshake level or shortly after. - // The exact error depends on timing -- it may fail on connect or on first RPC. - if let Ok(channel) = result { - let mut client = OpenShellClient::new(channel); - let rpc_result = client.health(HealthRequest {}).await; - assert!( - rpc_result.is_err(), - "expected RPC to fail without client cert" - ); - } + let channel = endpoint + .connect() + .await + .expect("should connect without client cert"); + let mut client = OpenShellClient::new(channel); + let response = client.health(HealthRequest {}).await.unwrap(); + assert_eq!(response.get_ref().status, ServiceStatus::Healthy as i32); server.abort(); } @@ -693,8 +686,7 @@ async fn mtls_wrong_ca_client_cert_rejected() { let tls_acceptor = TlsAcceptor::from_files( &temp.path().join("server-cert.pem"), &temp.path().join("server-key.pem"), - &temp.path().join("ca.pem"), - false, + Some(temp.path().join("ca.pem").as_path()), ) .unwrap(); diff --git a/deploy/helm/openshell/templates/statefulset.yaml b/deploy/helm/openshell/templates/statefulset.yaml index 69140a70c..899c67a14 100644 --- a/deploy/helm/openshell/templates/statefulset.yaml +++ b/deploy/helm/openshell/templates/statefulset.yaml @@ -118,8 +118,10 @@ spec: value: /etc/openshell-tls/server/tls.crt - name: OPENSHELL_TLS_KEY value: /etc/openshell-tls/server/tls.key + {{- if or .Values.server.tls.clientCaSecretName .Values.pkiInitJob.enabled (and .Values.certManager.enabled .Values.certManager.clientCaFromServerTlsSecret) }} - name: OPENSHELL_TLS_CLIENT_CA value: /etc/openshell-tls/client-ca/ca.crt + {{- end }} - name: OPENSHELL_CLIENT_TLS_SECRET_NAME value: {{ .Values.server.tls.clientTlsSecretName | quote }} {{- if .Values.server.disableGatewayAuth }} @@ -158,10 +160,12 @@ spec: - name: tls-cert mountPath: /etc/openshell-tls/server readOnly: true + {{- if or .Values.server.tls.clientCaSecretName .Values.pkiInitJob.enabled (and .Values.certManager.enabled .Values.certManager.clientCaFromServerTlsSecret) }} - name: tls-client-ca mountPath: /etc/openshell-tls/client-ca readOnly: true {{- end }} + {{- end }} ports: - name: grpc containerPort: {{ .Values.service.port }} @@ -204,6 +208,7 @@ spec: - name: tls-cert secret: secretName: {{ .Values.server.tls.certSecretName }} + {{- if or .Values.server.tls.clientCaSecretName .Values.pkiInitJob.enabled (and .Values.certManager.enabled .Values.certManager.clientCaFromServerTlsSecret) }} - name: tls-client-ca secret: {{- if or .Values.pkiInitJob.enabled (and .Values.certManager.enabled .Values.certManager.clientCaFromServerTlsSecret) }} @@ -215,6 +220,7 @@ spec: secretName: {{ .Values.server.tls.clientCaSecretName }} {{- end }} {{- end }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 17c0fedd9..387538b16 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -123,9 +123,8 @@ server: # Linux 5.12+. When enabled, container UID 0 maps to an unprivileged host # UID and capabilities become namespaced. enableUserNamespaces: false - # Disable gateway authentication (mTLS client certificate requirement). - # Set to true when the gateway sits behind a reverse proxy (e.g. - # Cloudflare Tunnel) that terminates TLS. + # Deprecated: client certificates are now always optional when a client CA + # is configured. Kept for backward compatibility. disableGatewayAuth: false # Disable TLS entirely — the server listens on plaintext HTTP. # Set to true when a reverse proxy / tunnel terminates TLS at the edge. @@ -136,7 +135,8 @@ server: tls: # K8s secret (type kubernetes.io/tls) with tls.crt and tls.key for the server certSecretName: openshell-server-tls - # K8s secret with ca.crt for client certificate verification + # K8s secret with ca.crt for client certificate verification (mTLS). + # Set to "" to disable mTLS and run HTTPS-only (use OIDC for auth instead). clientCaSecretName: openshell-server-client-ca # K8s secret mounted into sandbox pods for mTLS to the server clientTlsSecretName: openshell-client-tls