Skip to content
Open
Show file tree
Hide file tree
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
89 changes: 89 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/openshell-bootstrap/src/pki.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ pub fn generate_pki(extra_sans: &[String]) -> Result<PkiBundle> {
client_params
.distinguished_name
.push(DnType::CommonName, "openshell-client");
client_params
.distinguished_name
.push(DnType::OrganizationalUnitName, "openshell-user");
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TaylorMutch I was hoping to make this change fully backward compatible but the mTLS client cert for the supervisor needs to have a role now to get through authz.


let client_cert = client_params
.signed_by(&client_key, &ca_cert, &ca_key)
Expand Down
30 changes: 18 additions & 12 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,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 mechanism.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlsConfig {
/// Path to the TLS certificate file.
Expand All @@ -327,16 +332,17 @@ 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,
/// Path to the CA certificate file for client certificate verification.
/// When `Some`, client certs signed by this CA are validated.
/// When `None`, the server does not request client certs.
#[serde(default)]
pub client_ca_path: Option<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).
/// When `true` and `client_ca_path` is `Some`, the TLS handshake rejects
/// connections that do not present a valid client certificate.
/// When `false`, client certificates are accepted but not required.
#[serde(default)]
pub allow_unauthenticated: bool,
pub require_client_auth: bool,
}

/// OIDC (`OpenID` Connect) configuration for JWT-based authentication.
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ petname = "2"
ipnet = "2"
tempfile = "3"
nix = { workspace = true }
x509-parser = "0.16"

[features]
dev-settings = ["openshell-core/dev-settings"]
Expand Down
46 changes: 29 additions & 17 deletions crates/openshell-server/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use openshell_core::ComputeDriverKind;
use openshell_core::config::{DEFAULT_DOCKER_NETWORK_NAME, DEFAULT_SERVER_PORT, DEFAULT_SSH_PORT};
use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use tracing::info;
use tracing::{info, warn};
use tracing_subscriber::EnvFilter;

use crate::certgen;
Expand Down Expand Up @@ -238,12 +238,6 @@ 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")]
disable_gateway_auth: bool,

/// OIDC issuer URL for JWT-based authentication.
/// When set, the server validates `authorization: Bearer` tokens on gRPC
/// requests against the issuer's JWKS endpoint.
Expand Down Expand Up @@ -335,6 +329,15 @@ 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 has_oidc = args.oidc_issuer.is_some();

if args.disable_tls && has_client_ca {
return Err(miette::miette!(
"--disable-tls and --tls-client-ca are mutually exclusive. Client mTLS authentication requires that TLS be enabled."
));
}

let tls = if args.disable_tls {
None
} else {
Expand All @@ -346,16 +349,11 @@ 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,
require_client_auth: has_client_ca && !has_oidc,
client_ca_path: args.tls_client_ca,
})
};

Expand Down Expand Up @@ -461,9 +459,23 @@ 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");
warn!("TLS disabled — listening on plaintext HTTP");
} else {
info!("TLS enabled — listening on encrypted HTTPS");
}

if has_client_ca {
info!("mTLS authentication enabled");
}
if has_oidc {
info!("OIDC authentication enabled");
}

if !has_client_ca && !has_oidc {
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");
Expand Down
8 changes: 4 additions & 4 deletions crates/openshell-server/src/compute/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,8 +599,8 @@ 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),
require_client_auth: false,
}))
.with_grpc_endpoint("https://gateway.internal:8443");

Expand Down Expand Up @@ -635,8 +635,8 @@ 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),
require_client_auth: false,
}))
.with_grpc_endpoint("https://gateway.internal:8443");
let vm_config = VmComputeConfig {
Expand Down
10 changes: 7 additions & 3 deletions crates/openshell-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,8 @@ 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(),
tls.require_client_auth,
)?)
} else {
info!("TLS disabled — accepting plaintext connections");
Expand Down Expand Up @@ -488,7 +488,11 @@ fn spawn_gateway_connection(
Ok(ConnectionProtocol::Tls | ConnectionProtocol::Unknown) => {
match acceptor.inner().accept(stream).await {
Ok(tls_stream) => {
if let Err(e) = service.serve(tls_stream).await {
let peer_identity = multiplex::extract_peer_identity(&tls_stream);
if let Err(e) = service
.serve_with_peer_identity(tls_stream, peer_identity)
.await
{
if is_benign_connection_close(e.as_ref()) {
debug!(error = %e, client = %addr, "Connection closed");
} else {
Expand Down
Loading
Loading