feat(cli): automation bundle + identity-minimization features
Reduces manual setup steps and trims user-identifying data exposed by the server/client, in the spirit of the deployment story: an operator on the wire sees less, and the admin types fewer commands. New CLI subcommands: - `aura server-init`: one shot — pki init + issue-server + writes a ready server.toml with auto-detected egress iface; flags --enable-knock, --enable-cover-traffic, --no-nat, --run-as toggle the new transport defenses and privilege drop. - `aura provision-client`: issues a client cert and assembles the full bundle (ca.crt + client.crt + client.key + client.toml in one directory) ready to hand over to the client device. --id is optional (defaults to a fresh UUIDv4, so client identities don't have to encode anything real). Identity / log minimization: - `aura pki issue-client --id` is now optional — UUIDv4 by default. - `[server]/[client] no_logs = true` filters peer_id, client_ip, source_addr, client_id, local_ip, user, id, assigned_ip, peer field values through a custom tracing FormatFields layer (events still fire but the identifying fields are redacted before being written). - `[client] bridges = [...]`: secondary server addresses; build_dial_targets shuffles them after the primary, so blocking one IP doesn't kill the client. - Auto-detect egress iface in [server.nat] (via detect_default_egress_iface); egress_iface in config becomes optional with graceful fallback. Config examples updated; backward-compatible (all new sections optional with serde defaults). Workspace: 207 tests passed (+22), clippy -D warnings clean, fmt clean. No new workspace deps. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//! Integration test for [`aura_cli::no_logs::redacting_field_formatter`].
|
||||
//!
|
||||
//! The production code installs the same `FormatFields` against the global subscriber via
|
||||
//! [`aura_cli::no_logs::init_filtered_tracing`]. We cannot use a global subscriber inside a unit
|
||||
//! test (it stays installed for the whole test binary and leaks across tests). Instead we mount
|
||||
//! the same formatter on a *per-test* subscriber using the `with_default` guard, route output
|
||||
//! through an in-memory writer, and assert that the redacted field values are absent while
|
||||
//! non-redacted fields still appear.
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
|
||||
/// An in-memory writer factory: each `make_writer` returns a guard that locks the shared `Vec<u8>`
|
||||
/// and writes into it. Cheap enough for one-shot test setups.
|
||||
#[derive(Clone, Default)]
|
||||
struct BufWriter {
|
||||
inner: Arc<Mutex<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl BufWriter {
|
||||
fn snapshot(&self) -> String {
|
||||
let guard = self.inner.lock().unwrap();
|
||||
String::from_utf8(guard.clone()).expect("utf8")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MakeWriter<'a> for BufWriter {
|
||||
type Writer = BufWriterGuard;
|
||||
fn make_writer(&'a self) -> Self::Writer {
|
||||
BufWriterGuard {
|
||||
inner: Arc::clone(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BufWriterGuard {
|
||||
inner: Arc<Mutex<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl Write for BufWriterGuard {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let mut g = self.inner.lock().unwrap();
|
||||
g.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Drive `tracing::info!` with one redacted and one safe field, route output through the redacting
|
||||
/// formatter into a buffer, and assert the redacted value is absent while the safe value is present.
|
||||
#[test]
|
||||
fn no_logs_drops_peer_id_field_from_output() {
|
||||
let buf = BufWriter::default();
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_writer(buf.clone())
|
||||
.with_ansi(false)
|
||||
.fmt_fields(aura_cli::no_logs::redacting_field_formatter())
|
||||
.finish();
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
// peer_id (redacted) and bytes (kept) — the message itself ("client accepted") is fine.
|
||||
tracing::info!(
|
||||
peer_id = "SECRET-CLIENT-ID-12345",
|
||||
bytes = 64u64,
|
||||
"client accepted"
|
||||
);
|
||||
});
|
||||
|
||||
let out = buf.snapshot();
|
||||
assert!(
|
||||
!out.contains("SECRET-CLIENT-ID-12345"),
|
||||
"redacted peer_id leaked: {out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("bytes=64"),
|
||||
"non-redacted field missing: {out}"
|
||||
);
|
||||
assert!(out.contains("client accepted"), "message missing: {out}");
|
||||
}
|
||||
|
||||
/// Every spec-listed identifier is suppressed in one go.
|
||||
#[test]
|
||||
fn no_logs_drops_every_listed_identifier() {
|
||||
let buf = BufWriter::default();
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_writer(buf.clone())
|
||||
.with_ansi(false)
|
||||
.fmt_fields(aura_cli::no_logs::redacting_field_formatter())
|
||||
.finish();
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::info!(
|
||||
peer_id = "PEERVAL",
|
||||
client_ip = "CLIPVAL",
|
||||
source_addr = "SRCVAL",
|
||||
client_id = "CIDVAL",
|
||||
local_ip = "LIPVAL",
|
||||
user = "USERVAL",
|
||||
id = "IDVAL",
|
||||
assigned_ip = "ASSVAL",
|
||||
peer = "PEERVAL2",
|
||||
bytes = 42u64,
|
||||
"test"
|
||||
);
|
||||
});
|
||||
|
||||
let out = buf.snapshot();
|
||||
for redacted in [
|
||||
"PEERVAL", "CLIPVAL", "SRCVAL", "CIDVAL", "LIPVAL", "USERVAL", "IDVAL", "ASSVAL",
|
||||
"PEERVAL2",
|
||||
] {
|
||||
assert!(
|
||||
!out.contains(redacted),
|
||||
"value '{redacted}' leaked into output: {out}"
|
||||
);
|
||||
}
|
||||
// bytes is a kept field — must still be visible.
|
||||
assert!(out.contains("bytes=42"), "kept field missing: {out}");
|
||||
}
|
||||
|
||||
/// Sanity: the unfiltered default formatter (no `fmt_fields` swap) DOES emit the peer_id value —
|
||||
/// this guards against accidentally enabling redaction by default for non-`no_logs` deployments.
|
||||
#[test]
|
||||
fn default_formatter_keeps_peer_id() {
|
||||
let buf = BufWriter::default();
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_writer(buf.clone())
|
||||
.with_ansi(false)
|
||||
.finish();
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::info!(peer_id = "SHOULD-APPEAR", "ev");
|
||||
});
|
||||
let out = buf.snapshot();
|
||||
assert!(out.contains("SHOULD-APPEAR"), "default did not emit: {out}");
|
||||
}
|
||||
Reference in New Issue
Block a user