Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b1aff1f
bootstrap settings certs tab
j-chmielewski Mar 30, 2026
b823485
bootstrap cert settings pages
j-chmielewski Mar 30, 2026
a3b212a
ca cert styling
j-chmielewski Mar 30, 2026
75e71ae
download CA cert button
j-chmielewski Mar 30, 2026
fa18d85
Merge branch 'dev' into certificate-settings
j-chmielewski Mar 31, 2026
8b8156b
settings CA page
j-chmielewski Mar 31, 2026
ee33e57
extract CA email
j-chmielewski Mar 31, 2026
3ed451f
move ca image
j-chmielewski Mar 31, 2026
fffa485
proper icons for settings certs sections
j-chmielewski Mar 31, 2026
ccf0331
Merge branch 'dev' into certificate-settings
j-chmielewski Apr 1, 2026
d6ad6eb
fix firewall test import
j-chmielewski Apr 1, 2026
3267007
fix icons
j-chmielewski Apr 1, 2026
8b06467
bootstrap certs sections
j-chmielewski Apr 1, 2026
07394f2
get_certs API endpoint, None and SelfSigned core certs sections
j-chmielewski Apr 2, 2026
5ab6ea9
core own cert
j-chmielewski Apr 2, 2026
9970ad7
edge: none, self-signed, custom
j-chmielewski Apr 2, 2026
b4921ff
update custom cert description
j-chmielewski Apr 2, 2026
2d74d1c
core certs settings backend
j-chmielewski Apr 2, 2026
00cd7da
core cert settings wizard frontend
j-chmielewski Apr 2, 2026
e509831
edge cert settings wizard backend
j-chmielewski Apr 2, 2026
0e48be6
proxy certs settings
j-chmielewski Apr 2, 2026
7a01983
download CA cert
j-chmielewski Apr 3, 2026
043f9c5
fix retry loop in letsencrypt flow
j-chmielewski Apr 3, 2026
a588932
fix setting LE expiry; add cert info sections
j-chmielewski Apr 3, 2026
052107c
badges
j-chmielewski Apr 7, 2026
eaa4fc3
show badges for all cert types
j-chmielewski Apr 7, 2026
2684fb4
change certs button for self-signed sections
j-chmielewski Apr 7, 2026
f875482
fix "Change certificate" buttons
j-chmielewski Apr 7, 2026
88360bf
all cert-setting paths restart proxy web server
j-chmielewski Apr 7, 2026
da6f52a
restart web server after cert changes
j-chmielewski Apr 7, 2026
54d1df5
defer saving LE state until ACME challenge succeeds
j-chmielewski Apr 7, 2026
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
2 changes: 2 additions & 0 deletions crates/defguard/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ async fn main() -> Result<(), anyhow::Error> {
update_counts(&pool).await?;

let (proxy_control_tx, proxy_control_rx) = channel::<ProxyControlMessage>(100);
let (web_reload_tx, _web_reload_rx) = tokio::sync::broadcast::channel::<()>(8);
let proxy_secret_key = settings.secret_key_required()?;
let proxy_manager = ProxyManager::new(
pool.clone(),
Expand Down Expand Up @@ -270,6 +271,7 @@ async fn main() -> Result<(), anyhow::Error> {
webhook_tx,
webhook_rx,
gateway_tx.clone(),
web_reload_tx,
pool.clone(),
failed_logins,
api_event_tx,
Expand Down
43 changes: 22 additions & 21 deletions crates/defguard_certs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ use rcgen::{
use rustls_pki_types::{CertificateDer, CertificateSigningRequestDer, pem::PemObject};
use thiserror::Error;
use time::{Duration, OffsetDateTime};
use x509_parser::parse_x509_certificate;
use x509_parser::{
extensions::{GeneralName, ParsedExtension},
parse_x509_certificate,
};

const CA_NAME: &str = "Defguard CA";
const NOT_BEFORE_OFFSET_SECS: Duration = Duration::minutes(5);
Expand Down Expand Up @@ -144,6 +147,7 @@ impl CertificateAuthority<'_> {

pub struct CertificateInfo {
pub subject_common_name: String,
pub subject_email: Option<String>,
pub not_before: NaiveDateTime,
pub not_after: NaiveDateTime,
pub serial: String,
Expand All @@ -158,6 +162,17 @@ impl CertificateInfo {

let subject = &parsed.tbs_certificate.subject;
let serial = parsed.raw_serial_as_string();
let subject_email = parsed.tbs_certificate.extensions().iter().find_map(|ext| {
match ext.parsed_extension() {
ParsedExtension::SubjectAlternativeName(san) => {
san.general_names.iter().find_map(|name| match name {
GeneralName::RFC822Name(email) => Some(email.to_string()),
_ => None,
})
}
_ => None,
}
});

let cn = subject
.iter_common_name()
Expand All @@ -174,6 +189,7 @@ impl CertificateInfo {

Ok(Self {
subject_common_name: cn.to_string(),
subject_email,
not_before: chrono::DateTime::from_timestamp(not_before.unix_timestamp(), 0)
.ok_or_else(|| {
CertificateError::ParsingError(format!(
Expand Down Expand Up @@ -425,30 +441,15 @@ mod tests {

#[test]
fn test_ca_email() {
use x509_parser::parse_x509_certificate;

let expected_email = "contact@defguard.net";
let ca = CertificateAuthority::new("Test CA", expected_email, 365).unwrap();

let (_rem, parsed) = parse_x509_certificate(ca.cert_der()).unwrap();

let san_ext = parsed
.tbs_certificate
.extensions()
.iter()
.find(|ext| ext.oid == x509_parser::oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME)
.expect("Subject Alternative Name extension not found");

let san_value = san_ext.value;

let email_bytes = expected_email.as_bytes();
let email_found = san_value
.windows(email_bytes.len())
.any(|window| window == email_bytes);
let info = CertificateInfo::from_der(ca.cert_der()).unwrap();

assert!(
email_found,
"Email '{expected_email}' should be present in Subject Alternative Names"
assert_eq!(
info.subject_email.as_deref(),
Some(expected_email),
"Email should be parsed from Subject Alternative Names"
);
}

Expand Down
1 change: 1 addition & 0 deletions crates/defguard_common/src/types/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub enum ProxyControlMessage {
cert_pem: String,
key_pem: String,
},
ClearHttpsCerts,
}

#[derive(ToSchema, Serialize)]
Expand Down
3 changes: 3 additions & 0 deletions crates/defguard_core/src/appstate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct AppState {
pub pool: PgPool,
tx: UnboundedSender<AppEvent>,
pub wireguard_tx: Sender<GatewayEvent>,
pub web_reload_tx: tokio::sync::broadcast::Sender<()>,
pub failed_logins: Arc<Mutex<FailedLoginMap>>,
key: Key,
pub event_tx: UnboundedSender<ApiEvent>,
Expand Down Expand Up @@ -116,6 +117,7 @@ impl AppState {
tx: UnboundedSender<AppEvent>,
rx: UnboundedReceiver<AppEvent>,
wireguard_tx: Sender<GatewayEvent>,
web_reload_tx: tokio::sync::broadcast::Sender<()>,
key: Key,
failed_logins: Arc<Mutex<FailedLoginMap>>,
event_tx: UnboundedSender<ApiEvent>,
Expand All @@ -128,6 +130,7 @@ impl AppState {
pool,
tx,
wireguard_tx,
web_reload_tx,
failed_logins,
key,
event_tx,
Expand Down
4 changes: 2 additions & 2 deletions crates/defguard_core/src/enterprise/firewall/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::enterprise::{
AclRuleInfo, AclRuleNetwork, AclRuleUser, AliasKind, PortRange, RuleState,
},
firewall::try_get_location_firewall_config,
license::{License, LicenseTier, set_cached_license},
license::{License, LicenseTier, SupportType, set_cached_license},
};

mod all_locations;
Expand Down Expand Up @@ -55,7 +55,7 @@ fn set_test_license_business() {
customer_id: "0c4dcb5400544d47ad8617fcdf2704cb".into(),
limits: None,
subscription: false,
support_type: crate::enterprise::license::SupportType::Basic,
support_type: SupportType::Basic,
tier: LicenseTier::Business,
valid_until: None,
version_date_limit: None,
Expand Down
37 changes: 33 additions & 4 deletions crates/defguard_core/src/handlers/component_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ use axum::{
extract::{Path, Query},
response::sse::{Event, KeepAlive, Sse},
};
use chrono::NaiveDateTime;
use defguard_certs::der_to_pem;
use defguard_common::{
VERSION,
auth::claims::Claims,
db::{
Id,
models::{
Certificates,
Certificates, Settings,
certificates::ProxyCertSource,
gateway::Gateway,
initial_setup_wizard::{InitialSetupState, InitialSetupStep},
Expand Down Expand Up @@ -1115,6 +1116,16 @@ fn acme_step_name(step: AcmeStep) -> &'static str {
}
}

fn parse_cert_expiry(cert_pem: &str) -> Option<NaiveDateTime> {
let der = defguard_certs::parse_pem_certificate(cert_pem)
.map_err(|e| warn!("Failed to parse ACME cert PEM for expiry: {e}"))
.ok()?;
defguard_certs::CertificateInfo::from_der(&der)
.map(|info| info.not_after)
.map_err(|e| warn!("Failed to extract expiry from ACME cert: {e}"))
.ok()
}

/// Connects to the proxy's permanent `Proxy` gRPC service and calls `TriggerAcme`.
///
/// Returns `(cert_pem, key_pem, account_credentials_json)` on success, or
Expand Down Expand Up @@ -1233,12 +1244,27 @@ pub async fn stream_proxy_acme(
}
};

let domain = match certs.acme_domain.clone() {
Some(d) if !d.is_empty() => d,
let domain = match Settings::get_current_settings().public_proxy_url.trim() {
url if !url.is_empty() => match Url::parse(url)
.ok()
.and_then(|u| u.host_str().map(ToString::to_string))
.filter(|host| !host.is_empty())
{
Some(host) => host,
None => {
yield Ok(acme_error_event(
"Connecting",
"Public proxy URL is not configured with a valid hostname. Please re-submit the external URL settings with a valid domain."
.to_string(),
None,
));
return;
}
},
_ => {
yield Ok(acme_error_event(
"Connecting",
"No ACME domain configured. Please re-submit the external URL settings \
"Public proxy URL is not configured. Please re-submit the external URL settings \
with a Let's Encrypt domain."
.to_string(),
None,
Expand Down Expand Up @@ -1340,10 +1366,13 @@ pub async fn stream_proxy_acme(
// Progress channel closed - collect the final result.
match result_rx.await {
Ok(Ok((cert_pem, key_pem, new_account_credentials_json))) => {
let acme_cert_expiry = parse_cert_expiry(&cert_pem);
match Certificates::get_or_default(&pool).await {
Ok(mut updated_certs) => {
updated_certs.acme_domain = Some(domain.clone());
updated_certs.proxy_http_cert_pem = Some(cert_pem.clone());
updated_certs.proxy_http_cert_key_pem = Some(key_pem.clone());
updated_certs.proxy_http_cert_expiry = acme_cert_expiry;
updated_certs.acme_account_credentials =
Some(new_account_credentials_json);
updated_certs.proxy_http_cert_source =
Expand Down
Loading
Loading