Skip to content

Commit 80ca767

Browse files
committed
fix: use secret wrapper type for secrets in config
1 parent 85dfcca commit 80ca767

File tree

4 files changed

+98
-6
lines changed

4 files changed

+98
-6
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

objectstore-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ sentry = { version = "0.41.0", features = [
2121
"tracing",
2222
"logs",
2323
] }
24+
secrecy = { version = "0.10.3", features = ["serde"] }
2425
serde = { workspace = true }
2526
serde_json = { workspace = true }
2627
tokio = { workspace = true, features = ["full"] }

objectstore-server/src/config.rs

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,49 @@
11
use std::collections::BTreeMap;
2+
use std::fmt;
23
use std::net::SocketAddr;
34
use std::path::PathBuf;
45

56
use anyhow::Result;
67
use argh::FromArgs;
78
use figment::providers::{Env, Format, Serialized, Yaml};
9+
use secrecy::{CloneableSecret, SecretBox, SerializableSecret, zeroize::Zeroize};
810
use serde::{Deserialize, Serialize};
911

1012
const ENV_PREFIX: &str = "FSS_";
1113

14+
/// Newtype around `String` that may protect against accidental
15+
/// logging of secrets in our configuration struct. Use with
16+
/// [`secrecy::SecretBox`].
17+
#[derive(Clone, Default, Serialize, Deserialize, PartialEq)]
18+
pub(crate) struct ConfigSecret(String);
19+
20+
impl ConfigSecret {
21+
pub fn as_str(&self) -> &str {
22+
self.0.as_str()
23+
}
24+
}
25+
26+
impl std::ops::Deref for ConfigSecret {
27+
type Target = String;
28+
fn deref(&self) -> &Self::Target {
29+
&self.0
30+
}
31+
}
32+
33+
impl fmt::Debug for ConfigSecret {
34+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
35+
write!(f, "[redacted]")
36+
}
37+
}
38+
39+
impl CloneableSecret for ConfigSecret {}
40+
impl SerializableSecret for ConfigSecret {}
41+
impl Zeroize for ConfigSecret {
42+
fn zeroize(&mut self) {
43+
self.0.zeroize();
44+
}
45+
}
46+
1247
#[derive(Debug, Clone, Deserialize, Serialize)]
1348
#[serde(tag = "type", rename_all = "lowercase")]
1449
pub enum Storage {
@@ -33,7 +68,7 @@ pub enum Storage {
3368

3469
#[derive(Debug, Clone, Deserialize, Serialize)]
3570
pub struct Sentry {
36-
pub dsn: String,
71+
pub dsn: SecretBox<ConfigSecret>,
3772
pub sample_rate: Option<f32>,
3873
pub traces_sample_rate: Option<f32>,
3974
}
@@ -49,7 +84,7 @@ pub struct Config {
4984

5085
// others
5186
pub sentry: Option<Sentry>,
52-
pub datadog_key: Option<String>,
87+
pub datadog_key: Option<SecretBox<ConfigSecret>>,
5388
pub metric_tags: BTreeMap<String, String>,
5489
}
5590

@@ -103,8 +138,16 @@ pub struct Args {
103138
mod tests {
104139
use std::io::Write;
105140

141+
use secrecy::ExposeSecret;
142+
106143
use super::*;
107144

145+
impl PartialEq<str> for ConfigSecret {
146+
fn eq(&self, other: &str) -> bool {
147+
self.0 == other
148+
}
149+
}
150+
108151
#[test]
109152
fn configurable_via_env() {
110153
figment::Jail::expect_with(|jail| {
@@ -135,7 +178,7 @@ mod tests {
135178
sample_rate,
136179
traces_sample_rate,
137180
} = &dbg!(&config).sentry.as_ref().unwrap();
138-
assert_eq!(dsn, "abcde");
181+
assert_eq!(dsn.expose_secret(), "abcde");
139182
assert_eq!(sample_rate, &Some(0.5));
140183
assert_eq!(traces_sample_rate, &Some(0.5));
141184

@@ -177,8 +220,43 @@ mod tests {
177220
sample_rate,
178221
traces_sample_rate,
179222
} = &dbg!(&config).sentry.as_ref().unwrap();
180-
assert_eq!(dsn, "abcde");
223+
assert_eq!(dsn.expose_secret(), "abcde");
181224
assert_eq!(sample_rate, &Some(0.5));
182225
assert_eq!(traces_sample_rate, &Some(0.5));
183226
}
227+
228+
#[test]
229+
fn configured_with_env_and_yaml() {
230+
let mut tempfile = tempfile::NamedTempFile::new().unwrap();
231+
tempfile
232+
.write_all(
233+
br#"
234+
long_term_storage:
235+
type: s3compatible
236+
endpoint: http://localhost:8888
237+
bucket: whatever
238+
"#,
239+
)
240+
.unwrap();
241+
figment::Jail::expect_with(|jail| {
242+
jail.set_env("fss_long_term_storage__endpoint", "http://localhost:9001");
243+
244+
let args = Args {
245+
config: Some(tempfile.path().into()),
246+
};
247+
let config = Config::from_args(args).unwrap();
248+
249+
let Storage::S3Compatible {
250+
endpoint,
251+
bucket: _bucket,
252+
} = &dbg!(&config).long_term_storage
253+
else {
254+
panic!("expected s3 storage");
255+
};
256+
// Env should overwrite the yaml config
257+
assert_eq!(endpoint, "http://localhost:9001");
258+
259+
Ok(())
260+
});
261+
}
184262
}

objectstore-server/src/observability.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::env;
22

3+
use secrecy::ExposeSecret;
34
use sentry::integrations::tracing as sentry_tracing;
45
use tracing::Level;
56
use tracing::level_filters::LevelFilter;
@@ -12,7 +13,8 @@ pub fn maybe_initialize_metrics(config: &Config) -> std::io::Result<Option<merni
1213
.datadog_key
1314
.as_ref()
1415
.map(|api_key| {
15-
let mut builder = merni::datadog(api_key.as_str()).prefix("objectstore.");
16+
let mut builder =
17+
merni::datadog(api_key.expose_secret().as_str()).prefix("objectstore.");
1618
for (k, v) in &config.metric_tags {
1719
builder = builder.global_tag(k, v);
1820
}
@@ -24,7 +26,7 @@ pub fn maybe_initialize_metrics(config: &Config) -> std::io::Result<Option<merni
2426
pub fn maybe_initialize_sentry(config: &Config) -> Option<sentry::ClientInitGuard> {
2527
config.sentry.as_ref().map(|sentry_config| {
2628
sentry::init(sentry::ClientOptions {
27-
dsn: sentry_config.dsn.parse().ok(),
29+
dsn: sentry_config.dsn.expose_secret().parse().ok(),
2830
enable_logs: true,
2931
sample_rate: sentry_config.sample_rate.unwrap_or(1.0),
3032
traces_sample_rate: sentry_config.traces_sample_rate.unwrap_or(0.01),

0 commit comments

Comments
 (0)