Crenel is a zero-dependency CLI that keeps a split-horizon homelab honest.
One command exposes a service locally or publicly — across Caddy, AdGuard, and Cloudflare in one
atomic step — then re-reads the live edge to prove the change actually landed.
Nothing is reachable unless you explicitly opened it, and audit/drift can
prove that at any moment.
$ crenel expose photos --to immich:2283 \ --auth authelia # → proxy route on every edge, internal DNS # rewrite, public DNS record, auth gate — # previewed, applied atomically, then # VERIFIED by read-back. $ crenel status # exposed RIGHT NOW (live) $ crenel drift # non-zero if reality disagrees
Split-horizon on your own domain: clientless LAN devices (TVs, IoT, guests) resolving
app.your.domain to a local proxy at LAN speed, the same name resolving publicly
through a hardened edge, AdGuard or Pi-hole doing your filtering — and you owning every hop.
One logical change there touches four or five systems. Keeping them agreeing by hand is exactly where silent drift and accidental exposure live. Crenel makes it one verified command.
Remove Crenel tomorrow and your infrastructure keeps running untouched — it's a control plane over the tools you already trust, not another proxy or tunnel.
| By hand | IaC | Tunnel appliance | Mesh services | Crenel | |
|---|---|---|---|---|---|
| Reverse-proxy routes | manual | ✓ | ✓replaces yours | ✓its own | ✓drives your proxy |
| Public DNS records | manual | ✓ | wildcard once | manual CNAME | ✓ |
| Split-horizon LAN DNS clientless devices | manual | roll-your-own | ✕ | ✕mesh-only | ✓AdGuard / Pi-hole |
| Local ↔ public toggle per service | ✕ | ✕ | partial | ✓mesh = "local" | ✓ |
| Verified against live state | ✕ | ✕state file drifts | ✕ | ✕ | ✓read-back, every write |
| Default-deny proven, not assumed | ✕ | ✕ | ✕ | ✕ | ✓audit / drift |
| Keeps your existing stack | — | ✓ | ✕greenfield | ✕replaces it | ✓removable anytime |
Vendor-agnostic architecture (ports-and-adapters; a new driver is a contribution, not a rewrite) — but honesty about the implemented surface:
--allow-unverifiedmanaged-by:crenel — never touches records it didn't createAn exposure carries auth: authelia and Crenel renders the
per-edge reference — you own the auth config. Publishing a host public with no auth is
refused unless you say --auth none out loud.
One edge can serve hosts under several apex zones — zones: ["a.example", "b.example"],
one block per resolver box. Every host routes only to providers whose zone covers it; an ambiguous
bare name is refused with the candidate FQDNs.
A host that doesn't live at home gets a residency class (--residency vps) so each
internal resolver answers with its own vantage-correct address — a class no resolver covers is
refused at plan time, never silently defaulted.
Point crenel audit at the edge you already run — no settings file, read-only, nothing
written. Say the boundary out loud (--assume-public-boundary for an internet-facing edge,
--internal for a LAN-only one) and crenel reports what is exposed, whether default-deny is
structural, and where auth is missing:
$ crenel audit ./Caddyfile --assume-public-boundary $ crenel audit http://localhost:2019 --assume-public-boundary $ crenel audit /path/to/npm/data/nginx --assume-public-boundary $ crenel audit http://traefik:8080 --assume-public-boundary
A Caddyfile on disk, a running Caddy's admin API, an Nginx Proxy Manager data dir, or Traefik's API
(Pangolin and docker-labels setups included) — generator detected, routes enumerated, writes to foreign
generators refused. Only the URL you paste is ever contacted; anything beyond it is opt-in via
--probe. Foreign edges exit 0 when otherwise clean — cron it.
The claims that matter were exercised against real production edges: cross-edge atomic rollback, durable persist through container restarts, surgical writes on a shared production zone, a live Pi-hole v6 cycle — plus an independent third-party audit with no critical or high findings. The first live cross-chain write aborted atomically with zero changes applied when it surfaced a bug the fakes couldn't catch — which is the whole argument for read-back verification. Trial records on GitHub.
# stdlib only — zero deps $ go install github.com/crenelhq/crenel/cmd/crenel@latest
Or a static binary for linux/darwin (amd64/arm64) from Releases — verify by SHA256.
crenel mcp speaks the Model Context Protocol over stdio — read-only by
construction (a mutating call is unrepresentable, not merely refused); opt-in
--write adds a two-phase gated plan/apply pair with every CLI guardrail intact.
docs/MCP.md.
Marker-less AdGuard/Pi-hole value drift isn't detected (presence is verified; value-checking would cry wolf); path-granular routing is detected and declared, not modeled; Tailscale serve write support is unbuilt. Full current state.
A crenel (/ˈkrɛn.əl/, rhymes with kennel) is the gap in a castle's battlement — the deliberate opening you choose to expose. The wall is solid by default; you cut the gaps.