Skip to content

Commit 8bd9835

Browse files
authored
feat(dgw): make libsodium (secrets) an optional mlock feature (#1691)
1 parent c10ad70 commit 8bd9835

5 files changed

Lines changed: 83 additions & 20 deletions

File tree

.github/workflows/ci.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,13 +620,13 @@ jobs:
620620
if: ${{ matrix.os == 'linux' }}
621621
run: |
622622
sudo apt-get update
623-
sudo apt-get -o Acquire::Retries=3 install python3-wget python3-setuptools libsystemd-dev dh-make
623+
sudo apt-get -o Acquire::Retries=3 install python3-wget python3-setuptools libsystemd-dev dh-make libsodium-dev
624624
625625
- name: Configure Linux (arm) runner
626626
if: ${{ matrix.os == 'linux' && matrix.arch == 'arm64' }}
627627
run: |
628628
sudo dpkg --add-architecture arm64
629-
sudo apt-get -o Acquire::Retries=3 install -qy binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu qemu-user
629+
sudo apt-get -o Acquire::Retries=3 install -qy binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu qemu-user libsodium-dev:arm64
630630
rustup target add aarch64-unknown-linux-gnu
631631
echo "STRIP_EXECUTABLE=aarch64-linux-gnu-strip" >> $GITHUB_ENV
632632
@@ -663,6 +663,22 @@ jobs:
663663
Write-Output "windows_sdk_ver_bin_path=$path" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
664664
shell: pwsh
665665

666+
667+
- name: Enable mlock for production
668+
# arm64 is excluded: libsodium cross-compilation is not supported in the cbake sysroot.
669+
# arm64 production builds will emit a startup warning about missing mlock protection.
670+
# On Linux, libsodium-dev is installed in the configure steps above (apt-get).
671+
# On Windows, libsodium is installed here via vcpkg (deferred to production to avoid slow builds on PRs).
672+
if: ${{ needs.preflight.outputs.rust-profile == 'production' && matrix.arch != 'arm64' }}
673+
run: |
674+
if ($Env:RUNNER_OS -eq "Windows") {
675+
# Install libsodium via vcpkg for the mlock feature (requires static library)
676+
vcpkg install libsodium:x64-windows-static
677+
echo "VCPKG_ROOT=$Env:VCPKG_INSTALLATION_ROOT" >> $Env:GITHUB_ENV
678+
}
679+
echo "CARGO_FEATURES=mlock" >> $Env:GITHUB_ENV
680+
shell: pwsh
681+
666682
- name: Build
667683
run: |
668684
if ($Env:RUNNER_OS -eq "Linux") {

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,23 @@ immediately, without going through the acceptance testing process of our quality
2020

2121
### From sources
2222

23-
Ensure that you have [the Rust toolchain installed][install_rust] and [libsodium][libsodium] installed on your system, then clone this repository and run:
23+
Ensure that you have [the Rust toolchain installed][install_rust] and then clone this repository and run:
2424

2525
```shell
2626
cargo install --path ./devolutions-gateway
2727
```
2828

29-
> **Note:** `libsodium` is required as a native dependency for in-memory credential protection.
30-
> On Windows, it is vendored automatically via vcpkg.
29+
To enable enhanced in-memory credential protection (mlock via libsodium), build with the `mlock` feature:
30+
31+
```shell
32+
cargo install --path ./devolutions-gateway --features mlock
33+
```
34+
35+
> **Note:** The `mlock` feature requires [libsodium][libsodium] to be installed.
36+
> On Windows, it is found automatically via vcpkg.
3137
> On Linux and macOS, install it using your system package manager (e.g., `apt install libsodium-dev` or `brew install libsodium`).
38+
> Production builds should always include the `mlock` feature.
39+
> Without it, a startup warning is emitted in release builds.
3240
3341
## Configuration
3442

devolutions-gateway/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ workspace = true
1414

1515
[features]
1616
default = []
17+
mlock = ["dep:secrets"]
1718
openapi = ["dep:utoipa"]
1819

1920
[dependencies]
@@ -75,7 +76,7 @@ bitflags = "2.9"
7576
picky = { version = "7.0.0-rc.15", default-features = false, features = ["jose", "x509", "pkcs12", "time_conversion"] }
7677
zeroize = { version = "1.8", features = ["derive"] }
7778
chacha20poly1305 = "0.10"
78-
secrets = "1.2"
79+
secrets = { version = "1.2", optional = true }
7980
secrecy = { version = "0.10", features = ["serde"] }
8081
rand = "0.8"
8182
multibase = "0.9"

devolutions-gateway/src/credential/crypto.rs

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
//! In-memory credential encryption using ChaCha20-Poly1305.
22
//!
33
//! This module provides encryption-at-rest for passwords stored in the credential store.
4-
//! A randomly generated 256-bit master key is protected using libsodium's memory locking
5-
//! facilities (mlock/mprotect), and passwords are encrypted using ChaCha20-Poly1305 AEAD.
4+
//! A randomly generated 256-bit master key is stored in a zeroize-on-drop wrapper.
5+
//! When the `mlock` feature is enabled, libsodium's memory locking facilities
6+
//! (mlock/mprotect) are additionally used to prevent the key from being swapped to
7+
//! disk or appearing in core dumps.
68
//!
79
//! ## Security Properties
810
//!
9-
//! - Master key stored in mlock'd memory (excluded from core dumps)
1011
//! - Passwords encrypted at rest in regular heap memory
1112
//! - Decryption on-demand into short-lived zeroized buffers
1213
//! - ChaCha20-Poly1305 provides authenticated encryption
1314
//! - Random 96-bit nonces prevent nonce reuse
15+
//! - Master key zeroized on drop
16+
//! - With `mlock` feature: Master key stored in mlock'd memory (excluded from core dumps)
1417
1518
use core::fmt;
1619
use std::sync::LazyLock;
@@ -20,28 +23,33 @@ use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng};
2023
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
2124
use parking_lot::Mutex;
2225
use rand::RngCore as _;
26+
#[cfg(feature = "mlock")]
2327
use secrets::SecretBox;
28+
#[cfg(not(feature = "mlock"))]
29+
use zeroize::Zeroizing;
2430
use zeroize::{Zeroize, ZeroizeOnDrop};
2531

2632
/// Global master key for credential encryption.
2733
///
28-
/// Initialized lazily on first access. The key material is stored in memory
29-
/// protected by mlock/mprotect via libsodium's SecretBox, wrapped in a Mutex
30-
/// for thread-safe access.
34+
/// Initialized lazily on first access. The key material is wrapped in a Mutex
35+
/// for thread-safe access. With the `mlock` feature, key memory is additionally
36+
/// protected by mlock/mprotect via libsodium's SecretBox.
3137
pub(super) static MASTER_KEY: LazyLock<Mutex<MasterKeyManager>> = LazyLock::new(|| {
3238
Mutex::new(MasterKeyManager::new().expect("failed to initialize credential encryption master key"))
3339
});
3440

35-
/// Manages the master encryption key using libsodium's secure memory facilities.
41+
/// Manages the master encryption key.
3642
///
37-
/// The key is stored in memory that is:
43+
/// The key is zeroized on drop. When the `mlock` feature is enabled, the key
44+
/// memory is additionally:
3845
/// - Locked (mlock) to prevent swapping to disk
3946
/// - Protected (mprotect) with appropriate access controls
4047
/// - Excluded from core dumps
41-
/// - Zeroized on drop
4248
pub(super) struct MasterKeyManager {
43-
// SecretBox provides mlock/mprotect for the key material.
49+
#[cfg(feature = "mlock")]
4450
key_material: SecretBox<[u8; 32]>,
51+
#[cfg(not(feature = "mlock"))]
52+
key_material: Zeroizing<[u8; 32]>,
4553
}
4654

4755
impl MasterKeyManager {
@@ -51,22 +59,36 @@ impl MasterKeyManager {
5159
///
5260
/// Returns error if secure memory allocation fails or RNG fails.
5361
fn new() -> anyhow::Result<Self> {
54-
// SecretBox allocates memory with mlock and mprotect.
62+
#[cfg(feature = "mlock")]
5563
let key_material = SecretBox::try_new(|key_bytes: &mut [u8; 32]| {
5664
OsRng.fill_bytes(key_bytes);
5765
Ok::<_, anyhow::Error>(())
5866
})
5967
.context("failed to allocate secure memory for master key")?;
6068

69+
#[cfg(not(feature = "mlock"))]
70+
let key_material = {
71+
let mut key = Zeroizing::new([0u8; 32]);
72+
OsRng.fill_bytes(key.as_mut());
73+
key
74+
};
75+
6176
Ok(Self { key_material })
6277
}
6378

6479
/// Encrypt a password using ChaCha20-Poly1305.
6580
///
6681
/// Returns the nonce and ciphertext (which includes the Poly1305 auth tag).
6782
pub(super) fn encrypt(&self, plaintext: &str) -> anyhow::Result<EncryptedPassword> {
83+
#[cfg(feature = "mlock")]
6884
let key_ref = self.key_material.borrow();
69-
let cipher = ChaCha20Poly1305::new_from_slice(key_ref.as_ref()).expect("key is exactly 32 bytes");
85+
#[cfg(feature = "mlock")]
86+
let key_bytes: &[u8] = key_ref.as_ref();
87+
88+
#[cfg(not(feature = "mlock"))]
89+
let key_bytes: &[u8] = self.key_material.as_ref();
90+
91+
let cipher = ChaCha20Poly1305::new_from_slice(key_bytes).expect("key is exactly 32 bytes");
7092

7193
// Generate random 96-bit nonce (12 bytes for ChaCha20-Poly1305).
7294
let nonce = ChaCha20Poly1305::generate_nonce(OsRng);
@@ -84,8 +106,15 @@ impl MasterKeyManager {
84106
/// The returned `DecryptedPassword` should have a short lifetime.
85107
/// Use it immediately and let it drop to zeroize the plaintext.
86108
pub(super) fn decrypt(&self, encrypted: &EncryptedPassword) -> anyhow::Result<DecryptedPassword> {
109+
#[cfg(feature = "mlock")]
87110
let key_ref = self.key_material.borrow();
88-
let cipher = ChaCha20Poly1305::new_from_slice(key_ref.as_ref()).expect("key is exactly 32 bytes");
111+
#[cfg(feature = "mlock")]
112+
let key_bytes: &[u8] = key_ref.as_ref();
113+
114+
#[cfg(not(feature = "mlock"))]
115+
let key_bytes: &[u8] = self.key_material.as_ref();
116+
117+
let cipher = ChaCha20Poly1305::new_from_slice(key_bytes).expect("key is exactly 32 bytes");
89118

90119
let plaintext_bytes = cipher
91120
.decrypt(&encrypted.nonce, encrypted.ciphertext.as_ref())
@@ -98,7 +127,8 @@ impl MasterKeyManager {
98127
}
99128
}
100129

101-
// Note: SecretBox handles secure zeroization and munlock automatically on drop.
130+
// Note: With `mlock` feature, SecretBox handles secure zeroization and munlock automatically on drop.
131+
// Without `mlock` feature, Zeroizing handles secure zeroization on drop (no mlock).
102132

103133
/// Encrypted password stored in heap memory.
104134
///

devolutions-gateway/src/service.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ impl GatewayService {
4949

5050
info!(version = env!("CARGO_PKG_VERSION"));
5151

52+
// Warn in release builds if the mlock security feature is not compiled in.
53+
#[cfg(all(not(feature = "mlock"), not(debug_assertions)))]
54+
warn!(
55+
"Credential encryption master key does not have mlock memory protection. \
56+
Rebuild with the `mlock` feature (requires libsodium) to prevent key exposure \
57+
in core dumps and swap."
58+
);
59+
5260
let conf_file = conf_handle.get_conf_file();
5361
trace!(?conf_file);
5462

0 commit comments

Comments
 (0)