Skip to content

Commit 50b41a6

Browse files
authored
Add secrets.encryption_file config option (#4617)
2 parents ae71b75 + 1878388 commit 50b41a6

File tree

8 files changed

+124
-28
lines changed

8 files changed

+124
-28
lines changed

Cargo.lock

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

crates/cli/src/commands/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl Options {
123123
SC::Sync { prune, dry_run } => {
124124
let config = SyncConfig::extract(figment)?;
125125
let clock = SystemClock::default();
126-
let encrypter = config.secrets.encrypter();
126+
let encrypter = config.secrets.encrypter().await?;
127127

128128
// Grab a connection to the database
129129
let mut conn = database_connection_from_config(&config.database).await?;

crates/cli/src/commands/server.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ impl Options {
9494
.context("could not run database migrations")?;
9595
}
9696

97-
let encrypter = config.secrets.encrypter();
97+
let encrypter = config.secrets.encrypter().await?;
9898

9999
if self.no_sync {
100100
info!("Skipping configuration sync");
@@ -124,8 +124,10 @@ impl Options {
124124
.await
125125
.context("could not import keys from config")?;
126126

127-
let cookie_manager =
128-
CookieManager::derive_from(config.http.public_base.clone(), &config.secrets.encryption);
127+
let cookie_manager = CookieManager::derive_from(
128+
config.http.public_base.clone(),
129+
&config.secrets.encryption().await?,
130+
);
129131

130132
// Load and compile the WASM policies (and fallback to the default embedded one)
131133
info!("Loading and compiling the policy module");

crates/cli/src/commands/syn2mas.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ impl Options {
133133
// in the MAS database
134134
let config = SyncConfig::extract(figment)?;
135135
let clock = SystemClock::default();
136-
let encrypter = config.secrets.encrypter();
136+
let encrypter = config.secrets.encrypter().await?;
137137

138138
crate::sync::config_sync(
139139
config.upstream_oauth2,

crates/config/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ futures-util.workspace = true
2020
camino = { workspace = true, features = ["serde1"] }
2121
chrono.workspace = true
2222
figment.workspace = true
23+
hex.workspace = true
2324
ipnetwork = { version = "0.20.0", features = ["serde", "schemars"] }
2425
lettre.workspace = true
2526
schemars.workspace = true

crates/config/src/sections/secrets.rs

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -184,18 +184,70 @@ impl KeyConfig {
184184
}
185185
}
186186

187-
/// Application secrets
187+
/// Encryption config option.
188+
#[derive(Debug, Clone)]
189+
pub enum Encryption {
190+
File(Utf8PathBuf),
191+
Value([u8; 32]),
192+
}
193+
194+
/// Encryption fields as serialized in JSON.
188195
#[serde_as]
189-
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
190-
pub struct SecretsConfig {
191-
/// Encryption key for secure cookies
196+
#[derive(JsonSchema, Serialize, Deserialize, Debug, Clone)]
197+
struct EncryptionRaw {
198+
/// File containing the encryption key for secure cookies.
199+
#[schemars(with = "Option<String>")]
200+
#[serde(skip_serializing_if = "Option::is_none")]
201+
encryption_file: Option<Utf8PathBuf>,
202+
203+
/// Encryption key for secure cookies.
192204
#[schemars(
193-
with = "String",
205+
with = "Option<String>",
194206
regex(pattern = r"[0-9a-fA-F]{64}"),
195207
example = "example_secret"
196208
)]
197-
#[serde_as(as = "serde_with::hex::Hex")]
198-
pub encryption: [u8; 32],
209+
#[serde_as(as = "Option<serde_with::hex::Hex>")]
210+
#[serde(skip_serializing_if = "Option::is_none")]
211+
encryption: Option<[u8; 32]>,
212+
}
213+
214+
impl TryFrom<EncryptionRaw> for Encryption {
215+
type Error = anyhow::Error;
216+
217+
fn try_from(value: EncryptionRaw) -> Result<Encryption, Self::Error> {
218+
match (value.encryption, value.encryption_file) {
219+
(None, None) => bail!("Missing `encryption` or `encryption_file`"),
220+
(None, Some(path)) => Ok(Encryption::File(path)),
221+
(Some(encryption), None) => Ok(Encryption::Value(encryption)),
222+
(Some(_), Some(_)) => bail!("Cannot specify both `encryption` and `encryption_file`"),
223+
}
224+
}
225+
}
226+
227+
impl From<Encryption> for EncryptionRaw {
228+
fn from(value: Encryption) -> Self {
229+
match value {
230+
Encryption::File(path) => EncryptionRaw {
231+
encryption_file: Some(path),
232+
encryption: None,
233+
},
234+
Encryption::Value(encryption) => EncryptionRaw {
235+
encryption_file: None,
236+
encryption: Some(encryption),
237+
},
238+
}
239+
}
240+
}
241+
242+
/// Application secrets
243+
#[serde_as]
244+
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
245+
pub struct SecretsConfig {
246+
/// Encryption key for secure cookies
247+
#[schemars(with = "EncryptionRaw")]
248+
#[serde_as(as = "serde_with::TryFromInto<EncryptionRaw>")]
249+
#[serde(flatten)]
250+
encryption: Encryption,
199251

200252
/// List of private keys to use for signing and encrypting payloads
201253
#[serde(default)]
@@ -216,9 +268,33 @@ impl SecretsConfig {
216268
}
217269

218270
/// Derive an [`Encrypter`] out of the config
219-
#[must_use]
220-
pub fn encrypter(&self) -> Encrypter {
221-
Encrypter::new(&self.encryption)
271+
///
272+
/// # Errors
273+
///
274+
/// Returns an error when the Encryptor can not be created.
275+
pub async fn encrypter(&self) -> anyhow::Result<Encrypter> {
276+
Ok(Encrypter::new(&self.encryption().await?))
277+
}
278+
279+
/// Returns the encryption secret.
280+
///
281+
/// # Errors
282+
///
283+
/// Returns an error when the encryption secret could not be read from file.
284+
pub async fn encryption(&self) -> anyhow::Result<[u8; 32]> {
285+
// Read the encryption secret either embedded in the config file or on disk
286+
match self.encryption {
287+
Encryption::Value(encryption) => Ok(encryption),
288+
Encryption::File(ref path) => {
289+
let mut bytes = [0; 32];
290+
let content = tokio::fs::read(path).await?;
291+
hex::decode_to_slice(content, &mut bytes).context(
292+
"Content of `encryption_file` must contain hex characters \
293+
encoding exactly 32 bytes",
294+
)?;
295+
Ok(bytes)
296+
}
297+
}
222298
}
223299
}
224300

@@ -299,7 +375,7 @@ impl SecretsConfig {
299375
};
300376

301377
Ok(Self {
302-
encryption: Standard.sample(&mut rng),
378+
encryption: Encryption::Value(Standard.sample(&mut rng)),
303379
keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key],
304380
})
305381
}
@@ -340,7 +416,7 @@ impl SecretsConfig {
340416
};
341417

342418
Self {
343-
encryption: [0xEA; 32],
419+
encryption: Encryption::Value([0xEA; 32]),
344420
keys: vec![rsa_key, ecdsa_key],
345421
}
346422
}

docs/config.schema.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,25 +1515,26 @@
15151515
"SecretsConfig": {
15161516
"description": "Application secrets",
15171517
"type": "object",
1518-
"required": [
1519-
"encryption"
1520-
],
15211518
"properties": {
1522-
"encryption": {
1523-
"description": "Encryption key for secure cookies",
1524-
"examples": [
1525-
"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
1526-
],
1527-
"type": "string",
1528-
"pattern": "[0-9a-fA-F]{64}"
1529-
},
15301519
"keys": {
15311520
"description": "List of private keys to use for signing and encrypting payloads",
15321521
"default": [],
15331522
"type": "array",
15341523
"items": {
15351524
"$ref": "#/definitions/KeyConfig"
15361525
}
1526+
},
1527+
"encryption_file": {
1528+
"description": "File containing the encryption key for secure cookies.",
1529+
"type": "string"
1530+
},
1531+
"encryption": {
1532+
"description": "Encryption key for secure cookies.",
1533+
"examples": [
1534+
"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
1535+
],
1536+
"type": "string",
1537+
"pattern": "[0-9a-fA-F]{64}"
15371538
}
15381539
}
15391540
},

docs/reference/configuration.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,21 @@ secrets:
232232
-----END EC PRIVATE KEY-----
233233
```
234234

235+
### `secrets.encryption{_file}`
236+
237+
The encryption secret used for encrypting cookies and database fields. It takes
238+
the form of a 32-bytes-long hex-encoded string. To provide the encryption secret
239+
via file, set `secrets.encryption_file` to the file path; alternatively use
240+
`secrets.encryption` for declaring the secret inline. The options
241+
`secrets.encryption_file` and `secrets.encryption` are mutually exclusive.
242+
243+
If given via file, the encyption secret is only read at application startup.
244+
The secret is not updated when the content of the file changes.
245+
246+
> ⚠️ **Warning** – Do not change the encryption secret after the initial start!
247+
> Changing the encryption secret afterwards will lead to a loss of all encrypted
248+
> information in the database.
249+
235250
### `secrets.keys`
236251

237252
The service can use a number of key types for signing.

0 commit comments

Comments
 (0)