1
0
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:
Matt Gibson
2026-01-15 04:10:01 -08:00
parent 7d8a41f711
commit 9be303cddb
5 changed files with 64 additions and 3 deletions

1
akd/Cargo.lock generated
View File

@@ -2147,6 +2147,7 @@ dependencies = [
"common",
"config",
"serde",
"subtle",
"thiserror 2.0.17",
"tiberius",
"tokio",

View File

@@ -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 = [

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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)
}
}