refactor(maestro): drop allow_inbound policy gate, keep JWT session binding (NAP-339)

The peer (T2) attach path had a per-pair access-control layer
(`[daemon.policy].allow_inbound`): a target-side allow-list of daemon ids
permitted to broker-attach, enforced inside JWT verification, hot-reloadable via
SIGHUP / `POST /api/policy/reload`, with a `maestro policy` CLI. On a flat-trust
perimeter (the operator's VPN/firewall is the boundary for which hosts connect)
that granularity is unused, and deny-by-default made it config friction —
cross-daemon attach silently failed until every peer was listed.

Remove the allow-list; keep the registry-minted pair-attach JWT and ALL its
binding checks (iss=registry, aud=target, sid=session, exp). The firewall gates
host->port reachability; it does not stop a reachable host from attaching to a
session it wasn't handed or replaying a token elsewhere — that's the JWT's job
and it stays. Only the `sub in allow_inbound` check is dropped.

Removed: DaemonPolicySection / [daemon.policy].allow_inbound (config + daemon),
policy.rs (SIGHUP listener + GET/POST /api/policy), the `maestro policy` CLI,
PairAttachVerifier's allow-list field/reload/snapshot + JwtError::NotAllowed,
and the e2e broker_rejects_when_target_doesnt_allow_inbound test. The NAP-265
pre-upgrade-502 path stays covered by broker_attach_returns_502_on_perimeter_mismatch.

Also dropped the unit's ExecReload=kill -HUP: with no SIGHUP handler left,
default SIGHUP disposition is terminate, so wiring `systemctl reload` to it would
kill the daemon. Re-introducing a per-pair gate later is a verify-time change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 07:07:55 +00:00
co-authored by Claude Opus 4.8
parent 02475191df
commit b6833ede43
20 changed files with 60 additions and 722 deletions
+2 -30
View File
@@ -27,13 +27,12 @@ pub struct Daemon {
token: String,
}
/// Optional knobs for `Daemon::spawn_with` — perimeter + allow_inbound for
/// Optional knobs for `Daemon::spawn_with` — perimeter advertisement for
/// NAP-247 broker tests.
#[derive(Debug, Default, Clone)]
pub struct DaemonOptions {
pub perimeter_id: Option<String>,
pub private_addr: Option<String>,
pub allow_inbound: Vec<String>,
}
impl Daemon {
@@ -50,7 +49,7 @@ impl Daemon {
.await
}
/// Spawn with NAP-247 options (perimeter advertisement, allow_inbound).
/// Spawn with NAP-247 options (perimeter advertisement).
pub async fn spawn_with(
daemon_id: &str,
registry_base_url: &str,
@@ -116,11 +115,6 @@ impl Daemon {
let state = TempDir::new().expect("state tmpdir");
let tmux = TempDir::new().expect("tmux tmpdir");
// Write a minimal config.toml so allow_inbound (and any future
// file-only fields) reach the daemon. Required fields keep coming
// through env vars set by `launch`.
write_config(home.path(), &opts);
let daemon_url = format!("ws://{bind}");
let child = launch(
daemon_id,
@@ -219,25 +213,3 @@ fn launch(
cmd.spawn()
.unwrap_or_else(|e| panic!("spawn {} failed: {e}", bin.display()))
}
/// Write a minimal `~/.config/maestro/config.toml` carrying the NAP-247
/// `[daemon.policy] allow_inbound` list. Everything else still comes from
/// env vars set by `launch`.
fn write_config(home: &std::path::Path, opts: &DaemonOptions) {
if opts.allow_inbound.is_empty() {
return;
}
let dir = home.join(".config/maestro");
std::fs::create_dir_all(&dir).unwrap();
let mut allow = String::new();
for (i, id) in opts.allow_inbound.iter().enumerate() {
if i > 0 {
allow.push_str(", ");
}
allow.push('"');
allow.push_str(id);
allow.push('"');
}
let body = format!("[daemon.policy]\nallow_inbound = [{allow}]\n");
std::fs::write(dir.join("config.toml"), body).unwrap();
}