Documentation

Iddio Docs

Everything you need to deploy, configure, and operate Iddio as a security gateway for your AI agents. From first install to production policy management.

Get Iddio running in under five minutes. This guide covers installation, daemon setup, policy configuration, and your first proxied command.

1. Install

There are two ways to run Iddio locally: a desktop app with a GUI, or the CLI.

Desktop app (macOS)

A native macOS app with a visual policy editor, approval dialogs, and observe mode.

brew install --cask leonardaustin/tap/iddio-desktop

Or grab the latest .dmg from GitHub Releases.

The desktop app manages the daemon for you. Once it is open and the daemon is running, skip to Step 3 to configure your policy.

CLI (macOS and Linux)

A single binary you run in a terminal. Lightweight and scriptable.

brew install leonardaustin/tap/iddio

Linux users without Homebrew can download a .deb or .AppImage from GitHub Releases.

2. Install and start the daemon

The CLI installs iddio-daemon as an OS service (launchd on macOS, systemd on Linux) and seeds the default policy.

iddio install

Verify the daemon is running:

iddio status
# daemon          up
# pid             12345
# uptime          4s
# build_version   v26.0603.1148
# schema_version  2
# proxy_mode      enforce
# policy          loaded
# proxy_listen    127.0.0.1:9443
!

iddio install is idempotent — re-running it on an already-installed host re-renders the service unit and is a no-op otherwise.

3. Route your shell through the proxy

Print and source a shell snippet that sets HTTPS_PROXY and the matching CA-bundle env vars:

# bash / zsh — add to ~/.bashrc or ~/.zshrc
eval "$(iddio shell-init)"

# fish — add to ~/.config/fish/config.fish
iddio shell-init fish | source

The snippet is gated on iddio status --quiet, so a stopped daemon never leaves your CLIs pointing at a closed listener.

!

The proxy intercepts HTTPS traffic via a local MITM CA it generates at install time. On macOS, iddio install trusts this CA in the system keychain automatically. On Linux you may need to add ~/.iddio/ca.crt to your system trust store. Run iddio ca info to check trust state.

4. Review the policy

The default policy ships with rules for managed Kubernetes clusters (EKS, GKE, AKS), Google Cloud, and Fly.io. Open it to understand what is configured:

iddio policy show

Edit it interactively:

iddio policy edit

Or edit the file directly — changes hot-reload within a second:

$EDITOR ~/.iddio/policy.yaml

A minimal policy that allows read operations and escalates writes for a local cluster:

kubectl:
  inspect:
    - "127.0.0.1:*" # local cluster endpoint
    - "192.168.*.1:*"

  default: escalate

  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    delete:
      tiers: [sensitive]
    admin:
      tiers: [break-glass]

  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, delete, admin]

See Config Reference for the full policy schema.

5. Run a proxied command

With the shell snippet sourced, run any supported CLI tool normally. Read operations pass through without any prompt. Write and delete operations block until you approve them.

# Read — passes through instantly (observe tier)
kubectl get pods -n payments

# Write — blocks for approval (modify tier)
kubectl apply -f deployment.yaml

When a request is held for approval, you will see it listed in the desktop app or via the CLI:

iddio approval list
# ID         TIER      HOST                   METHOD  PATH
# a1b2c3d4   modify    my-cluster.example.com  POST    /apis/apps/v1/deployments

Approve or deny:

iddio approval allow a1b2c3d4
iddio approval deny  a1b2c3d4 --reason "not during business hours"

6. Check the audit log

Every request is appended to ~/.iddio/audit.jsonl. View recent entries:

iddio audit show

Follow new entries in real time:

iddio audit tail

Something wrong? See Troubleshooting.

Every request that flows through Iddio passes through a pipeline in the daemon's proxy handler. The proxy operates as a transparent HTTPS MITM — agents point their CLIs at HTTPS_PROXY=http://127.0.0.1:9443 and the daemon intercepts, classifies, and enforces policy before forwarding traffic to the real upstream.

01
INTERCEPT

The daemon generates its own ECDSA CA and per-host leaf certificates at install time. When a CLI connects, the proxy terminates TLS, presents a leaf cert signed by its CA, then opens a fresh TLS connection to the real upstream host. Agents see a valid certificate chain because the daemon's CA is trusted in the system keychain. Hosts on the user pinned list (via iddio pin) are spliced through without MITM. Hosts that match no policy inspect route are a transparent tunnel: spliced through untouched, with no audit row or record of any kind.

02
CLASSIFY

The HTTP method, host, and path are dispatched to a protocol-specific classifier. Registered classifiers cover kubectl, gcloud, aws (AWS CLI), github (gh), terraform/tofu, helm, cloudflare/wrangler, and fly/flyctl. Unrecognised hosts that match the generic: inspect list are tiered from the HTTP method alone. The result is one of four risk tiers: observe, modify, sensitive, or break-glass. Unknown or unclassifiable requests fail closed (escalate or deny).

03
POLICY

The classification and request context are evaluated against ~/.iddio/policy.yaml. Policy is protocol-keyed (kubectl:, gcloud:, fly:, generic:), with scope-sugar blocks (namespaces:, projects:, apps:) that are first-match-wins. Each block resolves to an action: allow, escalate, or deny. If no rule matches, the protocol's default: applies (typically escalate). Policy is hot-reloaded via fsnotify — edits take effect within a second without dropping in-flight requests.

04
APPROVE OR BLOCK

An allow decision forwards the request immediately. A deny returns an error to the agent. An escalate decision blocks the agent's connection and queues an approval request in the daemon's in-process broker. The desktop app and CLI both surface pending approvals (iddio approval list). A human approves (iddio approval allow <id>) or denies (iddio approval deny <id>) within the configured timeout (default ~30 s). The agent's HTTP connection is held open throughout — from the agent's perspective, the upstream is simply slow to respond.

05
AUDIT

Every request is appended to ~/.iddio/audit.jsonl as a best-effort append-only JSON line. Each row carries a timestamp_ms, an op field, an opaque random hash used only for pagination and deduplication, and a fields map with request context. The log is not hash-chained and carries no tamper-detection guarantees. View it with iddio audit show or iddio audit tail.

Approval flow

When a request is escalated, the daemon's in-process broker holds the agent's connection open until a human responds. There is no y/N terminal prompt, no webhook, and no Slack integration — approvals are handled through the desktop app or the iddio approval CLI commands.

Agent CLI             iddio daemon            Operator (desktop / CLI)
    │                      │                          │
    ├── HTTPS request ─────►│                          │
    │                      ├── classify → modify       │
    │                      ├── policy → escalate       │
    │   (connection held)  ├── approval queued ───────►│
    │                      │                 iddio approval list
    │                      │                 iddio approval allow <id>
    │                      │◄── approved ─────────────┤
    │                      ├── forward to upstream     │
    │◄── response ─────────┤                          │
    │                      ├── audit row written       │

Proxy modes

The daemon operates in one of three modes, switchable at runtime via iddio set-proxy-mode:

ModeBehaviour
off / passthroughTraffic is spliced through without classification or enforcement.
observeDry-run: classify policy-matched requests and record would-be decisions to the observe log; never block or escalate; the audit log is not written.
enforceFull policy evaluation; block on deny, hold on escalate.

Component architecture

ComponentLocationPurpose
MITM Proxydaemon/internal/proxy/proxy.goTLS intercept, cert generation, request routing
Classifier Registrydaemon/internal/classifier/registry.goProtocol dispatch to per-CLI classifiers
Policy Enginedaemon/internal/policy/YAML rule evaluation, hot-reload via fsnotify
Approval Brokerdaemon/internal/approvals/broker.goIn-process hold/resume with timeout
Audit Loggerdaemon/internal/audit/Best-effort append-only JSONL writer
IPC Serverdaemon/internal/runtime/Unix socket server; CLI and desktop connect here

Iddio ships as a single iddio binary. All state lives in ~/.iddio/. Every command that talks to the running daemon dials ~/.iddio/iddio.sock.


iddio install

Register iddio-daemon with the OS supervisor (launchd on macOS, systemd on Linux), copy the daemon binary to ~/.iddio/bin/, and seed a default policy.yaml.

iddio install [flags]

# Flags:
#   --system          Install as a system service (Linux + sudo only)
#   --no-service      Place binary + seed state only; skip supervisor registration
#   --no-trust-ca     Skip the macOS CA keychain trust step
#   --daemon-path     Override the source iddio-daemon binary to copy
#   --wait duration   How long to wait for the daemon socket after install (default 10s)
#   --print-unit      Render the service unit / plist to stdout and exit (read-only)

Idempotent — safe to re-run after an upgrade.


iddio status

Show daemon liveness, version, proxy mode, and the locations of its socket and log files.

iddio status [--json] [--quiet]

# Flags:
#   --json    Emit raw JSON instead of the table view
#   --quiet   Print nothing; signal daemon health via exit code only (0=up, 1=down)

iddio shell-init

Print a shell-rc snippet that exports HTTPS_PROXY and CA-bundle env vars so CLIs route through the local iddio proxy. The snippet is gated on iddio status --quiet.

iddio shell-init [bash|zsh|fish] [--listen-addr <addr>]

# Add to ~/.bashrc or ~/.zshrc:
eval "$(iddio shell-init)"

# Add to ~/.config/fish/config.fish:
iddio shell-init fish | source

iddio policy

Inspect and edit the daemon's active policy.

iddio policy show                     # Print the active policy.yaml
iddio policy edit                     # Open in $EDITOR and persist on save
iddio policy reload                   # Force re-read from disk
iddio policy validate --path <file>   # Validate a candidate file without persisting

iddio approval

Inspect and respond to pending escalations. Escalated requests block the agent's connection until responded to or timed out.

iddio approval list [--json]               # List pending approvals
iddio approval allow <id> [--reason text]  # Approve a pending request
iddio approval deny  <id> [--reason text]  # Deny a pending request
iddio approval watch                       # Stream approval events (follow mode)

iddio audit

Query the audit log (~/.iddio/audit.jsonl).

# Show recent entries (server-side filtered)
iddio audit show [flags]
#   --op string     Filter by op (e.g. mitm, pin_splice, block)
#   --host string   Filter by exact host
#   --last uint     Show at most N rows (default 50)
#   --since string  Only rows newer than this duration (e.g. 5m, 24h)
#   --json          Emit JSON

# Stream rows from disk (no daemon required)
iddio audit tail [flags]
#   --lines N        Print the last N matching rows then exit (0 = follow mode)
#   --filter key=val AND-filter rows by field value (repeat for multiple)
#   --json           Emit NDJSON instead of human-readable rows

iddio observe

Manage the daemon's observe-mode session. In observe mode the daemon classifies requests to hosts matching the policy's inspect routes and records what policy would do in enforce mode — without blocking anything, and without writing to the audit log (records go to observe.jsonl). To observe traffic to every domain, add "*" under the policy's generic.inspect block.

iddio observe start [--duration 30m]   # Start a bounded observe session
iddio observe stop                     # Stop the active session
iddio observe status [--json]          # Show session status
iddio observe show [flags]             # List recent observe records
#   --limit uint    Max records to return (default 100)
#   --offset uint   Pagination offset
#   --host string   Filter by exact host
#   --op string     Filter by would-be-decision (allow|escalate|deny)
#   --json          Render as JSON
iddio observe clear --force            # Truncate the observe log

iddio set-proxy-mode / get-proxy-mode

Change or query the daemon's proxy mode at runtime.

iddio set-proxy-mode <off|passthrough|observe|enforce>
iddio get-proxy-mode

iddio classify

Synthesise a request and print the daemon's classification.

iddio classify http <method> <url>
iddio classify cli  <argv>...

# Examples:
iddio classify http GET https://my-cluster.eks.amazonaws.com/api/v1/pods
iddio classify cli kubectl delete pod my-pod -n production

iddio decide

Run the classifier and policy engine together and print the resulting decision.

iddio decide <method> <host> <path> [--json]

iddio inspect

View or edit the MITM inspect list per protocol without editing policy.yaml directly.

iddio inspect summary [--json]                  # Print per-protocol inspect entries
iddio inspect add    <protocol> <entry>         # Add a host / suffix / CIDR
iddio inspect remove <protocol> <entry>         # Remove an entry

iddio pin / unpin / pinned

Manage the user pinned (never-MITM) list. Pinned hosts are spliced through without TLS interception.

iddio pin   <host> [--note text]   # Add a host to the never-MITM list
iddio unpin <host>                 # Remove a host from the list
iddio pinned                       # List all pinned hosts

iddio detector

Inspect and manage the daemon's auto-detected pin cache. Hosts where the upstream cert fails iddio's verification are auto-pinned temporarily.

iddio detector list    [--json]      # List active detector pins
iddio detector clear                 # Wipe the detector pin cache
iddio detector promote <host>        # Move a detector pin to the permanent user list

iddio events

Stream daemon-emitted events.

iddio events [--follow] [--json]

# Common event tags: proxy_mode_changed, policy_reloaded, mitm, splice,
# block, allow, escalate, approval_requested, approval_replied,
# approval_timed_out, host_pinned, host_unpinned

iddio ca

Inspect the daemon's dev CA.

iddio ca info [--json]   # Show path, fingerprint, validity, and trust state

iddio settings

Read and mutate persisted daemon settings (settings.yaml).

iddio settings get                  # Print current settings
iddio settings set <key=value>      # Mutate one setting
iddio settings reload               # Confirm hot-reload and print current proxy_listen

iddio doctor

Run a read-only diagnostic over the iddio install. Reports state-dir permissions, the supervisor unit, and the daemon process. Use iddio repair to fix anything doctor flags.

iddio doctor [--system] [--json]

iddio repair

Diagnose and fix a damaged iddio install.

iddio repair [--system] [--dry-run]
iddio repair --trust-ca              # macOS: (re-)trust the dev CA in the admin domain
sudo iddio repair --untrust-ca       # macOS: remove dev CA certs and orphaned trust settings

iddio uninstall

Remove the iddio-daemon service, binary, and state directory. The audit log is preserved by default.

iddio uninstall [--system] [--keep-data] [--yes]

iddio upgrade

Download and apply the latest iddio-daemon release from GitHub.

iddio upgrade [--check] [--dry-run] [--system]

iddio update

Manage the daemon-owned self-update state machine.

iddio update check    [--force]   # Force an immediate check against GitHub Releases
iddio update status   [--json]    # Show current update state-machine snapshot
iddio update apply                # Apply the staged update and restart the daemon

iddio gateway

Start the iddio daemon in foreground (useful for testing without a supervisor).

iddio gateway [--start]
!

Under a normal iddio install setup the daemon is managed by launchd or systemd. Use iddio gateway only for development or debugging.

Iddio classifies every intercepted request into one of four risk tiers. The classification happens in real-time with sub-millisecond latency and drives the policy engine's allow / escalate / deny decision.

OBSERVE

Read-only operations.

HTTP Methods: GET, HEAD, OPTIONS

Examples across supported CLIs:

  • kubectl get pods · kubectl describe node · kubectl logs deploy/api
  • aws ec2 describe-instances · gcloud compute instances list
  • gh repo view · terraform show · helm list

The default policy allows observe-tier requests without a prompt. Operators can override this per-namespace, per-project, or per-app in policy.yaml.

MODIFY

Standard write operations.

HTTP Methods: POST, PUT, PATCH

Examples:

  • kubectl apply -f manifest.yaml · kubectl patch deployment
  • aws s3 cp · gcloud compute instances create
  • fly deploy · terraform apply

Modify-tier requests escalate by default and block until a human approves them via the desktop app or iddio approval allow <id>.

SENSITIVE

Irreversible or high-impact operations.

HTTP Methods: DELETE, plus any request that touches a high-value resource regardless of method. For kubectl, reading Secrets is also elevated to sensitive because Secrets carry credentials.

Examples:

  • kubectl delete pod/x · kubectl get secret
  • aws s3 rm · gcloud projects delete
  • fly apps destroy

Sensitive-tier requests escalate by default.

BREAK-GLASS

Highest-risk operations. The default policy denies these outright. Operators must explicitly allow or escalate them in policy.yaml.

For kubectl: dangerous subresources (exec, attach, portforward, proxy, debug) and mutations on privilege-escalation resources (RBAC roles/bindings, webhooks, CRDs, namespaces, PersistentVolumes).

For other CLIs: IAM mutations, credential issuance, infrastructure teardown.

Examples:

  • kubectl exec -it pod -- sh · kubectl port-forward
  • aws iam create-user · gcloud iam service-accounts create
  • fly machines api-proxy

Fail-closed behaviour

Unknown commands, unrecognised CLI tools, and classifiers that cannot determine a tier all fail closed — the request is treated as escalate or deny, never silently allowed. This prevents a novel or unexpected command from bypassing enforcement by accident.

The generic: block in policy.yaml covers any host on the inspect list that has no dedicated classifier. Its tier assignment is purely HTTP-method-based: GET/HEAD/OPTIONS → observe, POST/PUT/PATCH → modify, DELETE → sensitive. Paths containing destructive-looking prefixes (delete, destroy, drop, purge, terminate, evict) are escalated to at least sensitive regardless of method.


Supported CLI classifiers

The following CLI tools have dedicated classifiers with command-level tier resolution:

CLI toolInspect protocol keyCLI aliases recognised
kubectlkubectlkubectl, kustomize
Google Cloud CLIgcloudgcloud
AWS CLIawsaws
GitHub CLIgithubgh
Terraform / OpenTofu(CLI only)terraform, tofu
Helm(CLI only)helm
Cloudflare Wranglercloudflarewrangler, flarectl
Fly.ioflyflyctl, fly

Any host not matched by a dedicated classifier falls through to the generic: block (HTTP-method tier only).

!

Use iddio classify cli <argv> or iddio classify http <method> <url> to see exactly which tier the daemon would assign to any specific request before putting it in front of a real workload.

Policy rules live at ~/.iddio/policy.yaml. The file is hot-reloaded by fsnotify — save it and the new policy takes effect within a second without restarting the daemon. If the YAML is invalid, the daemon logs a warning and keeps the previous working policy.

Schema overview

The top level is keyed by protocol (kubectl, gcloud, fly, generic, …). Each protocol block contains:

KeyPurpose
inspect:Hostnames, globs, IPs, or CIDRs the daemon should MITM for this protocol
default:Fallback action (allow / escalate / deny) when no rule matches
groups:Named selectors — match requests by tiers: or commands:
<scope>:First-match-wins scope-sugar list (namespaces, projects, regions, zones, orgs, apps)
rules:Flat allow/escalate/deny fallback, evaluated after scope blocks

Actions: allow (forward immediately), escalate (block until a human approves or denies), deny (return 403 immediately).

The four risk tiers

TierTypical operations
observeRead-only / GET; API discovery
modifyCreates, updates, patches
sensitiveDeletes, secret reads, credential operations
break-glassIAM grants, cluster-admin, destructive platform ops
!

There are four tiers in V2 (observe, modify, sensitive, break-glass). There is no Tier-1 "operate" or runbook tier.

Annotated example — kubectl

kubectl:
  # Hosts to intercept. Add your own cluster URLs here.
  inspect:
    - "*.eks.amazonaws.com" # EKS
    - "*.gke.goog" # GKE
    - "*.azmk8s.io" # AKS

  # Default: escalate anything that doesn't match a rule.
  default: escalate

  # Named groups — match by tier string or exact command label.
  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    delete:
      tiers: [sensitive]
    admin:
      tiers: [break-glass]
    exec:
      # commands: use the classifier's canonical "verb resource" form
      # (visible in ~/.iddio/iddio.log on `classified:` lines).
      commands: [exec, attach, port-forward, cp, debug, proxy]

  # Scope-sugar: first-match-wins across the list.
  namespaces:
    - names: [prod-*, production]
      rules:
        escalate:
          groups: [mutate, delete, admin]
          commands:
            - configmaps get
            - configmaps list
        allow:
          groups: [read]
        deny:
          groups: [exec]

    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, delete, admin]
        deny:
          groups: [exec]

  # Flat fallback — hits when no namespaces: entry matched
  # (cluster-scoped requests, API discovery, auth handshakes).
  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, delete, admin]
    deny:
      groups: [exec]

Annotated example — gcloud

gcloud:
  inspect:
    - "*.googleapis.com"

  default: escalate

  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    destructive:
      tiers: [sensitive]
    iam:
      tiers: [break-glass]

  projects:
    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, destructive, iam]

  # Flat fallback for OAuth token refreshes, cross-service IAM,
  # and discovery calls that carry no project label.
  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, destructive, iam]

Annotated example — fly

fly:
  inspect:
    - "api.fly.io"
    - "api.machines.dev"
    - "*.fly.dev"

  default: escalate

  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    destructive:
      tiers: [sensitive]
    admin:
      tiers: [break-glass]

  apps:
    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, destructive, admin]

  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, destructive, admin]

generic: — arbitrary hosts

The generic: block MITMs any host listed under inspect: and tiers requests from the HTTP method alone (GET/HEAD/OPTIONS → observe, POST/PUT/PATCH → modify, DELETE → sensitive). The wildcard * under inspect: is legal only in the generic: block.

generic:
  inspect: [] # add hosts here; empty ships by default
  default: escalate

  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    destructive:
      tiers: [sensitive]
    admin:
      tiers: [break-glass]

  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, destructive, admin]

Scope-sugar keyword reference

KeywordProtocolMatched label
namespaces:kubectlnamespace
projects:gcloudproject
regions:gcloudregion
zones:gcloudzone
orgs:gcloudorganization
apps:flyapp
orgs:flyorg

Scope blocks are first-match-wins down the list. Each entry needs a names: glob list and a rules: block.

Glob patterns

PatternMatches
"payments"Exact match
"staging-*"Any name starting with staging-
"*"Everything (catch-all)

Policy decisions

DecisionBehavior
allowRequest forwarded immediately
denyRequest rejected with 403
escalateRequest blocked; operator must approve or deny via desktop app or iddio approval allow <id> / iddio approval deny <id>
!

default: escalate is the safe default. Without an explicit rule, unknown commands surface an approval prompt rather than being silently allowed or denied.

!

Changes to policy.yaml are hot-reloaded automatically — no daemon restart needed. If the new YAML is invalid, the daemon logs a warning and keeps the previous working policy.

Iddio is a transparent MITM proxy. Agents present no identity to the daemon — there are no per-agent kubeconfigs to generate, no client certificates to mint, and no tokens to manage. You point the agent's tool at the proxy and the daemon intercepts and classifies traffic automatically.

How agents reach the proxy

The daemon listens on a local HTTPS port. Route an agent's CLI tool through it by setting the appropriate environment variable or config option before starting the agent.

HTTPS_PROXY (generic)

Most CLI tools and HTTP clients respect HTTPS_PROXY:

export HTTPS_PROXY=http://127.0.0.1:9443
# (9443 is the default; check `iddio status` for proxy_listen)

The easiest way to set this up is iddio shell-init, which prints a shell-rc snippet exporting HTTPS_PROXY/HTTP_PROXY plus the CA bundle variables (SSL_CERT_FILE, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS, AWS_CA_BUNDLE, GIT_SSL_CAINFO) pointing at $HOME/.iddio/ca-bundle.pem (system roots + the iddio CA).

Tools that use HTTPS_PROXY automatically: curl, many Go and Python HTTP clients, AWS CLI, Fly CLI, and others.

kubectl — KUBECONFIG

kubectl (client-go) respects HTTPS_PROXY, so with the shell-init environment active your existing kubeconfig keeps pointing at the real cluster — requests are routed through the daemon transparently.

One caveat: because the proxy re-signs TLS, kubectl must trust the iddio CA. If your kubeconfig pins the cluster CA with certificate-authority-data, replace it with the iddio bundle:

clusters:
  - name: my-cluster
    cluster:
      server: https://my-cluster.example.com # the REAL cluster URL
      certificate-authority: ~/.iddio/ca-bundle.pem
kubectl get pods   # observe-tier read — passes through, logged
!

kubectl performs TLS verification. Export the daemon's CA and add it to certificate-authority-data (base64-encoded) so kubectl trusts the proxy's certificates.

gcloud / Fly / other CLIs

For CLIs that use environment-level HTTPS proxying, set HTTPS_PROXY before launching the agent process. The daemon intercepts the TLS connection and applies policy from the matching protocol block in ~/.iddio/policy.yaml.

Selecting which hosts to intercept

The daemon only MITMs hosts listed under inspect: in the protocol block for that tool. If a host is not listed, the daemon passes the connection through without inspection.

Example — adding a private EKS cluster endpoint:

kubectl:
  inspect:
    - "*.eks.amazonaws.com"        # managed EKS
    - "my-private-cluster.corp"    # private cluster
  ...

See Policy Configuration for the full schema.

Trusting the daemon's CA

The daemon generates a local ECDSA P-256 CA on first run and stores it at ~/.iddio/ca.crt (public) and ~/.iddio/ca.key (private, 0600).

For tools that perform TLS verification independently of HTTPS_PROXY:

# Show CA path, fingerprint, validity, and trust state
iddio ca info

# Path to the CA certificate
~/.iddio/ca.crt

Add ~/.iddio/ca.crt to the tool's trusted CA bundle, or export it as certificate-authority-data in a kubeconfig.

Checking proxy status

# Show daemon status and listen address
iddio status

# Run diagnostic checks
iddio doctor

Policy applies to all intercepted traffic

Once an agent's tool is routed through the daemon, all requests matching an inspect: entry are classified and evaluated against ~/.iddio/policy.yaml. Read-only requests in the observe tier pass immediately (per the default policy). Write, sensitive, or break-glass requests escalate for human approval unless the policy explicitly allows them.

!

There is no per-agent identity in the current release. All traffic through the daemon is governed by the same policy file. If you need different policies for different agents, use separate daemon instances or wait for the per-user identity feature planned for a future release.

Every request through Iddio is logged to a best-effort append-only JSONL file at ~/.iddio/audit.jsonl. The writer is mutex-protected so concurrent goroutines can append without racing the file. Each row is fsynced before the function returns, so a row that commits survives a power loss.

!

The audit log is best-effort and not tamper-evident. There is no hash chain, no prev_hash linking, and no integrity-verification command. The hash field in each row is an opaque random row ID used only for pagination and deduplication — it is not derived from row content and does not form a chain.

Log entry format

Each entry is a single JSON line with four top-level fields:

{
  "timestamp_ms": 1736947921003,
  "op": "mitm",
  "hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "fields": {
    "host": "my-cluster.eks.amazonaws.com",
    "method": "GET",
    "path": "/api/v1/namespaces/payments/pods",
    "tier": "observe",
    "decision": "allow",
    "status": 200
  }
}

An escalated request that was approved:

{
  "timestamp_ms": 1736947965112,
  "op": "mitm",
  "hash": "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3",
  "fields": {
    "host": "my-cluster.eks.amazonaws.com",
    "method": "DELETE",
    "path": "/api/v1/namespaces/payments/pods/api-7d4b",
    "tier": "sensitive",
    "decision": "allow",
    "outcome": "approved"
  }
}

Row fields

FieldTypeDescription
timestamp_msnumberUnix epoch in milliseconds
opstringOperation type (e.g. mitm, pin_splice, block, control)
hashstringOpaque random 128-bit row ID (hex) — used for pagination/dedup only
fieldsobjectOp-specific payload (host, method, path, tier, decision, status, …)

The hash field is a random identifier, not a content hash and not part of a chain. Two identical events appended in the same millisecond will have different hash values.

Only hosts that match a policy inspect route are audited, and only in enforce mode. A host that matches no route is a transparent tunnel and leaves no trace; observe mode is a dry-run that records to the observe log (observe.jsonl) instead of the audit log. To audit everything, add "*" under the policy's generic.inspect block.

Viewing logs

# Show the last 50 rows (default)
iddio audit show

# Show the last 20 rows
iddio audit show --last 20

# Filter by host
iddio audit show --host my-cluster.eks.amazonaws.com

# Filter by op
iddio audit show --op mitm

# Show rows from the last hour
iddio audit show --since 1h

# Emit JSON for scripting
iddio audit show --json

# Stream new rows in real time (Ctrl-C to stop)
iddio audit tail

# Show last 10 rows then exit
iddio audit tail --lines 10

# Filter the tail stream
iddio audit tail --filter host=my-cluster.eks.amazonaws.com

# For advanced filtering, pipe the raw JSONL through jq
jq 'select(.op == "block")' ~/.iddio/audit.jsonl
jq 'select(.fields.tier == "sensitive")' ~/.iddio/audit.jsonl
jq 'select(.fields.decision == "allow")' ~/.iddio/audit.jsonl

File permissions

The audit log file is created with 0600 permissions (owner read/write only). It is append-only in normal operation; the daemon never overwrites or truncates it.

!

There is no iddio audit verify command. The audit log provides an operational record of what happened, not a cryptographic guarantee. For forensic integrity requirements, forward rows to an external log-aggregation system in real time.

Iddio is a transparent MITM HTTPS proxy. It intercepts TLS traffic between AI agent CLI tools and their upstream APIs, classifies each request into a risk tier, applies a YAML policy, and audit-logs everything. The agent never knows the proxy exists — from its perspective it is talking directly to the target API.

How it works

  1. The agent's CLI tool is pointed at the Iddio daemon via HTTPS_PROXY or a per-tool config (e.g. KUBECONFIG).
  2. The daemon terminates TLS using a locally-generated ECDSA CA, classifies the request, and evaluates the policy.
  3. If the policy says allow, the request is forwarded. If deny, a 403 is returned. If escalate, the connection blocks until a human approves or denies via the desktop app or CLI.
  4. Every request is audit-logged to ~/.iddio/audit.jsonl.

There is no system-wide packet capture (pf / nftables). Reach is entirely through HTTPS_PROXY / per-tool configuration.

Key properties

  • Local-first — the daemon runs on the operator's machine. No cloud control plane is required.
  • Fail-closed — unknown commands, unrecognized CLIs, and internal errors default to escalate (the policy default). Nothing silently passes.
  • Four risk tiersobserve (read-only), modify (writes), sensitive (deletes, secrets), break-glass (IAM, admin). There is no fifth tier.
  • Blocking approvals — escalated requests block the agent's HTTP connection until a human responds. No polling; no retries needed.
  • Best-effort audit — every request is logged. The log is not tamper-evident; there is no hash chain.
  • ECDSA CA — the daemon generates a local ECDSA P-256 CA and per-connection leaf certificates. Tools that verify TLS must trust this CA.

Transparent proxy model

Agents present no identity to Iddio. The proxy does not issue per-agent kubeconfigs, client certificates, or bearer tokens to agents. The agent's existing credentials are used as-is; the daemon intercepts the connection before it reaches the upstream.

This means:

  • No per-agent enrollment step.
  • No token management or rotation.
  • Policy is applied uniformly to all traffic that passes through the daemon, regardless of which agent sent it.

File permissions

All sensitive files are created with restricted permissions:

FilePermissionsDescription
~/.iddio/ca.key0600CA private key
~/.iddio/policy.yaml0600Access policy
~/.iddio/audit.jsonl0600Audit log
~/.iddio/0700Config directory

What Iddio does NOT provide

Be explicit about limits:

  • No tamper-evident audit — the audit log is best-effort append-only JSONL. There is no prev_hash chain and no iddio audit verify command. Forward rows to an external SIEM for forensic-grade integrity.
  • No agent identity — there is no mTLS / SPIFFE identity model, no bearer-token agent auth, and no per-agent kubeconfig or client certificates in the current release.
  • No JIT credentials — credential injection (short-lived ServiceAccount tokens per request) is not implemented.
  • No session recording — exec/attach sessions are not recorded.
  • No enterprise control plane — there is no PostgreSQL, OIDC, or gRPC control plane in the current release. See the Deployment page.
!

JIT credentials, session recording, and enterprise control plane are on the roadmap. This page will be updated as features ship.

Threat model

If an agent is compromised: its requests are still proxied through Iddio and subject to classification and policy. Write and sensitive operations require human approval. Read-only operations pass through (or can be denied by policy). The audit log records what was attempted.

Blast radius limits: policy can deny entire tiers or specific commands. Setting default: deny at the protocol level means any unmatched request is blocked.

Trust boundary: the daemon runs as the operator's OS user and holds the operator's cloud credentials. An attacker who can write ~/.iddio/policy.yaml or kill the daemon process can bypass policy. Protect the config directory with standard OS-level access controls.

All Iddio state lives under ~/.iddio/ (per-user install) or /var/lib/iddio/ and /etc/iddio/ (system install). Every file is created with 0600 permissions; directories are 0700.

Directory layout

~/.iddio/
├── ca.crt            # Dev CA certificate (ECDSA, generated at install)
├── ca.key            # Dev CA private key (0600)
├── ca-bundle.pem     # System roots + dev CA — used by SSL_CERT_FILE exports
├── policy.yaml       # YAML policy rules (hot-reloaded via fsnotify)
├── settings.yaml     # Daemon settings (hot-reloaded)
├── audit.jsonl       # Best-effort append-only audit log (0600)
├── observe.jsonl     # Observe-mode session log (0600)
├── iddio.sock        # Unix domain socket — CLI and desktop connect here
├── iddio.log         # Daemon log
├── install_mode      # "per_user" or "system" marker written by iddio install
├── ca_trusted        # Boolean marker: dev CA trusted in the OS keychain
└── bin/
    └── iddio-daemon  # Daemon binary copied in by iddio install
!

There is no tokens.yaml, no agents/ directory, and no per-agent kubeconfig. Iddio is a transparent MITM proxy — agents set HTTPS_PROXY and present no identity to the proxy. All authentication to the real upstream uses the operator's existing credentials.

policy.yaml

Defines protocol-keyed policy rules. Hot-reloaded by the daemon via fsnotify — changes take effect within a second without dropping in-flight requests. If the reload fails (invalid YAML), the daemon logs the error and enforces the last-good policy.

Top-level structure

<protocol>:
  inspect: [<hostname-glob>, <ip>, <cidr>, ...] # Hosts to MITM-decrypt
  default: escalate # Fallback action
  groups: # Named tier groups
    <group-name>:
      tiers: [<tier>, ...]
      # or:
      commands: [<command-string>, ...]
  namespaces: # (kubectl only) scope-sugar; first-match-wins
    - names: [<glob>, ...]
      rules:
        <action>:
          groups: [<group>, ...]
          commands: [<command>, ...]
  projects: # (gcloud only) scope-sugar
    - names: [...]
      rules: ...
  apps: # (fly only) scope-sugar
    - names: [...]
      rules: ...
  rules: # Flat fallback — consulted after scope-sugar blocks
    <action>:
      groups: [<group>, ...]

Valid actions: allow, escalate, deny.

Valid tiers: observe, modify, sensitive, break-glass.

Scope-sugar blocks (namespaces:, projects:, apps:) are first-match-wins on names: globs. The flat rules: block is consulted only when no scope-sugar entry matched.

protocol keys

KeyCovers
kubectlKubernetes API traffic (kubectl, kustomize)
gcloudGoogle Cloud API traffic (gcloud CLI)
flyFly.io API traffic (flyctl, fly)
awsAWS API traffic (aws CLI)
githubGitHub API traffic (gh CLI)
cloudflareCloudflare API traffic (wrangler, flarectl)
genericAny other host — tiered by HTTP method only

Example: kubectl with namespace scoping

kubectl:
  inspect:
    - "*.eks.amazonaws.com"
    - "*.gke.goog"
    - "*.azmk8s.io"

  default: escalate

  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    delete:
      tiers: [sensitive]
    admin:
      tiers: [break-glass]
    exec:
      commands: [exec, attach, port-forward, cp, debug, proxy]

  namespaces:
    - names: [prod-*, production]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, delete, admin]
        deny:
          groups: [exec]

    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, delete, admin]
        deny:
          groups: [exec]

  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, delete, admin]
    deny:
      groups: [exec]

Example: generic block for arbitrary hosts

generic:
  inspect:
    - "api.internal.example.com"
  default: escalate
  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    destructive:
      tiers: [sensitive]
    admin:
      tiers: [break-glass]
  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, destructive, admin]

settings.yaml

Daemon settings. Managed via iddio settings get|set. The file is hot-reloaded automatically on write.

SettingDescription
proxy_listenAddress and port the proxy listens on
log_levelLogging verbosity
default_modeDefault proxy mode at startup
telemetry_opt_inWhether anonymous telemetry is enabled

Environment variables (shell-init snippet)

The iddio shell-init snippet exports the following when the daemon is up:

VariableValuePurpose
HTTPS_PROXYhttp://127.0.0.1:9443Routes HTTPS traffic through the iddio proxy
HTTP_PROXYhttp://127.0.0.1:9443Routes HTTP traffic
SSL_CERT_FILE$HOME/.iddio/ca-bundle.pemSystem roots + dev CA for Go/C toolchains
REQUESTS_CA_BUNDLE$HOME/.iddio/ca-bundle.pemPython requests library
NODE_EXTRA_CA_CERTS$HOME/.iddio/ca-bundle.pemNode.js
AWS_CA_BUNDLE$HOME/.iddio/ca-bundle.pemAWS CLI
GIT_SSL_CAINFO$HOME/.iddio/ca-bundle.pemgit

The proxy address defaults to http://127.0.0.1:9443. Override with iddio shell-init --listen-addr <addr>.

Common issues and their solutions.

Daemon is not running

iddio status
# Error: dial unix /Users/you/.iddio/iddio.sock: connect: no such file or directory

The daemon is not running. Start it:

# Reinstall (idempotent) and wait for the socket to appear
iddio install

# Or if already installed, kick the OS supervisor:
# macOS
launchctl kickstart -k gui/$(id -u)/dev.iddio.daemon

# Linux (per-user)
systemctl --user restart iddio-daemon

Run iddio doctor for a full diagnostic of permissions, the service unit, and the daemon process. Run iddio repair to fix anything doctor flags.

TLS certificate errors

Your CLI tool is rejecting the iddio dev CA certificate.

Check trust state:

iddio ca info
# trust: untrusted  ← needs fixing

macOS — re-trust the CA:

sudo iddio repair --trust-ca

Linux — add the CA to the system trust store:

# Debian / Ubuntu
sudo cp ~/.iddio/ca.crt /usr/local/share/ca-certificates/iddio-dev-ca.crt
sudo update-ca-certificates

# RHEL / Fedora
sudo cp ~/.iddio/ca.crt /etc/pki/ca-trust/source/anchors/iddio-dev-ca.crt
sudo update-ca-trust

Go-based tools and Python / Node / AWS CLI read the env vars set by iddio shell-init, not the system trust store. Ensure the snippet is sourced:

eval "$(iddio shell-init)"

This sets SSL_CERT_FILE, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS, AWS_CA_BUNDLE, and GIT_SSL_CAINFO to $HOME/.iddio/ca-bundle.pem (system roots merged with the iddio dev CA).

A CLI command hangs indefinitely

The request was classified as modify, sensitive, or break-glass and is waiting for approval. Check pending approvals:

iddio approval list

Approve or deny:

iddio approval allow <id>
iddio approval deny  <id>

If approval list is empty, the proxy mode may be set to observe or off rather than enforce, or the policy for that request is set to deny (which returns immediately, not hang). Verify:

iddio status
# proxy_mode    enforce   ← must be enforce for policy to apply

Policy changes are not taking effect

Policy is hot-reloaded via fsnotify. If a reload fails (invalid YAML), the daemon logs an error and enforces the last-good policy. Check daemon status for parse errors:

iddio status
# policy    loaded (operator policy.yaml failed to reload: ...)

Validate the candidate before saving:

iddio policy validate --path ~/.iddio/policy.yaml

Or view the policy the daemon is actually enforcing:

iddio policy show

Host is not being intercepted

The host is not in any inspect: list in policy.yaml, or the host is on the user pinned (never-MITM) list.

Check the inspect list:

iddio inspect summary

Check the pinned list:

iddio pinned

If the host appears in the pinned list and should not, remove it:

iddio unpin <host>

If the host appears in the detector pin cache (auto-detected cert-pinned hosts), it will splice through without MITM:

iddio detector list

HTTPS_PROXY is set but traffic is not going through iddio

The shell-init snippet sets HTTPS_PROXY only when the daemon is running. If you source the snippet before starting the daemon, the variable is unset. Re-source after the daemon starts:

iddio install
eval "$(iddio shell-init)"

Some tools ignore HTTPS_PROXY for localhost/127.0.0.1 targets by default. This is expected — traffic to the proxy listener itself should not loop through the proxy.

Commands are slow even for read operations

Classification and policy evaluation add sub-millisecond overhead. If reads are slow, the bottleneck is the upstream API server, not iddio. Verify by checking iddio is not in the critical path:

# Temporarily bypass (does not stop the daemon)
iddio set-proxy-mode passthrough
kubectl get pods   # if this is fast, upstream was fine all along
iddio set-proxy-mode enforce

iddio doctor reports failures

Run iddio repair to fix most issues automatically:

iddio repair

For macOS CA trust issues specifically:

sudo iddio repair --trust-ca

Pass --dry-run to see what repair would do without making changes.


Still stuck? Open an issue on GitHub or reach out via the Pricing page to talk to engineering.

!

Coming soon. Session recording is on the roadmap and not yet available in the shipping release. This page describes the intended capability; commands and configuration are not final.

Session recording will capture the full byte stream of interactive kubectl exec, kubectl attach, and kubectl port-forward sessions as the proxy intercepts the HTTP upgrade. Each session will be stored as a JSONL file and linked to the corresponding audit log entry, giving operators a forensic-quality replay trail of what an AI agent sent and received during a live container session.

When a policy rule returns escalate, Iddio blocks the agent's HTTP connection and waits for a human operator to approve or deny. Approvals are handled by an in-process IPC broker inside the daemon.

How approvals work

01
ESCALATE

The daemon's policy engine returns escalate. The proxy goroutine submits the request to the approval broker and blocks the agent's connection.

02
NOTIFY

The broker mints an approval ID and publishes an approval_requested IPC event. The desktop app shows a banner; the CLI can list pending approvals.

03
DECIDE

The operator approves or denies via the desktop app or the iddio approval CLI commands.

04
RESUME

The broker receives the verdict and unblocks the proxy goroutine. The agent's connection is either forwarded to the upstream or returned a 403 — no retry or polling needed by the agent.

The default timeout is 30 seconds. If no decision is made within the timeout window, the request is automatically denied.

Desktop app

When the desktop app is running it shows a banner for each pending escalation. Click Allow or Deny to resolve it. The agent's blocked connection is released immediately.

CLI commands

# List pending approvals
iddio approval list

# Approve a specific request
iddio approval allow <id>

# Deny a specific request
iddio approval deny <id>

# Emit pending approvals as JSON (useful for scripts)
iddio approval list --json

# Include an operator-supplied reason in the audit record
iddio approval allow <id> --reason "scheduled maintenance window"
iddio approval deny <id> --reason "not expected at this time"

The <id> value is the approval ID shown by iddio approval list (format: appr_<8 hex chars>).

Watching for approvals

# Stream approval events in real time
iddio approval watch

iddio approval watch streams approval_requested and approval_replied IPC events as they arrive, useful for scripting or monitoring in a terminal.

!

There is one approval broker per daemon process. All approvals share the same 30-second timeout window. Multiple pending approvals can be queued simultaneously — each is independently resolvable.

!

There are no webhook, Slack, PagerDuty, or HMAC callback approval modes in the current release. All approvals go through the desktop app or the iddio approval CLI. External integrations are on the roadmap.

!

Coming soon. JIT credentials are on the roadmap and not yet available in the shipping release. This page describes the intended capability; commands and configuration are not final.

JIT (Just-In-Time) credential support will allow iddio to mint short-lived ServiceAccount tokens via the Kubernetes TokenRequest API instead of relying on a long-lived static kubeconfig. Tokens will be cached in memory, renewed automatically before expiry, and injected transparently into upstream requests — reducing the blast radius of a credential compromise to the token's TTL rather than the lifetime of the kubeconfig file.

Iddio runs as a local daemon on the operator's machine. The desktop app (macOS) and the CLI (iddio) are the two supported deployment surfaces in the current release.

Desktop app (macOS)

A native macOS app with a visual policy editor, approval banners, and audit log viewer.

Homebrew Cask:

brew install --cask leonardaustin/tap/iddio-desktop

DMG / pkg download — grab the latest release from GitHub Releases and drag Iddio Desktop to Applications.

The desktop app manages the daemon lifecycle. Launch the app and it starts the daemon automatically.

CLI (macOS / Linux)

A single binary. Lightweight and scriptable.

Homebrew:

brew install leonardaustin/tap/iddio

Linux (.deb / .AppImage): download from GitHub Releases.

First-run setup

# Install the daemon service (launchd / systemd)
iddio install

# Verify the daemon is running
iddio status

# Run diagnostics
iddio doctor

Starting / stopping the gateway

# Run the daemon in the foreground (debugging — `iddio install`
# registers it as a service for normal use)
iddio gateway

# Check status
iddio status
!

iddio install registers a launchd (macOS) or systemd (Linux) service so the daemon starts automatically at login. Use iddio uninstall to remove it.

Updating

iddio upgrade          # download and apply the latest release
iddio upgrade --check  # just check whether a newer release exists

# Or drive the daemon's self-update state machine directly:
iddio update check     # force an immediate check against GitHub Releases
iddio update status    # show the current update state
iddio update apply     # apply the currently-staged update and restart

Policy file location

The policy YAML is at ~/.iddio/policy.yaml on both platforms. Edit it in the desktop app's built-in editor or in any text editor — the daemon hot-reloads it automatically.

See Policy Configuration for the full schema.

Enterprise control plane — coming soon

!

Coming soon. A multi-user enterprise control plane (centralised policy management, OIDC/SSO, RBAC, team audit log) is on the roadmap. The current release is local-only: one daemon per developer machine, one policy file per user.

The planned enterprise tier will include:

  • Centralised policy management and version control
  • OIDC / SSO authentication
  • Role-based access control
  • Centralised audit log forwarding

No commands, configuration, or server addresses are final yet.

Iddio's policy engine supports rules scoped to multiple infrastructure protocols. A single ~/.iddio/policy.yaml file governs agent access across different systems.

Supported protocols

ProtocolClassifierScope keys
kubectlKubernetes API operationsnamespaces
gcloudGoogle Cloud CLI / googleapis.comprojects, regions, zones, orgs
flyFly.io CLI / api.fly.ioapps, orgs
awsAWS CLI operations(inspect-only; no scope-sugar)
githubGitHub CLI (gh)(inspect-only; no scope-sugar)
terraformTerraform operations(inspect-only; no scope-sugar)
helmHelm operations(inspect-only; no scope-sugar)
cloudflareCloudflare CLI(inspect-only; no scope-sugar)
genericAny HTTPS host, tiered by HTTP methodnone (flat rules: only)
!

SSH is not yet supported. SSH interception is on the roadmap; there is no ssh: policy block in the current release.

Policy format

The top level of policy.yaml is keyed by protocol name. Each block follows the same structure: inspect: (hosts to MITM), default: (fallback action), groups: (named tier/command selectors), optional scope-sugar lists, and a flat rules: fallback.

kubectl:
  inspect:
    - "*.eks.amazonaws.com"
    - "*.gke.goog"
    - "*.azmk8s.io"
  default: escalate
  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    delete:
      tiers: [sensitive]
    admin:
      tiers: [break-glass]
  namespaces:
    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, delete, admin]
  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, delete, admin]

gcloud:
  inspect:
    - "*.googleapis.com"
  default: escalate
  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    destructive:
      tiers: [sensitive]
    iam:
      tiers: [break-glass]
  projects:
    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, destructive, iam]
  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, destructive, iam]

fly:
  inspect:
    - "api.fly.io"
    - "api.machines.dev"
    - "*.fly.dev"
  default: escalate
  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    destructive:
      tiers: [sensitive]
    admin:
      tiers: [break-glass]
  apps:
    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, destructive, admin]
  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, destructive, admin]

Scope-sugar blocks

Scope-sugar lists filter rules to a specific project, namespace, app, or other label extracted from the request. Blocks are first-match-wins down the list.

kubectl — namespaces

kubectl:
  ...
  namespaces:
    - names: [prod-*, production]
      rules:
        escalate:
          groups: [mutate, delete, admin]
        allow:
          groups: [read]
        deny:
          groups: [exec]
    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, delete, admin]

gcloud — projects, regions, zones, orgs

gcloud:
  ...
  projects:
    - names: [my-prod-project]
      rules:
        escalate:
          groups: [mutate, destructive, iam]
        allow:
          groups: [read]
    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, destructive, iam]

fly — apps, orgs

fly:
  ...
  apps:
    - names: [my-prod-app]
      rules:
        escalate:
          groups: [mutate, destructive, admin]
        allow:
          groups: [read]
    - names: ["*"]
      rules:
        allow:
          groups: [read]
        escalate:
          groups: [mutate, destructive, admin]

generic: — arbitrary HTTPS hosts

The generic: block tiers requests from the HTTP method alone and applies to any host listed under inspect:. The wildcard * is legal under generic.inspect: only.

generic:
  inspect: [] # add hosts here; ships empty so nothing is MITMd by default
  default: escalate
  groups:
    read:
      tiers: [observe]
    mutate:
      tiers: [modify]
    destructive:
      tiers: [sensitive]
    admin:
      tiers: [break-glass]
  rules:
    allow:
      groups: [read]
    escalate:
      groups: [mutate, destructive, admin]

Glob patterns

All names: and inspect: values support glob patterns:

PatternMatches
"payments"Exact match
"staging-*"Any name starting with staging-
"*"Everything (catch-all)

SSH — coming soon

!

Coming soon. SSH interception is on the roadmap and not yet available in the shipping release. There is no ssh: policy block; adding one has no effect. This page will be updated when SSH support ships.

SSH gateway support will intercept SSH connections and apply tier-based policy to shell sessions and SCP transfers. No configuration is available yet.

Validation

The policy engine rejects documents that reference unknown groups, contain empty rules: branches, or use * under a non-generic inspect: list. Glob patterns are compiled at load time; malformed patterns are a load error.

!

If a protocol block fails validation the entire policy reload is rejected. The daemon logs the error and keeps the previous working policy — no requests are dropped.

!

Coming soon. The enterprise control plane is on the roadmap and not yet available in the shipping release. This page describes the intended capability; commands and configuration are not final.

The enterprise control plane is a planned multi-tenant server that will manage fleets of iddio daemons across an organisation. Intended capabilities include PostgreSQL-backed storage, OIDC-authenticated operator access, role-based access control, centralised policy management with version history, centralised audit aggregation, and a REST API for programmatic management. Each tenant would receive isolated storage and subdomain routing. The server/ module has been removed from the current codebase; this work is roadmap-only.

!

Coming soon. The MCP gateway is on the roadmap and not yet available in the shipping release. This page describes the intended capability; commands and configuration are not final.

The MCP gateway will allow iddio to act as a policy-enforced intermediary between AI agents and MCP (Model Context Protocol) tool servers. Planned capabilities include classifying each tool call into a risk tier, evaluating calls against the YAML policy, blocking for human approval when required, and writing every invocation to the audit log — using the same classify-enforce-audit pipeline that today governs kubectl, aws, gcloud, and the other supported CLIs.

Need help?

Can't find what you're looking for? Reach out to the engineering team.