A consent-driven network hardening daemon for small and home networks. codd watches your local traffic, lets a language model decide whether a candidate action is warranted, and requires a biometric passkey approval on your phone before any dangerous action runs.
codd is split across two hosts on your Tailnet. The seer runs on one device, watches the network, asks a language model a single yes/no question about each candidate action, and ships signed proposals to the executer. The executer runs on another device, verifies the signature against a pinned seer public key, enforces a hardcoded policy floor, pages your phone, and only executes once a WebAuthn passkey assertion comes back.
The split is deliberate. Compromise of the seer leaks network visibility but cannot execute anything — the signing key alone is worthless without the executer's floor and the phone. Compromise of the executer cannot forge a new proposal, because the signing key lives on a different host, and cannot bypass the phone ceremony, because assertions are bound to the exact action hash. Neither device holds enough authority to harm the network alone.
- Two-host architecture: seer and executer run on separate Tailnet nodes, communicate over HTTPS, and fail closed if either side is unreachable.
- Ed25519 signed proposals: every proposal is signed over
domain || action_hash || nonce || timestamp, bound to a pinned seer public key, rejected on stale timestamp, and replay-checked against an in-memory nonce cache on the executer. - LLM-gated proposals: the seer uses a language model to produce a binary yes/no verdict plus a short rationale. The brain runs on the seer only — the executer never calls an LLM.
- Hardcoded policy floor: a fixed set of action kinds (disable codd, modify firewall, enroll/revoke credentials, wipe logs, exec arbitrary commands) always require phone approval, regardless of what the seer says. The floor can only escalate a seer advisory; it can never demote one.
- Passkey approval on every dangerous action: WebAuthn over Tailscale HTTPS, Face ID plus iCloud Keychain, no custom mobile app.
- Append-only audit log: every proposal, decision, approval, and execution is written to SQLite on the executer.
- Passive by default: the seer observes via
conntrack, the executer applies nftables rules. codd is never in the data path.
# From source
git clone https://github.com/plyght/codd.git
cd codd
cargo build --release --workspaceBinaries produced:
| Binary | Runs on | Role |
|---|---|---|
coddd |
Executer (Linux) | Daemon: policy floor, WebAuthn RP, audit log, exec |
codd |
Executer (Linux) | Operator CLI over HTTP to coddd |
codd-seer |
Seer | Signs proposals, calls the brain, POSTs to the executer |
The executer currently requires Linux because codd-exec shells out to nft. A backend trait for macOS pf or remote nftables over SSH is planned but not in this release.
| Device | Role | Why |
|---|---|---|
| Raspberry Pi (Linux) | Executer (coddd) |
Native nftables, always-on, disposable, holds the audit log and WebAuthn RP |
| Mac mini | Seer (codd-seer) |
Holds the Ed25519 signing key and the Anthropic API key on a separate host from the executer |
| iPhone | Trusted root | Face ID + iCloud Keychain passkey; the only device that can authorize a dangerous action |
All three devices must be on the same Tailnet with MagicDNS and tailscale cert enabled. iOS passkey ceremonies require a real Let's Encrypt cert on a stable hostname, which Tailscale provides.
On the executer:
# Show daemon status (mode, uptime, counts) — run this first
codd status
# Enroll your phone
codd enroll --label "my iphone"
# List enrolled credentials and pending approvals
codd list
codd pending
# Tail the audit log (-n N, --since 1h, --json)
codd audit -n 20
# Interactive menu (cliclack-powered)
codd tui
# Shell completions
codd completions fish > ~/.config/fish/completions/codd.fishBare codd with no subcommand prints a bun-style help panel. --insecure requires CODD_INSECURE=1 in the environment as a two-step gate so it can't be a muscle-memory mistake in a script.
On the seer:
# Generate a signing key. Prints the base64 pubkey to stdout — paste it
# into /etc/coddd.toml under [seer].allowed_pubkeys on the executer.
codd-seer keygen --out /var/lib/codd-seer/seer.key
# Run as a daemon: start conntrack, score each event via the brain, and
# forward any event whose risk >= auto_propose_threshold as a signed
# AutonomousDecision proposal. This is what you'd put in the systemd unit.
codd-seer run
# One-shot: sign a single action and POST it. Useful for smoke tests.
codd-seer propose /tmp/demo-action.json
# Bypass the brain entirely for a smoke test
codd-seer propose /tmp/demo-action.json --force-verdict yesWhen the executer accepts a proposal, it runs through the policy floor, then pushes an ntfy notification to your phone. Tap it, Safari opens /approve/:id, Face ID authorizes a passkey assertion bound to the action hash, and the executer applies the action. The full ceremony is logged.
Each host has its own TOML file. The executer reads /etc/coddd.toml:
bind = "127.0.0.1:8080"
db_path = "/var/lib/coddd/codd.sqlite"
rp_id = "codd.tail1234.ts.net"
rp_origin = "https://codd.tail1234.ts.net"
rp_name = "codd"
public_base = "https://codd.tail1234.ts.net"
dry_run = true
enable_sensor = false
learned_threshold = 5
[exec]
# "local" shells out to nft on this host (default).
# "ssh" runs nft on a remote Linux host via ssh — use this when the
# executer runs on a Mac mini and the firewall lives on the Pi.
backend = "local"
table = "codd"
quarantine_set = "quarantine"
# [exec.ssh]
# destination = "pi@raspi.tail1234.ts.net"
# use_sudo = true
# extra_args = ["-o", "BatchMode=yes"]
[ntfy]
base = "https://ntfy.example.com"
topic = "codd-approvals"
[seer]
# Base64-nopad Ed25519 pubkeys of every seer allowed to POST /propose.
# Get this from `codd-seer keygen` on the seer device.
allowed_pubkeys = [
"REPLACE_ME_WITH_SEER_PUBKEY",
]The seer reads /etc/codd-seer.toml:
executer_url = "https://codd.tail1234.ts.net"
signing_key_path = "/var/lib/codd-seer/seer.key"
api_key_env = "ANTHROPIC_API_KEY"
model = "claude-haiku-4-5-20251001"
enable_sensor = true
auto_propose_threshold = 70
min_interval_secs = 30
insecure_tls = falseThe Anthropic key goes in /etc/codd-seer.env as ANTHROPIC_API_KEY=sk-ant-.... Start with dry_run = true on the executer until the consent loop is verified end to end.
seer (Mac mini) executer (Raspberry Pi) phone
--------------- ----------------------- -----
candidate action
|
v
brain (LLM yes/no)
|
v
sign(action || advisory || nonce || ts)
|
| POST /propose (Tailscale HTTPS)
v
verify signature
pin pubkey
reject stale/replay
|
v
HardcodedFloor
|
v
LearnedAllowlist
|
v ntfy
start_approval ---------------> push
|
v
Safari opens
/approve/:id
|
v
Face ID
|
assertion <----------------------'
|
v
codd-exec runs
(nft add rule ... / element ...)
|
v
audit_log row
A SignedProposal is:
{
"action": { "kind": "modify_firewall", "rule": {...}, "op": "add" },
"advisory": { "risk": 55, "rationale": "...", "verdict": "yes" },
"nonce": "c7f2...",
"issued_at": "2026-04-12T21:03:17Z",
"seer_pubkey_b64": "GPI3...",
"signature_b64": "zX9k..."
}The signature covers b"codd/proposal/v1\0" || sha256(canonical_action_json) || len(advisory_json) || advisory_json || nonce || issued_at || pubkey. The domain separator keeps the signature unusable in any other context. Tamper with any field and the signature fails; replay the same nonce and the executer rejects it; delay more than five minutes and the executer rejects it.
- Seer (Mac mini) — semi-trusted. Holds the signing key and the Anthropic API key. May produce proposals but cannot execute. Compromise impact: attacker can spam proposals and poison what the brain sees, but every dangerous action still requires the executer's floor and the phone's assertion.
- Executer (Pi) — semi-trusted. Holds the WebAuthn RP, SQLite audit log, and nftables capability. Compromise impact: attacker can directly run
nfton the Pi (this is accepted and recorded only as an off-book deviation), but cannot forge proposals (no signing key) and cannot bypass the phone ceremony (assertions are bound to specific action hashes). - Phone (iPhone) — trusted root. Hardware-backed Face ID plus iCloud Keychain. The only device that can authorize a dangerous action. Compromise impact: game over; explicitly out of scope.
codd-core— shared types.PrivilegedAction, canonical action hashing,Decision, and theproposalmodule that definesProposal,SignedProposal,SeerKey, andSeerVerifier.codd-sensor— passiveconntrackreader. Linux-only. Runs on the seer host if you want it.codd-brain— Anthropic Messages API client. Runs on the seer only.codd-policy—HardcodedFloorplusLearnedAllowlist. Runs on the executer. Can only escalate, never demote.codd-auth— WebAuthn relying party viawebauthn-rs. SQLite store for credentials and pending approvals.codd-notify—Notifiertrait plus ntfy implementation.codd-exec— applies approved actions. Shell-outs tonfton Linux. Backend abstraction overpfor remote SSH is a follow-up.coddd— the executer daemon. Axum HTTP server, approval broker, audit log,/simulateand/proposeendpoints.codd— operator CLI. Talks HTTP tocoddd.codd-seer— the seer binary.keygenandproposesubcommands.
cargo build --workspace
cargo test --workspaceKey tests:
codd-core::action— canonical action hashes are stable and differ per variant.codd-core::proposal— signed proposals roundtrip, an unknown seer is rejected, a tampered action breaks the signature, and stale timestamps are refused.codd-policy— the hardcoded floor cannot be bypassed by an advisory that says "auto allow"; the learned allowlist will not auto-allow floor action kinds even after prior approvals.
Requires Rust 1.80+. Key dependencies: tokio, axum, webauthn-rs, ed25519-dalek, rusqlite, reqwest, clap, tracing.
- Phone app — and ideally never.
codd-execbackend for macOSpf. The two supported backends are local nftables and remote nftables over SSH, so the executer process can live on any host that can reach a Linux box withnft.- Rule-driven action proposals. The seer currently wraps every above-threshold event in
AutonomousDecision, which is advisory-only — it pages your phone but cannot actually modify the firewall. Turning a sensor event into a concrete firewall proposal is a planned follow-up. - Inline or in-path mode — codd is passive in v0.
- Learning beyond frequency counting.
- Multi-operator or RBAC.
- Clustering or HA.
MIT OR Apache-2.0.