AID-I-002.002: Secure External AI Service Connectivity

Version 1.20261015 · Actionable sub-technique

Applies strict network path control, transport security, policy mediation, and monitoring specifically to connections originating from the AI system and targeting external services, particularly third-party or Model-as-a-Service (MaaS) foundation model APIs. Aims to prevent data exfiltration, Server-Side Request Forgery (SSRF), Man-in-the-Middle (MitM) attacks, and abuse of external dependencies. This also prevents prompt-injected agents from exfiltrating secrets or invoking arbitrary external services; they can only call approved upstreams through a governed path.

A2A applicability: The measured external reachability population includes every remote or third-party agent endpoint, Agent Card-derived route, callback, relay, broker bridge, and cross-organization peer connection. Default-deny unknown destinations and independently read back the effective egress and inbound exposure for the complete endpoint population.

Pillars: infra

Lifecycle: building, operation

Scope boundary

Owns default-deny network path control, transport security, mediation, and effective-reachability verification for external MaaS, SaaS, API, callback, relay, broker-bridge, and cross-organization agent connections. It does not define internal microsegmentation between organization-controlled components.

AID-I-002.001: AID-I-002.002 governs external and cross-organization service connectivity; AID-I-002.001 segments internal organization-controlled AI systems and trust zones. A privately addressed provider route can still be external by ownership and trust boundary, while an Internet-routable organization-owned endpoint may remain part of the internal administrative population.

AID-I-001.004: AID-I-002.002 owns external-service endpoint, transport, mediation, and reachability controls for the applicable AI-workload population; AID-I-001.004 is the primary owner for default-deny egress and bypass tests of sandboxed executions. For an external call originating inside a sandbox, reuse the sandbox network evidence without scoring the identical enforcement twice, and evaluate the additional external-service identity and mediation requirements here.

Implementation guidance

AID-I-002.002-G001

Integrate external MaaS/API endpoints via private network connections.
Concept:

Keep traffic to critical external AI services off the public internet. Use cloud provider services like AWS PrivateLink, Azure Private Link, or Google Private Service Connect to create a private endpoint in the workload network. The client security group must be able to reach only the endpoint security group, not every Internet destination on port 443.

Create a Private Endpoint (AWS PrivateLink example)

This example assumes the provider offers an accepted PrivateLink endpoint service. It uses separate security-group rule resources to avoid an inline-rule dependency cycle and leaves both groups without default broad egress.

# File: infrastructure/external_connectivity.tf
data "aws_vpc" "prod_vpc" {
  filter {
    name   = "tag:Name"
    values = ["aidefend-prod-vpc"]
  }
}

data "aws_subnets" "private_subnets" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.prod_vpc.id]
  }
  filter {
    name   = "tag:Tier"
    values = ["Private"]
  }
}

data "aws_vpc_endpoint_service" "maas_service" {
  service_name = var.maas_endpoint_service_name
}

resource "aws_security_group" "maas_client" {
  name_prefix = "maas-client-"
  description = "AI workloads allowed to call the MaaS PrivateLink endpoint"
  vpc_id      = data.aws_vpc.prod_vpc.id
  tags        = { Name = "maas-client" }
}

resource "aws_security_group" "maas_endpoint" {
  name_prefix = "maas-endpoint-"
  description = "PrivateLink endpoint ENIs"
  vpc_id      = data.aws_vpc.prod_vpc.id
  tags        = { Name = "maas-endpoint" }
}

resource "aws_vpc_security_group_ingress_rule" "endpoint_from_clients" {
  security_group_id            = aws_security_group.maas_endpoint.id
  referenced_security_group_id = aws_security_group.maas_client.id
  ip_protocol                  = "tcp"
  from_port                    = 443
  to_port                      = 443
}

resource "aws_vpc_security_group_egress_rule" "clients_to_endpoint" {
  security_group_id            = aws_security_group.maas_client.id
  referenced_security_group_id = aws_security_group.maas_endpoint.id
  ip_protocol                  = "tcp"
  from_port                    = 443
  to_port                      = 443
}

resource "aws_vpc_endpoint" "maas" {
  vpc_id              = data.aws_vpc.prod_vpc.id
  service_name        = data.aws_vpc_endpoint_service.maas_service.service_name
  vpc_endpoint_type   = "Interface"
  subnet_ids          = data.aws_subnets.private_subnets.ids
  security_group_ids  = [aws_security_group.maas_endpoint.id]
  private_dns_enabled = true
}

Verification evidence: Run terraform plan, read back the effective security-group rules with the cloud API, and perform one successful request through private DNS plus a negative request to an unrelated public HTTPS endpoint from the same workload identity. Store the plan, readback, and test results with the endpoint and security-group IDs.

Action: Attach aws_security_group.maas_client only to approved AI workloads. Block promotion if the readback shows CIDR-wide egress or if the negative test reaches the public Internet directly.

Bound the provider or mediator's onward path

A private endpoint proves only the client-to-service transport. When the service can fetch URLs, resolve packages, render pages, call plugins, or relay requests, require a separately enforced destination policy after every redirect and final DNS resolution. Strip caller-controlled destination and authorization headers and test SSRF relay, redirect chains, DNS rebinding, cache poisoning, and open-proxy behavior. Keep the private-link result and the onward-path result separate; neither inherits the other's PASS.

AID-I-002.002-G002

Enforce strict egress controls using firewalls and proxies with verified DNS/SNI allow-lists.
Concept:

Implement a default-deny egress policy. Explicitly allow connections only to approved external domains needed by the AI system. Use Layer 7 inspection (TLS SNI, FQDN filtering) for greater precision than IP-based rules.

Step 1: Configure Firewall/Proxy Allowlist with TLS Inspection

Use a stateful firewall that supports TLS inspection (like Azure Firewall Premium, AWS Network Firewall, or a dedicated proxy like Squid with SSL Bump) to filter outbound HTTPS traffic based on the FQDN.

// Azure Firewall Policy application rule collection fields; bind to the deployed API version
{
  "ruleCollectionType": "ApplicationRuleCollection",
  "name": "AllowExternalAIServicesHTTPS",
  "priority": 200,
  "action": { "type": "Allow" },
  "rules": [
    {
      "name": "AllowOpenAIAPI",
      "protocols": [ { "protocolType": "Https", "port": 443 } ],
      "sourceAddresses": [ "10.0.1.0/24" ],
      "targetFqdns": [ "api.openai.com" ]
    },
    {
      "name": "AllowWeatherAPI",
      "protocols": [ { "protocolType": "Https", "port": 443 } ],
      "sourceAddresses": [ "10.0.1.0/24" ],
      "targetFqdns": [ "api.weather.com" ]
    }
  ]
}
// Deny-all outbound TCP/UDP rule collection should exist with lower priority.
Step 2: Monitor for Violations with Name/IP Fallback

Use runtime tools or SIEM to detect connections to destinations not on the allowlist. Include a fallback check by IP when FQDN is unavailable.

# File: falco_rules/ai_egress_violation.yaml
- macro: aidefend_prod_ai_outbound
  condition: >
    evt.type = connect and
    (fd.typechar = 4 or fd.typechar = 6) and
    fd.ip != "0.0.0.0" and fd.net != "127.0.0.0/8" and
    (evt.rawres >= 0 or evt.res = EINPROGRESS)

- list: approved_domains
  items: [api.openai.com, api.weather.com, internal.registry.corp]
- list: approved_ips
  items: ["203.0.113.10", "203.0.113.11"]

- rule: Prod AI Pod Egress Violation
  desc: Egress from prod AI workloads to disallowed destinations
  condition: >
    aidefend_prod_ai_outbound and fd.l4proto in (tcp, udp) and
    (container.image.repository contains "ai-workload" or k8s.ns.name in (ai-prod, ai-inference)) and
    (
      (fd.sip.name exists and fd.sip.name not in (approved_domains)) or
      (not fd.sip.name exists and fd.sip not in (approved_ips))
    )
  output: >
    Disallowed egress detected (proc=%proc.name cmd=%proc.cmdline container=%container.name image=%container.image.repository k8s.ns=%k8s.ns.name k8s.pod=%k8s.pod.name dstip=%fd.sip dstdomain=%fd.sip.name)
  priority: CRITICAL
  tags: [network, aidefend]

Action: Enforce default-deny at the perimeter and allow only approved FQDNs/SNIs. Monitor violations using Falco/Tetragon or SIEM ingestion of firewall logs with both name and IP checks. Explicitly deny access to cloud instance metadata services (e.g. 169.254.169.254) unless you're routing through a hardened metadata proxy that strips credentials. This is critical for blocking SSRF-style credential theft.

AID-I-002.002-G003

Implement transport layer security (e.g., mTLS, Certificate Pinning) for critical egress connections.
Transfer and trust boundary

Use mTLS when the provider supports client identity. Use SPKI pinning only for a small critical endpoint whose certificate rotation process can publish a signed current-and-next pin set. A separate TLS preflight does not protect a later HTTP request; the pin must be enforced by the same TLS stack and transfer that sends and receives application data. Disable redirects or independently authorize and pin every redirect origin.

Same-transfer libcurl pinning
# File: external_clients/pinned_https.py
from __future__ import annotations

import base64
import hashlib
import io
import re
from dataclasses import dataclass
from typing import Callable
from urllib.parse import urlsplit

import pycurl


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


@dataclass(frozen=True)
class VerifiedTlsPolicy:
    policy_sha256: str
    endpoint_url: str
    spki_pins: tuple[str, ...]
    ca_bundle_path: str
    client_certificate_path: str | None
    client_private_key_path: str | None
    connect_timeout_ms: int
    total_timeout_ms: int
    max_response_bytes: int
    allowed_statuses: frozenset[int]


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


class BoundedBody:
    def __init__(self, maximum: int) -> None:
        self.maximum = maximum
        self.buffer = io.BytesIO()

    def write(self, chunk: bytes) -> int:
        if self.buffer.tell() + len(chunk) > self.maximum:
            raise OverflowError("response exceeds signed byte limit")
        return self.buffer.write(chunk)

    def value(self) -> bytes:
        return self.buffer.getvalue()


def validate_policy(policy: VerifiedTlsPolicy) -> None:
    if not SHA256.fullmatch(policy.policy_sha256):
        raise ValueError("invalid policy digest")
    parsed = urlsplit(policy.endpoint_url)
    if (
        parsed.scheme != "https"
        or not parsed.hostname
        or parsed.username
        or parsed.password
        or parsed.fragment
    ):
        raise ValueError("policy must contain one canonical HTTPS URL")
    if min(
        policy.connect_timeout_ms,
        policy.total_timeout_ms,
        policy.max_response_bytes,
    ) <= 0:
        raise ValueError("policy bounds must be positive")
    if not policy.allowed_statuses:
        raise ValueError("policy must enumerate accepted HTTP statuses")
    if not policy.spki_pins:
        raise ValueError("policy must contain a current SPKI pin")
    for pin in policy.spki_pins:
        if not pin.startswith("sha256//"):
            raise ValueError("libcurl pins must use sha256//base64 form")
        decoded = base64.b64decode(pin.removeprefix("sha256//"), validate=True)
        if len(decoded) != 32:
            raise ValueError("SPKI pin must encode one SHA-256 digest")
    if bool(policy.client_certificate_path) != bool(policy.client_private_key_path):
        raise ValueError("mTLS certificate and key must be configured together")


def get(
    *,
    policy: VerifiedTlsPolicy,
    evidence: EvidenceWriter,
) -> tuple[bytes, dict[str, str]]:
    validate_policy(policy)
    body = BoundedBody(policy.max_response_bytes)
    handle = pycurl.Curl()
    try:
        handle.setopt(pycurl.URL, policy.endpoint_url)
        handle.setopt(pycurl.FOLLOWLOCATION, False)
        handle.setopt(pycurl.SSL_VERIFYPEER, True)
        handle.setopt(pycurl.SSL_VERIFYHOST, 2)
        handle.setopt(pycurl.CAINFO, policy.ca_bundle_path)
        handle.setopt(pycurl.PINNEDPUBLICKEY, ";".join(policy.spki_pins))
        handle.setopt(pycurl.CONNECTTIMEOUT_MS, policy.connect_timeout_ms)
        handle.setopt(pycurl.TIMEOUT_MS, policy.total_timeout_ms)
        handle.setopt(pycurl.WRITEFUNCTION, body.write)
        if policy.client_certificate_path:
            handle.setopt(pycurl.SSLCERT, policy.client_certificate_path)
            handle.setopt(pycurl.SSLKEY, policy.client_private_key_path)
        handle.perform()
        status = int(handle.getinfo(pycurl.RESPONSE_CODE))
        primary_ip = str(handle.getinfo(pycurl.PRIMARY_IP))
        effective_url = str(handle.getinfo(pycurl.EFFECTIVE_URL))
    finally:
        handle.close()

    if effective_url != policy.endpoint_url:
        raise PermissionError("libcurl used an unexpected effective URL")
    if status not in policy.allowed_statuses:
        raise PermissionError("HTTP status is outside signed policy")
    payload = body.value()
    receipt = evidence.append_and_verify({
        "schema_version": "aidefend.pinned-transfer.v1",
        "control": "AID-I-002.002",
        "policy_sha256": policy.policy_sha256,
        "endpoint_url_sha256": hashlib.sha256(
            policy.endpoint_url.encode("utf-8")
        ).hexdigest(),
        "pin_set_sha256": hashlib.sha256(
            "\n".join(sorted(policy.spki_pins)).encode("ascii")
        ).hexdigest(),
        "primary_ip": primary_ip,
        "http_status": status,
        "response_bytes": len(payload),
        "response_sha256": hashlib.sha256(payload).hexdigest(),
        "mtls": bool(policy.client_certificate_path),
        "outcome": "PASS",
    })
    return payload, receipt
Policy, rotation, and independent verification

The policy adapter verifies issuer, audience, signature, monotonic version, exact endpoint URL, CA bundle digest, current/next pins, client-certificate identity, status set, and policy-owned time/size limits before constructing VerifiedTlsPolicy. Keep at most the current and staged-next pin during rotation, prove the new pin from a separately observed certificate chain, deploy the dual set, read back every client, rotate the certificate, then remove the retired pin. Do not pin an emergency endpoint that the provider can rotate without notice; CA validation plus mTLS may be the safer availability/security tradeoff.

AID-I-002.002-G004

Utilize API Gateways to mediate external AI service traffic and apply security policies.
Concept:

An API Gateway provides a centralized control point for outbound traffic toward external AI services. It can enforce authentication, rate limits, schema validation, and resilience patterns. Configure health checks on the upstream entity. Kong declarative configuration does not expand shell-style ${...} placeholders at runtime; use Kong Vault references or an explicit decK preprocessing step.

Configure Kong Gateway for External Upstream
# File: kong_config_external.yaml (Kong declarative configuration)
_format_version: "3.0"

# Before starting Kong, set the runtime secret and configure the env vault:
#   KONG_MAAS_API_KEY is injected by the workload secret manager at process start.
#   export KONG_VAULT_ENV_PREFIX='KONG_'
vaults:
- name: env
  prefix: env
  config:
    prefix: KONG_

upstreams:
- name: maas-upstream
  targets:
  - target: api.maas-provider.com:443
  healthchecks:
    active:
      https_verify_certificate: true
      healthy:
        http_statuses: [200, 201]
        successes: 2
      unhealthy:
        http_statuses: [429, 500, 503]
        timeouts: 3
        http_failures: 3
      interval: 10
      timeout: 2

services:
- name: external-maas-service
  host: maas-upstream
  port: 443
  protocol: https
  plugins:
  - name: request-transformer
    config:
      replace:
        headers:
        - "Authorization:Bearer {vault://env/maas-api-key}"
      remove:
        headers:
        - X-Forwarded-Host
        - X-Override-Host
  - name: rate-limiting
    config:
      minute: 500
      policy: local

routes:
- name: maas-route
  paths:
  - /internal/proxy/maas
  strip_path: true
  service: { name: external-maas-service }

Action: Route outbound MaaS traffic through the gateway. Inject secrets with Kong Vault references or a supported external secret store, not literal shell placeholders. If decK environment preprocessing is used instead, treat the rendered Admin API payload and state file as containing plaintext secrets and protect them accordingly. The gateway must also sanitize/override outbound headers so the agent cannot smuggle alternative destinations or custom Authorization headers to bypass policy.

AID-I-002.002-G005

Monitor external egress traffic for anomalies and policy violations.
Concept:

Continuously monitor volume, destinations, timing, and TLS metadata of outbound connections. Deviations can indicate exfiltration, C2, or policy bypass.

Verify the signed egress-detector policy and materialize its exact SPL
#!/usr/bin/env bash
set -euo pipefail
umask 077

: "${I002002_EGRESS_POLICY:?required}"
: "${I002002_EGRESS_POLICY_BUNDLE:?required}"
: "${I002002_EGRESS_POLICY_VERIFY_KEY:?required}"
: "${I002002_RENDERED_SPL_DIR:?required}"
: "${AIDEFEND_POLICY_VERIFY_TIMEOUT_SECONDS:?required}"

if [[ ! "$AIDEFEND_POLICY_VERIFY_TIMEOUT_SECONDS" =~ ^[1-9][0-9]*$ ]]; then
  echo "verification timeout must be a positive integer number of seconds" >&2
  exit 2
fi
for source_path in "$I002002_EGRESS_POLICY" "$I002002_EGRESS_POLICY_BUNDLE"; do
  if [[ ! -f "$source_path" || -L "$source_path" ]]; then
    echo "policy and bundle inputs must be regular, non-symlink files" >&2
    exit 2
  fi
done

snapshot_root="$(mktemp -d)"
chmod 0700 "$snapshot_root"
staged_spl=""
cleanup() {
  if [[ -n "$staged_spl" ]]; then rm -f -- "$staged_spl"; fi
  rm -rf -- "$snapshot_root"
}
trap cleanup EXIT

policy_snapshot="$snapshot_root/egress-policy.json"
bundle_snapshot="$snapshot_root/egress-policy.sigstore.json"
spl_snapshot="$snapshot_root/egress-anomaly.spl"
install -m 0400 -- "$I002002_EGRESS_POLICY" "$policy_snapshot"
install -m 0400 -- "$I002002_EGRESS_POLICY_BUNDLE" "$bundle_snapshot"

policy_sha256="$(sha256sum -- "$policy_snapshot" | awk '{print $1}')"
bundle_sha256="$(sha256sum -- "$bundle_snapshot" | awk '{print $1}')"

timeout -- "$AIDEFEND_POLICY_VERIFY_TIMEOUT_SECONDS" cosign verify-blob \
  --key "$I002002_EGRESS_POLICY_VERIFY_KEY" \
  --bundle "$bundle_snapshot" "$policy_snapshot" >/dev/null

if [[ "$(sha256sum -- "$policy_snapshot" | awk '{print $1}')" != "$policy_sha256" \
   || "$(sha256sum -- "$bundle_snapshot" | awk '{print $1}')" != "$bundle_sha256" ]]; then
  echo "policy snapshot changed during signature verification" >&2
  exit 1
fi

jq -e '
  type == "object"
  and ((keys | sort) == [
    "aggregation_span",
    "baseline_window_buckets",
    "boundary_interface_predicate",
    "flow_index",
    "flow_sourcetype",
    "minimum_average_bytes",
    "policy_version",
    "private_destination_predicate",
    "schema_version",
    "source_scope_predicate",
    "standard_deviation_multiplier"
  ])
  and .schema_version == "aidefend.egress-anomaly-detector-policy.v1"
  and (.policy_version | type == "string" and length > 0)
  and (.flow_index | type == "string" and test("^[A-Za-z0-9_.:-]+$"))
  and (.flow_sourcetype | type == "string" and test("^[A-Za-z0-9_.:-]+$"))
  and (.source_scope_predicate | type == "string" and length > 0 and (test("[\\r\\n]") | not))
  and (.boundary_interface_predicate | type == "string" and length > 0 and (test("[\\r\\n]") | not))
  and (.private_destination_predicate | type == "string" and length > 0 and (test("[\\r\\n]") | not))
  and (.aggregation_span | type == "string" and test("^[1-9][0-9]*(s|m|h|d|w)$"))
  and (.baseline_window_buckets | type == "number" and floor == . and . > 0)
  and (.standard_deviation_multiplier | type == "number" and . > 0)
  and (.minimum_average_bytes | type == "number" and . >= 0)
' "$policy_snapshot" >/dev/null

jq -r '
  [
    "index=\(.flow_index) sourcetype=\(.flow_sourcetype) direction=egress",
    "\(.source_scope_predicate) AND \(.boundary_interface_predicate)",
    "NOT (\(.private_destination_predicate))",
    "| timechart span=\(.aggregation_span) sum(bytes) as bytes_out by srcaddr",
    "| streamstats window=\(.baseline_window_buckets) global=f avg(bytes_out) as avg_bytes_out stddev(bytes_out) as stddev_bytes_out by srcaddr",
    "| eval threshold_upper = avg_bytes_out + (\(.standard_deviation_multiplier) * stddev_bytes_out)",
    "| where bytes_out > threshold_upper AND avg_bytes_out > \(.minimum_average_bytes)",
    "| table _time, srcaddr, bytes_out, avg_bytes_out, stddev_bytes_out, threshold_upper"
  ] | .[]
' "$policy_snapshot" > "$spl_snapshot"
chmod 0400 "$spl_snapshot"

if [[ "$(sha256sum -- "$policy_snapshot" | awk '{print $1}')" != "$policy_sha256" \
   || "$(sha256sum -- "$bundle_snapshot" | awk '{print $1}')" != "$bundle_sha256" ]]; then
  echo "verified policy snapshot changed before materialization completed" >&2
  exit 1
fi

if [[ "$I002002_RENDERED_SPL_DIR" != /* \
   || ! -d "$I002002_RENDERED_SPL_DIR" \
   || -L "$I002002_RENDERED_SPL_DIR" ]]; then
  echo "render directory must be an existing canonical absolute non-symlink directory" >&2
  exit 2
fi
rendered_spl_dir="$(realpath -e -- "$I002002_RENDERED_SPL_DIR")"
if [[ "$I002002_RENDERED_SPL_DIR" != "$rendered_spl_dir" \
   || "$(stat -c '%u' -- "$rendered_spl_dir")" != "$(id -u)" \
   || "$(stat -c '%a' -- "$rendered_spl_dir")" != "700" ]]; then
  echo "render directory must be canonical, owned by this uid, and mode 0700" >&2
  exit 2
fi

installed_spl="$rendered_spl_dir/egress-anomaly.spl"
if [[ -e "$installed_spl" || -L "$installed_spl" ]]; then
  echo "refusing to replace an existing rendered SPL" >&2
  exit 1
fi
staged_spl="$(mktemp --tmpdir="$rendered_spl_dir" .egress-anomaly.spl.XXXXXX)"
install -m 0400 -- "$spl_snapshot" "$staged_spl"
ln -- "$staged_spl" "$installed_spl"
rm -f -- "$staged_spl"
staged_spl=""

spl_sha256="$(sha256sum -- "$spl_snapshot" | awk '{print $1}')"
if [[ "$(sha256sum -- "$installed_spl" | awk '{print $1}')" != "$spl_sha256" ]]; then
  echo "installed SPL does not match the verified-policy rendering" >&2
  exit 1
fi
printf 'installed_spl=%s sha256=%s policy_version=%s\n' \
  "$installed_spl" "$spl_sha256" "$(jq -r '.policy_version' "$policy_snapshot")"
Deploy only the installed query

Configure the Splunk deployer to consume only the new mode-0400 egress-anomaly.spl file from I002002_RENDERED_SPL_DIR. It must never reopen the mutable source policy or execute a placeholder template. Retain the verified policy and Sigstore bundle digests with the deployment record.

Action: Ingest boundary egress logs and baseline normal volumes and destinations. Alert on significant deviations and newly observed ASNs/domains for AI subnets.

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-002.002: Secure External AI Service Connectivity | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version