ies
[tool.capabilities."wasi:http"]
description = "Fetch configuration from trusted registry."
[[tool.capabilities."wasi:http".grants]]
host = "config.registry.internal"
scheme = "https"
methods = ["GET"]
ports = [443]
The build toolchain (e.g., `agent-cli bundle`) parses this manifest, validates syntax, and embeds the capability table into a custom section of the Wasm binary. Components with missing declarations or empty grant lists are rejected at load time, preventing implicit ambient access.
### 2. Operator Runtime Constraints
Operators define runtime constraints via CLI flags or configuration profiles. These constraints represent the operator's security posture and can be more restrictive than the component's declaration but never more permissive.
**Runtime Configuration:**
```bash
agent run data-processor.wasm \
--fs-policy allowlist \
--fs-allow "/var/db/processing/results.db" \
--http-policy allowlist \
--http-allow "host=config.registry.internal;scheme=https" \
--http-deny "cidr=169.254.169.254/32"
Alternatively, operators can define reusable profiles in ~/.config/agent/policy.toml:
[profile.production.policy]
fs = { mode = "allowlist", allow = [
{ path = "/var/db/processing/results.db", mode = "read-write" },
]}
http = { mode = "allowlist", allow = [
{ host = "config.registry.internal", scheme = "https" },
], deny_cidr = ["169.254.169.254/32"] }
3. Effective Policy Computation
At component load time, the host runtime computes the effective policy by intersecting the declared capabilities with the operator's constraints. This computation occurs before any WASI imports are wired up.
Runtime Implementation (policy_engine.rs):
pub struct PolicyEngine {
declared: CapabilityManifest,
operator: RuntimeConstraints,
}
impl PolicyEngine {
pub fn compute_effective(&self) -> EffectivePolicy {
let fs_effective = self.intersect_fs();
let http_effective = self.intersect_http();
EffectivePolicy {
filesystem: fs_effective,
http: http_effective,
}
}
fn intersect_fs(&self) -> FsAccessScope {
// Intersection logic: only grants present in both
// declared and operator sets are retained.
// Operator grants outside the declared ceiling are dropped.
self.operator.fs.allow
.iter()
.filter(|op_grant| self.declared.fs.is_covered_by(op_grant))
.cloned()
.collect()
}
}
If the intersection results in an empty set for a required capability, the component fails to load. This ensures that the component cannot run with insufficient permissions, and the operator cannot grant unauthorized access.
4. DNS-Layer Enforcement
HTTP policy enforcement extends to the DNS resolution phase to prevent metadata service access and IP-based bypasses. A custom DNS resolver intercepts resolution requests and validates resolved IPs against operator-defined CIDR deny rules before the TCP connection is initiated.
Secure Resolver (dns_resolver.rs):
pub struct SecureResolver {
deny_cidrs: Vec<IpNet>,
}
impl Resolve for SecureResolver {
fn resolve(&self, name: &str) -> Result<Vec<IpAddr>, DnsError> {
let ips = upstream_resolve(name)?;
for ip in &ips {
if self.is_denied(ip) {
// Return DnsError to mask existence of the IP
return Err(DnsError::PolicyViolation);
}
}
Ok(ips)
}
fn is_denied(&self, ip: &IpAddr) -> bool {
self.deny_cidrs.iter().any(|cidr| cidr.contains(ip))
}
}
Returning a DnsError instead of a connection refusal prevents attackers from distinguishing between non-existent hosts and blocked hosts, mitigating information leakage. This is critical for blocking cloud metadata endpoints like 169.254.169.254.
5. Per-Hop Redirect Validation
Naive allowlists are vulnerable to redirect smuggling, where an allowed host redirects to a denied host. The HTTP client implementation includes a custom redirect policy that re-evaluates every hop in the redirect chain against the effective policy.
Redirect Policy (http_client.rs):
fn build_redirect_policy(policy: &EffectivePolicy) -> reqwest::redirect::Policy {
reqwest::redirect::Policy::custom(move |attempt| {
let url = attempt.url();
if policy.http.is_allowed(url) {
attempt.follow()
} else {
attempt.stop()
}
})
}
This ensures that a 302 redirect to a denied domain fails immediately, closing the redirect-smuggling bypass vector.
6. Ancestor-Aware Filesystem Matching
WASI path resolution requires statting intermediate directories when opening nested files. A naive policy that allows /var/db/processing/results.db would fail because the runtime cannot stat /var/db or /var/db/processing. The filesystem matcher implements implicit ancestor traversal for allowed leaf paths.
Path Matcher (fs_matcher.rs):
pub struct PathMatcher {
allowed_prefixes: Vec<PathBuf>,
}
impl PathMatcher {
pub fn check(&self, target: &Path) -> AccessDecision {
if self.allowed_prefixes.iter().any(|prefix| {
target.starts_with(prefix) || self.is_ancestor(target, prefix)
}) {
AccessDecision::Allow
} else {
AccessDecision::Deny
}
}
fn is_ancestor(&self, target: &Path, prefix: &Path) -> bool {
// Allows traversal through ancestors of an allowed leaf
prefix.ancestors().any(|ancestor| target.starts_with(ancestor))
}
}
This logic permits directory traversal to reach allowed files while maintaining strict denial for sibling files or unauthorized directories.
Pitfall Guide
1. The Isolation-Authorization Fallacy
Explanation: Assuming that wasmtime isolation eliminates the need for capability policy. Isolation prevents raw syscalls, but WASI imports provide controlled access to resources. Without policy, imports can be wired up with broad permissions.
Fix: Always deploy the policy layer. Treat WASI imports as capability gates that must be filtered by the intersection of declaration and operator intent.
2. Redirect Smuggling Bypass
Explanation: Implementing HTTP allowlists that only check the initial request URL. Attackers can use an allowed host to redirect requests to denied hosts, bypassing the policy.
Fix: Implement per-hop redirect validation. Every URL in the redirect chain must be evaluated against the effective policy before following the redirect.
Explanation: Relying solely on hostname allowlists. Attackers can resolve allowed hostnames to IPs that include metadata service addresses, or use DNS rebinding to access internal networks.
Fix: Implement DNS-layer enforcement. Validate all resolved IPs against CIDR deny rules before connection. Return DnsError for blocked IPs to prevent existence probing.
4. WASI Stat Traversal Failures
Explanation: Defining filesystem allowlists with leaf paths only. WASI requires stat access to parent directories, causing legitimate file opens to fail with permission errors.
Fix: Use ancestor-aware matching. When a leaf path is allowed, implicitly permit traversal through its ancestor directories without granting access to sibling files.
5. Empty Allow List Ambiguity
Explanation: Declaring a capability in the manifest but providing no allow rules. This can lead to undefined behavior or accidental open access if the runtime defaults to permissive mode.
Fix: Enforce hard denial for empty allow lists. The build toolchain should reject manifests with empty grants, and the runtime should fail to load components with unresolved capability requirements.
6. Operator Over-Granting
Explanation: Operators attempting to grant permissions beyond the component's declared ceiling via runtime flags. This creates a false sense of security if the runtime silently accepts the excess grants.
Fix: Implement strict intersection logic. The runtime must drop any operator grants that exceed the declared ceiling and log warnings for such mismatches.
7. Build-Time vs. Run-Time Drift
Explanation: Modifications to the component binary after build time that alter the embedded capability manifest, or running components without validating the manifest against the source.
Fix: Verify manifest integrity at load time. Use cryptographic hashes or signatures to ensure the embedded manifest matches the expected build artifact. Reject components with tampered manifests.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal Trusted Tools | Allowlist with broad declaration ceiling | Reduces operational friction while maintaining auditability. | Low |
| Public Third-Party Tools | Strict allowlist with narrow declaration ceiling | Minimizes blast radius for untrusted code. | Medium |
| High-Security Workloads | Intersection with CIDR deny and DNS enforcement | Prevents metadata access and network pivoting. | High |
| Development/Testing | Relaxed intersection with verbose logging | Facilitates debugging without compromising production standards. | Low |
Configuration Template
agent-policy.toml (Production Profile):
[profile.production.policy]
# Filesystem: Strict allowlist
fs = { mode = "allowlist", allow = [
{ path = "/var/db/processing/results.db", mode = "read-write" },
{ path = "/tmp/work/**", mode = "read-write" },
]}
# HTTP: Allowlist with CIDR deny
http = { mode = "allowlist", allow = [
{ host = "config.registry.internal", scheme = "https", methods = ["GET"], ports = [443] },
], deny_cidr = [
"169.254.169.254/32",
"10.0.0.0/8",
]}
# Logging: Enable policy intersection logs
logging = { level = "info", policy_events = true }
Quick Start Guide
- Install CLI: Install the agent runtime and build tools (
agent-cli).
- Create Manifest: Define capabilities in
agent-tool.toml with explicit grants for filesystem and HTTP access.
- Build Component: Run
agent-cli bundle to validate the manifest and embed capabilities into the Wasm binary.
- Run with Policy: Execute the component using
agent run with operator constraints or a policy profile.
- Verify Enforcement: Check logs for policy intersection events and test access to denied resources to confirm enforcement.