Skip to content

Commit a138a33

Browse files
http healthcheck endpoints always respond (#234)
1 parent a9d266e commit a138a33

File tree

2 files changed

+50
-24
lines changed

2 files changed

+50
-24
lines changed

src/grpc.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
net::SocketAddr,
55
sync::{
66
atomic::{AtomicBool, AtomicU64, Ordering},
7-
Arc, Mutex,
7+
Arc, Mutex, RwLock,
88
},
99
};
1010

@@ -44,19 +44,19 @@ pub(crate) struct ProxyServer {
4444
current_id: Arc<AtomicU64>,
4545
clients: Arc<Mutex<ClientMap>>,
4646
results: Arc<Mutex<HashMap<u64, oneshot::Sender<core_response::Payload>>>>,
47-
http_channel: mpsc::UnboundedSender<Key>,
4847
pub(crate) connected: Arc<AtomicBool>,
4948
pub(crate) core_version: Arc<Mutex<Option<Version>>>,
5049
config: Arc<Mutex<Option<Configuration>>>,
50+
cookie_key: Arc<RwLock<Option<Key>>>,
5151
setup_in_progress: Arc<AtomicBool>,
5252
}
5353

5454
impl ProxyServer {
5555
#[must_use]
5656
/// Create new `ProxyServer`.
57-
pub(crate) fn new(http_channel: mpsc::UnboundedSender<Key>) -> Self {
57+
pub(crate) fn new(cookie_key: Arc<RwLock<Option<Key>>>) -> Self {
5858
Self {
59-
http_channel,
59+
cookie_key,
6060
current_id: Arc::new(AtomicU64::new(1)),
6161
clients: Arc::new(Mutex::new(HashMap::new())),
6262
results: Arc::new(Mutex::new(HashMap::new())),
@@ -193,7 +193,7 @@ impl Clone for ProxyServer {
193193
results: Arc::clone(&self.results),
194194
connected: Arc::clone(&self.connected),
195195
core_version: Arc::clone(&self.core_version),
196-
http_channel: self.http_channel.clone(),
196+
cookie_key: Arc::clone(&self.cookie_key),
197197
config: Arc::clone(&self.config),
198198
setup_in_progress: Arc::clone(&self.setup_in_progress),
199199
}
@@ -252,10 +252,7 @@ impl proxy_server::Proxy for ProxyServer {
252252
Key::generate()
253253
}
254254
};
255-
self.http_channel.send(key).map_err(|err| {
256-
error!("Failed to send private cookies key to HTTP server: {err:?}");
257-
Status::internal("Failed to send private cookies key to HTTP server")
258-
})?;
255+
*self.cookie_key.write().unwrap() = Some(key);
259256

260257
let (tx, rx) = mpsc::unbounded_channel();
261258
self.clients

src/http.rs

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{
33
fs::read_to_string,
44
net::{IpAddr, Ipv4Addr, SocketAddr},
55
path::Path,
6-
sync::{atomic::Ordering, Arc, LazyLock},
6+
sync::{atomic::Ordering, Arc, LazyLock, RwLock},
77
time::Duration,
88
};
99

@@ -13,6 +13,7 @@ use axum::{
1313
extract::{ConnectInfo, FromRef, State},
1414
http::{header::HeaderValue, Request, Response, StatusCode},
1515
middleware::{self, Next},
16+
response::IntoResponse,
1617
routing::{get, post},
1718
serve, Json, Router,
1819
};
@@ -22,7 +23,7 @@ use defguard_version::{server::DefguardVersionLayer, Version};
2223
use serde::Serialize;
2324
use tokio::{
2425
net::TcpListener,
25-
sync::{mpsc, oneshot, Mutex},
26+
sync::{oneshot, Mutex},
2627
task::JoinSet,
2728
};
2829
use tower_governor::{
@@ -63,7 +64,7 @@ pub(crate) struct AppState {
6364
pub(crate) grpc_server: ProxyServer,
6465
pub(crate) remote_mfa_sessions:
6566
Arc<tokio::sync::Mutex<HashMap<String, oneshot::Sender<String>>>>,
66-
key: Key,
67+
cookie_key: Arc<RwLock<Option<Key>>>,
6768
url: Url,
6869
}
6970

@@ -85,7 +86,10 @@ impl AppState {
8586

8687
impl FromRef<AppState> for Key {
8788
fn from_ref(state: &AppState) -> Self {
88-
state.key.clone()
89+
let maybe_key = state.cookie_key.read().unwrap().clone();
90+
// We return the dummy key only to satisfy the `FromRef` trait, but it is never
91+
// used in practice because of the `ensure_configured` middleware.
92+
maybe_key.unwrap_or_else(|| Key::from(&[0; 64]))
8993
}
9094
}
9195

@@ -173,18 +177,44 @@ async fn powered_by_header<B>(mut response: Response<B>) -> Response<B> {
173177
response
174178
}
175179

180+
/// Middleware that gates all HTTP endpoints except health checks until the proxy
181+
/// is fully configured.
182+
///
183+
/// The proxy cannot safely handle requests that rely on encrypted cookies
184+
/// (e.g. OpenID / MFA flows) until it receives the cookie encryption key from
185+
/// the core. This key is provided asynchronously after the core connects.
186+
///
187+
/// Until the key is available, only health check endpoints are served and all
188+
/// other requests return HTTP 503 (Service Unavailable). Once the key is set,
189+
/// the middleware becomes a no-op and all routes are enabled.
190+
async fn ensure_configured(
191+
State(state): State<AppState>,
192+
request: Request<Body>,
193+
next: Next,
194+
) -> Response<Body> {
195+
// Allow healthchecks even before core connects and gives us the cookie key.
196+
let path = request.uri().path();
197+
if matches!(path, "/api/v1/health" | "/api/v1/health-grpc") {
198+
return next.run(request).await;
199+
}
200+
201+
// Block all other requests until cookie key is configured.
202+
if state.cookie_key.read().unwrap().is_none() {
203+
return StatusCode::SERVICE_UNAVAILABLE.into_response();
204+
}
205+
206+
next.run(request).await
207+
}
208+
176209
pub async fn run_server(config: Config) -> anyhow::Result<()> {
177210
info!("Starting Defguard Proxy server");
178211
debug!("Using config: {config:?}");
179212

180213
let mut tasks = JoinSet::new();
181-
182-
// Prepare the channel for gRPC -> http server communication.
183-
// The channel sends private cookies key once core connects to gRPC.
184-
let (tx, mut rx) = mpsc::unbounded_channel::<Key>();
214+
let cookie_key = Default::default();
185215

186216
// connect to upstream gRPC server
187-
let grpc_server = ProxyServer::new(tx);
217+
let grpc_server = ProxyServer::new(Arc::clone(&cookie_key));
188218

189219
let server_clone = grpc_server.clone();
190220

@@ -256,15 +286,10 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> {
256286
}
257287
});
258288

259-
// Wait for core to connect to gRPC and send private cookies encryption key.
260-
let Some(key) = rx.recv().await else {
261-
return Err(anyhow::Error::msg("http channel closed"));
262-
};
263-
264289
// build application
265290
debug!("Setting up API server");
266291
let shared_state = AppState {
267-
key,
292+
cookie_key,
268293
grpc_server,
269294
remote_mfa_sessions: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
270295
url: config.url.clone(),
@@ -324,6 +349,10 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> {
324349
.route("/info", get(app_info)),
325350
)
326351
.fallback_service(get(handle_404))
352+
.layer(middleware::from_fn_with_state(
353+
shared_state.clone(),
354+
ensure_configured,
355+
))
327356
.layer(middleware::map_response(powered_by_header))
328357
.layer(middleware::from_fn_with_state(
329358
shared_state.clone(),

0 commit comments

Comments
 (0)