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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ range may break in any release.

### Added

- **Agent anti-leak hardening (core dumps + ptrace).** On startup the agent now
disables core dumps and marks itself non-dumpable (which also blocks same-user
ptrace), so the in-memory user key / refresh token can't leak to a core file
or a debugger. Done via the `secmem-proc` crate (audited `rustix` syscalls,
keeps the agent's `#![forbid(unsafe_code)]`); best-effort — a sandbox that
restricts `setrlimit` logs a warning and the agent keeps running. PRD §12 M7
hardening groundwork. (`mlock`/no-swap remains a tracked follow-up.)

- **Post-quantum transport (`pqc` feature, off by default).** A GPL-clean hybrid
**X25519MLKEM768** key-exchange group is added to the rustls client config, so
a TLS 1.3 handshake negotiates a post-quantum-secure secret when the server
Expand Down
130 changes: 130 additions & 0 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ An optional **post-quantum transport** (`--features pqc`) prefers the hybrid
X25519MLKEM768 key exchange on the HTTPS client, off by default — see
[`docs/pqc.md`](docs/pqc.md).

The agent hardens itself at startup: it **disables core dumps and ptrace** (so
the in-memory user key can't leak to a core file or a debugger), on top of the
`0600` socket and zeroized key buffers.

## Getting started

```sh
Expand Down
3 changes: 3 additions & 0 deletions crates/vault-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ uuid = { workspace = true }
url = { workspace = true }
thiserror = { workspace = true }
arboard = { workspace = true, optional = true }
# Process hardening: disable core dumps + ptrace at startup (safe rustix-backed
# API, keeps the crate's #![forbid(unsafe_code)]). See `harden.rs`.
secmem-proc = "0.3"
vault-core = { path = "../vault-core" }
vault-ipc = { path = "../vault-ipc" }
vault-store = { path = "../vault-store" }
Expand Down
52 changes: 52 additions & 0 deletions crates/vault-agent/src/harden.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-3.0-or-later

//! Process anti-leak hardening, run once at agent startup.
//!
//! The agent holds the unwrapped user key (and refresh token) in memory. This
//! locks down two ways that material could escape the process:
//!
//! - **Core dumps** — a crash must not write key bytes to a core file on disk.
//! - **ptrace** — another process running as the same user must not be able to
//! attach a debugger and read the agent's memory.
//!
//! Both are handled by [`secmem_proc::harden_process`] (lowers `RLIMIT_CORE`
//! and sets the process non-dumpable via the audited `rustix` syscall layer),
//! which keeps this crate's `#![forbid(unsafe_code)]` intact.
//!
//! This does **not** yet cover swap exposure (`mlock`); that's a tracked
//! follow-up. mlock is out of scope here.

/// Harden the current process against core dumps and ptrace. Best-effort: if a
/// step fails (e.g. a sandbox that restricts `setrlimit`), log a warning and
/// keep running — a partial-hardening failure must never take the agent down.
pub fn harden_process() {
if let Err(e) = secmem_proc::harden_process() {
eprintln!("vault-agent: process hardening incomplete (continuing): {e}");
}
}

#[cfg(target_os = "linux")]
#[cfg(test)]
mod tests {
/// After hardening, the process must be marked non-dumpable — that's what
/// blocks core dumps and same-user ptrace. Where `/proc/self/status`
/// exposes the `Dumpable` field (a normal kernel) we assert it is `0`; some
/// sandboxed `/proc` mounts omit the field, in which case there is nothing
/// to check (the call still must not panic). `PR_SET_DUMPABLE(0)` is
/// unprivileged, so the assertion is deterministic where it applies.
#[test]
fn harden_sets_process_non_dumpable() {
super::harden_process();
let status = std::fs::read_to_string("/proc/self/status").expect("read /proc/self/status");
if let Some(dumpable) = status
.lines()
.find_map(|l| l.strip_prefix("Dumpable:"))
.map(str::trim)
{
assert_eq!(
dumpable, "0",
"process should be non-dumpable after hardening"
);
}
}
}
4 changes: 4 additions & 0 deletions crates/vault-agent/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use vault_ipc::default_socket_path;

#[cfg(feature = "clipboard")]
mod clipboard;
mod harden;
mod server;
mod session;
mod state;
Expand Down Expand Up @@ -97,6 +98,9 @@ enum ClipboardBackendArg {
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
// Disable core dumps + ptrace before any key material can enter memory
// (in particular before `try_resume` reloads a key from the keyring).
harden::harden_process();
let path = pick_socket(args.socket)?;
#[cfg(feature = "clipboard")]
let agent = {
Expand Down
Loading