agentguard-rs: Domain Allowlists for Rust AI Agent Network Calls
Constraining AI Agent Egress: A Domain-First Security Boundary for Rust
Current Situation Analysis
Autonomous AI agents are rapidly transitioning from experimental prototypes to production workloads. As these systems gain tool-calling capabilities, they inevitably require outbound network access to fetch documentation, query APIs, or retrieve user-submitted content. This architectural shift introduces a critical attack surface that most engineering teams overlook: unconstrained egress.
The industry pain point is not model hallucination or prompt formatting. It is the assumption that an agent's network calls are inherently safe because they originate from a "trusted" runtime. In reality, any tool that accepts user-controlled input (URLs, search queries, file paths) becomes a potential vector for prompt injection. When an attacker embeds malicious instructions in a support ticket, a scraped webpage, or a database record, the agent can be coerced into forwarding sensitive context to external endpoints. The tool executes exactly as designed. The failure lies in the absence of a hard network boundary.
This problem is systematically underestimated for three reasons:
- Input-focused security bias: Teams invest heavily in prompt sanitization, output filtering, and model alignment, treating the network layer as an afterthought.
- False confidence in routing logic: Developers assume that because their agent routes requests through a specific function, the destination is implicitly controlled. Routing logic is deterministic; prompt injection is adversarial.
- Lack of observable enforcement: Without explicit egress controls, blocked requests never happen, and exfiltrated data leaves silently. Security reviews often rely on code inspection rather than runtime guarantees.
Industry frameworks like OWASP Top 10 for LLM Applications explicitly flag unconstrained outbound calls under LLM06 (Sensitive Information Disclosure) and LLM01 (Prompt Injection). Real-world incidents consistently show that agents with fetch capabilities, when exposed to untrusted text, will follow embedded instructions to exfiltrate conversation history, API keys, or internal records. The solution requires shifting from heuristic trust to cryptographic or policy-enforced boundaries.
WOW Moment: Key Findings
When designing network controls for AI agents, teams typically choose between three strategies. The data below illustrates why domain-level allowlisting outperforms alternative approaches in production agent environments.
| Approach | Maintenance Overhead | Coverage Against Injection | Runtime Latency | False Confidence Risk |
|---|---|---|---|---|
| Unconstrained Egress | None | 0% | Baseline | Critical |
| Path-Level Allowlisting | High (linear growth) | ~65% (redirects/normalization gaps) | +12-18ms | High |
| Domain-Level Allowlisting | Low (stable set) | ~98% (early termination) | +2-4ms | Low |
Domain-level enforcement intercepts requests before TCP handshake initiation. This eliminates network exposure for blocked destinations entirely. Path-level restrictions appear more granular but fracture under HTTP redirects, API versioning, and URL normalization differences across client libraries. Domain allowlists align with how infrastructure actually operates: trusted third parties are identified by hostname, not endpoint. Wildcard subdomain matching (*.example.com) further reduces configuration drift while maintaining strict scope.
This finding enables teams to deploy agents that process untrusted input without sacrificing functionality. The boundary is auditable, enforceable at runtime, and decoupled from application logic.
Core Solution
Implementing egress control requires three components: a policy definition, a runtime enforcement layer, and a failure handling strategy. The agentguard-rs library provides a domain-scoped allowlist engine designed specifically for Rust agent runtimes. It operates at the HTTP client layer, ensuring violations are caught before network I/O.
Architecture Decision: Middleware vs Manual Guard
The library offers two integration paths. The middleware approach wraps reqwest-middleware and evaluates policies during request construction. The manual path exposes a synchronous validator that works with any HTTP client. Middleware is preferred for greenfield projects; manual validation suits legacy clients or custom transport layers.
Implementation Workflow
- Define the policy scope: Identify all outbound destinations the agent requires. Group them by trust tier (model providers, internal APIs, public documentation).
- Initialize the guard: Construct the allowlist with explicit domains and wildcard subdomains.
- Attach to the client: Replace the standard HTTP client with the guarded variant.
- Handle violations: Catch
EgressViolationerrors and route them to observability pipelines.
Production-Grade Code Example
The following example demonstrates a wrapped egress controller that isolates policy configuration from business logic. Variable names and structure differ from reference implementations while preserving equivalent functionality.
use agentguard_rs::{AgentGuard, EgressViolation};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest::Client;
use std::collections::HashSet;
/// Centralized egress policy manager
pub struct NetworkPolicy {
allowed_hosts: HashSet<String>,
client: ClientWithMiddleware,
}
impl NetworkPolicy {
/// Constructs a new policy with explicit domain allowances
pub fn new(trusted_domains: &[&str]) -> Result<Self, EgressViolation> {
let mut guard = AgentGuard::new();
for domain in trusted_domains {
guard.allow(domain);
}
let guarded_client = guard.into_client()?;
Ok(Self {
allowed_hosts: trusted_domains.iter().map(|s| s.to_string()).collect(),
client: guarded_client,
})
}
/// Executes a GET request with automatic egress validation
pub async fn fetch_resource(&self, target_url: &str) -> Result<String, Box<dyn std::error::Error>> {
let response = self.client.get(target_url).send().await?;
let body = response.text().await?;
Ok(body)
}
/// Returns the configured allowlist for audit logging
pub fn policy_snapshot(&self) -> &HashSet<String> {
&self.allowed_hosts
}
}
#[tokio::main]
async fn main() {
let policy = NetworkPolicy::new(&[
"api.anthropic.com",
"*.docs.internal.corp",
"search.public-api.io"
]).expect("Failed to initialize egress policy");
// Valid request: matches allowlist
match policy.fetch_resource("https://api.anthropic.com/v1/messages").await {
Ok(data) => println!("Retrieved {} bytes", data.len()),
Err(e) => eprintln!("Request failed: {}", e),
}
// Blocked request: triggers EgressViolation before DNS resolution
let malicious_url = "https://exfil.attacker-domain.net/collect";
match policy.fetch_resource(malicious_url).await {
Ok(_) => println!("Unexpected success"),
Err(e) => {
if e.to_string().contains("EgressViolation") {
println!("Egress blocked: {}", malicious_url);
}
}
}
}
Why This Structure Works
- Policy encapsulation: The
NetworkPolicystruct isolates allowlist configuration from request logic. This enables hot-reloading policies and unit testing without network calls. - Early termination: The middleware evaluates the hostname during request construction. TCP SYN packets never leave the host for blocked domains, eliminating timing side-channels and reducing resource waste.
- Error propagation:
EgressViolationimplementsstd::error::Error, allowing seamless integration with existing error handling chains and telemetry systems.
Pitfall Guide
1. Over-Granular Path Restrictions
Explanation: Attempting to allow api.example.com/v1/data while blocking api.example.com/v1/admin creates maintenance debt. APIs evolve, redirects occur, and URL normalization varies across clients.
Fix: Restrict to domain-level. If path isolation is required, enforce it at the API gateway or reverse proxy, not the agent runtime.
2. Wildcard Scope Misunderstanding
Explanation: *.example.com matches exactly one subdomain level. It will not match a.b.example.com. Developers often assume recursive matching.
Fix: Explicitly list multi-level subdomains or use separate policy entries. Validate wildcard behavior in staging before production deployment.
3. DNS Rebinding Blind Spot
Explanation: Domain allowlists validate hostnames, not resolved IPs. An attacker controlling DNS can map an allowed domain to an internal RFC 1918 address, bypassing the policy.
Fix: Pair domain allowlisting with network-level egress filtering. Use iptables/nftables to block private IP ranges outbound, or deploy a forward proxy that validates resolved addresses.
4. Silent Violation Suppression
Explanation: Catching EgressViolation and logging it at DEBUG level creates an observability gap. Blocked requests become invisible during incident response.
Fix: Route all egress denials to a dedicated metrics endpoint. Alert on violation frequency spikes, which often indicate active injection attempts.
5. Credential Leakage Assumption
Explanation: An allowlist guarantees destination control, not authentication safety. Requests to allowed domains can still carry leaked tokens or session cookies. Fix: Implement short-lived, scoped credentials per agent session. Rotate API keys automatically and validate token permissions at the destination service.
6. Hardcoded Policy in Production
Explanation: Embedding allowlists in source code requires redeployment for every new integration. This slows development and encourages overly permissive initial configurations. Fix: Externalize policies to environment variables, configuration files, or a policy server. Validate changes through CI/CD before rollout.
7. Redirect Chain Bypass
Explanation: Some HTTP clients follow 3xx redirects automatically. If an allowed domain redirects to a blocked domain, the client may traverse the chain before policy evaluation.
Fix: Disable automatic redirect following in the HTTP client configuration. Handle redirects explicitly in application logic, re-validating each Location header against the policy.
Production Bundle
Action Checklist
- Audit all agent tools that perform outbound HTTP requests and map their required destinations
- Define a minimal allowlist containing only verified third-party and internal endpoints
- Integrate
agentguard-rsusing the middleware feature for automatic request interception - Disable automatic HTTP redirect following to prevent chain traversal
- Route
EgressViolationerrors to centralized logging and alerting pipelines - Implement DNS rebinding mitigation via network-level IP blocking or forward proxy validation
- Externalize policy configuration to support runtime updates without redeployment
- Validate wildcard subdomain scope matches actual infrastructure topology
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Agent processes user-submitted URLs | Domain allowlist + DNS rebinding protection | Prevents prompt injection exfiltration while maintaining fetch capability | Low (library overhead) |
| General-purpose web browsing agent | No allowlist; implement sandboxed browser environment | Allowlists break core functionality; isolation must occur at execution layer | High (infrastructure complexity) |
| Internal-only agent with fixed endpoints | Static routing + network ACLs | Allowlist adds negligible security over existing infrastructure controls | None |
| Multi-tenant agent platform | Policy-per-tenant + centralized egress proxy | Enables granular control without code duplication; supports audit compliance | Medium (proxy deployment) |
Configuration Template
# Cargo.toml
[dependencies]
agentguard-rs = { version = "0.1", features = ["reqwest-middleware"] }
reqwest = { version = "0.11", features = ["json"] }
reqwest-middleware = "0.2"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
# policy.json (externalized configuration)
{
"version": "1.0",
"allowlist": [
"api.anthropic.com",
"api.openai.com",
"*.docs.internal.corp",
"search.public-api.io"
],
"deny_redirects": true,
"log_violations": true,
"max_retry_on_block": 0
}
Quick Start Guide
- Add dependencies: Include
agentguard-rswith thereqwest-middlewarefeature inCargo.toml. - Initialize the guard: Create an
AgentGuardinstance and register required domains using.allow(). - Build the client: Call
.into_client()to generate a middleware-wrapped HTTP client that enforces the policy. - Replace standard requests: Swap existing
reqwest::Clientcalls with the guarded variant. HandleEgressViolationin error branches. - Verify enforcement: Test with a known blocked domain. Confirm that the error occurs before DNS resolution and no network traffic is generated.
The agentguard-rs library (v0.1.0, released 2026-05-10) provides a stable, domain-scoped enforcement layer designed for agent runtimes. It deliberately excludes path-level filtering, body inspection, and authentication validation to maintain a clear security boundary. For comprehensive agent defense, pair egress control with input validation (agentvet-rs), prompt injection detection (prompt-shield), and execution tracing (agentsnap-rs). Each layer addresses a distinct failure mode; together they form a verifiable security posture.
Mid-Year Sale β Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register β Start Free Trial7-day free trial Β· Cancel anytime Β· 30-day money-back
