Skip to content

Commit 28a5976

Browse files
Update core/edge url when changing cert configuration (#2725)
- ensure https schema in Settings::{defguard_url, public_proxy_url} when switching to CA/own cert/LE - don't modify schema when switching to no cert in case ssl is not terminated by reverse proxy
1 parent 38be80c commit 28a5976

4 files changed

Lines changed: 131 additions & 39 deletions

File tree

crates/defguard_core/src/cert_settings.rs

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use defguard_certs::{
44
parse_pem_certificate,
55
};
66
use defguard_common::db::models::{
7-
Certificates, CoreCertSource, ProxyCertSource, settings::update_current_settings,
7+
Certificates, CoreCertSource, ProxyCertSource, Settings, settings::update_current_settings,
88
};
99
use serde::{Deserialize, Serialize};
1010
use sqlx::PgPool;
@@ -94,6 +94,14 @@ pub struct ExternalUrlSettingsConfig {
9494
pub key_pem: Option<String>,
9595
}
9696

97+
fn ensure_https(url: &str) -> String {
98+
if let Some(rest) = url.strip_prefix("http://") {
99+
format!("https://{rest}")
100+
} else {
101+
url.to_owned()
102+
}
103+
}
104+
97105
/// Core logic for applying internal URL certificate settings using the current Defguard URL.
98106
/// Returns cert info if a certificate was generated/uploaded, `None` for `ssl_type = None`.
99107
pub async fn apply_internal_url_settings(
@@ -106,11 +114,17 @@ pub async fn apply_internal_url_settings(
106114
defguard_url, config.ssl_type,
107115
);
108116

109-
let mut settings = defguard_common::db::models::Settings::get_current_settings();
110-
settings.defguard_url = defguard_url.to_string();
111-
update_current_settings(pool, settings).await?;
117+
let mut settings = Settings::get_current_settings();
118+
let mut transaction = pool.begin().await?;
119+
120+
// Modify url schema if necessary
121+
settings.defguard_url = match config.ssl_type {
122+
InternalSslType::None => defguard_url.to_string(),
123+
InternalSslType::DefguardCa | InternalSslType::OwnCert => ensure_https(defguard_url),
124+
};
125+
update_current_settings(&mut *transaction, settings).await?;
112126

113-
let mut certs = Certificates::get_or_default(pool)
127+
let mut certs = Certificates::get_or_default(&mut *transaction)
114128
.await
115129
.map_err(WebError::from)?;
116130

@@ -120,7 +134,10 @@ pub async fn apply_internal_url_settings(
120134
certs.core_http_cert_pem = None;
121135
certs.core_http_cert_key_pem = None;
122136
certs.core_http_cert_expiry = None;
123-
certs.save(pool).await.map_err(WebError::from)?;
137+
certs
138+
.save(&mut *transaction)
139+
.await
140+
.map_err(WebError::from)?;
124141
None
125142
}
126143
InternalSslType::DefguardCa => {
@@ -156,7 +173,10 @@ pub async fn apply_internal_url_settings(
156173
certs.core_http_cert_pem = Some(cert_pem);
157174
certs.core_http_cert_key_pem = Some(key_pem);
158175
certs.core_http_cert_expiry = Some(expiry);
159-
certs.save(pool).await.map_err(WebError::from)?;
176+
certs
177+
.save(&mut *transaction)
178+
.await
179+
.map_err(WebError::from)?;
160180

161181
Some(CertInfoResponse {
162182
common_name: info.subject_common_name,
@@ -181,7 +201,10 @@ pub async fn apply_internal_url_settings(
181201
certs.core_http_cert_pem = Some(cert_pem_str);
182202
certs.core_http_cert_key_pem = Some(key_pem_str);
183203
certs.core_http_cert_expiry = Some(expiry);
184-
certs.save(pool).await.map_err(WebError::from)?;
204+
certs
205+
.save(&mut *transaction)
206+
.await
207+
.map_err(WebError::from)?;
185208

186209
Some(CertInfoResponse {
187210
common_name: info.subject_common_name,
@@ -192,6 +215,7 @@ pub async fn apply_internal_url_settings(
192215
}
193216
};
194217

218+
transaction.commit().await?;
195219
Ok(cert_info)
196220
}
197221

@@ -207,28 +231,37 @@ pub async fn apply_external_url_settings(
207231
public_proxy_url, config.ssl_type,
208232
);
209233

210-
let mut certs = Certificates::get_or_default(pool)
234+
let mut transaction = pool.begin().await?;
235+
let mut certs = Certificates::get_or_default(&mut *transaction)
211236
.await
212237
.map_err(WebError::from)?;
213238

214-
let hostname = if matches!(
215-
config.ssl_type,
216-
ExternalSslType::LetsEncrypt | ExternalSslType::DefguardCa
217-
) {
218-
let url = public_proxy_url.trim();
219-
if url.is_empty() {
220-
return Err(WebError::BadRequest(
221-
"Public proxy URL is not configured".to_string(),
222-
));
239+
// Modify url schema if necessary
240+
let mut settings = Settings::get_current_settings();
241+
settings.public_proxy_url = match config.ssl_type {
242+
ExternalSslType::None => public_proxy_url.to_string(),
243+
ExternalSslType::LetsEncrypt | ExternalSslType::DefguardCa | ExternalSslType::OwnCert => {
244+
ensure_https(public_proxy_url)
245+
}
246+
};
247+
update_current_settings(&mut *transaction, settings).await?;
248+
249+
let hostname = match config.ssl_type {
250+
ExternalSslType::None | ExternalSslType::OwnCert => String::new(),
251+
ExternalSslType::DefguardCa | ExternalSslType::LetsEncrypt => {
252+
let url = public_proxy_url.trim();
253+
if url.is_empty() {
254+
return Err(WebError::BadRequest(
255+
"Public proxy URL is not configured".to_string(),
256+
));
257+
}
258+
259+
reqwest::Url::parse(url)
260+
.ok()
261+
.and_then(|u| u.host_str().map(ToString::to_string))
262+
.filter(|host| !host.is_empty())
263+
.unwrap_or_else(|| url.to_string())
223264
}
224-
225-
reqwest::Url::parse(url)
226-
.ok()
227-
.and_then(|u| u.host_str().map(ToString::to_string))
228-
.filter(|host| !host.is_empty())
229-
.unwrap_or_else(|| url.to_string())
230-
} else {
231-
String::new()
232265
};
233266

234267
let cert_info = match config.ssl_type {
@@ -239,7 +272,10 @@ pub async fn apply_external_url_settings(
239272
certs.proxy_http_cert_pem = None;
240273
certs.proxy_http_cert_key_pem = None;
241274
certs.proxy_http_cert_expiry = None;
242-
certs.save(pool).await.map_err(WebError::from)?;
275+
certs
276+
.save(&mut *transaction)
277+
.await
278+
.map_err(WebError::from)?;
243279
None
244280
}
245281
ExternalSslType::LetsEncrypt => {
@@ -278,7 +314,10 @@ pub async fn apply_external_url_settings(
278314
certs.proxy_http_cert_pem = Some(cert_pem);
279315
certs.proxy_http_cert_key_pem = Some(key_pem);
280316
certs.proxy_http_cert_expiry = Some(expiry);
281-
certs.save(pool).await.map_err(WebError::from)?;
317+
certs
318+
.save(&mut *transaction)
319+
.await
320+
.map_err(WebError::from)?;
282321

283322
Some(CertInfoResponse {
284323
common_name: info.subject_common_name,
@@ -304,7 +343,10 @@ pub async fn apply_external_url_settings(
304343
certs.proxy_http_cert_pem = Some(cert_pem_str);
305344
certs.proxy_http_cert_key_pem = Some(key_pem_str);
306345
certs.proxy_http_cert_expiry = Some(expiry);
307-
certs.save(pool).await.map_err(WebError::from)?;
346+
certs
347+
.save(&mut *transaction)
348+
.await
349+
.map_err(WebError::from)?;
308350

309351
Some(CertInfoResponse {
310352
common_name: info.subject_common_name,
@@ -315,5 +357,6 @@ pub async fn apply_external_url_settings(
315357
}
316358
};
317359

360+
transaction.commit().await?;
318361
Ok(cert_info)
319362
}

crates/defguard_core/src/handlers/core_certs.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use axum::{Extension, Json, extract::State, http::StatusCode};
22
use defguard_certs::{CertificateInfo, der_to_pem, parse_pem_certificate};
3-
use defguard_common::db::models::Certificates;
3+
use defguard_common::{
4+
db::models::{Certificates, Settings},
5+
types::proxy::ProxyControlMessage,
6+
};
47
use serde_json::json;
58
use sqlx::PgPool;
69

@@ -25,12 +28,7 @@ fn cert_common_name(cert_pem: Option<&str>) -> Option<String> {
2528
async fn broadcast_proxy_https_certs(appstate: &AppState, cert_pem: String, key_pem: String) {
2629
if let Err(err) = appstate
2730
.proxy_control_tx
28-
.send(
29-
defguard_common::types::proxy::ProxyControlMessage::BroadcastHttpsCerts {
30-
cert_pem,
31-
key_pem,
32-
},
33-
)
31+
.send(ProxyControlMessage::BroadcastHttpsCerts { cert_pem, key_pem })
3432
.await
3533
{
3634
error!("Failed to broadcast HttpsCerts to proxies: {err:?}");
@@ -41,7 +39,7 @@ async fn broadcast_proxy_https_certs(appstate: &AppState, cert_pem: String, key_
4139
async fn clear_proxy_https_certs(appstate: &AppState) {
4240
if let Err(err) = appstate
4341
.proxy_control_tx
44-
.send(defguard_common::types::proxy::ProxyControlMessage::ClearHttpsCerts)
42+
.send(ProxyControlMessage::ClearHttpsCerts)
4543
.await
4644
{
4745
error!("Failed to broadcast ClearHttpsCerts to proxies: {err:?}");
@@ -78,7 +76,7 @@ pub(crate) async fn set_internal_url_settings(
7876
"User {} applying core internal URL certificate settings",
7977
session.user.username
8078
);
81-
let settings = defguard_common::db::models::Settings::get_current_settings();
79+
let settings = Settings::get_current_settings();
8280
let cert_info = apply_internal_url_settings(&pool, &settings.defguard_url, config).await?;
8381
reload_core_web_server(&appstate);
8482
info!(
@@ -116,7 +114,7 @@ pub(crate) async fn set_external_url_settings(
116114
"User {} applying proxy external URL certificate settings",
117115
session.user.username
118116
);
119-
let settings = defguard_common::db::models::Settings::get_current_settings();
117+
let settings = Settings::get_current_settings();
120118
let ssl_type = config.ssl_type.clone();
121119
let cert_info = apply_external_url_settings(&pool, &settings.public_proxy_url, config).await?;
122120

crates/defguard_core/tests/integration/api/core_certs.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ async fn test_internal_url_settings_endpoint(_: PgPoolOptions, options: PgConnec
4848
.send()
4949
.await;
5050
assert_eq!(response.status(), StatusCode::CREATED);
51+
let mut settings = Settings::get(&pool).await.unwrap().unwrap();
52+
// Don't touch the URL if setting no cert
53+
assert_eq!(settings.defguard_url, "https://defguard.example.com");
5154

5255
let saved = Certificates::get(&pool).await.unwrap().unwrap();
5356
assert_eq!(saved.core_http_cert_source, CoreCertSource::None);
@@ -57,12 +60,17 @@ async fn test_internal_url_settings_endpoint(_: PgPoolOptions, options: PgConnec
5760

5861
seed_ca(&pool).await;
5962

63+
settings.defguard_url = "http://defguard.example.com".to_string();
64+
update_current_settings(&pool, settings).await.unwrap();
6065
let response = client
6166
.post("/api/v1/core/cert/internal_url_settings")
6267
.json(&json!({ "ssl_type": "defguard_ca" }))
6368
.send()
6469
.await;
6570
assert_eq!(response.status(), StatusCode::CREATED);
71+
let mut settings = Settings::get(&pool).await.unwrap().unwrap();
72+
// Url schema changed to https
73+
assert_eq!(settings.defguard_url, "https://defguard.example.com");
6674

6775
let body: serde_json::Value = response.json::<serde_json::Value>().await;
6876
assert!(!body["cert_info"].is_null());
@@ -79,6 +87,8 @@ async fn test_internal_url_settings_endpoint(_: PgPoolOptions, options: PgConnec
7987
.contains("BEGIN CERTIFICATE")
8088
);
8189

90+
settings.defguard_url = "http://defguard.example.com".to_string();
91+
update_current_settings(&pool, settings).await.unwrap();
8292
let (cert_pem, key_pem) = generate_test_cert_pem("uploaded.example.com");
8393
let response = client
8494
.post("/api/v1/core/cert/internal_url_settings")
@@ -90,6 +100,9 @@ async fn test_internal_url_settings_endpoint(_: PgPoolOptions, options: PgConnec
90100
.send()
91101
.await;
92102
assert_eq!(response.status(), StatusCode::CREATED);
103+
let mut settings = Settings::get(&pool).await.unwrap().unwrap();
104+
// Url schema changed to https
105+
assert_eq!(settings.defguard_url, "https://defguard.example.com");
93106

94107
let body: serde_json::Value = response.json::<serde_json::Value>().await;
95108
assert_eq!(body["cert_info"]["common_name"], "uploaded.example.com");
@@ -98,6 +111,8 @@ async fn test_internal_url_settings_endpoint(_: PgPoolOptions, options: PgConnec
98111
assert_eq!(saved.core_http_cert_source, CoreCertSource::Custom);
99112
assert!(saved.core_http_cert_expiry.is_some());
100113

114+
settings.defguard_url = "http://defguard.example.com".to_string();
115+
update_current_settings(&pool, settings).await.unwrap();
101116
let (_, mismatched_key_pem) = generate_test_cert_pem("different.example.com");
102117
let response = client
103118
.post("/api/v1/core/cert/internal_url_settings")
@@ -109,6 +124,9 @@ async fn test_internal_url_settings_endpoint(_: PgPoolOptions, options: PgConnec
109124
.send()
110125
.await;
111126
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
127+
let mut settings = Settings::get(&pool).await.unwrap().unwrap();
128+
// Url schema unchanged on errors
129+
assert_eq!(settings.defguard_url, "http://defguard.example.com");
112130

113131
let response = client
114132
.post("/api/v1/core/cert/internal_url_settings")
@@ -120,6 +138,8 @@ async fn test_internal_url_settings_endpoint(_: PgPoolOptions, options: PgConnec
120138
.await;
121139
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
122140

141+
settings.defguard_url = "http://defguard.example.com".to_string();
142+
update_current_settings(&pool, settings).await.unwrap();
123143
let (expired_cert_pem, expired_key_pem) = generate_expired_test_cert_pem("expired.example.com");
124144
let response = client
125145
.post("/api/v1/core/cert/internal_url_settings")
@@ -131,6 +151,9 @@ async fn test_internal_url_settings_endpoint(_: PgPoolOptions, options: PgConnec
131151
.send()
132152
.await;
133153
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
154+
let settings = Settings::get(&pool).await.unwrap().unwrap();
155+
// Url schema unchanged on errors
156+
assert_eq!(settings.defguard_url, "http://defguard.example.com");
134157
let body: serde_json::Value = response.json().await;
135158
assert_eq!(body["msg"], "Certificate has expired");
136159
}

0 commit comments

Comments
 (0)