Skip to content

Commit 96e496b

Browse files
committed
feat(server): separate HTTPS from mTLS authentication
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.
1 parent 6deb1f0 commit 96e496b

20 files changed

Lines changed: 420 additions & 240 deletions

File tree

Cargo.lock

Lines changed: 89 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/openshell-core/src/config.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -326,10 +326,15 @@ pub struct ServiceRoutingConfig {
326326

327327
/// TLS configuration.
328328
///
329-
/// By default mTLS is enforced — all clients must present a certificate
330-
/// signed by the given CA. When `allow_unauthenticated` is `true`, the
331-
/// TLS handshake also accepts connections without a client certificate
332-
/// (needed for reverse-proxy deployments like Cloudflare Tunnel).
329+
/// Two modes are supported:
330+
/// - **HTTPS with optional mTLS** (`client_ca_path = Some`):
331+
/// Client certificates are validated against the given CA when presented,
332+
/// but never required. Clients may connect with or without a certificate.
333+
/// - **HTTPS-only** (`client_ca_path = None`):
334+
/// Server-side TLS only; no client certificates are requested.
335+
///
336+
/// In both modes, authentication is handled at the application layer
337+
/// (e.g. OIDC bearer tokens). mTLS is an additional mechanism.
333338
#[derive(Debug, Clone, Serialize, Deserialize)]
334339
pub struct TlsConfig {
335340
/// Path to the TLS certificate file.
@@ -338,16 +343,17 @@ pub struct TlsConfig {
338343
/// Path to the TLS private key file.
339344
pub key_path: PathBuf,
340345

341-
/// Path to the CA certificate file for client certificate verification (mTLS).
342-
/// The server requires all clients to present a valid certificate signed by
343-
/// this CA.
344-
pub client_ca_path: PathBuf,
346+
/// Path to the CA certificate file for client certificate verification.
347+
/// When `Some`, client certs signed by this CA are validated.
348+
/// When `None`, the server does not request client certs.
349+
#[serde(default)]
350+
pub client_ca_path: Option<PathBuf>,
345351

346-
/// When `true`, the TLS handshake succeeds even without a client
347-
/// certificate. Application-layer middleware must then enforce auth
348-
/// (e.g. via a CF JWT header).
352+
/// When `true` and `client_ca_path` is `Some`, the TLS handshake rejects
353+
/// connections that do not present a valid client certificate.
354+
/// When `false`, client certificates are accepted but not required.
349355
#[serde(default)]
350-
pub allow_unauthenticated: bool,
356+
pub require_client_auth: bool,
351357
}
352358

353359
/// OIDC (`OpenID` Connect) configuration for JWT-based authentication.

crates/openshell-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ petname = "2"
8888
ipnet = "2"
8989
tempfile = "3"
9090
nix = { workspace = true }
91+
x509-parser = "0.16"
9192

9293
[features]
9394
dev-settings = ["openshell-core/dev-settings"]

crates/openshell-server/src/cli.rs

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use openshell_core::config::{
1212
};
1313
use std::net::{IpAddr, SocketAddr};
1414
use std::path::PathBuf;
15-
use tracing::info;
15+
use tracing::{info, warn};
1616
use tracing_subscriber::EnvFilter;
1717

1818
use crate::certgen;
@@ -248,12 +248,6 @@ struct RunArgs {
248248
#[arg(long, env = "OPENSHELL_DISABLE_TLS")]
249249
disable_tls: bool,
250250

251-
/// Disable gateway authentication (mTLS client certificate requirement).
252-
/// When set, the TLS handshake accepts connections without a client
253-
/// certificate. Ignored when --disable-tls is set.
254-
#[arg(long, env = "OPENSHELL_DISABLE_GATEWAY_AUTH")]
255-
disable_gateway_auth: bool,
256-
257251
/// OIDC issuer URL for JWT-based authentication.
258252
/// When set, the server validates `authorization: Bearer` tokens on gRPC
259253
/// requests against the issuer's JWKS endpoint.
@@ -345,6 +339,15 @@ async fn run_from_args(args: RunArgs) -> Result<()> {
345339

346340
let bind = SocketAddr::new(args.bind_address, args.port);
347341

342+
let has_client_ca = args.tls_client_ca.is_some();
343+
let has_oidc = args.oidc_issuer.is_some();
344+
345+
if args.disable_tls && has_client_ca {
346+
return Err(miette::miette!(
347+
"--disable-tls and --tls-client-ca are mutually exclusive. Client mTLS authentication requires that TLS be enabled."
348+
));
349+
}
350+
348351
let tls = if args.disable_tls {
349352
None
350353
} else {
@@ -356,16 +359,11 @@ async fn run_from_args(args: RunArgs) -> Result<()> {
356359
let key_path = args.tls_key.ok_or_else(|| {
357360
miette::miette!("--tls-key is required when TLS is enabled (use --disable-tls to skip)")
358361
})?;
359-
let client_ca_path = args.tls_client_ca.ok_or_else(|| {
360-
miette::miette!(
361-
"--tls-client-ca is required when TLS is enabled (use --disable-tls to skip)"
362-
)
363-
})?;
364362
Some(openshell_core::TlsConfig {
365363
cert_path,
366364
key_path,
367-
client_ca_path,
368-
allow_unauthenticated: args.disable_gateway_auth,
365+
require_client_auth: has_client_ca && !has_oidc,
366+
client_ca_path: args.tls_client_ca,
369367
})
370368
};
371369

@@ -476,9 +474,23 @@ async fn run_from_args(args: RunArgs) -> Result<()> {
476474
};
477475

478476
if args.disable_tls {
479-
info!("TLS disabled — listening on plaintext HTTP");
480-
} else if args.disable_gateway_auth {
481-
info!("Gateway auth disabled — accepting connections without client certificates");
477+
warn!("TLS disabled — listening on plaintext HTTP");
478+
} else {
479+
info!("TLS enabled — listening on encrypted HTTPS");
480+
}
481+
482+
if has_client_ca {
483+
info!("mTLS authentication enabled");
484+
}
485+
if has_oidc {
486+
info!("OIDC authentication enabled");
487+
}
488+
489+
if !has_client_ca && !has_oidc {
490+
warn!(
491+
"Neither mTLS (--tls-client-ca) nor OIDC (--oidc-issuer) is configured — \
492+
the gateway has no authentication mechanism"
493+
);
482494
}
483495

484496
info!(bind = %config.bind_address, "Starting OpenShell server");

crates/openshell-server/src/compute/vm.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -610,8 +610,8 @@ mod tests {
610610
let config = Config::new(Some(TlsConfig {
611611
cert_path: server_cert,
612612
key_path: server_key,
613-
client_ca_path: server_ca,
614-
allow_unauthenticated: false,
613+
client_ca_path: Some(server_ca),
614+
require_client_auth: false,
615615
}))
616616
.with_grpc_endpoint("https://gateway.internal:8443");
617617

@@ -646,8 +646,8 @@ mod tests {
646646
let config = Config::new(Some(TlsConfig {
647647
cert_path: server_cert.clone(),
648648
key_path: server_key.clone(),
649-
client_ca_path: server_ca,
650-
allow_unauthenticated: false,
649+
client_ca_path: Some(server_ca),
650+
require_client_auth: false,
651651
}))
652652
.with_grpc_endpoint("https://gateway.internal:8443");
653653
let vm_config = VmComputeConfig {

crates/openshell-server/src/lib.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,8 +289,8 @@ pub async fn run_server(
289289
Some(TlsAcceptor::from_files(
290290
&tls.cert_path,
291291
&tls.key_path,
292-
&tls.client_ca_path,
293-
tls.allow_unauthenticated,
292+
tls.client_ca_path.as_deref(),
293+
tls.require_client_auth,
294294
)?)
295295
} else {
296296
info!("TLS disabled — accepting plaintext connections");
@@ -484,7 +484,11 @@ fn spawn_gateway_connection(
484484
Ok(ConnectionProtocol::Tls | ConnectionProtocol::Unknown) => {
485485
match acceptor.inner().accept(stream).await {
486486
Ok(tls_stream) => {
487-
if let Err(e) = service.serve(tls_stream).await {
487+
let peer_identity = multiplex::extract_peer_identity(&tls_stream);
488+
if let Err(e) = service
489+
.serve_with_peer_identity(tls_stream, peer_identity)
490+
.await
491+
{
488492
error!(error = %e, client = %addr, "Connection error");
489493
}
490494
}

0 commit comments

Comments
 (0)