feat(maestro): add [daemon.policy] allow_inbound to config (NAP-247)
New [daemon.policy] section with allow_inbound: Vec<String>. Default empty (deny all peer attaches). The daemon plumbs it through DaemonConfig. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,9 @@ pub struct DaemonSection {
|
||||
/// back to peer daemons as a T2 dial target.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub private_addr: Option<String>,
|
||||
/// `[daemon.policy]` — per-daemon attach policy (NAP-247).
|
||||
#[serde(default, skip_serializing_if = "DaemonPolicySection::is_empty")]
|
||||
pub policy: DaemonPolicySection,
|
||||
}
|
||||
|
||||
impl DaemonSection {
|
||||
@@ -153,6 +156,23 @@ impl DaemonSection {
|
||||
&& self.worktree_root.is_none()
|
||||
&& self.perimeter_id.is_none()
|
||||
&& self.private_addr.is_none()
|
||||
&& self.policy.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// `[daemon.policy]` — application-layer access control complementing network
|
||||
/// ACLs. Default deny: an empty `allow_inbound` rejects every cross-daemon
|
||||
/// attach. Local attach (no JWT, same daemon) always bypasses.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DaemonPolicySection {
|
||||
/// daemon-ids permitted to broker-attach into this daemon's sessions.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub allow_inbound: Vec<String>,
|
||||
}
|
||||
|
||||
impl DaemonPolicySection {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.allow_inbound.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +331,9 @@ mod tests {
|
||||
worktree_root: Some(PathBuf::from("/var/lib/maestrod/wt")),
|
||||
perimeter_id: Some("wg-naps62".into()),
|
||||
private_addr: Some("10.0.0.7:7070".into()),
|
||||
policy: DaemonPolicySection {
|
||||
allow_inbound: vec!["konishi".into()],
|
||||
},
|
||||
},
|
||||
registry: RegistrySection {
|
||||
bind_addr: Some("0.0.0.0:8080".parse().unwrap()),
|
||||
|
||||
@@ -83,6 +83,9 @@ pub struct DaemonConfig {
|
||||
/// Daemon's reachable address on the declared perimeter. Only meaningful
|
||||
/// when `perimeter_id` is set.
|
||||
pub private_addr: Option<String>,
|
||||
/// daemon-ids permitted to broker-attach to this daemon's sessions.
|
||||
/// Default empty (deny all). Local attach bypasses. (NAP-247.)
|
||||
pub allow_inbound: Vec<String>,
|
||||
}
|
||||
|
||||
impl DaemonConfig {
|
||||
@@ -194,6 +197,7 @@ pub fn load_config(path: &Path, env: EnvOverrides) -> Result<DaemonConfig, Confi
|
||||
|
||||
let perimeter_id = env.perimeter_id.or(daemon.perimeter_id);
|
||||
let private_addr = env.private_addr.or(daemon.private_addr);
|
||||
let allow_inbound = daemon.policy.allow_inbound;
|
||||
|
||||
Ok(DaemonConfig {
|
||||
daemon_id,
|
||||
@@ -206,6 +210,7 @@ pub fn load_config(path: &Path, env: EnvOverrides) -> Result<DaemonConfig, Confi
|
||||
worktree_root,
|
||||
perimeter_id,
|
||||
private_addr,
|
||||
allow_inbound,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,9 @@ pub fn run(
|
||||
// are operator-edited in config.toml directly. Leave None on bootstrap.
|
||||
perimeter_id: None,
|
||||
private_addr: None,
|
||||
// Default deny: empty allow_inbound means no peer can attach until the
|
||||
// operator opts in. Set in config.toml under [daemon.policy].
|
||||
policy: Default::default(),
|
||||
};
|
||||
file.save(path).map_err(ConfigError::from)?;
|
||||
let _ = handle; // stdout handle only used during prompts; binary prints "wrote ..."
|
||||
|
||||
Reference in New Issue
Block a user