AID-I-001.004: Sandbox Network Egress Restrictions

Version 1.20261015 · Actionable sub-technique

Restrict outbound network destinations for sandboxed executions with default-deny egress policy and narrow allowlists to reduce exfiltration, callback traffic, and lateral movement after code execution.

Pillars: infra

Lifecycle: operation

Scope boundary

Owns named sandbox egress policies, destination allowlists, externally enforced proxies or network controls, and bypass testing for sandboxed execution. Other controls may bind to an egress-policy reference, but they do not inherit proof that its effective network path is enforced.

AID-I-001.003: AID-I-001.004 restricts outbound network paths; AID-I-001.003 owns fresh single-use sandbox creation and teardown. A sandbox may be ephemeral without default-deny egress, and egress restriction can apply to a reused or long-lived runtime; both properties require separate evidence.

AID-H-019.003: AID-I-001.004 owns the reusable sandbox egress-policy object and its external enforcement; AID-H-019.003 owns admission and observation export for a browser runtime. The browser control binds to this policy but does not redefine the allowlist, proxy enforcement, or network-bypass tests.

AID-I-002.002: AID-I-001.004 is the primary owner for default-deny egress and bypass resistance of sandboxed execution; AID-I-002.002 owns governed connectivity from AI workloads to external MaaS, SaaS, APIs, callbacks, relays, and cross-organization agents. When a sandbox calls an external AI service, identical network-policy evidence may be reused, but the same default-deny enforcement must not receive two PASS outcomes; AID-I-002.002 must additionally prove its external endpoint, transport, and mediation population.

Implementation guidance

AID-I-001.004-G001

Enforce default-deny outbound egress allowlists for sandboxed runtimes.
Concept and enforcement path

Default-deny egress must be enforced outside the sandboxed process. This Kubernetes example allows only sandbox Pods labeled role=sandboxed-tool to reach the inference-gateway Pod population on TCP 8443. It uses the immutable built-in namespace-name label and direct Pod IPs for probes, so DNS access is neither accidentally required nor mistaken for egress enforcement.

Default deny plus the one approved egress destination
# File: k8s/policies/sandbox-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: ai-sandbox
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress: []
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-to-inference-gateway
  namespace: ai-sandbox
spec:
  podSelector:
    matchLabels:
      role: sandboxed-tool
  policyTypes: [Egress]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ai-core-services
      podSelector:
        matchLabels:
          app: inference-gateway
    ports:
    - protocol: TCP
      port: 8443
Probe every allowed target and one known-live denied target
# File: k8s/tests/verify_sandbox_egress.sh
#!/usr/bin/env bash
set -euo pipefail

SANDBOX_NS=ai-sandbox
GATEWAY_NS=ai-core-services
IMAGE='registry.k8s.io/e2e-test-images/agnhost@sha256:99c6b4bb4a1e1df3f0b3752168c89358794d02258ebebc26bf21c29399011a85'
: "${NP_CONNECT_TIMEOUT:?load NP_CONNECT_TIMEOUT from the verified network-test policy}"
: "${NP_CONVERGENCE_ATTEMPTS:?load NP_CONVERGENCE_ATTEMPTS from the verified network-test policy}"

[[ "$NP_CONNECT_TIMEOUT" =~ ^[1-9][0-9]*(ms|s|m)$ ]] || {
  echo "invalid policy-derived NP_CONNECT_TIMEOUT" >&2
  exit 2
}
[[ "$NP_CONVERGENCE_ATTEMPTS" =~ ^[1-9][0-9]*$ ]] || {
  echo "invalid policy-derived NP_CONVERGENCE_ATTEMPTS" >&2
  exit 2
}

RUN_ID="$(printf '%x%x' "$$" "$RANDOM")"
SANDBOX_PROBE="egress-client-$RUN_ID"
DENIED_CANARY="egress-denied-$RUN_ID"
cleanup() {
  kubectl delete pod -n "$SANDBOX_NS" "$SANDBOX_PROBE" "$DENIED_CANARY"     --ignore-not-found --wait=true >/dev/null
}
trap cleanup EXIT

kubectl apply --server-side -f k8s/policies/sandbox-egress.yaml
GATEWAYS="$(kubectl get pods -n "$GATEWAY_NS" -l app=inference-gateway -o json)"
TOTAL="$(jq '.items | length' <<<"$GATEWAYS")"
READY="$(jq '[.items[]
  | select(.status.phase == "Running")
  | select(any(.status.conditions[]?; .type == "Ready" and .status == "True"))
  | select(.status.podIP != null)] | length' <<<"$GATEWAYS")"
if [[ "$TOTAL" -eq 0 || "$READY" -ne "$TOTAL" ]]; then
  echo "gateway target population is empty or not completely ready" >&2
  exit 2
fi
mapfile -t GATEWAY_IPS < <(jq -r '.items[].status.podIP' <<<"$GATEWAYS")

kubectl run "$DENIED_CANARY" -n "$SANDBOX_NS" --restart=Never   --image="$IMAGE" --labels="app=egress-denied-canary,aidefend.io/probe-run=$RUN_ID"   --command -- /agnhost netexec --http-port=8443 --udp-port=-1
kubectl run "$SANDBOX_PROBE" -n "$SANDBOX_NS" --restart=Never   --image="$IMAGE" --labels="role=sandboxed-tool,aidefend.io/probe-run=$RUN_ID"   --command -- /agnhost pause
kubectl wait -n "$SANDBOX_NS" --for=condition=Ready   --timeout="$NP_CONNECT_TIMEOUT" "pod/$DENIED_CANARY" "pod/$SANDBOX_PROBE"

CANARY_IP="$(kubectl get pod -n "$SANDBOX_NS" "$DENIED_CANARY"   -o jsonpath='{.status.podIP}')"
[[ -n "$CANARY_IP" ]] || { echo "denied canary has no Pod IP" >&2; exit 2; }
canary_endpoint="$CANARY_IP:8443"
[[ "$CANARY_IP" == *:* ]] && canary_endpoint="[${CANARY_IP}]:8443"
canary_live=false
for ((attempt=1; attempt<=NP_CONVERGENCE_ATTEMPTS; attempt++)); do
  if kubectl exec -n "$SANDBOX_NS" "$DENIED_CANARY" --     /agnhost connect --timeout="$NP_CONNECT_TIMEOUT"     --protocol=tcp 127.0.0.1:8443; then
    canary_live=true
    break
  fi
done
if [[ "$canary_live" != true ]]; then
  echo "denied canary listener never became reachable locally" >&2
  exit 2
fi

for ip in "${GATEWAY_IPS[@]}"; do
  endpoint="$ip:8443"
  [[ "$ip" == *:* ]] && endpoint="[${ip}]:8443"
  allowed=false
  for ((attempt=1; attempt<=NP_CONVERGENCE_ATTEMPTS; attempt++)); do
    if kubectl exec -n "$SANDBOX_NS" "$SANDBOX_PROBE" --       /agnhost connect --timeout="$NP_CONNECT_TIMEOUT"       --protocol=tcp "$endpoint"; then
      allowed=true
      break
    fi
  done
  if [[ "$allowed" != true ]]; then
    echo "approved gateway path failed for target $ip" >&2
    exit 1
  fi
done

if kubectl exec -n "$SANDBOX_NS" "$SANDBOX_PROBE" --   /agnhost connect --timeout="$NP_CONNECT_TIMEOUT"   --protocol=tcp "$canary_endpoint"; then
  echo "sandbox reached a known-live destination outside the allowlist" >&2
  exit 1
fi
printf 'verified_gateway_targets=%s denied_canary=blocked\n'   "${#GATEWAY_IPS[@]}"

Action: run the probe in a controlled pre-production environment with the production CNI and admission policy. The canary self-probe proves that the denied listener is live; the sandbox probe must then reach every ready gateway Pod and fail to reach that canary. Treat an API-only policy readback, incomplete gateway population, missing canary proof, host-network Pod, or ambiguous probe failure as unresolved. If service names are used instead of direct IPs, add a narrow DNS rule and test DNS separately; never add unrestricted UDP/TCP 53 egress by default.

AID-I-001.004-G002

Publish named egress policy references for managed sandboxes and verify that enforcement occurs outside the sandboxed process.
When to use the egress gateway

Use this method for managed or self-hosted sandboxes whose outbound traffic can be forced through a gateway, transparent proxy, CNI, host firewall, or provider egress control that the sandbox identity cannot reconfigure. The registry stores only signature-verified, monotonic policies. Each policy enumerates exact destination authorities, ports, protocols, public/private address classes, redirect behavior, DNS resolution rules, and freshness windows. Hostname or SNI matching alone is insufficient: the external enforcement point must resolve and classify every connection target, reject raw IPs and prohibited address classes, and reauthorize every redirect or new connection.

Bind launch and verify external decisions
# File: sandbox_egress/verified_binding.py
from __future__ import annotations

import ipaddress
import re
from dataclasses import dataclass
from typing import Callable
from urllib.parse import urlsplit


SHA256 = re.compile(r"^[0-9a-f]{64}$")


@dataclass(frozen=True)
class AllowedAuthority:
    scheme: str
    hostname: str
    port: int


@dataclass(frozen=True)
class VerifiedEgressPolicy:
    policy_ref: str
    policy_sha256: str
    authorities: frozenset[AllowedAuthority]
    block_private: bool
    block_ip_literals: bool
    follow_redirects: bool
    enforcement_point_id: str


@dataclass(frozen=True)
class SandboxBinding:
    sandbox_id: str
    generation_id: str
    tenant_id: str
    task_id: str
    manifest_sha256: str


@dataclass(frozen=True)
class EnforcementAdapter:
    """Required authenticated runtime adapter callables; no fallback implementation."""
    bind_policy: Callable[[SandboxBinding, VerifiedEgressPolicy], dict[str, object]]
    read_effective_binding: Callable[[SandboxBinding], dict[str, object]]
    query_flow_receipts: Callable[[SandboxBinding], list[dict[str, object]]]


@dataclass(frozen=True)
class EvidenceWriter:
    """Required authenticated runtime adapter callables; no fallback implementation."""
    append_and_verify: Callable[[dict[str, object]], dict[str, str]]


def canonical_authority(url: str) -> AllowedAuthority:
    parsed = urlsplit(url)
    if parsed.scheme not in {"https"} or not parsed.hostname:
        raise PermissionError("only canonical HTTPS authorities are eligible")
    if parsed.username or parsed.password or parsed.fragment:
        raise PermissionError("userinfo and fragments are forbidden")
    try:
        ipaddress.ip_address(parsed.hostname)
    except ValueError:
        pass
    else:
        raise PermissionError("raw IP literal is forbidden")
    host = parsed.hostname.rstrip(".").encode("idna").decode("ascii").lower()
    return AllowedAuthority("https", host, parsed.port or 443)


def bind_and_verify(
    *,
    binding: SandboxBinding,
    policy: VerifiedEgressPolicy,
    enforcement: EnforcementAdapter,
    evidence: EvidenceWriter,
) -> dict[str, str]:
    if not SHA256.fullmatch(binding.manifest_sha256):
        raise ValueError("invalid manifest digest")
    if not SHA256.fullmatch(policy.policy_sha256):
        raise ValueError("invalid policy digest")
    if not binding.sandbox_id or not binding.generation_id:
        raise ValueError("sandbox and generation IDs are required")
    if not policy.authorities:
        raise ValueError("signed policy must enumerate authorities")

    write_receipt = enforcement.bind_policy(binding, policy)
    effective = enforcement.read_effective_binding(binding)
    expected = {
        "sandbox_id": binding.sandbox_id,
        "generation_id": binding.generation_id,
        "policy_ref": policy.policy_ref,
        "policy_sha256": policy.policy_sha256,
        "enforcement_point_id": policy.enforcement_point_id,
    }
    for key, value in expected.items():
        if effective.get(key) != value:
            raise PermissionError("effective egress binding mismatch: " + key)

    return evidence.append_and_verify({
        "schema_version": "aidefend.sandbox-egress-binding.v1",
        "control": "AID-I-001.004",
        **expected,
        "tenant_id": binding.tenant_id,
        "task_id": binding.task_id,
        "manifest_sha256": binding.manifest_sha256,
        "write_receipt_sha256": write_receipt["receipt_sha256"],
        "readback_receipt_sha256": effective["receipt_sha256"],
        "outcome": "PASS",
    })
Gateway policy and bypass suite

The external gateway authenticates sandbox and generation identity, verifies the policy digest, performs its own resolver lookup, rejects loopback, link-local, RFC 1918/4193, multicast, reserved, cloud-metadata, and policy-prohibited addresses after every resolution, connects to the approved IP itself, verifies TLS hostname and trust policy, and logs a signed decision bound to policy, sandbox, generation, canonical authority, resolved IP, protocol, port, byte counts, and connection time. It disables redirects unless the signed policy explicitly permits them; an allowed redirect is treated as a new authorization. For arbitrary attacker-controlled URLs, consume the canonical safe-fetch control in AID-H-002.002; this method re-verifies the safe-fetch receipt and limits its own result to outbound enforcement.

Run probes from the exact sandbox population for every allowed authority and for unlisted domains, raw IPv4/IPv6, loopback, private, link-local, metadata, encoded/numeric IP forms, alternate ports, DNS rebinding, CNAME chains, redirect chains, TLS/SNI mismatch, direct sockets, package managers, browser prefetch, and helper subprocesses. Reconcile external flow receipts with the sandbox invocation population and require zero unobserved flows.

Prove effective egress beyond an allowed mediator

An allowed package mirror, browser cache, HTTP proxy, screenshot service, artifact service, or API gateway can itself fetch onward and therefore is not the final destination boundary. Inventory every permitted mediator and bind its own destination policy and enforcement receipt to the sandbox invocation. Negative-test attacker-controlled destination and authorization headers, redirects, DNS rebinding, SSRF relay, cache poisoning, and open-proxy behavior. Separately deny the sandbox workload identity permission to create an Internet-facing load balancer, alter routes or security groups, or disable network and monitoring controls. A sandbox-to-mediator PASS never proves the mediator's onward path.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-I-001.004: Sandbox Network Egress Restrictions | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version