Releases: cadi-sh/cadish
Release list
cadish v0.2.3
Changelog
Others
- 51da2ea: cadish v0.2.3 — clustering hardening, edge/cache-key correctness, perf & scale (@hectorteamsquad)
cadish v0.2.2
Clustering hardening + a Cadish Edge batch + cache-isolation/redirect security fixes. Every change is fail-closed and costs nothing when unused — a non-clustered, non-edge cadish on the request/response fast path is byte-for-byte unchanged. Go↔JS edge conformance is kept in lockstep (IR contract v6 → v9, additive fields only).
Highlights
Clustering — "one sharded cache", store-once restored
Surfaced by exercising a real 3-node cluster on staging (the in-process tests passed, but the binary behind a shared client hostname fetched every object from origin twice):
- Objects are cached once per region again. A non-owner reverse-proxying to the owning peer dropped the original client
Hostand client IP, so the owner keyed a proxied request differently from a direct one — storing each object twice and doubling origin load. cadish now forwards the clientHost+ resolved client IP on the peer hop (and overwrites any client-forgedX-Forwarded-For, closing a peer-hop spoof). [D103] - Correct client IP for owner-side ACL / rate-limit / geo decisions on proxied requests (same root cause). [D103]
read_through: apassgoes straight to origin (no peer hop), and writes (POST/PUT/…) reach origin intact instead of being sharded to a peer. [D102]- Self-dial guard is health-aware — a brief owner health flap can no longer route an object back to the local node and stall the coalescer.
All clustering changes are gated by the cluster { } block. A new 3-node single-location deployment guide is in deploy/README.md.
Cadish Edge — cache a workload it previously couldn't
On a high-cardinality media workload, cadish edge build went from 22 forced-pass directives (one forced-pass on a selecting directive fail-opens the whole site → the edge caches nothing) down to a small residual that correctly delegates to the server behind:
all(AND-composite) andquery NAME VALUEmatchers are now edge-native (were server-only → site-wide pass). A sub-term it can't honor still fails closed.query_present NAME+— per-name non-empty-value modifier (Varnish=[^&]+parity).redirect … no_store— marks a personalized redirect uncacheable.+cache_agedeliver special and{device}/{geo*}/{query.NAME}template tokens — reproduces the original worker'sX-CF-Cache-Age/X-CF-Vary-*debug headers, cache-safe by construction (a class token forwarded to origin renders empty unless the selectedcache_keyrecipe varies on that class).cadish edge deploy -origin passthrough— fetch-through mode for a multi-host origin in the same CF zone (fixes the canonicalizing-origin redirect loop).- Edge worker-route exclusions —
edge { bypass_passes }(auto-derived) andedge { bypass PATTERN… }(operator-declared): paths the edge only ever passes are projected as CF routes that run no worker. Plus anX-Cadish-Edgeresponse marker (its absence = served directly by origin). cadish edge deploy -allow-public-values— acknowledge non-secret literal cookie/header values past the deploy safety gate (does not relax the forced-pass gate).
Security
cache_credentialed: cross-user cache leak on a no-signal response — fixed (fail-closed). A credentialed in-scope response carrying no per-response signal, yet made cacheable by a co-existingcache_ttl default ttl N, was stored under the shared credential-free key — serving one user's private body to the next. The response phase now refuses unconditionally without the signal. Server and edge (both had the identical fail-open; present since 0.2.1).- Open redirect via a
$Ncapture in aredirecttarget authority — fixed. A request-sourced token in the Location authority (e.g.GET /index.php@evil.example.com/) could drive an off-origin redirect. A new runtime post-expansion authority assertion (server + edge, byte-identical) re-expands with request-sourced inputs neutralized and suppresses the redirect if the authority differs — also normalizing away embedded/trailing C0 control bytes before the check. - Admin
/api/config: absolute-path disclosure in diagnostic messages — fixed (message now scrubbed, not just position).
Full detail in CHANGELOG.md. Signed binaries + SBOMs are attached below; the multi-arch image is at ghcr.io/cadi-sh/cadish:0.2.2, and the Helm chart at oci://ghcr.io/cadi-sh/charts/cadish --version 0.2.2.
cadish v0.2.1
[0.2.1] — 2026-06-27
A batch of capabilities surfaced by consolidating a real multi-tier caching, TLS, and
load-balancing stack into a single cadish process. Every one is fail-closed and costs
nothing when unused — the request/response fast path is byte-for-byte unchanged unless you
reach for the feature. All are mirrored in the
Cloudflare-Workers edge tier where they apply (server-only ones are explicitly delegated),
with Go↔JS conformance kept in lockstep.
Added
- Normalized cookie vary —
classify {TOKEN} { derives_from cookie NAME… }derives a
low-cardinality cache axis from per-user cookies, then strips those cookies and keys by
the normalized axis (the classic cardinality collapse, e.g. 1.2M → ~64 variations). The strip is the single
load-bearing safety mechanism: the origin sees an anonymous request and a per-user body can
never land under the shared key. Any cookie you don't declare-and-key still bypasses. An
explicitderives_from cookie NAME… forward(aliaskeep) keys by the axis but forwards
the cookie to the origin unchanged (for backends that personalize from the raw cookie) —
covered only when its token is in the selected key recipe; a loudcookie-forward-uncollapsed
warning flags the opt-in. - WebSocket /
Upgradepassthrough —upgrade @scopeopts a route into end-to-end
Connection: Upgradetunnelling (socket.io, live, SSE-over-WS). Off the cache path;
reuses the routed upstream's health/sticky pick and transport; idle-timeout teardown; an
active-tunnel gauge (cadish_upgrades_active). - Per-upstream origin-TLS control —
tls_insecure(skip origin certificate verification),
ca_file <pem>(verify against a private CA), andalpn(pin origin
ALPN). Default stays fully verifying;tls_insecurewarns atcadish check; each upstream
is isolated (one insecure origin never relaxes verification for another). - Origin-header-driven grace —
cache_ttl … grace_from_header NAME/
max_stale_from_header NAME(the literal stays as the fallback). - Upstream-liveness probe — the
upstream_healthy NAME…matcher (true when a named pool
has a live backend); compose withrespondfor an L4/DNS health endpoint that returns
200/503 by pool liveness. - Configurable DNS resolution — per-upstream
resolve [<interval>] [nameserver <ip:port>…]
fordns://upstreams (custom nameserver + re-resolve interval). Custom and system resolver
answers are both filtered against link-local/cloud-metadata addresses. - Derived tokens in redirect targets —
{classify.*},{geo}/{geo.continent}/
{geo.region},{client_ip},{http.NAME}, and{proto}/{scheme}now resolve inside a
redirectLocation(the target host stays the validated host — the open-redirect defense
is preserved). Aredirect @scope PATH_REGEX CODE TARGETform combines a matcher scope
and$Npath-regex captures in one rule (e.g. language-conditioned bidirectional slug
translation,couples↔parejas). {proto}/{scheme}template token —httpswhen cadish terminated TLS, elsehttp
(e.g.header X-Forwarded-Proto {proto}).- Global inbound limits — a top-level
server { maxconn N; read_timeout D; idle_timeout D }
block (caps the inbound accept set; overrides the inbound timeouts). - Cache-key query denylist —
cache_key … query_strip NAME…: key on the whole query
minus a name/glob denylist (utm_* gclid …) so tracking params stop fragmenting the
cache. The dual ofquery_allow(mutually exclusive with it). - Response-header-scoped TTL —
cache_ttl resp_header NAME VALUE …branches freshness on
what the origin returned (e.g.X-Powered-By: Express), evaluated in the response phase. - Opt out of client-forced revalidation —
client_cache_control ignoremakes a site not
honor a requestCache-Control: no-cache/max-age=0(orPragma: no-cache), so a browser
hard-refresh can't bust the shared cache and hammer the origin. Default (absent) still
honors it per RFC 9111 §5.2.1.4. - Origin-authoritative caching of credentialed requests —
cache_credentialed @scope
opts a scope out of the safe-default credential bypass. Normally a request carrying a
Cookie/Authorizationthe key doesn't cover is never shared-cached; in this scope caching
becomes origin-authoritative. Storage is gated solely by a per-response origin cache
signal (an in-scopecache_ttl … from_header NAMEfiring, or an originmax-age/
s-maxage) — a static blanket TTL never authorizes it. When the signal fires, the response
is stored under the shared key withSet-Cookieand the weak control headers
(no-store/private/no-cache,Pragma, a pastExpires) stripped from both the
stored entry and the delivered response; no signal ⇒ never stored (fail-closed), so a
per-user route that omits the marker is never shared-cached. The original cookies are
forwarded to the origin for authentication but never enter the cache key. Astrip_cookies
rule over the same scope is a compile error,cache_unsafecannot open an alternate store
path, and acache-credentialed-origin-trustcheck warning flags the origin-trust
requirement. Mirrored at the edge (fail-closed when the scope cannot be projected). - Reserved
/.cadish/readyzwarm-readiness probe — a built-in readiness endpoint that
reports ready only after the ingress/gateway controller completes its first successful
reconcile, so a pod is not declared ready while still cold. It is served as 200/503 on plain
:80even in TLS-redirect mode (exempt from the HTTP→HTTPS redirect), so a Kubernetes
httpGetreadiness probe is never masked by a 301. Controller readiness probes use it,
fixing rollout 502/404 when a pod became routable before it was warm.
Changed
- A global-only block (
server,admin,proxy_protocol,strict_host,security,
access_log) placed inside a site body is now a positioned error in bothcadish check
andcadish run(previously silently ignored).
Fixed
- A legitimately duplicated forward-covered cookie (same value sent more than once — e.g. a
domain-scoped and a host-scoped copy of one cookie) no longer forces a cache bypass: it is
keyed by its derived axis, so identical occurrences cache normally. A differing-value
duplicate, or a duplicate of a raw-value-keyed cookie, still bypasses (the same fix applies
at the edge). - A passed/uncached request now forwards the client's original cookies to the origin
(previouslycookie_allow/derives_fromstripped them before the pass/cache decision,
breaking auth/session on everypassed per-user endpoint — a logged-in user read as GUEST).
Cacheable requests still normalize the cookie for the key; the same fix applies at the edge. - The origin control headers a
from_headerTTL/grace rule consumes (X-Cache-Ttl,
X-Cache-Grace,X-Cache-Max-Stale) are now stripped from the delivered response (and at
the edge) instead of leaking to the client. - A lone
to dns://hostupstream now loads (previously failed with a base-URL error). - Multi-line site address lists no longer silently drop all-but-the-last line — the parser
now reads the full list of comma-wrapped addresses, so a static TLS cert covers every
declared host (a comma-less wrap or a stray comma is a positioned parse error rather than a
silent truncation that broke SNI).cadish checkalso warns (tls-cert-uncovered-address)
when a static cert does not cover every declared address.
Security
- The sandboxed
/api/validateadmin path never reads files from disk:ca_fileis
structure-validated there and the PEM is loaded only at run time, preserving the
no-filesystem trust boundary (and a missing CA is a deploy-time warning, not a hard error). - The custom-nameserver and default DNS resolution paths both drop link-local and
cloud-metadata answers (incl. the AWS IPv6 IMDS endpoint).
cadish v0.2.0
Changelog
Others
- 24052c7: cadish v0.2.0 BETA — initial public release (Hector Ros hector@teamsquad.io)