Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
0bfe9cc
feat(hush): add broker daemon and metadata catalog
AojdevStudio Oct 13, 2026
3c38647
fix(hush): harden broker token, socket, bws, and fallback paths
AojdevStudio Oct 13, 2026
9b09338
fix(hush): read token metadata from the open file so the check builds…
AojdevStudio Oct 13, 2026
e651b46
fix(hush): harden trusted path, dev flag, bws timeout, socket dir
AojdevStudio Oct 13, 2026
c107963
test(hush): pin broker test temp dirs to 0700 so a loose umask does n…
AojdevStudio Oct 13, 2026
c1cc927
fix(hush): let debug same-user daemons trust root-owned ancestors of bws
AojdevStudio Oct 13, 2026
36dfe94
fix(hush): apply review round fixes to the broker core
AojdevStudio Oct 14, 2026
f9b79c5
ci(gitguardian): ignore the ADR CLI example placeholder finding
AojdevStudio Oct 14, 2026
41efb02
fix(hush): bound the socket liveness probe and wake the test listener
AojdevStudio Oct 14, 2026
5dca726
fix(hush): keep saturated macOS listeners alive during the liveness p…
AojdevStudio Oct 14, 2026
a1e8b0b
fix(hush): make the saturation test deterministic and umask-proof
AojdevStudio Oct 14, 2026
beb0504
fix(hush): reach queue saturation deterministically in the probe test
AojdevStudio Oct 14, 2026
e5dd00e
docs(gitguardian): use the documented ggshield config filename
AojdevStudio Oct 14, 2026
ba3057d
chore(hush): trigger a fresh GitGuardian scan after the incident disp…
AojdevStudio Oct 14, 2026
1559939
fix(hush): a failed poll in the liveness probe leaves the socket in p…
AojdevStudio Oct 14, 2026
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
3 changes: 3 additions & 0 deletions bws-tui/Cargo.lock

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

6 changes: 5 additions & 1 deletion bws-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ ratatui = "0.29"
crossterm = "0.28"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
secrecy = "0.10"
secrecy = { version = "0.10", features = ["serde"] }
zeroize = "1"
anyhow = "1"
humantime = "2"
clap = { version = "4", features = ["derive"] }
arboard = "3"
base64 = "0.22"
libc = "0.2"

[dev-dependencies]
static_assertions = "1"
4 changes: 4 additions & 0 deletions bws-tui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ That narrow boundary is the point. `hush` is an interface, not another secrets p
- **Scoped reads:** `get` and `exec` fetch single secrets by id; only `list`/`sync` refreshes see the full metadata set, and nothing persists values.
- **Names-only audit log:** every operation is recorded with timestamp, verb, and key names — never values.

### Broker daemon (in progress)

A hidden `hush daemon` runs the broker from [ADR 0002](../docs/adr/0002-hush-broker.md). When it is reachable on the socket, `hush list` and `hush list --json` answer from the daemon metadata catalog. When no daemon is reachable, they keep today's direct `bws` behavior. The daemon refuses a token file that is not mode 0600 owned by root. The debug-only `--dev-same-user` flag relaxes ownership to the daemon user for same-user development.

### Known upstream limit: argv exposure

`bws` 2.1.0 accepts create/edit values only as positional argv. That makes a value briefly visible to local process inspection (`ps`) while the subprocess runs. `hush` cannot remove that exposure until upstream supports stdin or file descriptors.
Expand Down
104 changes: 104 additions & 0 deletions bws-tui/src/broker/audit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//! Daemon-owned audit log. Names only, never values or tokens.

use super::peercred::PeerCredentials;
use anyhow::{bail, Context, Result};
use std::fs::{OpenOptions, Permissions};
use std::io::Write;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::SystemTime;

/// The daemon records every catalog request with kernel peer identity.
pub(crate) trait AuditLog: Send + Sync {
fn record_catalog(&self, peer: PeerCredentials, project: Option<&str>) -> Result<()>;
}

/// Append-only file log. The daemon owns this file, not the client audit log.
pub(crate) struct FileAuditLog {
file: Mutex<std::fs::File>,
}

impl FileAuditLog {
pub(crate) fn open(path: &Path) -> Result<Self> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = OpenOptions::new()
.create(true)
.append(true)
.mode(0o600)
.open(path)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.with_context(|| format!("failed to open daemon audit log at {}", path.display()))?;
let metadata = file
.metadata()
.with_context(|| format!("failed to inspect daemon audit log at {}", path.display()))?;
// SAFETY: geteuid takes no arguments and cannot fail.
let euid = unsafe { libc::geteuid() };
if metadata.uid() != euid {
bail!("daemon audit log must be owned by the daemon user");
}
if metadata.mode() & 0o077 != 0 {
bail!("daemon audit log must not be group or world readable or writable");
}
// A created file can still lose bits to umask; pin 0600 for every daemon run.
file.set_permissions(Permissions::from_mode(0o600))
.with_context(|| format!("failed to secure daemon audit log at {}", path.display()))?;
Ok(Self {
file: Mutex::new(file),
})
}
}

/// Escape client-controlled text so one record stays exactly one line.
/// Newlines, carriage returns, other control bytes, and backslash become
/// escapes, so injected lines cannot look like daemon-authored records.
fn sanitize(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for ch in value.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
c if (c as u32) < 0x20 || c == '\u{7f}' => {
out.push_str(&format!("\\x{:02x}", c as u32))
}
c => out.push(c),
}
}
out
}

impl AuditLog for FileAuditLog {
fn record_catalog(&self, peer: PeerCredentials, project: Option<&str>) -> Result<()> {
let line = format!(
"{} catalog uid={} gid={} pid={} project={}\n",
humantime::format_rfc3339_seconds(SystemTime::now()),
peer.uid,
peer.gid,
peer.pid,
sanitize(project.unwrap_or("-")),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let mut file = self
.file
.lock()
.map_err(|_| anyhow::anyhow!("audit log lock poisoned"))?;
file.write_all(line.as_bytes())?;
Ok(())
}
}

/// Default daemon audit path. The `daemon` subcommand can override it.
pub(crate) fn default_audit_path() -> PathBuf {
if let Some(path) = std::env::var_os("HUSH_AUDIT_LOG") {
return PathBuf::from(path);
}
#[cfg(target_os = "macos")]
{
PathBuf::from("/var/db/hush/audit.log")
}
#[cfg(not(target_os = "macos"))]
{
PathBuf::from("/var/log/hush/audit.log")
}
}
Loading
Loading