Skip to main content

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 webhooks table (decrypted)
  • smtp_config host (from system_config)
  • mqtt_republish_host (from system_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 through pin_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=False blocks HTTP(S)_PROXY env vars from routing around the pin.
  • SMTP — every smtplib.SMTP() call in alert_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 — pywebpush uses requests internally 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:

  1. O.D.I.N. backend — ODIN_ITAR_MODE=1.
  2. Ollama running locally — Qwen2.5-32B-Instruct, Llama 3.3 70B, or similar. No API calls leave the box.
  3. odin-print-farm-mcp@2 — stdio MCP server, same host as the agent.
  4. 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​

FeatureReason
Apple Push (APNs)api.push.apple.com is public.
Browser Web Pushpywebpush uses vendor push endpoints (FCM, Mozilla, etc.) and cannot be pinned.
Public OIDC providersRefuses 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 webhooksRefuses boot if any webhook URL resolves public.
Public license serverRefuses 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​