Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/cli/src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ impl Options {
SC::Sync { prune, dry_run } => {
let config = SyncConfig::extract(figment)?;
let clock = SystemClock::default();
let encrypter = config.secrets.encrypter();
let encrypter = config.secrets.encrypter().await?;

// Grab a connection to the database
let mut conn = database_connection_from_config(&config.database).await?;
Expand Down
8 changes: 5 additions & 3 deletions crates/cli/src/commands/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl Options {
.context("could not run database migrations")?;
}

let encrypter = config.secrets.encrypter();
let encrypter = config.secrets.encrypter().await?;

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

let cookie_manager =
CookieManager::derive_from(config.http.public_base.clone(), &config.secrets.encryption);
let cookie_manager = CookieManager::derive_from(
config.http.public_base.clone(),
&config.secrets.encryption().await?,
);

// Load and compile the WASM policies (and fallback to the default embedded one)
info!("Loading and compiling the policy module");
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/commands/syn2mas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl Options {
// in the MAS database
let config = SyncConfig::extract(figment)?;
let clock = SystemClock::default();
let encrypter = config.secrets.encrypter();
let encrypter = config.secrets.encrypter().await?;

crate::sync::config_sync(
config.upstream_oauth2,
Expand Down
1 change: 1 addition & 0 deletions crates/config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ anyhow.workspace = true
camino = { workspace = true, features = ["serde1"] }
chrono.workspace = true
figment.workspace = true
hex.workspace = true
ipnetwork = { version = "0.20.0", features = ["serde", "schemars"] }
lettre.workspace = true
schemars.workspace = true
Expand Down
100 changes: 88 additions & 12 deletions crates/config/src/sections/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,70 @@ pub struct KeyConfig {
key_file: Option<Utf8PathBuf>,
}

/// Application secrets
/// Encryption config option.
#[derive(Debug, Clone)]
pub enum Encryption {
File(Utf8PathBuf),
Value([u8; 32]),
}

/// Encryption fields as serialized in JSON.
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SecretsConfig {
/// Encryption key for secure cookies
#[derive(JsonSchema, Serialize, Deserialize, Debug, Clone)]
struct EncryptionRaw {
/// File containing the encryption key for secure cookies.
#[schemars(with = "Option<String>")]
#[serde(skip_serializing_if = "Option::is_none")]
encryption_file: Option<Utf8PathBuf>,

/// Encryption key for secure cookies.
#[schemars(
with = "String",
with = "Option<String>",
regex(pattern = r"[0-9a-fA-F]{64}"),
example = "example_secret"
)]
#[serde_as(as = "serde_with::hex::Hex")]
pub encryption: [u8; 32],
#[serde_as(as = "Option<serde_with::hex::Hex>")]
#[serde(skip_serializing_if = "Option::is_none")]
encryption: Option<[u8; 32]>,
}

impl TryFrom<EncryptionRaw> for Encryption {
type Error = anyhow::Error;

fn try_from(value: EncryptionRaw) -> Result<Encryption, Self::Error> {
match (value.encryption, value.encryption_file) {
(None, None) => bail!("Missing `encryption` or `encryption_file`"),
(None, Some(path)) => Ok(Encryption::File(path)),
(Some(encryption), None) => Ok(Encryption::Value(encryption)),
(Some(_), Some(_)) => bail!("Cannot specify both `encryption` and `encryption_file`"),
}
}
}

impl From<Encryption> for EncryptionRaw {
fn from(value: Encryption) -> Self {
match value {
Encryption::File(path) => EncryptionRaw {
encryption_file: Some(path),
encryption: None,
},
Encryption::Value(encryption) => EncryptionRaw {
encryption_file: None,
encryption: Some(encryption),
},
}
}
}

/// Application secrets
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SecretsConfig {
/// Encryption key for secure cookies
#[schemars(with = "EncryptionRaw")]
#[serde_as(as = "serde_with::TryFromInto<EncryptionRaw>")]
#[serde(flatten)]
encryption: Encryption,

/// List of private keys to use for signing and encrypting payloads
#[serde(default)]
Expand Down Expand Up @@ -118,9 +170,33 @@ impl SecretsConfig {
}

/// Derive an [`Encrypter`] out of the config
#[must_use]
pub fn encrypter(&self) -> Encrypter {
Encrypter::new(&self.encryption)
///
/// # Errors
///
/// Returns an error when the Encryptor can not be created.
pub async fn encrypter(&self) -> anyhow::Result<Encrypter> {
Ok(Encrypter::new(&self.encryption().await?))
}

/// Returns the encryption secret.
///
/// # Errors
///
/// Returns an error when the encryption secret could not be read from file.
pub async fn encryption(&self) -> anyhow::Result<[u8; 32]> {
// Read the encryption secret either embedded in the config file or on disk
match self.encryption {
Encryption::Value(encryption) => Ok(encryption),
Encryption::File(ref path) => {
let mut bytes = [0; 32];
let content = tokio::fs::read(path).await?;
hex::decode_to_slice(content, &mut bytes).context(
"Content of `encryption_file` must contain hex characters \
encoding exactly 32 bytes",
)?;
Ok(bytes)
}
}
}
}

Expand Down Expand Up @@ -246,7 +322,7 @@ impl SecretsConfig {
};

Ok(Self {
encryption: Standard.sample(&mut rng),
encryption: Encryption::Value(Standard.sample(&mut rng)),
keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key],
})
}
Expand Down Expand Up @@ -291,7 +367,7 @@ impl SecretsConfig {
};

Self {
encryption: [0xEA; 32],
encryption: Encryption::Value([0xEA; 32]),
keys: vec![rsa_key, ecdsa_key],
}
}
Expand Down
23 changes: 12 additions & 11 deletions docs/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1515,25 +1515,26 @@
"SecretsConfig": {
"description": "Application secrets",
"type": "object",
"required": [
"encryption"
],
"properties": {
"encryption": {
"description": "Encryption key for secure cookies",
"examples": [
"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
],
"type": "string",
"pattern": "[0-9a-fA-F]{64}"
},
"keys": {
"description": "List of private keys to use for signing and encrypting payloads",
"default": [],
"type": "array",
"items": {
"$ref": "#/definitions/KeyConfig"
}
},
"encryption_file": {
"description": "File containing the encryption key for secure cookies.",
"type": "string"
},
"encryption": {
"description": "Encryption key for secure cookies.",
"examples": [
"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
],
"type": "string",
"pattern": "[0-9a-fA-F]{64}"
}
}
},
Expand Down
15 changes: 15 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,21 @@ secrets:
-----END EC PRIVATE KEY-----
```

### `secrets.encryption{_file}`

The encryption secret used for encrypting cookies and database fields. It takes
the form of a 32-bytes-long hex-encoded string. To provide the encryption secret
via file, set `secrets.encryption_file` to the file path; alternatively use
`secrets.encryption` for declaring the secret inline. The options
`secrets.encryption_file` and `secrets.encryption` are mutually exclusive.

If given via file, the encyption secret is only read at application startup.
The secret is not updated when the content of the file changes.

> ⚠️ **Warning** – Do not change the encryption secret after the initial start!
> Changing the encryption secret afterwards will lead to a loss of all encrypted
> information in the database.

### `secrets.keys`

The service can use a number of key types for signing.
Expand Down
Loading