The production problem
A remote policy URL can change agent behavior without shipping new code. That is convenient for rollouts and dangerous for the same reason.
If attackers influence that fetch path, they can try redirects, DNS rebinding, metadata endpoint access, or oversized payloads. One weak boundary turns your governance layer into a network client under attacker direction.
The hard part is not listing SSRF payloads. The hard part is building fetch rules that survive real bypass techniques while keeping operations practical.
What top results miss
| Source | Strong coverage | Missing piece |
|---|---|---|
| OWASP SSRF Prevention Cheat Sheet | Allowlist guidance, parser pitfalls, DNS/IP validation, and network-layer controls. | No control-plane mapping for policy loaders where fetched bytes directly change runtime decisions. |
| PortSwigger URL Validation Bypass Cheat Sheet | Practical bypass payloads for host validation, parser confusion, and redirect abuse. | No production operator guidance for safe policy fetch rollouts in autonomous agent control planes. |
| AWS Security Blog: IMDSv2 migration | Cloud metadata abuse risk and hardening controls that reduce credential theft impact. | No mapping to policy-engine fetch pipelines where policy bytes directly change decision logic. |
The gap is control-plane specificity: how these protections map to policy reload, signature requirements, and fail-open/fail-closed governance semantics.
They also miss trust-surface details that matter in real systems: suffix allowlists, redirect downgrade edges, and who owns each permitted subdomain.
Attack path model
| Step | Attacker move | Required defense |
|---|---|---|
| Attacker-controlled URL value | Supply external endpoint or crafted hostname to influence policy source | Load policy only from explicit allowlisted hostnames |
| DNS rebinding | Safe hostname resolves to private IP after initial validation | Resolve host during validation and again before dial |
| Redirect pivot | Allowed URL redirects to internal service or metadata endpoint | Revalidate every redirect target and cap chain length |
| Oversized payload | Large response attempts resource exhaustion | Enforce hard byte limits and reject bodies above limit |
| Unsigned policy injection | Serve reachable but malicious policy file | Require signature verification before parse and snapshot update |
Most incident writeups stop at metadata theft. For AI control planes, the blast radius can include policy mutation itself. Treat the policy URL as a high-trust artifact path.
Cordum runtime behavior
| Boundary | Current behavior | Operational impact |
|---|---|---|
| Scheme handling | Remote fetch is only entered for `http://` or `https://` policy sources. | Non-HTTP schemes do not enter this fetch path. |
| Production TLS rule | `http://` policy URLs are rejected in production. | Reduces MITM and downgrade risk on policy transport. |
| Host policy | `SAFETY_POLICY_URL_ALLOWLIST` supports comma-separated host constraints. | Limits fetches to approved domains and blocks unknown hosts. |
| Allowlist match semantics | Host checks allow exact match and subdomain suffix match (`host == entry || hasSuffix('.'+entry)`). | `example.com` also trusts `a.example.com`; review subdomain ownership and takeover risk. |
| Private network block | Private/loopback/link-local hosts are denied unless `SAFETY_POLICY_URL_ALLOW_PRIVATE=true`. | Blocks common SSRF targets such as metadata services by default. |
| Rebinding resistance | DNS resolution runs in URL validation and again in `DialContext`. | Reduces time-of-check vs time-of-use gaps. |
| Operational limits | HTTP client timeout is 10s, redirect limit is 5, size default is 2,097,152 bytes. | Caps fetch latency and payload size during policy reloads. |
| Redirect scheme rule | In production, initial URL must be HTTPS; redirect targets are host-validated but not explicitly HTTPS-gated. | Add a redirect-scheme guard to prevent HTTPS-to-HTTP downgrade hops. |
Implementation examples
Fetch guard path (Go)
func fetchPolicyURL(raw string) ([]byte, error) {
parsed, err := url.Parse(raw)
if err != nil {
return nil, fmt.Errorf("invalid policy url: %w", err)
}
if env.IsProduction() && parsed.Scheme == "http" {
return nil, fmt.Errorf("HTTPS required for policy URL in production")
}
if err := validatePolicyURL(parsed); err != nil {
return nil, err
}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.DialContext = policyDialContext
client := &http.Client{
Timeout: 10 * time.Second,
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 5 {
return errors.New("policy fetch redirect limit exceeded")
}
return validatePolicyURL(req.URL)
},
}
// Fetch response and enforce policyMaxBytes() before parse.
return readPolicyBody(resp.Body, policyMaxBytes())
}Recommended redirect-scheme guard
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 5 {
return errors.New("policy fetch redirect limit exceeded")
}
if env.IsProduction() && req.URL.Scheme != "https" {
return fmt.Errorf("HTTPS redirect target required: %s", req.URL.Host)
}
return validatePolicyURL(req.URL)
}Production env baseline
# Remote policy source export SAFETY_POLICY_URL=https://policy.corp.example/prod/safety.yaml # SSRF controls export SAFETY_POLICY_URL_ALLOWLIST=policy.corp.example export SAFETY_POLICY_URL_ALLOW_PRIVATE=false export SAFETY_POLICY_MAX_BYTES=2097152 # Integrity control (recommended with remote URLs) export SAFETY_POLICY_SIGNATURE_REQUIRED=true export SAFETY_POLICY_PUBLIC_KEY="<base64-or-hex-ed25519-public-key>" export SAFETY_POLICY_SIGNATURE_PATH=/etc/cordum/policy.sig
Abuse simulation drill
# 1) Confirm happy path host passes allowlist export SAFETY_POLICY_URL=https://policy.corp.example/prod/safety.yaml # 2) Attempt metadata endpoint (should fail) export SAFETY_POLICY_URL=http://169.254.169.254/latest/meta-data/ # expect: "policy url host not allowed" # 3) Attempt non-allowlisted host (should fail) export SAFETY_POLICY_URL=https://attacker.example/policy.yaml # expect: "policy url host not allowed" # 4) Restore valid URL and verify signature check remains enabled export SAFETY_POLICY_URL=https://policy.corp.example/prod/safety.yaml
Limitations and tradeoffs
- - Strict allowlists reduce risk but add rollout friction for new policy hosts.
- - Blocking private hosts by default can conflict with internal artifact storage designs.
- - DNS checks lower rebinding risk, but resolver trust and DNS cache behavior still matter.
- - Suffix allowlists simplify operations and increase blast radius if one subdomain is compromised.
- - Initial HTTPS enforcement is not enough if redirect targets are allowed to downgrade to HTTP.
- - A secure URL path still needs signature enforcement, or reachable tampered policy can pass through.
If you enable `SAFETY_POLICY_URL_ALLOW_PRIVATE=true` in production, document the reason and add explicit compensating controls. Future you will ask why this was allowed.
Next step
Run this hardening checklist this week:
- 1. Pin `SAFETY_POLICY_URL` to a dedicated host and enforce `SAFETY_POLICY_URL_ALLOWLIST`.
- 2. Keep `SAFETY_POLICY_URL_ALLOW_PRIVATE=false` in production unless you have a reviewed exception.
- 3. Add redirect-scheme enforcement so production redirects cannot downgrade to `http`.
- 4. Set `SAFETY_POLICY_SIGNATURE_REQUIRED=true` and verify reload behavior on bad signatures.
- 5. Execute one SSRF drill with redirect and metadata targets, then capture alert evidence.
Continue with Policy Signature Verification and LLM Safety Kernel.