mirror of
https://github.com/bitwarden/server
synced 2026-01-29 07:43:22 +00:00
Add authentication to publisher endpoints
The plan is to limit access to the publisher through a firewall, but this further limits access in a belt-and-suspenders fashion.
This commit is contained in:
1
akd/Cargo.lock
generated
1
akd/Cargo.lock
generated
@@ -2147,6 +2147,7 @@ dependencies = [
|
||||
"common",
|
||||
"config",
|
||||
"serde",
|
||||
"subtle",
|
||||
"thiserror 2.0.17",
|
||||
"tiberius",
|
||||
"tokio",
|
||||
|
||||
@@ -24,6 +24,7 @@ tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
async-std = { version = "1.13.2", features = ["attributes"] }
|
||||
subtle = "2.6.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
tiberius = { version = "0.12.3", default-features = false, features = [
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use akd_storage::akd_storage_config::AkdStorageConfig;
|
||||
use config::{Config, ConfigError, Environment, File};
|
||||
use serde::Deserialize;
|
||||
use subtle::ConstantTimeEq;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_EPOCH_DURATION_MS: u64 = 30000; // 30 seconds
|
||||
|
||||
/// Application configuration for the AKD Publisher
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ApplicationConfig {
|
||||
pub storage: AkdStorageConfig,
|
||||
@@ -15,6 +17,12 @@ pub struct ApplicationConfig {
|
||||
/// The address the web server will bind to. Defaults to "127.0.0.1:3000".
|
||||
#[serde(default = "default_web_server_bind_address")]
|
||||
web_server_bind_address: String,
|
||||
/// The API key required to access the web server endpoints.
|
||||
///
|
||||
/// NOTE: constant-time comparison is used, but mismatched string length cause immediate failure.
|
||||
/// For this reason, timing attacks can be used to at least determine the valid key length and a
|
||||
/// sufficiently long key should be used to mitigate this risk.
|
||||
pub web_server_api_key: String,
|
||||
// web_server: WebServerConfig,
|
||||
}
|
||||
|
||||
@@ -22,6 +30,7 @@ fn default_web_server_bind_address() -> String {
|
||||
"127.0.0.1:3000".to_string()
|
||||
}
|
||||
|
||||
/// Configuration for how the AKD updates
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct PublisherConfig {
|
||||
/// The duration of each publishing epoch in milliseconds. Defaults to 30 seconds.
|
||||
@@ -90,6 +99,13 @@ impl ApplicationConfig {
|
||||
.parse()
|
||||
.expect("Invalid web server bind address")
|
||||
}
|
||||
|
||||
pub fn api_key_valid(&self, api_key: &str) -> bool {
|
||||
self.web_server_api_key
|
||||
.as_bytes()
|
||||
.ct_eq(api_key.as_bytes())
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl PublisherConfig {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use akd_storage::{PublishQueue, PublishQueueType};
|
||||
use anyhow::{Context, Result};
|
||||
use axum::Router;
|
||||
use axum::{middleware::from_fn_with_state, Router};
|
||||
use bitwarden_akd_configuration::BitwardenV1Configuration;
|
||||
use common::BitAkdDirectory;
|
||||
use tokio::{net::TcpListener, sync::broadcast::Receiver};
|
||||
@@ -10,6 +10,7 @@ mod config;
|
||||
mod routes;
|
||||
|
||||
pub use crate::config::ApplicationConfig;
|
||||
use crate::routes::auth;
|
||||
|
||||
pub struct AppHandles {
|
||||
pub write_handle: tokio::task::JoinHandle<()>,
|
||||
@@ -127,9 +128,13 @@ async fn start_web(
|
||||
config: &ApplicationConfig,
|
||||
mut shutdown_rx: Receiver<()>,
|
||||
) -> Result<()> {
|
||||
let app_state = routes::AppState { publish_queue };
|
||||
let app_state = routes::AppState {
|
||||
publish_queue,
|
||||
app_config: config.clone(),
|
||||
};
|
||||
let app = Router::new()
|
||||
.merge(routes::api_routes())
|
||||
.route_layer(from_fn_with_state(app_state.clone(), auth))
|
||||
.with_state(app_state);
|
||||
|
||||
let listener = TcpListener::bind(&config.socket_address())
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
use akd_storage::PublishQueueType;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
use crate::ApplicationConfig;
|
||||
|
||||
mod health;
|
||||
mod publish;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct AppState {
|
||||
pub app_config: ApplicationConfig,
|
||||
pub publish_queue: PublishQueueType,
|
||||
}
|
||||
|
||||
@@ -14,3 +23,32 @@ pub fn api_routes() -> axum::Router<AppState> {
|
||||
.route("/health", get(health::health_handler))
|
||||
.route("/publish", post(publish::publish_handler))
|
||||
}
|
||||
|
||||
pub async fn auth(
|
||||
State(AppState { app_config, .. }): State<AppState>,
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let auth_header = req
|
||||
.headers()
|
||||
.get("x-api-key")
|
||||
.and_then(|header| header.to_str().ok());
|
||||
|
||||
let auth_header = if let Some(auth_header) = auth_header {
|
||||
auth_header
|
||||
} else {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
};
|
||||
|
||||
tracing::trace!(
|
||||
auth_header,
|
||||
key = app_config.web_server_api_key,
|
||||
"Authenticating request with provided API key"
|
||||
);
|
||||
if app_config.api_key_valid(auth_header) {
|
||||
// API key matches, proceed to the next handler
|
||||
Ok(next.run(req).await)
|
||||
} else {
|
||||
Err(StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user