Compare commits

...

1 Commits

Author SHA1 Message Date
trinity-1686a 33d50666b5 wrap secrets in new type 2026-09-05 16:25:49 +02:00
10 changed files with 60 additions and 16 deletions
Generated
+1
View File
@@ -1890,6 +1890,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9", "sha2 0.10.9",
"subtle",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"toml", "toml",
+1
View File
@@ -76,6 +76,7 @@ pnet_datalink = "0.35"
rand = "0.9" rand = "0.9"
sha1 = "0.10" sha1 = "0.10"
sha2 = "0.10" sha2 = "0.10"
subtle = "2.6.1"
timeago = { version = "0.5", default-features = false } timeago = { version = "0.5", default-features = false }
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
+2 -2
View File
@@ -191,7 +191,7 @@ impl RequestHandler for GetCurrentAdminTokenInfoRequest {
.admin .admin
.metrics_token .metrics_token
.as_ref() .as_ref()
.is_some_and(|s| s == &self.admin_token) .is_some_and(|s| s.eq_ct(&self.admin_token))
{ {
return Ok(GetCurrentAdminTokenInfoResponse( return Ok(GetCurrentAdminTokenInfoResponse(
GetAdminTokenInfoResponse { GetAdminTokenInfoResponse {
@@ -210,7 +210,7 @@ impl RequestHandler for GetCurrentAdminTokenInfoRequest {
.admin .admin
.admin_token .admin_token
.as_ref() .as_ref()
.is_some_and(|s| s == &self.admin_token) .is_some_and(|s| s.eq_ct(&self.admin_token))
{ {
return Ok(GetCurrentAdminTokenInfoResponse( return Ok(GetCurrentAdminTokenInfoResponse(
GetAdminTokenInfoResponse { GetAdminTokenInfoResponse {
+8 -2
View File
@@ -117,8 +117,14 @@ impl AdminApiServer {
#[cfg(feature = "metrics")] exporter: PrometheusExporter, #[cfg(feature = "metrics")] exporter: PrometheusExporter,
) -> Arc<Self> { ) -> Arc<Self> {
let cfg = &garage.config.admin; let cfg = &garage.config.admin;
let metrics_token = cfg.metrics_token.as_deref().map(hash_bearer_token); let metrics_token = cfg
let admin_token = cfg.admin_token.as_deref().map(hash_bearer_token); .metrics_token
.as_ref()
.map(|token| hash_bearer_token(token.extract_secret()));
let admin_token = cfg
.admin_token
.as_ref()
.map(|token| hash_bearer_token(token.extract_secret()));
let metrics_require_token = cfg.metrics_require_token; let metrics_require_token = cfg.metrics_require_token;
let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into()); let endpoint = garage.system.netapp.endpoint(ADMIN_RPC_PATH.into());
+2 -1
View File
@@ -307,7 +307,8 @@ async fn cli_command(opt: Opt) -> Result<(), Error> {
let net_key_hex_str = rpc_secret.ok_or("No RPC secret provided")?; let net_key_hex_str = rpc_secret.ok_or("No RPC secret provided")?;
let network_key = NetworkKey::from_slice( let network_key = NetworkKey::from_slice(
&hex::decode(&net_key_hex_str).err_context("Invalid RPC secret key (bad hex)")?[..], &hex::decode(net_key_hex_str.extract_secret())
.err_context("Invalid RPC secret key (bad hex)")?[..],
) )
.ok_or("Invalid RPC secret provided (wrong length)")?; .ok_or("Invalid RPC secret provided (wrong length)")?;
+8 -5
View File
@@ -2,7 +2,7 @@ use std::path::PathBuf;
use structopt::StructOpt; use structopt::StructOpt;
use garage_util::config::Config; use garage_util::config::{Config, Secret};
use garage_util::error::Error; use garage_util::error::Error;
/// Structure for secret values or paths that are passed as CLI arguments or environment /// Structure for secret values or paths that are passed as CLI arguments or environment
@@ -99,7 +99,7 @@ pub fn fill_secrets(mut config: Config, secrets: Secrets) -> Result<Config, Erro
} }
pub(crate) fn fill_secret( pub(crate) fn fill_secret(
config_secret: &mut Option<String>, config_secret: &mut Option<Secret<String>>,
config_secret_file: &Option<PathBuf>, config_secret_file: &Option<PathBuf>,
cli_secret: &Option<String>, cli_secret: &Option<String>,
cli_secret_file: &Option<PathBuf>, cli_secret_file: &Option<PathBuf>,
@@ -110,7 +110,7 @@ pub(crate) fn fill_secret(
(Some(_), Some(_)) => { (Some(_), Some(_)) => {
return Err(format!("only one of `{}` and `{}_file` can be set", name, name).into()); return Err(format!("only one of `{}` and `{}_file` can be set", name, name).into());
} }
(Some(secret), None) => Some(secret.to_string()), (Some(secret), None) => Some(Secret::new(secret.to_string())),
(None, Some(file)) => Some(read_secret_file(file, allow_world_readable)?), (None, Some(file)) => Some(read_secret_file(file, allow_world_readable)?),
(None, None) => None, (None, None) => None,
}; };
@@ -132,7 +132,10 @@ pub(crate) fn fill_secret(
Ok(()) Ok(())
} }
fn read_secret_file(file_path: &PathBuf, allow_world_readable: bool) -> Result<String, Error> { fn read_secret_file(
file_path: &PathBuf,
allow_world_readable: bool,
) -> Result<Secret<String>, Error> {
if !allow_world_readable { if !allow_world_readable {
#[cfg(unix)] #[cfg(unix)]
{ {
@@ -152,7 +155,7 @@ fn read_secret_file(file_path: &PathBuf, allow_world_readable: bool) -> Result<S
// trim_end: allows for use case such as `echo "$(openssl rand -hex 32)" > somefile`. // trim_end: allows for use case such as `echo "$(openssl rand -hex 32)" > somefile`.
// also editors sometimes add a trailing newline // also editors sometimes add a trailing newline
Ok(String::from(secret_buf.trim_end())) Ok(Secret::new(String::from(secret_buf.trim_end())))
} }
#[cfg(test)] #[cfg(test)]
+1 -1
View File
@@ -137,7 +137,7 @@ impl Garage {
info!("Initializing RPC..."); info!("Initializing RPC...");
let network_key = hex::decode(config.rpc_secret.as_ref().ok_or_message( let network_key = hex::decode(config.rpc_secret.as_ref().ok_or_message(
"rpc_secret value is missing, not present in config file or in environment", "rpc_secret value is missing, not present in config file or in environment",
)?) )?.extract_secret())
.ok() .ok()
.and_then(|x| NetworkKey::from_slice(&x)) .and_then(|x| NetworkKey::from_slice(&x))
.ok_or_message("Invalid RPC secret key: expected 32 bytes of random hex, please check the documentation for requirements")?; .ok_or_message("Invalid RPC secret key: expected 32 bytes of random hex, please check the documentation for requirements")?;
+1 -1
View File
@@ -115,7 +115,7 @@ impl ConsulDiscovery {
let mut headers = reqwest::header::HeaderMap::new(); let mut headers = reqwest::header::HeaderMap::new();
headers.insert( headers.insert(
"x-consul-token", "x-consul-token",
reqwest::header::HeaderValue::from_str(token)?, reqwest::header::HeaderValue::from_str(token.extract_secret())?,
); );
builder = builder.default_headers(headers); builder = builder.default_headers(headers);
} }
+1
View File
@@ -32,6 +32,7 @@ lazy_static.workspace = true
tracing.workspace = true tracing.workspace = true
rand.workspace = true rand.workspace = true
sha2.workspace = true sha2.workspace = true
subtle.workspace = true
chrono.workspace = true chrono.workspace = true
rmp-serde.workspace = true rmp-serde.workspace = true
+35 -4
View File
@@ -90,7 +90,7 @@ pub struct Config {
pub allow_world_readable_secrets: bool, pub allow_world_readable_secrets: bool,
/// RPC secret key: 32 bytes hex encoded /// RPC secret key: 32 bytes hex encoded
pub rpc_secret: Option<String>, pub rpc_secret: Option<Secret<String>>,
/// Optional file where RPC secret key is read from /// Optional file where RPC secret key is read from
pub rpc_secret_file: Option<PathBuf>, pub rpc_secret_file: Option<PathBuf>,
/// Address to bind for RPC /// Address to bind for RPC
@@ -205,6 +205,37 @@ pub struct WebConfig {
pub add_host_to_metrics: bool, pub add_host_to_metrics: bool,
} }
#[derive(Deserialize, Clone)]
#[serde(transparent)]
pub struct Secret<T>(T);
impl<T> Secret<T> {
pub fn new(secret: T) -> Self {
Secret(secret)
}
pub fn extract_secret(&self) -> &T {
&self.0
}
}
impl<T: std::ops::Deref<Target = str>> Secret<T> {
pub fn eq_ct(&self, other: &T) -> bool {
use subtle::ConstantTimeEq;
self.0
.deref()
.as_bytes()
.ct_eq(other.deref().as_bytes())
.into()
}
}
impl<T> std::fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Secret").finish_non_exhaustive()
}
}
/// Configuration for the admin and monitoring HTTP API /// Configuration for the admin and monitoring HTTP API
#[derive(Deserialize, Debug, Clone, Default)] #[derive(Deserialize, Debug, Clone, Default)]
pub struct AdminConfig { pub struct AdminConfig {
@@ -212,7 +243,7 @@ pub struct AdminConfig {
pub api_bind_addr: Option<UnixOrTCPSocketAddress>, pub api_bind_addr: Option<UnixOrTCPSocketAddress>,
/// Bearer token to use to scrape metrics /// Bearer token to use to scrape metrics
pub metrics_token: Option<String>, pub metrics_token: Option<Secret<String>>,
/// File to read metrics token from /// File to read metrics token from
pub metrics_token_file: Option<PathBuf>, pub metrics_token_file: Option<PathBuf>,
/// Whether to require an access token for accessing the metrics endpoint /// Whether to require an access token for accessing the metrics endpoint
@@ -220,7 +251,7 @@ pub struct AdminConfig {
pub metrics_require_token: bool, pub metrics_require_token: bool,
/// Bearer token to use to access Admin API endpoints /// Bearer token to use to access Admin API endpoints
pub admin_token: Option<String>, pub admin_token: Option<Secret<String>>,
/// File to read admin token from /// File to read admin token from
pub admin_token_file: Option<PathBuf>, pub admin_token_file: Option<PathBuf>,
@@ -252,7 +283,7 @@ pub struct ConsulDiscoveryConfig {
/// Client TLS key to use when connecting to Consul /// Client TLS key to use when connecting to Consul
pub client_key: Option<String>, pub client_key: Option<String>,
/// /// Token to use for connecting to consul /// /// Token to use for connecting to consul
pub token: Option<String>, pub token: Option<Secret<String>>,
/// Skip TLS hostname verification /// Skip TLS hostname verification
#[serde(default)] #[serde(default)]
pub tls_skip_verify: bool, pub tls_skip_verify: bool,