AID-H-021.002: Runtime Integrity Enforcement (Signed Configurations)
Pillars: app, infra
Lifecycle: operation
Scope boundary
Owns fail-closed verification and admission of signed startup and runtime control artifacts, configuration, tool manifests, and integrity metadata before the agent operates or reloads state. It does not semantically classify authentic content as safe.
AID-H-021.001: AID-H-021.002 enforces authenticity and approved state in the running agent; AID-H-021.001 prevents insecure configuration during development. Runtime verification remains necessary even after pre-commit policy passes.
AID-D-004.005: AID-H-021.002 rejects untrusted or integrity-failed context at the runtime boundary; AID-D-004.005 verifies turn envelopes and chains and emits tamper, replay, order, or provenance findings. The detector reports evidence, while this control owns fail-closed admission.
AID-H-021.004: AID-H-021.002 proves configuration artifacts are signed and approved; AID-H-021.004 prevents the agent identity from accessing or administering the oversight surfaces that enforce those artifacts. A valid configuration can still be dangerous if it grants self-administration of supervision.
AID-H-021.005: AID-H-021.002 verifies approved startup and runtime control artifacts; AID-H-021.005 binds a workspace trust decision to the effective manifest of execution-capable surfaces. A valid signed artifact is one input to workspace trust, not proof that every activated surface was declared and accepted.
AID-M-001.002: AID-H-021.002 verifies and admits signed startup or runtime control artifacts for an agent; AID-M-001.002 inventories the release-bound model-interface tuple and reconciles effective composition across deployed replicas. Agent configuration bytes may support both checks, but runtime admission and deployed composition reconciliation remain separate outcomes and must not be scored twice.
Implementation guidance
- AID-H-021.002-G001: Cryptographically sign approved configuration files in the CI/CD pipeline.
- AID-H-021.002-G002: Require the agent to verify the configuration signature at startup before executing.
- AID-H-021.002-G003: Reject unsigned local configuration overrides and dangerous environment-based overrides at agent startup.
- AID-H-021.002-G004: Reject runtime configuration changes that do not originate from a signed, approved source of truth.
- AID-H-021.002-G005: Verify agent tool and plugin files against a signed hash manifest before startup or hot-reload.
- AID-H-021.002-G006: Continuously reconcile running configuration to the signed desired state and automatically restore approved values when drift is detected.
- AID-H-021.002-G007: Generate verifiable supply chain attestations for agent configurations.
- AID-H-021.002-G008: Implement secure key management and rotation for signing keys.
AID-H-021.002-G001
Concept
Sign only the exact configuration bytes that passed H-021.001. Use a hardware-backed asymmetric KMS key; never import a raw long-lived private key into a general-purpose runner. This guidance produces the detached signature and build receipt. Startup verification remains a separate local claim in the next guidance.
Sign and read back in CI
set -euo pipefail
: "\${CONFIG_SIGNING_KMS_URI:?set an immutable approved KMS key URI}"
CONFIG="configs/production_agent.yaml"
SIGNATURE="$CONFIG.sig"
EVIDENCE="artifacts/config-signing-receipt.json"
mkdir -p artifacts
SNAPSHOT_DIR=$(mktemp -d)
trap 'rm -rf -- "$SNAPSHOT_DIR"' EXIT
CONFIG_SNAPSHOT="$SNAPSHOT_DIR/production_agent.yaml"
install -m 0600 -- "$CONFIG" "$CONFIG_SNAPSHOT"
CONFIG_SHA256="$(sha256sum "$CONFIG_SNAPSHOT" | awk '{print $1}')"
cosign sign-blob --yes \
--key "$CONFIG_SIGNING_KMS_URI" \
--bundle "$SIGNATURE" \
"$CONFIG_SNAPSHOT"
cosign verify-blob \
--key "/opt/aidefend/trust/agent-config-signing.pub" \
--bundle "$SIGNATURE" \
"$CONFIG_SNAPSHOT"
jq -n \
--arg schema "aidefend.agent-config-signing.v1" \
--arg config_sha256 "$CONFIG_SHA256" \
--arg signing_key "$CONFIG_SIGNING_KMS_URI" \
--arg repository "$GITHUB_REPOSITORY" \
--arg commit "$GITHUB_SHA" \
--arg workflow_ref "$GITHUB_WORKFLOW_REF" \
'{schema_version:$schema,status:"PASS",config_sha256:$config_sha256,signing_key:$signing_key,repository:$repository,commit_sha:$commit,workflow_ref:$workflow_ref}' \
> "$EVIDENCE"
Verification and evidence
Upload the config, detached signature, and receipt as one immutable artifact set after schema/policy validation. Independently verify the signature and compare the receipt's commit, workflow ref, config digest, and approved immutable KMS key identity. Missing artifacts are INSUFFICIENT_DATA; a digest or signature mismatch is FAIL; signer/tool malfunction is ERROR. A signature proves approved origin and bytes, not semantic safety.
Action: keep the signing identity in KMS/HSM, restrict Sign to the protected release workload, and publish the exact bytes plus signature and receipt together.
AID-H-021.002-G002
Concept
Verify and parse the same bounded regular-file bytes. Do not verify a pathname and then reopen it: that creates a verifier/use race. The public trust key and JSON Schema are pinned in the workload image; request or environment data may not select either.
Production startup loader
# File: agent/verified_config.py
from __future__ import annotations
import hashlib
import json
import math
import os
import stat
import subprocess
import tempfile
from pathlib import Path
import jsonschema
import yaml
CONFIG = Path("/app/configs/production_agent.yaml")
SIGNATURE = Path("/app/configs/production_agent.yaml.sig")
TRUST_KEY = Path("/opt/aidefend/trust/agent-config-signing.pub")
SCHEMA = Path("/opt/aidefend/policy/agent-config.schema.json")
MAX_CONFIG_BYTES = int(os.environ["VERIFIED_AGENT_CONFIG_MAX_BYTES"])
MAX_SIGNATURE_BYTES = int(os.environ["VERIFIED_AGENT_CONFIG_SIGNATURE_MAX_BYTES"])
COSIGN_TIMEOUT_SECONDS = float(os.environ["VERIFIED_AGENT_CONFIG_SIGNATURE_TIMEOUT_SECONDS"])
if (
min(MAX_CONFIG_BYTES, MAX_SIGNATURE_BYTES) < 1
or not math.isfinite(COSIGN_TIMEOUT_SECONDS)
or COSIGN_TIMEOUT_SECONDS <= 0
):
raise RuntimeError("verified agent-config bounds are invalid")
def stable_bytes(path: Path, limit: int) -> bytes:
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
fd = os.open(path, flags)
try:
before = os.fstat(fd)
if not stat.S_ISREG(before.st_mode) or before.st_size < 1 or before.st_size > limit:
raise ValueError(f"invalid bounded regular file: {path}")
chunks: list[bytes] = []
remaining = before.st_size
while remaining:
chunk = os.read(fd, min(remaining, 64 * 1024))
if not chunk:
raise OSError("short read")
chunks.append(chunk)
remaining -= len(chunk)
if os.read(fd, 1):
raise OSError("file grew during read")
after = os.fstat(fd)
if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != (
after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns
):
raise OSError("file changed during read")
return b"".join(chunks)
finally:
os.close(fd)
class StrictSafeLoader(yaml.SafeLoader):
pass
def construct_mapping(loader: StrictSafeLoader, node, deep=False):
loader.flatten_mapping(node)
result = {}
for key_node, value_node in node.value:
key = loader.construct_object(key_node, deep=deep)
if key in result:
raise ValueError(f"duplicate configuration key: {key!r}")
result[key] = loader.construct_object(value_node, deep=deep)
return result
StrictSafeLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct_mapping
)
def load_verified_config() -> tuple[dict, dict]:
config_bytes = stable_bytes(CONFIG, MAX_CONFIG_BYTES)
signature_bytes = stable_bytes(SIGNATURE, MAX_SIGNATURE_BYTES)
with tempfile.TemporaryDirectory(prefix="aidefend-config-") as directory:
root = Path(directory)
config_copy = root / "config.yaml"
signature_copy = root / "config.sig"
config_copy.write_bytes(config_bytes)
signature_copy.write_bytes(signature_bytes)
subprocess.run(
[
"cosign", "verify-blob", "--key", str(TRUST_KEY),
"--bundle", str(signature_copy), str(config_copy),
],
check=True, capture_output=True, text=True, timeout=COSIGN_TIMEOUT_SECONDS,
)
parsed = yaml.load(config_bytes.decode("utf-8", "strict"), Loader=StrictSafeLoader)
schema = json.loads(stable_bytes(SCHEMA, MAX_CONFIG_BYTES))
jsonschema.Draft202012Validator(
schema, format_checker=jsonschema.FormatChecker()
).validate(parsed)
receipt = {
"schema_version": "aidefend.agent-config-startup.v1",
"status": "PASS",
"config_sha256": hashlib.sha256(config_bytes).hexdigest(),
"signature_sha256": hashlib.sha256(signature_bytes).hexdigest(),
"schema_sha256": hashlib.sha256(
json.dumps(schema, sort_keys=True, separators=(",", ":")).encode()
).hexdigest(),
}
return parsed, receipt
AID-H-021.002-G003
Concept
After the agent verifies the signed baseline configuration, it must refuse to load any untrusted local override file and must reject environment variables that attempt to weaken security-critical behavior. This is a runtime, fail-closed control: local convenience settings must never outrank the approved signed configuration.
Step 1: Define a strict configuration precedence model
- Trusted baseline: the signed configuration artifact produced by CI/CD.
- Allowed runtime inputs: a small allowlist of non-security tuning variables such as log level or worker count.
- Forbidden overrides: any local file or environment variable that changes confirmation requirements, sandboxing, policy bundle selection, or network / tool safety boundaries.
Step 2: Fail closed on unsigned local files and forbidden overrides
# File: agent/startup_config.py
from __future__ import annotations
import os
from pathlib import Path
from typing import Any
from agent.verified_config import load_verified_config
class SecurityException(RuntimeError):
pass
LOCAL_OVERRIDE_PATHS = [
Path('./local_config.yaml'),
Path.home() / '.agent' / 'config.yaml',
]
ALLOWED_ENV_OVERRIDES = {
'AGENT_LOG_LEVEL',
'AGENT_WORKER_COUNT',
}
BLOCKED_ENV_OVERRIDES = {
'AGENT_UNSAFE_MODE',
'AGENT_DISABLE_CONFIRMATIONS',
'AGENT_DISABLE_SANDBOX',
'AGENT_POLICY_BUNDLE',
}
def reject_local_override_files() -> None:
offenders = [str(p) for p in LOCAL_OVERRIDE_PATHS if p.exists()]
if offenders:
raise SecurityException(
'Unsigned local configuration overrides are forbidden: ' + ', '.join(offenders)
)
def apply_allowed_env_overrides(config: dict[str, Any]) -> dict[str, Any]:
for key in BLOCKED_ENV_OVERRIDES:
if key in os.environ:
raise SecurityException(f'Forbidden security override via environment variable: {key}')
if 'AGENT_LOG_LEVEL' in os.environ:
config.setdefault('runtime', {})['log_level'] = os.environ['AGENT_LOG_LEVEL']
if 'AGENT_WORKER_COUNT' in os.environ:
config.setdefault('runtime', {})['worker_count'] = int(os.environ['AGENT_WORKER_COUNT'])
unexpected = [
k for k in os.environ
if k.startswith('AGENT_') and k not in ALLOWED_ENV_OVERRIDES and k not in BLOCKED_ENV_OVERRIDES
]
if unexpected:
raise SecurityException(
'Unknown AGENT_* override variables are not permitted: ' + ', '.join(sorted(unexpected))
)
return config
def load_startup_configuration() -> tuple[dict[str, Any], dict]:
# Exact dependency: H-021.002 guidance 2 returns the parsed bytes and its
# local signature/schema receipt. Missing or non-PASS receipt fails startup.
config, verification = load_verified_config()
if verification.get('status') != 'PASS':
raise SecurityException('signed baseline did not pass local verification')
reject_local_override_files()
effective = apply_allowed_env_overrides(config)
return effective, {
**verification,
'override_policy': 'allowlisted-non-security-fields-v1',
'effective_status': 'PASS',
}
Step 3: Emit auditable startup failures
When startup rejects a local override or blocked environment variable, emit a structured security event with the host identity, agent instance, offending source, and reason code. Do not continue in degraded mode.
Operational notes
- Keep the allowlist small: if an environment variable changes agent authority, prompting behavior, sandboxing, or network reach, it does not belong in runtime overrides.
- Prefer explicit parsing: parse only named allowed variables instead of dynamically mapping arbitrary AGENT_* names into configuration keys.
- Use the same trust root: the startup loader should reuse the same signature-verification trust chain described in the signed-configuration guidance above.
Action: Treat the signed configuration artifact as the single source of truth at startup. Refuse unsigned local override files, block security-sensitive environment overrides, and fail closed with an auditable security event when an override attempt is detected.
AID-H-021.002-G004
Concept
Startup verification is not enough if the agent supports live reload, hot policy updates, or dynamic prompt/config refresh. Every runtime change request must be tied to a signed config bundle and an approved deployment record from the control plane. Unsigned or ad-hoc reload requests must be rejected.
Step 1: Require every reload request to carry deployment metadata
{
"deployment_id": "cfg-rollout-2026-04-08-17",
"config_digest": "sha256:cfb79f4ed61e88f248d69a56b7b22b0149f3d6544413eef235659018b4d558cb",
"config_url": "s3://aidefend-configs/prod/agent-v17.yaml",
"signature_url": "s3://aidefend-configs/prod/agent-v17.yaml.sig"
}
Step 2: Verify the signed artifact and deployment approval before applying it
# File: runtime/reload_gate.py
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
class ReloadDenied(RuntimeError):
pass
FetchDeploymentRecord = Callable[[str], tuple[dict, dict]]
VerifyConfigBundle = Callable[[str, str, str], dict]
@dataclass(frozen=True)
class DeploymentControlPlane:
fetch_authenticated_record: FetchDeploymentRecord
def get_authenticated_record(self, deployment_id: str) -> tuple[dict, dict]:
result = self.fetch_authenticated_record(deployment_id)
if not isinstance(result, tuple) or len(result) != 2 or any(not isinstance(value, dict) for value in result):
raise RuntimeError("control plane returned an invalid authenticated record")
record, receipt = result
if receipt.get("status") != "PASS" or not receipt.get("record_sha256"):
raise RuntimeError("control-plane identity or readback verification failed")
return record, receipt
@dataclass(frozen=True)
class ConfigBundleVerifier:
verify_bundle: VerifyConfigBundle
def verify_exact_bundle(
self, *, config_url: str, signature_url: str, expected_digest: str
) -> dict:
receipt = self.verify_bundle(config_url, signature_url, expected_digest)
if not isinstance(receipt, dict) or receipt.get("status") != "PASS":
raise RuntimeError("local config bundle verification failed")
if receipt.get("config_digest") != expected_digest:
raise RuntimeError("config bundle receipt is not bound to the requested digest")
return receipt
RELOAD_FIELDS = {
"deployment_id", "config_digest", "config_url", "signature_url",
}
APPROVAL_FIELDS = RELOAD_FIELDS | {
"status", "agent_id", "approved_at", "expires_at", "policy_version",
}
def authorize_runtime_reload(
reload_request: dict,
*,
expected_agent_id: str,
control_plane: DeploymentControlPlane,
verifier: ConfigBundleVerifier,
) -> dict:
if not isinstance(reload_request, dict) or set(reload_request) != RELOAD_FIELDS:
raise ReloadDenied("reload_request_schema_mismatch")
if any(not isinstance(value, str) or not value for value in reload_request.values()):
raise ReloadDenied("reload_request_field_invalid")
approved, control_plane_receipt = control_plane.get_authenticated_record(
reload_request["deployment_id"]
)
if not isinstance(approved, dict) or set(approved) != APPROVAL_FIELDS:
raise ReloadDenied("approved_deployment_schema_mismatch")
if control_plane_receipt.get("status") != "PASS":
raise ReloadDenied("control_plane_identity_not_verified")
if approved["status"] != "approved":
raise ReloadDenied("deployment_not_approved")
if approved["agent_id"] != expected_agent_id:
raise ReloadDenied("deployment_agent_mismatch")
for field in RELOAD_FIELDS:
if approved[field] != reload_request[field]:
raise ReloadDenied(f"deployment_{field}_mismatch")
if approved["config_digest"] != reload_request["config_digest"]:
raise ReloadDenied("config_digest_mismatch")
local_receipt = verifier.verify_exact_bundle(
config_url=reload_request["config_url"],
signature_url=reload_request["signature_url"],
expected_digest=reload_request["config_digest"],
)
if local_receipt.get("status") != "PASS":
raise ReloadDenied("local_bundle_verification_failed")
return {
"schema_version": "aidefend.agent-config-reload.v1",
"status": "PASS",
"deployment_id": approved["deployment_id"],
"agent_id": approved["agent_id"],
"config_digest": approved["config_digest"],
"policy_version": approved["policy_version"],
"control_plane_receipt": control_plane_receipt,
"local_verification_receipt": local_receipt,
}
DeploymentControlPlane must authenticate a fixed control-plane origin with workload identity/mTLS, validate the exact response schema and approval expiry, and bind its receipt to deployment ID, agent ID, policy version, and response digest. ConfigBundleVerifier must fetch immutable object versions from allowlisted storage and locally verify the exact bytes, digest, signature, and schema using a pinned trust root. Missing dependency evidence is INSUFFICIENT_DATA; a mismatch or expired/non-approved record is FAIL; transport/verifier malfunction is ERROR. The returned local receipt is this guidance's evidence; an upstream approval is not a local pass.
Action: Accept runtime config changes only when this local gate returns PASS, then apply them atomically and retain both dependency receipts. Never let operators hot-edit security-sensitive configuration directly on the host or inside the container.
AID-H-021.002-G005
Concept
Configuration integrity alone is not enough if the runtime also loads executable tool or plugin files from disk. Treat those startup-loaded artifacts as part of the runtime trust boundary: build a signed manifest in CI/CD, verify every listed file before the service starts, and fail closed on any mismatch or unexpected file.
Step 1: Generate a signed tool manifest during the trusted build
# File: ci/build_tool_manifest.sh
set -euo pipefail
# Manifest paths are relative to the tool root in both producer and verifier.
(cd agent/tools && find . -type f -name '*.py' -print0 | sort -z | xargs -0 -r sha256sum --zero) > tool_manifest.sha256
cosign sign-blob --yes --key "$CONFIG_SIGNING_KMS_URI" --bundle tool_manifest.sha256.sig tool_manifest.sha256
Step 2: Verify the signed manifest and every startup-loaded file
# File: agent/verify_tools.py
from __future__ import annotations
import hashlib
import json
import math
import os
import stat
import subprocess
import tempfile
from pathlib import Path
class IntegrityError(RuntimeError):
pass
MAX_MANIFEST_BYTES = int(os.environ["VERIFIED_H021_TOOL_MANIFEST_MAX_BYTES"])
MAX_SIGNATURE_BYTES = int(os.environ["VERIFIED_H021_TOOL_SIGNATURE_MAX_BYTES"])
MAX_TOOL_BYTES = int(os.environ["VERIFIED_H021_TOOL_FILE_MAX_BYTES"])
COSIGN_TIMEOUT_SECONDS = float(os.environ["VERIFIED_H021_TOOL_COSIGN_TIMEOUT_SECONDS"])
if (
min(MAX_MANIFEST_BYTES, MAX_SIGNATURE_BYTES, MAX_TOOL_BYTES) < 1
or not math.isfinite(COSIGN_TIMEOUT_SECONDS)
or COSIGN_TIMEOUT_SECONDS <= 0
):
raise RuntimeError("verified tool-integrity bounds are invalid")
def stable_bytes(path: Path, maximum_bytes: int) -> bytes:
if maximum_bytes < 1 or not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_CLOEXEC"):
raise RuntimeError("secure bounded tool-file read is unavailable")
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC)
try:
before = os.fstat(fd)
if not stat.S_ISREG(before.st_mode) or before.st_size < 1 or before.st_size > maximum_bytes:
raise IntegrityError("tool-integrity input is not a bounded regular file")
chunks = []
remaining = maximum_bytes + 1
while remaining:
chunk = os.read(fd, min(remaining, 1024 * 1024))
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
payload = b"".join(chunks)
after = os.fstat(fd)
if (
len(payload) != before.st_size
or len(payload) > maximum_bytes
or (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns)
!= (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns)
):
raise IntegrityError("tool-integrity input changed during read")
return payload
finally:
os.close(fd)
def sha256_file(path: Path) -> str:
return hashlib.sha256(stable_bytes(path, MAX_TOOL_BYTES)).hexdigest()
def load_manifest(path: Path, signature: Path, trust_key: Path) -> dict[str, str]:
raw = stable_bytes(path, MAX_MANIFEST_BYTES)
sig = stable_bytes(signature, MAX_SIGNATURE_BYTES)
with tempfile.TemporaryDirectory(prefix="aidefend-tools-") as directory:
root = Path(directory)
manifest_copy = root / "manifest"
signature_copy = root / "manifest.sig"
manifest_copy.write_bytes(raw)
signature_copy.write_bytes(sig)
subprocess.run(
[
"cosign", "verify-blob", "--key", str(trust_key),
"--bundle", str(signature_copy), str(manifest_copy),
],
check=True, capture_output=True, text=True, timeout=COSIGN_TIMEOUT_SECONDS,
)
expected: dict[str, str] = {}
for record in raw.split(b"\0"):
if not record:
continue
file_hash, separator, rel_path = record.partition(b" ")
if separator != b" " or len(file_hash) != 64:
raise IntegrityError("signed tool manifest record is malformed")
decoded_path = rel_path.decode("utf-8", "strict")
if decoded_path.startswith("./"):
decoded_path = decoded_path[2:]
if (not decoded_path or "\\" in decoded_path or decoded_path.startswith("/")
or any(part in {"", ".", ".."} for part in decoded_path.split("/"))):
raise IntegrityError("tool manifest path is not canonical and root-relative")
if decoded_path in expected:
raise IntegrityError("signed tool manifest contains a duplicate path")
bytes.fromhex(file_hash.decode("ascii"))
expected[decoded_path] = file_hash.decode("ascii")
if not expected:
raise IntegrityError("signed tool manifest is empty")
return expected
def verify_tool_tree(
tool_root: Path, manifest_path: Path, signature: Path, trust_key: Path
) -> dict:
expected = load_manifest(manifest_path, signature, trust_key)
discovered = sorted(
str(path.relative_to(tool_root)).replace("\\", "/")
for path in tool_root.rglob("*.py") if path.is_file() and not path.is_symlink()
)
if sorted(expected.keys()) != discovered:
raise IntegrityError("Tool inventory mismatch between runtime tree and signed manifest")
for rel_path, expected_hash in expected.items():
actual_hash = sha256_file(tool_root / rel_path)
if actual_hash != expected_hash:
raise IntegrityError(f"Tool hash mismatch for {rel_path}")
return {
"schema_version": "aidefend.agent-tool-integrity.v1",
"status": "PASS",
"manifest_sha256": hashlib.sha256(
stable_bytes(manifest_path, MAX_MANIFEST_BYTES)
).hexdigest(),
"tool_count": len(expected),
}
Step 3: Refuse startup on integrity failure and emit a security event
If manifest signature verification fails, if the runtime tree differs from the approved manifest, or if any file hash mismatches, the agent must not start. Emit a structured integrity event to SIEM with the manifest version, failing file, and workload identity.
Action: Treat tool and plugin file verification as a startup gate, not a best-effort scan. If the runtime cannot prove that every executable helper matches the signed manifest, block launch and escalate.
AID-H-021.002-G006
Concept
Runtime integrity is stronger when the platform continuously compares live config with the signed desired state. If a process modifies prompt text, tool policy, network allowlists, or safety thresholds in memory or on disk, the reconciler should restore the approved version and emit a high-signal drift event.
Step 1: Publish the signed desired-state digest
{
"agent_id": "support-bot-prod",
"desired_config_digest": "sha256:cfb79f4ed61e88f248d69a56b7b22b0149f3d6544413eef235659018b4d558cb",
"policy_version": "cfg-v17",
"source_url": "s3://aidefend-configs/prod/agent-v17.yaml"
}
Step 2: Reconcile live state against that digest on a fixed cadence
# File: runtime/config_reconciler.py
from __future__ import annotations
import hashlib
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
from agent.verified_config import stable_bytes
CONFIG_PATH = Path("/app/configs/production_agent.yaml")
LoadDesiredState = Callable[[], tuple[dict, dict]]
RestoreSignedConfig = Callable[[dict], dict]
@dataclass(frozen=True)
class DesiredStateProvider:
load_desired_state: LoadDesiredState
def load_verified(self) -> tuple[dict, dict]:
result = self.load_desired_state()
if not isinstance(result, tuple) or len(result) != 2 or any(not isinstance(value, dict) for value in result):
raise RuntimeError("desired-state provider returned an invalid contract")
desired, receipt = result
if receipt.get("status") != "PASS" or not receipt.get("desired_state_sha256"):
raise RuntimeError("desired-state signature or readback verification failed")
return desired, receipt
@dataclass(frozen=True)
class SignedConfigRestorer:
restore_signed_config: RestoreSignedConfig
def restore_exact(self, desired: dict) -> dict:
receipt = self.restore_signed_config(desired)
if not isinstance(receipt, dict) or receipt.get("status") != "PASS":
raise RuntimeError("signed config restore failed")
if receipt.get("restored_config_digest") != desired.get("desired_config_digest"):
raise RuntimeError("restored config readback differs from signed desired state")
return receipt
def sha256_file(path: Path) -> str:
return "sha256:" + hashlib.sha256(stable_bytes(path, 1_000_000)).hexdigest()
def reconcile_once(
provider: DesiredStateProvider, restorer: SignedConfigRestorer
) -> dict:
desired, desired_receipt = provider.load_verified()
required = {
"agent_id", "desired_config_digest", "policy_version", "source_url",
}
if (
not isinstance(desired, dict) or set(desired) != required
or desired_receipt.get("status") != "PASS"
):
raise RuntimeError("verified desired-state contract is unavailable")
live_digest = sha256_file(CONFIG_PATH)
if live_digest == desired["desired_config_digest"]:
return {
"schema_version": "aidefend.config-reconciliation.v1",
"status": "PASS",
"action": "no_drift",
"agent_id": desired["agent_id"],
"observed_digest": live_digest,
"policy_version": desired["policy_version"],
"desired_state_receipt": desired_receipt,
}
restore_receipt = restorer.restore_exact(desired)
restored_digest = sha256_file(CONFIG_PATH)
if (
restore_receipt.get("status") != "PASS"
or restored_digest != desired["desired_config_digest"]
):
raise RuntimeError("signed configuration restore did not read back")
return {
"schema_version": "aidefend.config-reconciliation.v1",
"status": "FAIL",
"action": "drift_restored",
"agent_id": desired["agent_id"],
"expected_digest": desired["desired_config_digest"],
"observed_digest": live_digest,
"restored_digest": restored_digest,
"policy_version": desired["policy_version"],
"desired_state_receipt": desired_receipt,
"restore_receipt": restore_receipt,
}
def reconcile_loop(
provider: DesiredStateProvider, restorer: SignedConfigRestorer
) -> None:
while True:
print(reconcile_once(provider, restorer))
time.sleep(30)
Action: Run reconciliation from a trusted sidecar, controller, or host agent that can restore the signed desired state without trusting the compromised process. Treat every auto-restore event as an incident lead, not just a maintenance message.
AID-H-021.002-G007
Concept
Generate provenance only after the exact validated and signed configuration bytes are final. The attestation is a separate claim about repository, workflow, commit, and build path; it does not replace the detached configuration signature or prove that the policy content is safe.
Generate GitHub artifact provenance
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Attest final configuration
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-path: configs/production_agent.yaml
Verify before deployment
set -euo pipefail
CONFIG="configs/production_agent.yaml"
EXPECTED_REPOSITORY="acme/agent-config"
EXPECTED_WORKFLOW="acme/agent-config/.github/workflows/build-config.yml"
mkdir -p artifacts
gh attestation verify "$CONFIG" \
--repo "$EXPECTED_REPOSITORY" \
--signer-workflow "$EXPECTED_WORKFLOW" \
--format json \
> artifacts/config-provenance-verification.json
test -s artifacts/config-provenance-verification.json
Outcome and evidence
Bind the verification JSON to the configuration digest, detached-signature receipt, expected repository/workflow, commit and protected ref policy, verifier version, and deployment ID. Missing online/offline trust material is INSUFFICIENT_DATA; identity/digest mismatch is FAIL; verifier failure is ERROR. The deployment gate must separately evaluate organization policy over the verified statement; a cryptographically valid but unapproved workflow is not PASS.
Action: attest the final configuration in the protected build workflow and require independent provenance verification before deployment.
AID-H-021.002-G008
Concept:
The security of your signing process depends on the security of your signing keys. These keys must be managed in a secure system (like a KMS or HSM), have their access tightly controlled, and be rotated periodically to limit the impact of a potential key compromise.
Use a KMS for Signing Operations
Instead of handling raw key files in your CI/CD runner, use a Key Management Service (KMS) for all signing operations. The private key never leaves the KMS.
# File: signing/kms_sign.py
from __future__ import annotations
import base64
import hashlib
from pathlib import Path
import boto3
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, utils
KMS_KEY_ID = 'alias/agent-config-signing-key'
ALGORITHM = 'RSASSA_PSS_SHA_256'
def sign_file(path: str) -> dict:
payload = Path(path).read_bytes()
digest = hashlib.sha256(payload).digest()
kms = boto3.client('kms')
response = kms.sign(
KeyId=KMS_KEY_ID,
Message=digest,
MessageType='DIGEST',
SigningAlgorithm=ALGORITHM,
)
public_key = kms.get_public_key(KeyId=response['KeyId'])['PublicKey']
verifier = serialization.load_der_public_key(public_key)
verifier.verify(
response['Signature'],
digest,
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.DIGEST_LENGTH),
utils.Prehashed(hashes.SHA256()),
)
return {
'schema': 'aidefend.kms-signature/v1',
'key_id': response['KeyId'],
'algorithm': ALGORITHM,
'payload_sha256': digest.hex(),
'signature_b64': base64.b64encode(response['Signature']).decode('ascii'),
'public_key_der_b64': base64.b64encode(public_key).decode('ascii'),
}Rotate without breaking verification
For asymmetric signing keys, create and approve a successor key, publish both old and new key IDs/public keys to verifiers, switch the signer alias to the successor, and keep the old public key accepted for the versioned artifact-retention or rollout grace period. Independently read back the key ARN returned by Sign; never record only the mutable alias. Disable signing with the old key after all producers have moved, and remove old verification trust only after every still-valid artifact has expired or been re-signed.
Verification and evidence
Verify a valid signature locally, then test a modified payload, modified signature, unauthorized CI role, disabled key, and alias rollover. Preserve the payload digest, immutable KMS key ARN, public-key fingerprint, algorithm, signature, CI workload identity, KMS audit event, verifier result, rotation phase, and explicit PASS/FAIL/ERROR verdict.
Action: Store your configuration signing keys in a hardware-backed KMS. Use an approved asymmetric-key rollover runbook and IAM policies that restrict signing to the intended CI workload identity and key ARN.
Open-source tools
- pyca/cryptography
- GnuPG
- SPIRE (workload identity only; separate artifact signing required)
- Cosign
- in-toto
- Notation
Source-available / open-weight tools
- HashiCorp Vault Community (BUSL-1.1; source-available)
Commercial / hosted tools
- Azure Key Vault
- HashiCorp Vault Enterprise
- HCP Vault Dedicated
- AWS Key Management Service (KMS)
- Google Cloud Key Management Service
- CyberArk Code Sign Manager
- DigiCert Software Trust Manager
Threat mappings
MITRE ATLAS
- AML.T0011 User Execution
- AML.T0011.002 User Execution: Poisoned AI Agent Tool
- AML.T0081 Modify AI Agent Configuration
- AML.T0010 AI Supply Chain Compromise (signed configs detect supply chain-compromised config files)
- AML.T0121 AI Agent Environment Reconstruction (unsigned reconstructed configuration and tool manifests fail integrity verification at startup)
MAESTRO
- Supply Chain Attacks (Cross-Layer) (signed configs detect supply chain tampering)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection (signed runtime admission and drift restoration block unauthorized persistent changes to prompt text and agent-control artifacts but do not judge approved content semantics)
- LLM03:2026 Excessive Agency (signature verification preserves approved agent control artifacts against tampering but does not itself define least privilege)
- LLM05:2026 Data and Model Poisoning
OWASP ML Top 10 2023
- ML06:2023 AI Supply Chain Attacks
OWASP Top 10 for Agentic Applications 2026
- ASI04:2026 Agentic Supply Chain Vulnerabilities
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.2 Tool Poisoning
- AISubtech-5.2.1 Agent Profile Tampering
- AISubtech-9.3.1 Malicious Package / Tool Injection (Verifies the complete loaded tool/plugin file population)
- AISubtech-9.3.3 Dependency Replacement / Rug Pull (Verifies the complete loaded tool/plugin file population)
- AITech-12.1 Tool Exploitation
- AITech-5.2 Configuration Persistence
- AITech-9.3 Dependency / Plugin Compromise (Verifies the complete loaded tool/plugin file population)
Google Secure AI Framework 2.0 - Risks
- MDT: Model Deployment Tampering (signed configs detect deployment-stage config tampering)
Databricks AI Security Framework 3.0
- Model 7.4: Source code control attack
- Model 7.3: ML Supply chain vulnerabilities
- Agents - Tools MCP Server 13.21: Supply Chain Attacks