Skip to content
Open
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.

1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ RUN apt-get update \
ca-certificates \
curl \
git \
openssl \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid 1000 buzz \
&& useradd --system --uid 1000 --gid 1000 --home-dir /var/lib/buzz \
Expand Down
3 changes: 3 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,9 @@ test-unit:
# `cargo test --workspace`; without this step a manifest edit that
# diverges Rust from the corpus ships green.
cargo nextest run -p buzz-agent --lib
# buzz-relay --lib: 906 unit tests; the Postgres/Redis-backed paths
# are #[ignore]d or runtime-skipped, so this stays infra-free.
cargo nextest run -p buzz-relay --lib
else
./scripts/run-tests.sh unit
fi
Expand Down
90 changes: 86 additions & 4 deletions crates/buzz-backend-kubernetes/src/reconcile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,23 @@ mod tests {
}
}

/// The fake mirrors the equality-based selector emitted by
/// `AgentIdentity::selector`, rather than returning every object in its
/// namespace. This keeps tests honest about the isolation supplied by the
/// apiserver before reconciliation and GC inspect candidates.
fn selector_matches(selector: &str, labels: Option<&BTreeMap<String, String>>) -> bool {
let Some(labels) = labels else {
return false;
};

selector.split(',').all(|requirement| {
let Some((key, value)) = requirement.split_once('=') else {
panic!("fake does not support label selector requirement {requirement:?}");
};
labels.get(key).map(String::as_str) == Some(value)
})
}

impl Substrate for Fake {
async fn ensure_namespace(&self, namespace: &str) -> Result<(), String> {
match &self.namespace_error {
Expand All @@ -570,16 +587,27 @@ mod tests {

async fn list_pods(
&self,
_selector: &str,
selector: &str,
) -> Result<(Vec<Pod>, Option<DateTime<Utc>>), String> {
Ok((
self.pods.borrow().values().cloned().collect(),
self.pods
.borrow()
.values()
.filter(|pod| selector_matches(selector, pod.metadata.labels.as_ref()))
.cloned()
.collect(),
self.server_now,
))
}

async fn list_secrets(&self, _selector: &str) -> Result<Vec<Secret>, String> {
Ok(self.secrets.borrow().clone())
async fn list_secrets(&self, selector: &str) -> Result<Vec<Secret>, String> {
Ok(self
.secrets
.borrow()
.iter()
.filter(|secret| selector_matches(selector, secret.metadata.labels.as_ref()))
.cloned()
.collect())
}

async fn secret_exists(&self, name: &str) -> Result<bool, String> {
Expand Down Expand Up @@ -806,6 +834,60 @@ mod tests {
);
}

#[test]
fn fake_lists_only_objects_matching_the_requested_identity() {
let id = identity();
let other = identity();
let cfg = config();
let ours = our_pod(&id, &cfg, Some(running()));
let theirs = our_pod(&other, &cfg, Some(running()));
let our_secret = crate::pod::build_secret(&id, &cfg.namespace, "gen-ours", env());
let their_secret = crate::pod::build_secret(&other, &cfg.namespace, "gen-theirs", env());
let fake = Fake::default().with_pod(ours).with_pod(theirs);
fake.secrets.borrow_mut().extend([our_secret, their_secret]);

let (pods, _) = block_on(fake.list_pods(&id.selector())).unwrap();
let secrets = block_on(fake.list_secrets(&id.selector())).unwrap();

assert_eq!(pods.len(), 1);
assert_eq!(
pods[0].metadata.name.as_deref(),
Some(id.pod_name().as_str())
);
assert_eq!(secrets.len(), 1);
assert_eq!(
secrets[0].metadata.name.as_deref(),
Some(id.secret_name("gen-ours").as_str())
);
}

#[test]
fn unrelated_tenant_objects_do_not_enter_reconciliation_or_gc() {
let id = identity();
let other = identity();
let cfg = config();
let ours = our_pod(&id, &cfg, Some(running()));
let theirs = our_pod(&other, &cfg, Some(terminated()));
let their_pod_name = theirs.metadata.name.clone().unwrap();
let their_secret = crate::pod::build_secret(&other, &cfg.namespace, "gen-existing", env());
let their_secret_name = their_secret.metadata.name.clone().unwrap();
let fake = Fake::default().with_pod(ours).with_pod(theirs);
fake.secrets.borrow_mut().push(their_secret);

assert_eq!(run(&fake, &id, &cfg).unwrap(), id.pod_name());
assert_eq!(
fake.mutations(),
[format!("ensure_namespace {}", cfg.namespace)],
"another tenant affected this deploy"
);
assert!(fake.pods.borrow().contains_key(&their_pod_name));
assert!(fake
.secrets
.borrow()
.iter()
.any(|secret| secret.metadata.name.as_deref() == Some(&their_secret_name)));
}

/// The strict no-op row: a started pod returns its id having mutated
/// nothing at all. Asserted on the *call log*, not on final state — a
/// delete-then-recreate would leave identical final state.
Expand Down
6 changes: 3 additions & 3 deletions crates/buzz-core/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ fn filter_match_one(f: &Filter, ev: &StoredEvent) -> bool {
}

for (tag_key, tag_values) in f.generic_tags.iter() {
let tag_key_str = tag_key.to_string();
let tag_key_str = tag_key.as_str();
let has_match = tag_values.iter().any(|filter_val| {
ev.event
.tags
.iter()
.filter(|t| t.kind().to_string() == tag_key_str)
.filter(|t| t.kind().as_str() == tag_key_str)
.filter_map(|t| t.content())
.any(|event_val| event_val == filter_val.as_str())
});
Expand All @@ -81,7 +81,7 @@ fn filter_match_one(f: &Filter, ev: &StoredEvent) -> bool {
// fallback ONLY when the event has no h-tags at all — if the event
// has explicit h-tags, those are authoritative and must match.
if !has_match && tag_key_str == "h" {
let event_has_h_tags = ev.event.tags.iter().any(|t| t.kind().to_string() == "h");
let event_has_h_tags = ev.event.tags.iter().any(|t| t.kind().as_str() == "h");
if !event_has_h_tags {
if let Some(ch_id) = ev.channel_id {
let ch_str = ch_id.to_string();
Expand Down
1 change: 1 addition & 0 deletions crates/buzz-relay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ serde_yaml = { workspace = true }
sha2 = { workspace = true }
hmac = { workspace = true }
subtle = { workspace = true }
zeroize = { workspace = true }
rand = { workspace = true }
hex = { workspace = true }
url = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions crates/buzz-relay/src/api/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1799,6 +1799,7 @@ async fn handle_bridge_search(
page: search_page,
per_page: limit,
mode: search_mode,
cursor: None,
};

let search_result = state
Expand Down
48 changes: 32 additions & 16 deletions crates/buzz-relay/src/api/git/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
//! - Fail-closed: curl failure, timeout, non-200 → exit 1
//! - Quarantine vars inherited for ancestry checks
//! - HMAC binds callback to specific push operation
//! - The HMAC key never appears in a child process's argv: it is handed to
//! `buzz-relay hook-hmac` on file descriptor 3, so a same-UID process
//! reading `/proc/<pid>/cmdline` or `ps` cannot recover it

use std::path::Path;

Expand All @@ -21,7 +24,9 @@ use tracing::{error, info};
///
/// Environment variables set by the relay before spawning git receive-pack:
/// - `BUZZ_HOOK_URL` — internal policy endpoint (http://127.0.0.1:{port}/internal/git/policy)
/// - `BUZZ_HOOK_SECRET` — per-push HMAC secret
/// - `BUZZ_HOOK_SECRET` — deployment-wide HMAC secret (`git_hook_hmac_secret`,
/// injected by `transport.rs`; shared by every push and every replica, so it
/// must never reach a child process's argv — see the signing step below)
/// - `BUZZ_REPO_ID` — repo identifier (d-tag)
/// - `BUZZ_COMMUNITY_ID` — server-resolved community UUID for the git HTTP request
/// - `BUZZ_PUSHER_PUBKEY` — authenticated pusher's hex pubkey
Expand Down Expand Up @@ -112,7 +117,10 @@ if [ -f "$HMAC_FILE" ]; then
fi
HMAC_INPUT="${HMAC_INPUT}|${TIMESTAMP}"

SIGNATURE=$(printf '%s' "$HMAC_INPUT" | openssl dgst -sha256 -hmac "$BUZZ_HOOK_SECRET" -hex 2>/dev/null | sed 's/.*= //')
# Feed the secret over fd 3, never as an openssl command-line argument. The
# helper is the same Rust binary as the relay, selected by argv[1]; using a
# dedicated fd leaves stdin available for the canonical payload.
SIGNATURE=$(printf '%s' "$HMAC_INPUT" | /usr/local/bin/buzz-relay hook-hmac 3<<<"$BUZZ_HOOK_SECRET" 2>/dev/null)
if [ -z "$SIGNATURE" ]; then
echo "error: failed to compute HMAC signature" >&2
exit 1
Expand Down Expand Up @@ -182,26 +190,34 @@ mod tests {
use super::PRE_RECEIVE_HOOK;

#[test]
fn runtime_image_installs_pre_receive_hook_tools() {
fn hook_uses_in_image_tools_and_keeps_hmac_secret_off_argv() {
let dockerfile = include_str!("../../../../../Dockerfile");
let runtime_stage = dockerfile
.split("FROM debian:${DEBIAN_VERSION}-slim AS runtime")
.split("FROM debian:${DEBIAN_VERSION}-slim AS runtime-base")
.nth(1)
.expect("Dockerfile should have a runtime stage");
.expect("Dockerfile should have a runtime-base stage");
let runtime_setup = runtime_stage
.split("COPY --from=builder")
.split("COPY --from=web-builder")
.next()
.expect("runtime stage should copy built artifacts after package setup");

for tool in ["curl", "openssl"] {
assert!(
PRE_RECEIVE_HOOK.contains(tool),
"test setup expected the pre-receive hook to invoke {tool}"
);
assert!(
runtime_setup.contains(&format!("\n {tool} \\")),
"relay runtime image must install {tool}; the git pre-receive hook uses it and fails closed without it"
);
}
assert!(PRE_RECEIVE_HOOK.contains("curl"));
assert!(
runtime_setup.contains("\n curl \\"),
"relay runtime image must install curl; the git hook fails closed without it"
);
assert!(
PRE_RECEIVE_HOOK
.contains("/usr/local/bin/buzz-relay hook-hmac 3<<<\"$BUZZ_HOOK_SECRET\""),
"the hook must pass its HMAC secret over a dedicated fd"
);
assert!(
!PRE_RECEIVE_HOOK.contains("-hmac \"$BUZZ_HOOK_SECRET\""),
"the hook secret must not be exposed in process argv"
);
assert!(
!runtime_setup.contains("\n openssl \\"),
"the hook no longer needs the openssl CLI in the runtime image"
);
}
}
Loading