Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Cargo.lock

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

100 changes: 23 additions & 77 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ use clap::crate_version;
use defguard_version::{Version, server::DefguardVersionLayer};
use serde::Serialize;
use tokio::{
fs::OpenOptions,
io::AsyncWriteExt,
sync::{broadcast, mpsc, oneshot},
task::JoinSet,
};
Expand Down Expand Up @@ -53,10 +51,7 @@ const DEFGUARD_CORE_VERSION_HEADER: &str = "defguard-core-version";
const RATE_LIMITER_CLEANUP_PERIOD: Duration = Duration::from_secs(60);
const X_FORWARDED_FOR: &str = "x-forwarded-for";
const X_POWERED_BY: &str = "x-powered-by";
pub const GRPC_CERT_NAME: &str = "proxy_grpc_cert.pem";
pub const GRPC_KEY_NAME: &str = "proxy_grpc_key.pem";
pub const GRPC_CA_CERT_NAME: &str = "grpc_ca_cert.pem";
pub const CORE_CLIENT_CERT_NAME: &str = "core_client_cert.pem";
pub use crate::setup::{CORE_CLIENT_CERT_NAME, GRPC_CA_CERT_NAME, GRPC_CERT_NAME, GRPC_KEY_NAME};

#[derive(Clone)]
pub(crate) struct AppState {
Expand Down Expand Up @@ -181,7 +176,6 @@ async fn powered_by_header<B>(mut response: Response<B>) -> Response<B> {
}

pub async fn run_setup(env_config: &EnvConfig, logs_rx: LogsReceiver) -> anyhow::Result<TlsConfig> {
let setup_server = ProxySetupServer::new(logs_rx);
let cert_dir = Path::new(&env_config.cert_dir);
if !cert_dir.exists() {
tokio::fs::create_dir_all(cert_dir).await.map_err(|err| {
Expand All @@ -196,13 +190,34 @@ pub async fn run_setup(env_config: &EnvConfig, logs_rx: LogsReceiver) -> anyhow:
})?;
#[cfg(unix)]
tokio::fs::set_permissions(cert_dir, Permissions::from_mode(0o700)).await?;
} else {
// verify write access before starting the setup server
let test_path = cert_dir.join(".write_test");
match tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&test_path)
.await
{
Ok(_) => {
let _ = tokio::fs::remove_file(&test_path).await;
}
Err(err) if err.kind() == ErrorKind::PermissionDenied => {
anyhow::bail!(
"Certificate directory {} is not writable. Permission denied.",
cert_dir.display()
);
}
Err(err) => return Err(err.into()),
}
}

// Only attempt setup if not already configured
info!(
"No gRPC TLS certificates found at {}, new certificates will be obtained during setup",
cert_dir.display()
);
let setup_server = ProxySetupServer::new(logs_rx, cert_dir.to_path_buf());
let tls_config = setup_server
.await_initial_setup(
SocketAddr::new(
Expand All @@ -216,75 +231,6 @@ pub async fn run_setup(env_config: &EnvConfig, logs_rx: LogsReceiver) -> anyhow:
.await?;
info!("Generated new gRPC TLS certificates and signed by Defguard Core");

let TlsConfig {
grpc_cert_pem,
grpc_key_pem,
grpc_ca_cert_pem,
core_client_cert_der,
} = &tls_config;

let cert_path = cert_dir.join(GRPC_CERT_NAME);
let key_path = cert_dir.join(GRPC_KEY_NAME);
// Certificate and its key will be accessed only to this process's user.
let mut options = OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
options.mode(0o600); // rw-------

// Write certificate to a file.
options
.clone()
.open(&cert_path)
.await?
.write_all(grpc_cert_pem.as_bytes())
.await.map_err(|err| {
if err.kind() == ErrorKind::PermissionDenied {
anyhow::anyhow!(
"Cannot write certificate file {}. Permission denied for certificate directory {}.",
cert_path.display(),
cert_dir.display()
)
} else {
err.into()
}
})?;
// Write key to a file.
options
.clone()
.open(&key_path)
.await?
.write_all(grpc_key_pem.as_bytes())
.await
.map_err(|err| {
if err.kind() == ErrorKind::PermissionDenied {
anyhow::anyhow!(
"Cannot write key file {}. Permission denied for certificate directory {}.",
key_path.display(),
cert_dir.display()
)
} else {
err.into()
}
})?;
// Write CA certificate to a file.
options
.clone()
.open(cert_dir.join(GRPC_CA_CERT_NAME))
.await?
.write_all(grpc_ca_cert_pem.as_bytes())
.await?;
// Write Core client certificate (PEM-encoded) to a file for serial pinning on restart.
let core_client_cert_pem =
defguard_certs::der_to_pem(core_client_cert_der, defguard_certs::PemLabel::Certificate)
.map_err(|err| {
anyhow::anyhow!("Failed to PEM-encode Core client certificate: {err}")
})?;
options
.open(cert_dir.join(CORE_CLIENT_CERT_NAME))
.await?
.write_all(core_client_cert_pem.as_bytes())
.await?;

Ok(tls_config)
}

Expand Down
117 changes: 115 additions & 2 deletions src/setup.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{
net::SocketAddr,
path::{Path, PathBuf},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
Expand All @@ -12,7 +13,11 @@ use defguard_version::{
server::{DefguardVersionLayer, grpc::DefguardVersionInterceptor},
};
use rustls_pki_types::{CertificateDer, UnixTime};
use tokio::sync::{mpsc, oneshot};
use tokio::{
fs::OpenOptions,
io::AsyncWriteExt,
sync::{mpsc, oneshot},
};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tonic::{Request, Response, Status, service::InterceptorLayer, transport::Server};
use webpki::{KeyUsage, anchor_from_trusted_cert};
Expand All @@ -27,6 +32,11 @@ use crate::{

const AUTH_HEADER: &str = "authorization";

pub const GRPC_CERT_NAME: &str = "proxy_grpc_cert.pem";
pub const GRPC_KEY_NAME: &str = "proxy_grpc_key.pem";
pub const GRPC_CA_CERT_NAME: &str = "grpc_ca_cert.pem";
pub const CORE_CLIENT_CERT_NAME: &str = "core_client_cert.pem";

/// Verify that both `component_der` and `core_client_der` are signed by `ca_der`.
///
/// Uses ECDSA P-256 via `aws-lc-rs` (Linux-only deployment; FIPS-capable).
Expand Down Expand Up @@ -88,13 +98,104 @@ fn validate_cert_bundle(
Ok(())
}

async fn save_tls_certs(tls_config: &TlsConfig, cert_dir: &Path) -> Result<(), String> {
let cert_path = cert_dir.join(GRPC_CERT_NAME);
let key_path = cert_dir.join(GRPC_KEY_NAME);
let ca_cert_path = cert_dir.join(GRPC_CA_CERT_NAME);
let client_cert_path = cert_dir.join(CORE_CLIENT_CERT_NAME);

let mut options = OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
options.mode(0o600); // rw-------

// PEM-encode the Core client certificate DER for serial pinning on restart.
let core_client_cert_pem = defguard_certs::der_to_pem(
&tls_config.core_client_cert_der,
defguard_certs::PemLabel::Certificate,
)
.map_err(|err| format!("Failed to PEM-encode Core client certificate: {err}"))?;

// Write component (server) certificate.
options
.clone()
.open(&cert_path)
.await
.map_err(|err| {
format!(
"Cannot open certificate file {}: {err}",
cert_path.display()
)
})?
.write_all(tls_config.grpc_cert_pem.as_bytes())
.await
.map_err(|err| {
format!(
"Cannot write certificate file {}: {err}",
cert_path.display()
)
})?;

// Write private key.
options
.clone()
.open(&key_path)
.await
.map_err(|err| format!("Cannot open key file {}: {err}", key_path.display()))?
.write_all(tls_config.grpc_key_pem.as_bytes())
.await
.map_err(|err| format!("Cannot write key file {}: {err}", key_path.display()))?;

// Write CA certificate.
options
.clone()
.open(&ca_cert_path)
.await
.map_err(|err| {
format!(
"Cannot open CA certificate file {}: {err}",
ca_cert_path.display()
)
})?
.write_all(tls_config.grpc_ca_cert_pem.as_bytes())
.await
.map_err(|err| {
format!(
"Cannot write CA certificate file {}: {err}",
ca_cert_path.display()
)
})?;

// Write Core client certificate (PEM) for serial pinning on restart.
options
.open(&client_cert_path)
.await
.map_err(|err| {
format!(
"Cannot open Core client certificate file {}: {err}",
client_cert_path.display()
)
})?
.write_all(core_client_cert_pem.as_bytes())
.await
.map_err(|err| {
format!(
"Cannot write Core client certificate file {}: {err}",
client_cert_path.display()
)
})?;

Ok(())
}

pub(crate) struct ProxySetupServer {
key_pair: Arc<Mutex<Option<defguard_certs::RcGenKeyPair>>>,
logs_rx: LogsReceiver,
current_session_token: Arc<Mutex<Option<String>>>,
setup_tx: Arc<tokio::sync::Mutex<Option<oneshot::Sender<TlsConfig>>>>,
setup_rx: Arc<tokio::sync::Mutex<oneshot::Receiver<TlsConfig>>>,
adoption_expired: Arc<AtomicBool>,
cert_dir: Arc<PathBuf>,
}

impl Clone for ProxySetupServer {
Expand All @@ -106,12 +207,13 @@ impl Clone for ProxySetupServer {
setup_tx: Arc::clone(&self.setup_tx),
setup_rx: Arc::clone(&self.setup_rx),
adoption_expired: Arc::clone(&self.adoption_expired),
cert_dir: Arc::clone(&self.cert_dir),
}
}
}

impl ProxySetupServer {
pub fn new(logs_rx: LogsReceiver) -> Self {
pub fn new(logs_rx: LogsReceiver, cert_dir: PathBuf) -> Self {
let (setup_tx, setup_rx) = oneshot::channel();
Self {
key_pair: Arc::new(Mutex::new(None)),
Expand All @@ -120,6 +222,7 @@ impl ProxySetupServer {
setup_tx: Arc::new(tokio::sync::Mutex::new(Some(setup_tx))),
setup_rx: Arc::new(tokio::sync::Mutex::new(setup_rx)),
adoption_expired: Arc::new(AtomicBool::new(false)),
cert_dir: Arc::new(cert_dir),
}
}

Expand Down Expand Up @@ -490,6 +593,16 @@ impl proxy_setup_server::ProxySetup for ProxySetupServer {
core_client_cert_der: bundle.core_client_cert_der,
};

debug!("Saving TLS certificate files to disk");
if let Err(err) = save_tls_certs(&configuration, &self.cert_dir).await {
error!("Failed to save TLS certificates: {err}");
self.clear_setup_session();
return Err(Status::internal(format!(
"Failed to save TLS certificates: {err}"
)));
}
debug!("TLS certificate files saved successfully");

debug!("Passing configuration to gRPC server for finalization");
let Some(sender) = self.setup_tx.lock().await.take() else {
error!("Setup channel sender already consumed");
Expand Down
Loading