AID-H-038.001: Inference Runtime Listener & Protocol Conformance
Pillars: infra, model
Lifecycle: building, operation
Scope boundary
Owns post-startup process-level enumeration and conformance of actual inference-runtime listeners, sockets, internal transports, bind addresses, ports or approved ranges, and declared roles. It does not own TLS/mTLS, caller identity, network reachability, debug content, discovery, management-operation authorization, or model-interface readback.
AID-I-002.001: AID-H-038.001 proves that the serving process starts only approved listeners; AID-I-002.001 proves that network policy permits only approved source-to-destination paths. Process surface and network reachability require separate readbacks and can fail independently.
AID-M-001.005: AID-M-001.005 discovers organization-owned Internet-reachable AI endpoints from an outside vantage point; AID-H-038.001 enforces the signed listener profile inside the serving workload. External discovery cannot prove that an internal or blocked listener was never started.
AID-H-038.002: AID-H-038.001 controls whether a listener or protocol exists; AID-H-038.002 controls which authenticated identity may invoke a privileged operation exposed through an approved listener. A clean listener inventory does not authorize the operations behind it.
AID-H-034.001: AID-H-034.001 owns the MCP server front end's deployment profile, transport, Origin and protocol checks, TLS or mTLS, runtime hardening, egress, and coarse exposure limits; AID-H-038.001 owns the listeners and internal transports started by the underlying inference-serving process. An MCP-fronted inference deployment must evaluate both process populations, and one listener readback cannot satisfy both controls.
AID-H-033.005: AID-H-033.005 resolves and enforces the effective provider, account, model, deployment, endpoint, region, route, and fallback immediately before dispatch; AID-H-038.001 verifies which local listeners and protocols the inference process actually starts. An approved dispatch route does not authorize an extra process listener, and a conforming listener does not prove the requested model route is approved.
AID-D-004.008: AID-D-004.008 monitors executable code and modules actually loaded by the runtime; AID-H-038.001 verifies the runtime's process-level listener and protocol surface. A conforming binary and module population can still expose an unexpected port, and a clean listener inventory does not prove loaded-code integrity.
AID-M-001.006: AID-M-001.006 discovers organization-owned internal AI endpoints and agent services from authorized network and control-plane scope; AID-H-038.001 enforces the signed listener profile inside each applicable inference workload. Discovery reports what is reachable or registered but cannot prove that blocked, loopback, Unix-socket, distributed-control, or KV-transfer listeners were never started.
Implementation guidance
- AID-H-038.001-G001: Enumerate every listener owned by the inference process tree after readiness and compare it with a signed, role-labeled endpoint profile.
AID-H-038.001-G001
Define the exact allowed surface
Record one profile per runtime image and serving topology. Use exact bind addresses and Unix paths; use a bounded port range only when the runtime truly allocates a dynamic distributed or KV-transfer port. Mark whether each endpoint is required. TLS and firewall policy are verified by their owning controls.
{
"schema_version": "aidefend.inference-surface.v1",
"profile_version": "fraud-serving-12",
"workload_id": "fraud-ai/fraud-llm",
"endpoints": [
{"role":"inference_http","family":"ipv4","transport":"tcp","address":"127.0.0.1","port_min":8000,"port_max":8000,"required":true},
{"role":"kv_transfer","family":"ipv4","transport":"tcp","address":"10.42.7.18","port_min":14500,"port_max":14515,"required":true},
{"role":"engine_ipc","family":"unix","transport":"stream","path":"/run/vllm/engine.sock","required":true}
]
}Read back the process, not only Kubernetes Service objects
# File: serving_security/verify_listeners.py
from __future__ import annotations
import hashlib, hmac, json, os, socket
from datetime import datetime, timezone
from pathlib import Path
import psutil
PROFILE = Path("policy/inference-surface.json")
EVIDENCE = Path("evidence/inference-surface.json")
ROOT_PID = int(os.environ["VERIFIED_H038_INFERENCE_ROOT_PID"])
MAX_PROFILE_BYTES = int(os.environ["VERIFIED_H038_SURFACE_PROFILE_MAX_BYTES"])
EXPECTED_PROFILE_SHA256 = os.environ["VERIFIED_H038_SURFACE_PROFILE_SHA256"].lower()
if (len(EXPECTED_PROFILE_SHA256) != 64
or set(EXPECTED_PROFILE_SHA256) - set("0123456789abcdef")):
raise RuntimeError("verified surface-profile digest is invalid")
def strict_json(raw: bytes):
def unique(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate profile key: " + key)
result[key] = value
return result
return json.loads(raw.decode("utf-8", errors="strict"), object_pairs_hook=unique,
parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value)))
def validate(profile: dict) -> list[dict]:
if (not isinstance(profile, dict)
or set(profile) != {"schema_version", "profile_version", "workload_id", "endpoints"}
or profile["schema_version"] != "aidefend.inference-surface.v1"):
raise ValueError("surface profile schema differs")
endpoints = profile["endpoints"]
if not isinstance(endpoints, list) or not endpoints:
raise ValueError("endpoint population is empty")
keys = set()
for item in endpoints:
common = {"role", "family", "transport", "required"}
expected = common | ({"path"} if item.get("family") == "unix"
else {"address", "port_min", "port_max"})
if not isinstance(item, dict) or set(item) != expected or type(item["required"]) is not bool:
raise ValueError("endpoint schema differs")
if item["family"] == "unix":
if item["transport"] not in {"stream", "datagram"} or not item["path"].startswith("/"):
raise ValueError("Unix endpoint is invalid")
key = ("unix", item["path"])
else:
if (item["family"] not in {"ipv4", "ipv6"} or item["transport"] not in {"tcp", "udp"}
or type(item["port_min"]) is not int or type(item["port_max"]) is not int
or not 1 <= item["port_min"] <= item["port_max"] <= 65535):
raise ValueError("IP endpoint is invalid")
socket.inet_pton(socket.AF_INET if item["family"] == "ipv4" else socket.AF_INET6,
item["address"])
key = (item["family"], item["transport"], item["address"],
item["port_min"], item["port_max"])
if key in keys:
raise ValueError("endpoint rule is duplicated")
keys.add(key)
return endpoints
def observe(root_pid: int) -> list[dict]:
root = psutil.Process(root_pid)
processes = [root, *root.children(recursive=True)]
rows = set()
for process in processes:
for connection in process.net_connections(kind="all"):
if connection.family in {socket.AF_INET, socket.AF_INET6}:
transport = ("tcp" if connection.type == socket.SOCK_STREAM else
"udp" if connection.type == socket.SOCK_DGRAM else
"socket_type_" + str(int(connection.type)))
if transport == "tcp" and connection.status != psutil.CONN_LISTEN:
continue
if not connection.laddr or (transport == "udp" and connection.raddr):
continue
family = "ipv4" if connection.family == socket.AF_INET else "ipv6"
rows.add((process.pid, family, transport, connection.laddr.ip, connection.laddr.port, ""))
elif connection.family == socket.AF_UNIX and connection.laddr:
transport = ("stream" if connection.type == socket.SOCK_STREAM else
"datagram" if connection.type == socket.SOCK_DGRAM else
"socket_type_" + str(int(connection.type)))
rows.add((process.pid, "unix", transport, "", 0, str(connection.laddr)))
return [{"pid": row[0], "family": row[1], "transport": row[2], "address": row[3],
"port": row[4], "path": row[5]} for row in sorted(rows)]
def matches(rule: dict, row: dict) -> bool:
if rule["family"] == "unix":
return (row["family"] == "unix" and row["transport"] == rule["transport"]
and row["path"] == rule["path"])
return (row["family"] == rule["family"] and row["transport"] == rule["transport"]
and row["address"] == rule["address"]
and rule["port_min"] <= row["port"] <= rule["port_max"])
def main() -> None:
raw = PROFILE.read_bytes()
if not raw or MAX_PROFILE_BYTES < 1 or len(raw) > MAX_PROFILE_BYTES:
raise ValueError("verified profile is empty or oversized")
actual_profile_sha256 = hashlib.sha256(raw).hexdigest()
if not hmac.compare_digest(actual_profile_sha256, EXPECTED_PROFILE_SHA256):
raise PermissionError("surface profile differs from the verified digest")
profile = strict_json(raw)
rules = validate(profile)
observed = observe(ROOT_PID)
unexpected = [row for row in observed if sum(matches(rule, row) for rule in rules) != 1]
missing = [rule["role"] for rule in rules
if rule["required"] and not any(matches(rule, row) for row in observed)]
evidence = {"schema_version":"aidefend.inference-surface-evidence.v1",
"control":"AID-H-038.001", "profile_version":profile["profile_version"],
"profile_sha256":actual_profile_sha256, "root_pid":ROOT_PID,
"observed":observed, "unexpected":unexpected, "missing_required":missing,
"observed_at":datetime.now(timezone.utc).isoformat(),
"outcome":"FAIL" if unexpected or missing else "PASS"}
EVIDENCE.parent.mkdir(parents=True, exist_ok=True)
EVIDENCE.write_text(json.dumps(evidence, sort_keys=True, separators=(",", ":")) + "\n")
if evidence["outcome"] != "PASS":
raise SystemExit(1)
if __name__ == "__main__":
main()Admission must inject VERIFIED_H038_SURFACE_PROFILE_SHA256 from the exact signature-verified profile; the verifier rejects any later file substitution. Run after readiness, after every scale, worker replacement, model reload, KV-transfer change, endpoint-plugin change, and runtime upgrade. Grant only the read permissions needed to inspect the target process tree. A permission error, unstable process population, or incomplete readback is ERROR or INSUFFICIENT_DATA, never PASS. Independently repeat the enumeration from the host or workload namespace and retain the profile, image and release digests, process identities, complete listener population, result, and signed replay receipt.
Action: Run this verifier after each serving process reaches readiness and stop promotion when any required listener is missing or any unapproved listener is present.
Open-source tools
- psutil
- vLLM
- SGLang
- NVIDIA Triton Inference Server
- Kubernetes
Threat mappings
MITRE ATLAS
- AML.T0040 AI Model Inference API Access
- AML.T0049 Exploit Public-Facing Application
- AML.T0122 Exploitation of Remote Services (unapproved inference-runtime listeners and control transports are disabled)
- AML.T0132 Misconfigured or Publicly Exposed AI Services (listener readback blocks deployment of unknown or unexpectedly broad inference endpoints)
- AML.T0006 Active Scanning (approved listener profiles remove unintended runtime scan targets)
- AML.T0006.002 Active Scanning: Scan for Exposed AI Infrastructure (unapproved public inference listeners fail the serving release gate)
MAESTRO
- Orchestration Attacks (L4)
OWASP LLM Top 10 2026
- N/A
OWASP ML Top 10 2023
- ML05:2023 Model Theft (closing unintended inference listeners removes unauthorized query paths)
OWASP Top 10 for Agentic Applications 2026
- N/A
NIST Adversarial Machine Learning 2025
- NISTAML.031 Model Extraction
Cisco Integrated AI Security and Safety Framework
- AITech-14.1 Unauthorized Access (blocks deployment of unapproved inference-runtime listeners)
Google Secure AI Framework 2.0 - Risks
- MXF: Model Exfiltration
- IIC: Insecure Integrated Component
Databricks AI Security Framework 3.0
- Model Serving - Inference requests 9.11: Model Inference API Access