From 2cced770da14ecba956968fcdf1f0d1a080d5b84 Mon Sep 17 00:00:00 2001 From: John Spray Date: Wed, 27 Sep 2023 13:12:13 +0100 Subject: [PATCH] pageserver: add control_plane_api_token config (#5383) ## Problem Control plane API calls in prod will need authentication. ## Summary of changes `control_plane_api_token` config is loaded and set as HTTP `Authorization` header. Closes: https://github.com/neondatabase/neon/issues/5139 --- libs/utils/src/logging.rs | 18 ++++++++++++++++++ pageserver/src/config.rs | 16 ++++++++++++++-- pageserver/src/control_plane_client.rs | 12 ++++++++---- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/libs/utils/src/logging.rs b/libs/utils/src/logging.rs index f69c0603d507..7f17970c4c4c 100644 --- a/libs/utils/src/logging.rs +++ b/libs/utils/src/logging.rs @@ -216,6 +216,24 @@ impl std::fmt::Debug for PrettyLocation<'_, '_> { } } +/// When you will store a secret but want to make sure it won't +/// be accidentally logged, wrap it in a SecretString, whose Debug +/// implementation does not expose the contents. +#[derive(Clone, Eq, PartialEq)] +pub struct SecretString(String); + +impl SecretString { + pub fn get_contents(&self) -> &str { + self.0.as_str() + } +} + +impl std::fmt::Debug for SecretString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[SECRET]") + } +} + #[cfg(test)] mod tests { use metrics::{core::Opts, IntCounterVec}; diff --git a/pageserver/src/config.rs b/pageserver/src/config.rs index ed767b764e23..c3f2f14a74ef 100644 --- a/pageserver/src/config.rs +++ b/pageserver/src/config.rs @@ -11,6 +11,7 @@ use std::env; use storage_broker::Uri; use utils::crashsafe::path_with_suffix_extension; use utils::id::ConnectionId; +use utils::logging::SecretString; use once_cell::sync::OnceCell; use reqwest::Url; @@ -207,6 +208,9 @@ pub struct PageServerConf { pub background_task_maximum_delay: Duration, pub control_plane_api: Option, + + /// JWT token for use with the control plane API. + pub control_plane_api_token: Option, } /// We do not want to store this in a PageServerConf because the latter may be logged @@ -283,6 +287,7 @@ struct PageServerConfigBuilder { background_task_maximum_delay: BuilderValue, control_plane_api: BuilderValue>, + control_plane_api_token: BuilderValue>, } impl Default for PageServerConfigBuilder { @@ -347,6 +352,7 @@ impl Default for PageServerConfigBuilder { .unwrap()), control_plane_api: Set(None), + control_plane_api_token: Set(None), } } } @@ -567,6 +573,9 @@ impl PageServerConfigBuilder { control_plane_api: self .control_plane_api .ok_or(anyhow!("missing control_plane_api"))?, + control_plane_api_token: self + .control_plane_api_token + .ok_or(anyhow!("missing control_plane_api_token"))?, }) } } @@ -945,6 +954,7 @@ impl PageServerConf { ondemand_download_behavior_treat_error_as_warn: false, background_task_maximum_delay: Duration::ZERO, control_plane_api: None, + control_plane_api_token: None, } } } @@ -1168,7 +1178,8 @@ background_task_maximum_delay = '334 s' background_task_maximum_delay: humantime::parse_duration( defaults::DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY )?, - control_plane_api: None + control_plane_api: None, + control_plane_api_token: None }, "Correct defaults should be used when no config values are provided" ); @@ -1224,7 +1235,8 @@ background_task_maximum_delay = '334 s' test_remote_failures: 0, ondemand_download_behavior_treat_error_as_warn: false, background_task_maximum_delay: Duration::from_secs(334), - control_plane_api: None + control_plane_api: None, + control_plane_api_token: None }, "Should be able to parse all basic config values correctly" ); diff --git a/pageserver/src/control_plane_client.rs b/pageserver/src/control_plane_client.rs index 555f76e5239e..3375392373aa 100644 --- a/pageserver/src/control_plane_client.rs +++ b/pageserver/src/control_plane_client.rs @@ -53,12 +53,16 @@ impl ControlPlaneClient { segs.pop_if_empty().push(""); } - let client = reqwest::ClientBuilder::new() - .build() - .expect("Failed to construct http client"); + let mut client = reqwest::ClientBuilder::new(); + + if let Some(jwt) = &conf.control_plane_api_token { + let mut headers = hyper::HeaderMap::new(); + headers.insert("Authorization", jwt.get_contents().parse().unwrap()); + client = client.default_headers(headers); + } Some(Self { - http_client: client, + http_client: client.build().expect("Failed to construct HTTP client"), base_url: url, node_id: conf.id, cancel: cancel.clone(),