Skip to content

plyght/codd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

codd

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.

Overview

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.

Features

  • 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.

Installation

# From source
git clone https://github.com/plyght/codd.git
cd codd
cargo build --release --workspace

Binaries 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.

Recommended hardware layout

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.

Usage

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.fish

Bare 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 yes

When 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.

Configuration

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 = false

The 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.

Architecture

  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

Wire protocol

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.

Trust zones

  • 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 nft on 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.

Crates

  • codd-core — shared types. PrivilegedAction, canonical action hashing, Decision, and the proposal module that defines Proposal, SignedProposal, SeerKey, and SeerVerifier.
  • codd-sensor — passive conntrack reader. Linux-only. Runs on the seer host if you want it.
  • codd-brain — Anthropic Messages API client. Runs on the seer only.
  • codd-policyHardcodedFloor plus LearnedAllowlist. Runs on the executer. Can only escalate, never demote.
  • codd-auth — WebAuthn relying party via webauthn-rs. SQLite store for credentials and pending approvals.
  • codd-notifyNotifier trait plus ntfy implementation.
  • codd-exec — applies approved actions. Shell-outs to nft on Linux. Backend abstraction over pf or remote SSH is a follow-up.
  • coddd — the executer daemon. Axum HTTP server, approval broker, audit log, /simulate and /propose endpoints.
  • codd — operator CLI. Talks HTTP to coddd.
  • codd-seer — the seer binary. keygen and propose subcommands.

Development

cargo build --workspace
cargo test  --workspace

Key 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.

What is explicitly not here in v0

  • Phone app — and ideally never.
  • codd-exec backend for macOS pf. 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 with nft.
  • 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.

License

MIT OR Apache-2.0.

About

codd (consent driven daemon)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages