Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
1,851 changes: 1,781 additions & 70 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ homepage = "https://github.com/DefGuard/proxy"
repository = "https://github.com/DefGuard/proxy"

[dependencies]
defguard_certs = { git = "https://github.com/DefGuard/defguard.git", rev = "01957186101fc105803d56f1190efbdb5102df2f" }
defguard_version = { git = "https://github.com/DefGuard/defguard.git", rev = "01957186101fc105803d56f1190efbdb5102df2f" }
defguard_certs = { git = "https://github.com/DefGuard/defguard.git", rev = "8ac70d3157d8a47b038bd1022ab9806bda642da4" }
defguard_grpc_tls = { git = "https://github.com/DefGuard/defguard.git", rev = "8ac70d3157d8a47b038bd1022ab9806bda642da4" }
defguard_version = { git = "https://github.com/DefGuard/defguard.git", rev = "8ac70d3157d8a47b038bd1022ab9806bda642da4" }
rustls-webpki = { version = "0.103", features = ["aws-lc-rs", "std"] }
rustls-pki-types = "1"
# base `axum` deps
axum = { version = "0.8", features = ["ws"] }
axum-client-ip = "0.7"
Expand All @@ -20,7 +23,7 @@ axum-extra = { version = "0.10", features = [
axum-server = { version = "0.8", features = ["tls-rustls"] }
# match axum-extra -> cookies
time = { version = "0.3", default-features = false }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time"] }
tokio-stream = "0.1"
tower-http = { version = "0.6", features = ["fs", "trace"] }
# logging/tracing
Expand Down Expand Up @@ -61,6 +64,9 @@ rustls = { version = "0.23", default-features = false, features = [
instant-acme = { version = "0.8", features = ["hyper-rustls", "aws-lc-rs"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "json"] }

[dev-dependencies]
tokio = { version = "1", features = ["net"] }

Comment thread
wojcik91 marked this conversation as resolved.
[build-dependencies]
tonic-prost-build = "0.14"
vergen-git2 = { version = "9.1", features = ["build"] }
Expand Down
16 changes: 15 additions & 1 deletion deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ feature-depth = 1
#db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = []
ignore = [
{ id = "RUSTSEC-2023-0071", reason = "https://github.com/RustCrypto/RSA/issues/19" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
Expand Down Expand Up @@ -115,6 +117,18 @@ exceptions = [
"AGPL-3.0-only",
"AGPL-3.0-or-later",
], crate = "defguard_certs" },
{ allow = [
"AGPL-3.0-only",
"AGPL-3.0-or-later",
], crate = "defguard_grpc_tls" },
{ allow = [
"AGPL-3.0-only",
"AGPL-3.0-or-later",
], crate = "defguard_common" },
{ allow = [
"AGPL-3.0-only",
"AGPL-3.0-or-later",
], crate = "model_derive" },
]

# Some crates don't have (easily) machine readable licensing information,
Expand Down
12 changes: 6 additions & 6 deletions flake.lock

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

2 changes: 1 addition & 1 deletion proto
32 changes: 0 additions & 32 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ use std::{fs::read_to_string, net::IpAddr, path::PathBuf, time::Duration};
use clap::Parser;
use log::LevelFilter;
use serde::Deserialize;
use url::Url;

fn default_url() -> Url {
Url::parse("http://localhost:8080").unwrap()
}

fn default_adoption_timeout() -> u64 {
10
Expand All @@ -29,22 +24,6 @@ pub struct EnvConfig {
#[arg(long, env = "DEFGUARD_PROXY_GRPC_PORT", default_value_t = 50051)]
pub grpc_port: u16,

#[arg(long, env = "DEFGUARD_PROXY_GRPC_CERT")]
#[serde(skip_serializing)]
#[deprecated(
since = "2.0.0",
note = "Certificates are automatically generated by Core CA"
)]
pub grpc_cert: Option<String>,

#[arg(long, env = "DEFGUARD_PROXY_GRPC_KEY")]
#[serde(skip_serializing)]
#[deprecated(
since = "2.0.0",
note = "Certificates are automatically generated by Core CA"
)]
pub grpc_key: Option<String>,

#[arg(long, env = "DEFGUARD_PROXY_LOG_LEVEL", default_value_t = LevelFilter::Info)]
pub log_level: LevelFilter,

Expand All @@ -54,17 +33,6 @@ pub struct EnvConfig {
#[arg(long, env = "DEFGUARD_PROXY_RATELIMIT_BURST", default_value_t = 0)]
pub rate_limit_burst: u32,

#[arg(
long,
env = "DEFGUARD_PROXY_URL",
value_parser = Url::parse,
default_value = "http://localhost:8080"
)]
#[serde(default = "default_url")]
#[serde(skip_serializing)]
#[deprecated(since = "2.0.0", note = "Public URL is generated by Core instead")]
pub url: Url,

/// Configuration file path
#[arg(long = "config", short)]
#[serde(skip)]
Expand Down
105 changes: 63 additions & 42 deletions src/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,26 @@ use std::{
};

use axum_extra::extract::cookie::Key;
use defguard_certs::{CertificateError, CertificateInfo};
use defguard_grpc_tls::{certs::server_tls_config, server::certificate_serial_interceptor};
use defguard_version::{
ComponentInfo, DefguardComponent, Version, get_tracing_variables,
server::{DefguardVersionLayer, grpc::DefguardVersionInterceptor},
};
use tokio::sync::{broadcast, mpsc, oneshot};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tonic::{
Request, Response, Status, Streaming,
transport::{Identity, Server, ServerTlsConfig},
use tokio::{
fs::remove_file,
sync::{broadcast, mpsc, oneshot},
};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tonic::{Request, Response, Status, Streaming, service::InterceptorLayer, transport::Server};
use tower::ServiceBuilder;
use tracing::Instrument;

use crate::{
LogsReceiver, MIN_CORE_VERSION, VERSION, acme,
acme::Port80Permit,
error::ApiError,
http::{GRPC_CERT_NAME, GRPC_KEY_NAME},
http::{CORE_CLIENT_CERT_NAME, GRPC_CA_CERT_NAME, GRPC_CERT_NAME, GRPC_KEY_NAME},
proto::{
AcmeCertificate, AcmeChallenge, AcmeIssueEvent, AcmeLogs, AcmeProgress, AcmeStep,
CoreRequest, CoreResponse, DeviceInfo, acme_issue_event, core_request, core_response,
Expand All @@ -40,9 +42,13 @@ use crate::{
type ClientMap = HashMap<SocketAddr, mpsc::UnboundedSender<Result<CoreRequest, Status>>>;

#[derive(Debug, Clone, Default)]
pub struct Configuration {
pub struct TlsConfig {
pub grpc_key_pem: String,
pub grpc_cert_pem: String,
/// PEM-encoded CA certificate used to verify Core's mTLS client certificate chain.
pub grpc_ca_cert_pem: String,
/// DER-encoded Core client certificate; used to extract and pin the expected serial.
pub core_client_cert_der: Vec<u8>,
}

pub(crate) struct ProxyServer {
Expand All @@ -51,7 +57,7 @@ pub(crate) struct ProxyServer {
results: Arc<RwLock<HashMap<u64, oneshot::Sender<core_response::Payload>>>>,
pub(crate) connected: Arc<AtomicBool>,
pub(crate) core_version: Arc<Mutex<Option<Version>>>,
config: Arc<Mutex<Option<Configuration>>>,
tls_config: Arc<Mutex<Option<TlsConfig>>>,
cookie_key: Arc<RwLock<Option<Key>>>,
cert_dir: PathBuf,
reset_tx: broadcast::Sender<()>,
Expand Down Expand Up @@ -87,7 +93,7 @@ impl ProxyServer {
results: Arc::new(RwLock::new(HashMap::new())),
connected: Arc::new(AtomicBool::new(false)),
core_version: Arc::new(Mutex::new(None)),
config: Arc::new(Mutex::new(None)),
tls_config: Arc::new(Mutex::new(None)),
cert_dir,
reset_tx,
https_cert_tx,
Expand All @@ -98,17 +104,17 @@ impl ProxyServer {
}
}

pub(crate) fn configure(&self, config: Configuration) {
pub(crate) fn configure(&self, config: TlsConfig) {
let mut lock = self
.config
.tls_config
.lock()
.expect("Failed to acquire lock on config mutex when applying proxy configuration");
*lock = Some(config);
}

pub(crate) fn get_configuration(&self) -> Option<Configuration> {
pub(crate) fn get_tls_config(&self) -> Option<TlsConfig> {
let lock = self
.config
.tls_config
.lock()
.expect("Failed to acquire lock on config mutex when retrieving proxy configuration");
lock.clone()
Expand All @@ -119,19 +125,27 @@ impl ProxyServer {
F: Future<Output = ()> + Send + 'static,
{
info!("Starting gRPC server on {addr}");
let config = self.get_configuration();
let (grpc_cert, grpc_key) = if let Some(cfg) = config {
(cfg.grpc_cert_pem, cfg.grpc_key_pem)
} else {
return Err(anyhow::anyhow!("gRPC server configuration is missing"));
};

let identity = Identity::from_pem(grpc_cert, grpc_key);
let mut builder =
Server::builder().tls_config(ServerTlsConfig::new().identity(identity))?;
let tls_config = self
.get_tls_config()
.ok_or_else(|| anyhow::anyhow!("gRPC server TLS configuration is missing"))?;

// Extract Core client cert serial for pinning (None in no-TLS mode).
let expected_serial = CertificateInfo::from_der(&tls_config.core_client_cert_der)
.map_err(|e: CertificateError| anyhow::anyhow!("invalid core client cert DER: {e}"))?
.serial;

let tls_config = server_tls_config(
&tls_config.grpc_cert_pem,
&tls_config.grpc_key_pem,
&tls_config.grpc_ca_cert_pem,
)?;
let mut builder = Server::builder().tls_config(tls_config)?;

let own_version = Version::parse(VERSION)?;
let versioned_service = ServiceBuilder::new()
.layer(InterceptorLayer::new(certificate_serial_interceptor(
expected_serial,
)))
.layer(tonic::service::InterceptorLayer::new(
DefguardVersionInterceptor::new(
own_version.clone(),
Expand Down Expand Up @@ -197,7 +211,7 @@ impl ProxyServer {

pub(crate) fn setup_completed(&self) -> bool {
let lock = self
.config
.tls_config
.lock()
.expect("Failed to acquire lock on config mutex when checking setup status");
lock.is_some()
Expand All @@ -213,7 +227,7 @@ impl Clone for ProxyServer {
connected: Arc::clone(&self.connected),
core_version: Arc::clone(&self.core_version),
cookie_key: Arc::clone(&self.cookie_key),
config: Arc::clone(&self.config),
tls_config: Arc::clone(&self.tls_config),
cert_dir: self.cert_dir.clone(),
reset_tx: self.reset_tx.clone(),
https_cert_tx: self.https_cert_tx.clone(),
Expand Down Expand Up @@ -343,26 +357,33 @@ impl proxy_server::Proxy for ProxyServer {
debug!("Received purge request, removing gRPC certificate files");
let cert_path = self.cert_dir.join(GRPC_CERT_NAME);
let key_path = self.cert_dir.join(GRPC_KEY_NAME);
let ca_cert_path = self.cert_dir.join(GRPC_CA_CERT_NAME);
let core_client_cert_path = self.cert_dir.join(CORE_CLIENT_CERT_NAME);

let remove_cert_file = async |path: &std::path::Path, label: &str| -> Result<(), Status> {
match remove_file(path).await {
Ok(()) => {
info!("Removed {label} at {}", path.display());
Ok(())
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
debug!("{label} not found at {}, skipping removal", path.display());
Ok(())
}
Err(err) => {
error!("Failed to remove {label} at {}: {err}", path.display());
Err(Status::internal(format!("Failed to remove {label}")))
}
}
};

if let Err(err) = tokio::fs::remove_file(&cert_path).await
&& err.kind() != std::io::ErrorKind::NotFound
{
error!(
"Failed to remove gRPC certificate at {:?}: {err}",
cert_path
);
return Err(Status::internal("Failed to remove gRPC certificate"));
}

if let Err(err) = tokio::fs::remove_file(&key_path).await
&& err.kind() != std::io::ErrorKind::NotFound
{
error!("Failed to remove gRPC key at {:?}: {err}", key_path);
return Err(Status::internal("Failed to remove gRPC key"));
}
remove_cert_file(&cert_path, "gRPC certificate").await?;
remove_cert_file(&key_path, "gRPC key").await?;
remove_cert_file(&ca_cert_path, "CA certificate").await?;
remove_cert_file(&core_client_cert_path, "Core client certificate").await?;

*self
.config
.tls_config
.lock()
.expect("Failed to lock config mutex during purge") = None;
*self
Expand Down
Loading
Loading