Skip to content

Commit 9a90a12

Browse files
authored
add core client cert validation (#309)
* update protos * update core dependencies * store new certs during setup * load new certs on startup * add helper for loading tls certs * update purge handler * add helper for removing files * validate received certs during setup * add interceptor to verify core cert serial * don't allow gateway gRPC server to start in non-TLS mode * update interceptor signature * add basic mTLS tests * align crypto libs * limit tokio features * review cleanup * use shared setup function * cleanup * expand crypto provider comment * review fixes * update dependencies * fix missing crypto provider init * remove unused config options * update cargo deny config * update protos * review fixes * update core dependencies
1 parent 529b0ef commit 9a90a12

13 files changed

Lines changed: 2698 additions & 206 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ axum = "0.8"
88
base64 = "0.22"
99
chrono = "0.4"
1010
clap = { version = "4.6", features = ["derive", "env"] }
11-
defguard_certs = { git = "https://github.com/DefGuard/defguard.git", rev = "01957186101fc105803d56f1190efbdb5102df2f" }
12-
defguard_version = { git = "https://github.com/DefGuard/defguard.git", rev = "01957186101fc105803d56f1190efbdb5102df2f" }
11+
defguard_certs = { git = "https://github.com/DefGuard/defguard.git", rev = "8ac70d3157d8a47b038bd1022ab9806bda642da4" }
12+
defguard_grpc_tls = { git = "https://github.com/DefGuard/defguard.git", rev = "8ac70d3157d8a47b038bd1022ab9806bda642da4" }
13+
defguard_version = { git = "https://github.com/DefGuard/defguard.git", rev = "8ac70d3157d8a47b038bd1022ab9806bda642da4" }
14+
rustls = { version = "0.23", default-features = false, features = ["ring"] }
15+
rustls-pki-types = "1"
16+
rustls-webpki = { version = "0.103", features = ["ring", "std"] }
1317
defguard_wireguard_rs = "0.9"
1418
env_logger = "0.11"
1519
ipnetwork = "0.21"
@@ -20,7 +24,7 @@ prost-types = "0.14"
2024
serde = { version = "1.0", features = ["derive"] }
2125
syslog = "7.0"
2226
thiserror = "2.0"
23-
tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread", "signal"] }
27+
tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread", "signal", "sync", "time"] }
2428
tokio-stream = { version = "0.1", features = [] }
2529
toml = { version = "1.0", default-features = false, features = [
2630
"parse",
@@ -46,7 +50,8 @@ mnl = "0.3"
4650
nix = { version = "0.31", default-features = false, features = ["ioctl"] }
4751

4852
[dev-dependencies]
49-
tokio = { version = "1", features = ["io-std", "io-util"] }
53+
defguard_certs = { git = "https://github.com/DefGuard/defguard.git", rev = "8ac70d3157d8a47b038bd1022ab9806bda642da4" }
54+
tokio = { version = "1", features = ["net"] }
5055
tonic = { version = "0.14", default-features = false, features = [
5156
"codegen",
5257
"router",

deny.toml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ feature-depth = 1
7070
# A list of advisory IDs to ignore. Note that ignored advisories will still
7171
# output a note when they are encountered.
7272
ignore = [
73+
{ id = "RUSTSEC-2023-0071", reason = "https://github.com/RustCrypto/RSA/issues/19" },
7374
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained" },
74-
{ id = "RUSTSEC-2025-0142", reason = "Awaiting upstream patch" },
7575
]
7676
# If this is true, then cargo deny will use the git executable to fetch advisory database.
7777
# If this is false, then it uses a built-in git library.
@@ -112,14 +112,29 @@ confidence-threshold = 0.8
112112
# aren't accepted for every possible crate as with the normal allow list
113113
exceptions = [
114114
{ allow = [
115+
"AGPL-3.0-only",
115116
"AGPL-3.0-or-later",
116117
], crate = "defguard-gateway" },
117118
{ allow = [
119+
"AGPL-3.0-only",
118120
"AGPL-3.0-or-later",
119121
], crate = "defguard_version" },
120122
{ allow = [
123+
"AGPL-3.0-only",
121124
"AGPL-3.0-or-later",
122125
], crate = "defguard_certs" },
126+
{ allow = [
127+
"AGPL-3.0-only",
128+
"AGPL-3.0-or-later",
129+
], crate = "defguard_grpc_tls" },
130+
{ allow = [
131+
"AGPL-3.0-only",
132+
"AGPL-3.0-or-later",
133+
], crate = "defguard_common" },
134+
{ allow = [
135+
"AGPL-3.0-only",
136+
"AGPL-3.0-or-later",
137+
], crate = "model_derive" },
123138
]
124139

125140
# Some crates don't have (easily) machine readable licensing information,

flake.lock

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

proto

src/config.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,6 @@ pub struct Config {
3535
#[arg(long, env = "DEFGUARD_GRPC_PORT", default_value = "50066")]
3636
pub(crate) grpc_port: u16,
3737

38-
/// Gateway gRPC server certificate.
39-
#[arg(long, env = "DEFGUARD_GATEWAY_GRPC_CERT")]
40-
pub(crate) grpc_cert: Option<String>,
41-
42-
/// Gateway gRPC server private key.
43-
#[arg(long, env = "DEFGUARD_GATEWAY_GRPC_KEY")]
44-
pub(crate) grpc_key: Option<String>,
45-
4638
/// Use userspace WireGuard implementation e.g. wireguard-go
4739
#[arg(long, short = 'u', env = "DEFGUARD_USERSPACE")]
4840
pub userspace: bool,
@@ -159,8 +151,6 @@ impl Default for Config {
159151
log_level: "info".into(),
160152
grpc_port: 50066,
161153
userspace: false,
162-
grpc_cert: None,
163-
grpc_key: None,
164154
stats_period: 15,
165155
ifname: "wg0".into(),
166156
pidfile: None,

src/gateway.rs

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,24 @@ use std::{
99
time::{Duration, SystemTime},
1010
};
1111

12+
use defguard_certs::{CertificateError, CertificateInfo};
13+
use defguard_grpc_tls::{certs::server_tls_config, server::certificate_serial_interceptor};
1214
use defguard_version::{
1315
ComponentInfo, DefguardComponent, Version, get_tracing_variables, server::DefguardVersionLayer,
1416
};
1517
use defguard_wireguard_rs::{WireguardInterfaceApi, net::IpAddrMask};
1618
use tokio::{
19+
fs::remove_file,
1720
sync::{mpsc, oneshot},
1821
time::interval,
1922
};
2023
use tokio_stream::wrappers::UnboundedReceiverStream;
21-
use tonic::{
22-
Request, Response, Status, Streaming,
23-
transport::{Identity, Server, ServerTlsConfig},
24-
};
24+
use tonic::{Request, Response, Status, Streaming, service::InterceptorLayer, transport::Server};
2525
use tower::ServiceBuilder;
2626
use tracing::instrument;
2727

2828
use crate::{
29-
GRPC_CERT_NAME, GRPC_KEY_NAME, VERSION,
29+
CORE_CLIENT_CERT_NAME, GRPC_CA_CERT_NAME, GRPC_CERT_NAME, GRPC_KEY_NAME, VERSION,
3030
config::Config,
3131
enterprise::firewall::{
3232
FirewallConfig, FirewallError, FirewallRule, SnatBinding,
@@ -144,6 +144,10 @@ type PubKey = String;
144144
pub struct TlsConfig {
145145
pub grpc_cert_pem: String,
146146
pub grpc_key_pem: String,
147+
/// PEM-encoded CA certificate used to verify Core's mTLS client certificate chain.
148+
pub grpc_ca_cert_pem: String,
149+
/// DER-encoded Core client certificate; used to extract and pin the expected serial.
150+
pub core_client_cert_der: Vec<u8>,
147151
}
148152

149153
pub struct Gateway {
@@ -558,9 +562,11 @@ impl GatewayServer {
558562
}
559563

560564
/// Starts the gateway process.
561-
/// * Retrieves configuration and configuration updates from Defguard gRPC server
562-
/// * Manages the interface according to configuration and updates
563-
/// * Sends interface statistics to Defguard server periodically
565+
/// * Requires a valid mTLS configuration to be set (via `set_tls_config`) before starting;
566+
/// returns an error if TLS configuration is absent - the gRPC server never starts in plain-text mode
567+
/// * Retrieves configuration and configuration updates from Defguard core via a mTLS-secured gRPC server
568+
/// * Manages the WireGuard interface according to configuration and updates
569+
/// * Sends interface statistics to Defguard core periodically
564570
pub async fn start(self, config: Config) -> Result<(), GatewayError> {
565571
info!("Starting Defguard Gateway version {VERSION} with configuration: {config:?}");
566572

@@ -593,36 +599,42 @@ impl GatewayServer {
593599
execute_command(post_up)?;
594600
}
595601

596-
let grpc_cert = self
597-
.gateway
598-
.lock()
599-
.unwrap()
600-
.tls_config
601-
.as_ref()
602-
.map(|c| c.grpc_cert_pem.clone());
603-
let grpc_key = self
602+
let tls_config = self
604603
.gateway
605604
.lock()
606-
.unwrap()
605+
.expect("gateway mutex poison")
607606
.tls_config
608-
.as_ref()
609-
.map(|c| c.grpc_key_pem.clone());
607+
.clone();
610608

611609
// Build gRPC server.
612610
let addr = config.grpc_socket();
613611
info!("gRPC server is listening on {addr}");
614-
let mut builder = if let (Some(cert), Some(key)) = (grpc_cert, grpc_key) {
615-
let identity = Identity::from_pem(cert, key);
616-
Server::builder().tls_config(ServerTlsConfig::new().identity(identity))?
617-
} else {
618-
Server::builder()
619-
};
612+
613+
let tls = tls_config.ok_or_else(|| {
614+
GatewayError::SetupError(
615+
"TLS configuration is required; gateway gRPC server cannot start without mTLS"
616+
.into(),
617+
)
618+
})?;
619+
620+
let tls_config =
621+
server_tls_config(&tls.grpc_cert_pem, &tls.grpc_key_pem, &tls.grpc_ca_cert_pem)
622+
.map_err(|e| GatewayError::SetupError(e.to_string()))?;
623+
let mut builder = Server::builder().tls_config(tls_config)?;
624+
625+
// Extract Core client cert serial for pinning.
626+
let expected_serial = CertificateInfo::from_der(&tls.core_client_cert_der)
627+
.map_err(|e: CertificateError| GatewayError::SetupError(e.to_string()))?
628+
.serial;
620629

621630
// Start gRPC server. This should run indefinitely.
622631
debug!("Serving gRPC");
623632
builder
624633
.add_service(
625634
ServiceBuilder::new()
635+
.layer(InterceptorLayer::new(certificate_serial_interceptor(
636+
expected_serial,
637+
)))
626638
.layer(DefguardVersionLayer::new(Version::parse(VERSION)?))
627639
.service(gateway_server::GatewayServer::new(self)),
628640
)
@@ -760,25 +772,30 @@ impl gateway_server::Gateway for GatewayServer {
760772
debug!("Received purge request, removing gRPC certificate files");
761773
let cert_path = self.cert_dir.join(GRPC_CERT_NAME);
762774
let key_path = self.cert_dir.join(GRPC_KEY_NAME);
775+
let ca_cert_path = self.cert_dir.join(GRPC_CA_CERT_NAME);
776+
let core_client_cert_path = self.cert_dir.join(CORE_CLIENT_CERT_NAME);
777+
778+
let remove_cert_file = async |path: &std::path::Path, label: &str| -> Result<(), Status> {
779+
match remove_file(path).await {
780+
Ok(()) => {
781+
info!("Removed {label} at {}", path.display());
782+
Ok(())
783+
}
784+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
785+
debug!("{label} not found at {}, skipping removal", path.display());
786+
Ok(())
787+
}
788+
Err(err) => {
789+
error!("Failed to remove {label} at {}: {err}", path.display());
790+
Err(Status::internal(format!("Failed to remove {label}")))
791+
}
792+
}
793+
};
763794

764-
if let Err(err) = tokio::fs::remove_file(&cert_path).await
765-
&& err.kind() != std::io::ErrorKind::NotFound
766-
{
767-
error!(
768-
"Failed to remove gRPC certificate at {}: {err}",
769-
cert_path.display()
770-
);
771-
return Err(Status::internal("Failed to remove gRPC certificate"));
772-
}
773-
info!("Removed gRPC certificate at {}", cert_path.display());
774-
775-
if let Err(err) = tokio::fs::remove_file(&key_path).await
776-
&& err.kind() != std::io::ErrorKind::NotFound
777-
{
778-
error!("Failed to remove gRPC key at {}: {err}", key_path.display());
779-
return Err(Status::internal("Failed to remove gRPC key"));
780-
}
781-
info!("Removed gRPC key at {}", cert_path.display());
795+
remove_cert_file(&cert_path, "gRPC certificate").await?;
796+
remove_cert_file(&key_path, "gRPC key").await?;
797+
remove_cert_file(&ca_cert_path, "CA certificate").await?;
798+
remove_cert_file(&core_client_cert_path, "Core client certificate").await?;
782799

783800
// Prepare underlying `Gateway` to enter setup mode.
784801
self.gateway

src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,27 @@ pub mod enterprise;
5757
pub mod logging;
5858
pub mod setup;
5959

60+
#[cfg(test)]
61+
mod tests;
62+
6063
pub const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "+", env!("VERGEN_GIT_SHA"));
6164

65+
/// Install the `ring` CryptoProvider as the process-wide default for rustls.
66+
///
67+
/// Must be called once near process startup, before any TLS code runs. Both
68+
/// `ring` and `aws-lc-rs` may be present as transitive dependencies; without
69+
/// an explicit selection rustls panics at runtime. Subsequent calls are
70+
/// silently ignored (`.ok()` swallows the `AlreadySet` error).
71+
pub fn init_crypto_provider() {
72+
rustls::crypto::ring::default_provider()
73+
.install_default()
74+
.ok();
75+
}
76+
6277
pub const GRPC_CERT_NAME: &str = "gateway_grpc_cert.pem";
6378
pub const GRPC_KEY_NAME: &str = "gateway_grpc_key.pem";
79+
pub const GRPC_CA_CERT_NAME: &str = "grpc_ca_cert.pem";
80+
pub const CORE_CLIENT_CERT_NAME: &str = "core_client_cert.pem";
6481

6582
/// Masks object's field with "***" string.
6683
/// Used to log sensitive/secret objects.

0 commit comments

Comments
 (0)