ITAR / CMMC Mode
O.D.I.N. ships with a dedicated fail-closed air-gap mode for deployments that cannot leak data to public addresses — ITAR-regulated defense shops, CMMC L2 contractors, aerospace subs, and anyone else running on a restricted network.
Enable it by setting one environment variable:
services:
odin:
environment:
- ODIN_ITAR_MODE=1
That's it. The mode is either on or off — there is no "warn" state. A misconfigured ITAR container refuses to boot.
What It Enforces
Boot audit (fail at startup)
At container start, O.D.I.N. resolves every configured outbound destination and refuses to boot if any one of them resolves to a public address:
license_server_url,update_server_url(settings)- Every row in the
webhookstable (decrypted) smtp_confighost (fromsystem_config)mqtt_republish_host(fromsystem_config)oidc_config.discovery_url(if OIDC is enabled)spoolman_url(from settings)- Per-org webhook URLs in
groups.settings_json
A public destination prints a clear operator message and the container exits with a non-zero status. Fix the config or unset ODIN_ITAR_MODE.
Runtime guards (fail at connect)
Boot-time DNS can drift after startup (split-horizon DNS, resolver rotation, poisoned cache). Every outbound HTTP/SMTP/MQTT call re-validates the destination before the socket connect:
- HTTP —
safe_post, license server, OIDC discovery + token + userinfo + JWKS, Spoolman, printer adapters (Moonraker, PrusaLink, smart plug) all route throughpin_for_request(url). Resolver output is validated (all addresses private) and the connection is DNS-pinned to those IPs so the socket cannot be redirected.trust_env=FalseblocksHTTP(S)_PROXYenv vars from routing around the pin. - SMTP — every
smtplib.SMTP()call inalert_dispatcher,channels, org password-reset, user-invite, and scheduled-digest paths is host-validated before connect. - MQTT — the external-broker republisher validates the host on every (re)connect and on the admin "Test Connection" button.
Hard-disabled subsystems
Some transports are fundamentally public — no amount of DNS pinning makes them compliant. Under ITAR, these are refused outright:
- APNs (Apple Push) —
api.push.apple.com. The provider reports as "not configured" under ITAR regardless of credentials. - Web Push —
pywebpushusesrequestsinternally and cannot be DNS-pinned. The send is refused even for privately-resolved subscription endpoints (the library's transport bypasses our pin).
If you need mobile / browser push in an ITAR deployment, route through an internal push gateway and configure it as a standard user webhook — safe_post validates + pins those.
Context-safe under async load
The DNS pin uses contextvars.ContextVar so concurrent async requests on one event loop cannot overwrite each other's pin between validation and connect. Propagation is verified against asyncio.to_thread and anyio.to_thread.run_sync (the primitives httpx uses under the hood).
Agent-Native Under ITAR
ITAR mode and the agent-native surface are designed together. The typical ITAR agent stack:
- O.D.I.N. backend —
ODIN_ITAR_MODE=1. - Ollama running locally — Qwen2.5-32B-Instruct, Llama 3.3 70B, or similar. No API calls leave the box.
odin-print-farm-mcp@2— stdio MCP server, same host as the agent.- Agent client (Claude Desktop / Claude Code with a local model backend, OpenClaw, etc.).
Every step stays inside the compliance boundary. The agent issues tool calls → MCP client talks to O.D.I.N. over http://localhost:8000 → O.D.I.N. talks to printers on the LAN. Zero outbound packets.
What Still Works Under ITAR
Everything in the core operator surface:
- Printer control (Bambu, Moonraker, PrusaLink, smart plug) — all via LAN.
- Job queue, scheduling, orders, approvals.
- Filament inventory, spool tracking, maintenance logs.
- Alerts, quiet-hours digests (to internal SMTP).
- Vision AI, timelapses, print archive.
- Multi-org, RBAC, MFA, OIDC (with an internal IdP like ADFS or Authentik).
- Agent-native MCP surface with idempotent retries and dry-run previews.
ITAR only blocks calls that would leave your network.
What's Disabled Under ITAR
| Feature | Reason |
|---|---|
| Apple Push (APNs) | api.push.apple.com is public. |
| Browser Web Push | pywebpush uses vendor push endpoints (FCM, Mozilla, etc.) and cannot be pinned. |
| Public OIDC providers | Refuses boot if discovery_url resolves public. Use an internal IdP. |
| Public SMTP relay (Gmail, SendGrid, Mailgun, etc.) | Refuses boot if smtp_config.host resolves public. Use internal SMTP. |
| Public webhooks | Refuses boot if any webhook URL resolves public. |
| Public license server | Refuses boot if license_server_url resolves public. Use locally-signed licenses. |
The agent-native push alternative: wire an internal push gateway (gotify, ntfy, your own) and configure it as a standard webhook.
Verifying a Clean Deployment
Log line on successful boot:
ODIN_ITAR_MODE=1 boot audit passed — all configured URLs are private.
Log line on a blocked runtime egress attempt:
smart_plug: ITAR blocked outbound to http://public.example.com/: ODIN_ITAR_MODE=1 refused outbound request — public.example.com resolves to a public address.
enforce_boot_config raises RuntimeError with a list of every violating URL on boot failure — there is no silent degradation.
See Also
- MCP Server — agent-native operation designed to run alongside ITAR.
- Environment Variables —
ODIN_ITAR_MODEand related. - OIDC / SSO — configure an internal IdP for ITAR compatibility.