Disclose node attach capability grants
This commit is contained in:
parent
c514ab7f81
commit
fe6c2d5042
3 changed files with 187 additions and 2 deletions
|
|
@ -618,12 +618,16 @@ struct NodeAttachPlan {
|
|||
node: String,
|
||||
coordinator: Option<String>,
|
||||
capabilities: NodeCapabilities,
|
||||
grant_disclosures: Vec<CapabilityGrantDisclosure>,
|
||||
enrollment: Option<NodeEnrollmentPlan>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
struct NodeAttachReport {
|
||||
command: String,
|
||||
node: String,
|
||||
plan: NodeAttachPlan,
|
||||
grant_disclosures: Vec<CapabilityGrantDisclosure>,
|
||||
boundary: NodeAttachBoundaryEvidence,
|
||||
coordinator_response: Value,
|
||||
heartbeat_response: Value,
|
||||
|
|
@ -638,6 +642,15 @@ struct NodeAttachBoundaryEvidence {
|
|||
coordinator_session_requests: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
struct CapabilityGrantDisclosure {
|
||||
capability: Capability,
|
||||
grant: String,
|
||||
description: String,
|
||||
risk: String,
|
||||
coordinator_policy_limited: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
struct NodeEnrollmentPlan {
|
||||
grant: String,
|
||||
|
|
@ -1919,6 +1932,32 @@ fn human_report(value: &Value) -> String {
|
|||
}
|
||||
}
|
||||
}
|
||||
if let Some(disclosures) = value
|
||||
.get("grant_disclosures")
|
||||
.and_then(Value::as_array)
|
||||
.filter(|disclosures| !disclosures.is_empty())
|
||||
{
|
||||
for disclosure in disclosures {
|
||||
let grant = disclosure
|
||||
.get("grant")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("capability");
|
||||
let description = disclosure
|
||||
.get("description")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("capability grant");
|
||||
let policy = if disclosure
|
||||
.get("coordinator_policy_limited")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"policy-limited"
|
||||
} else {
|
||||
"unbounded"
|
||||
};
|
||||
lines.push(format!("grant {grant}: {description} ({policy})"));
|
||||
}
|
||||
}
|
||||
|
||||
lines.dedup();
|
||||
lines.join("\n")
|
||||
|
|
@ -3768,15 +3807,94 @@ fn attach_plan(args: AttachArgs) -> NodeAttachPlan {
|
|||
public_key_fingerprint: Digest::sha256(public_key),
|
||||
exchanges_short_lived_grant_for_long_lived_node_identity: true,
|
||||
});
|
||||
let grant_disclosures = capability_grant_disclosures(&capabilities);
|
||||
|
||||
NodeAttachPlan {
|
||||
node,
|
||||
coordinator: args.coordinator,
|
||||
capabilities,
|
||||
grant_disclosures,
|
||||
enrollment,
|
||||
}
|
||||
}
|
||||
|
||||
fn capability_grant_disclosures(capabilities: &NodeCapabilities) -> Vec<CapabilityGrantDisclosure> {
|
||||
let mut disclosures = Vec::new();
|
||||
let mut push = |capability: Capability, grant: &str, description: &str, risk: &str| {
|
||||
if capabilities.capabilities.contains(&capability) {
|
||||
disclosures.push(CapabilityGrantDisclosure {
|
||||
capability,
|
||||
grant: grant.to_owned(),
|
||||
description: description.to_owned(),
|
||||
risk: risk.to_owned(),
|
||||
coordinator_policy_limited: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
push(
|
||||
Capability::Command,
|
||||
"native_command_execution",
|
||||
"placed tasks may run native commands on this node",
|
||||
"local process execution under the node account",
|
||||
);
|
||||
push(
|
||||
Capability::WindowsCommandDev,
|
||||
"native_command_execution",
|
||||
"placed tasks may run Windows developer commands on this node",
|
||||
"local process execution under the node account",
|
||||
);
|
||||
push(
|
||||
Capability::SourceFilesystem,
|
||||
"source_access",
|
||||
"placed tasks may read the local project/source checkout exposed by this node",
|
||||
"broad local source visibility",
|
||||
);
|
||||
push(
|
||||
Capability::SourceGit,
|
||||
"source_access",
|
||||
"placed tasks may use Git-backed source access exposed by this node",
|
||||
"source-provider visibility",
|
||||
);
|
||||
push(
|
||||
Capability::Network,
|
||||
"network_access",
|
||||
"placed tasks may use outbound network access from this node",
|
||||
"network egress from the node environment",
|
||||
);
|
||||
push(
|
||||
Capability::HostFilesystem,
|
||||
"host_filesystem_access",
|
||||
"placed tasks may access configured host filesystem mounts",
|
||||
"host file visibility outside the project checkout",
|
||||
);
|
||||
push(
|
||||
Capability::Secrets,
|
||||
"secret_access",
|
||||
"placed tasks may receive configured secret material",
|
||||
"secret exposure to authorized task code",
|
||||
);
|
||||
push(
|
||||
Capability::InboundPorts,
|
||||
"inbound_ports",
|
||||
"placed tasks may expose inbound ports from this node",
|
||||
"network service exposure from the node environment",
|
||||
);
|
||||
push(
|
||||
Capability::ArbitrarySyscalls,
|
||||
"arbitrary_syscalls",
|
||||
"placed tasks may use broader host syscall surface",
|
||||
"reduced host isolation",
|
||||
);
|
||||
|
||||
disclosures.sort_by(|left, right| {
|
||||
left.grant
|
||||
.cmp(&right.grant)
|
||||
.then_with(|| left.capability.cmp(&right.capability))
|
||||
});
|
||||
disclosures
|
||||
}
|
||||
|
||||
fn execute_node_attach(args: AttachArgs) -> Result<NodeAttachReport> {
|
||||
let coordinator = args
|
||||
.coordinator
|
||||
|
|
@ -3831,6 +3949,9 @@ fn execute_node_attach(args: AttachArgs) -> Result<NodeAttachReport> {
|
|||
}))?;
|
||||
|
||||
Ok(NodeAttachReport {
|
||||
command: "node attach".to_owned(),
|
||||
node: plan.node.clone(),
|
||||
grant_disclosures: plan.grant_disclosures.clone(),
|
||||
plan,
|
||||
boundary: NodeAttachBoundaryEvidence {
|
||||
cli_contacted_coordinator: true,
|
||||
|
|
@ -3922,6 +4043,11 @@ fn parse_capability(cap: &str) -> Option<Capability> {
|
|||
"rootless-podman" => Some(Capability::RootlessPodman),
|
||||
"source-filesystem" => Some(Capability::SourceFilesystem),
|
||||
"source-git" => Some(Capability::SourceGit),
|
||||
"host-filesystem" => Some(Capability::HostFilesystem),
|
||||
"network" => Some(Capability::Network),
|
||||
"secrets" => Some(Capability::Secrets),
|
||||
"inbound-ports" => Some(Capability::InboundPorts),
|
||||
"arbitrary-syscalls" => Some(Capability::ArbitrarySyscalls),
|
||||
"vfs-artifacts" => Some(Capability::VfsArtifacts),
|
||||
"wasmtime" => Some(Capability::Wasmtime),
|
||||
"windows-command-dev" => Some(Capability::WindowsCommandDev),
|
||||
|
|
@ -4006,6 +4132,54 @@ mod tests {
|
|||
assert!(!plan.capabilities.arch.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_attach_discloses_dangerous_capability_grants() {
|
||||
let Cli {
|
||||
command:
|
||||
Commands::Node {
|
||||
command: NodeCommands::Attach(args),
|
||||
},
|
||||
} = parse(&[
|
||||
"disasmer",
|
||||
"node",
|
||||
"attach",
|
||||
"--cap",
|
||||
"network",
|
||||
"--cap",
|
||||
"host-filesystem",
|
||||
"--cap",
|
||||
"secrets",
|
||||
])
|
||||
else {
|
||||
panic!("wrong command");
|
||||
};
|
||||
let plan = attach_plan(args);
|
||||
let grants = plan
|
||||
.grant_disclosures
|
||||
.iter()
|
||||
.map(|disclosure| disclosure.grant.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(grants.contains(&"native_command_execution"));
|
||||
assert!(grants.contains(&"source_access"));
|
||||
assert!(grants.contains(&"network_access"));
|
||||
assert!(grants.contains(&"host_filesystem_access"));
|
||||
assert!(grants.contains(&"secret_access"));
|
||||
assert!(plan
|
||||
.grant_disclosures
|
||||
.iter()
|
||||
.all(|disclosure| disclosure.coordinator_policy_limited));
|
||||
|
||||
let rendered = human_report(&json!({
|
||||
"command": "node attach",
|
||||
"node": plan.node,
|
||||
"grant_disclosures": plan.grant_disclosures,
|
||||
}));
|
||||
assert!(rendered.contains("grant native_command_execution"));
|
||||
assert!(rendered.contains("grant network_access"));
|
||||
assert!(rendered.contains("policy-limited"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agents_can_select_hosted_with_public_key_identity() {
|
||||
let args = RunArgs {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue