AID-H-038.001: Inference Runtime Listener & Protocol Conformance

Version 1.20261015 · Actionable sub-technique

Define a signed exposure profile for every inference-serving process and verify after startup that its actual TCP, UDP, Unix-socket, distributed-control, and KV-transfer listeners match approved addresses, ports or ranges, transports, and roles. Disable unused listeners and fail deployment on an unknown or unexpectedly broad bind.

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.
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

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-H-038.001: Inference Runtime Listener & Protocol Conformance | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version