Skip to content

Commit 8d73369

Browse files
Merge pull request #31 from Spacecraft-Software/agent-harden
feat(vault-agent): anti-leak hardening — disable core dumps + ptrace
2 parents 7eb5a7c + d22bd4c commit 8d73369

6 files changed

Lines changed: 201 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ range may break in any release.
1010

1111
### Added
1212

13+
- **Agent anti-leak hardening (core dumps + ptrace).** On startup the agent now
14+
disables core dumps and marks itself non-dumpable (which also blocks same-user
15+
ptrace), so the in-memory user key / refresh token can't leak to a core file
16+
or a debugger. Done via the `secmem-proc` crate (audited `rustix` syscalls,
17+
keeps the agent's `#![forbid(unsafe_code)]`); best-effort — a sandbox that
18+
restricts `setrlimit` logs a warning and the agent keeps running. PRD §12 M7
19+
hardening groundwork. (`mlock`/no-swap remains a tracked follow-up.)
20+
1321
- **Post-quantum transport (`pqc` feature, off by default).** A GPL-clean hybrid
1422
**X25519MLKEM768** key-exchange group is added to the rustls client config, so
1523
a TLS 1.3 handshake negotiates a post-quantum-secure secret when the server

Cargo.lock

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

README.md

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

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

5963
```sh

crates/vault-agent/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ uuid = { workspace = true }
4242
url = { workspace = true }
4343
thiserror = { workspace = true }
4444
arboard = { workspace = true, optional = true }
45+
# Process hardening: disable core dumps + ptrace at startup (safe rustix-backed
46+
# API, keeps the crate's #![forbid(unsafe_code)]). See `harden.rs`.
47+
secmem-proc = "0.3"
4548
vault-core = { path = "../vault-core" }
4649
vault-ipc = { path = "../vault-ipc" }
4750
vault-store = { path = "../vault-store" }

crates/vault-agent/src/harden.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
//! Process anti-leak hardening, run once at agent startup.
4+
//!
5+
//! The agent holds the unwrapped user key (and refresh token) in memory. This
6+
//! locks down two ways that material could escape the process:
7+
//!
8+
//! - **Core dumps** — a crash must not write key bytes to a core file on disk.
9+
//! - **ptrace** — another process running as the same user must not be able to
10+
//! attach a debugger and read the agent's memory.
11+
//!
12+
//! Both are handled by [`secmem_proc::harden_process`] (lowers `RLIMIT_CORE`
13+
//! and sets the process non-dumpable via the audited `rustix` syscall layer),
14+
//! which keeps this crate's `#![forbid(unsafe_code)]` intact.
15+
//!
16+
//! This does **not** yet cover swap exposure (`mlock`); that's a tracked
17+
//! follow-up. mlock is out of scope here.
18+
19+
/// Harden the current process against core dumps and ptrace. Best-effort: if a
20+
/// step fails (e.g. a sandbox that restricts `setrlimit`), log a warning and
21+
/// keep running — a partial-hardening failure must never take the agent down.
22+
pub fn harden_process() {
23+
if let Err(e) = secmem_proc::harden_process() {
24+
eprintln!("vault-agent: process hardening incomplete (continuing): {e}");
25+
}
26+
}
27+
28+
#[cfg(target_os = "linux")]
29+
#[cfg(test)]
30+
mod tests {
31+
/// After hardening, the process must be marked non-dumpable — that's what
32+
/// blocks core dumps and same-user ptrace. Where `/proc/self/status`
33+
/// exposes the `Dumpable` field (a normal kernel) we assert it is `0`; some
34+
/// sandboxed `/proc` mounts omit the field, in which case there is nothing
35+
/// to check (the call still must not panic). `PR_SET_DUMPABLE(0)` is
36+
/// unprivileged, so the assertion is deterministic where it applies.
37+
#[test]
38+
fn harden_sets_process_non_dumpable() {
39+
super::harden_process();
40+
let status = std::fs::read_to_string("/proc/self/status").expect("read /proc/self/status");
41+
if let Some(dumpable) = status
42+
.lines()
43+
.find_map(|l| l.strip_prefix("Dumpable:"))
44+
.map(str::trim)
45+
{
46+
assert_eq!(
47+
dumpable, "0",
48+
"process should be non-dumpable after hardening"
49+
);
50+
}
51+
}
52+
}

crates/vault-agent/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use vault_ipc::default_socket_path;
2121

2222
#[cfg(feature = "clipboard")]
2323
mod clipboard;
24+
mod harden;
2425
mod server;
2526
mod session;
2627
mod state;
@@ -97,6 +98,9 @@ enum ClipboardBackendArg {
9798
#[tokio::main(flavor = "current_thread")]
9899
async fn main() -> anyhow::Result<()> {
99100
let args = Args::parse();
101+
// Disable core dumps + ptrace before any key material can enter memory
102+
// (in particular before `try_resume` reloads a key from the keyring).
103+
harden::harden_process();
100104
let path = pick_socket(args.socket)?;
101105
#[cfg(feature = "clipboard")]
102106
let agent = {

0 commit comments

Comments
 (0)