Skip to content

Releases: cadi-sh/cadish

cadish v0.2.3

Choose a tag to compare

@github-actions github-actions released this 02 Jul 19:56

Changelog

Others

cadish v0.2.2

Choose a tag to compare

@github-actions github-actions released this 30 Jun 20:17

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 Host and 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 client Host + resolved client IP on the peer hop (and overwrites any client-forged X-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: a pass goes 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) and query NAME VALUE matchers 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_age deliver special and {device} / {geo*} / {query.NAME} template tokens — reproduces the original worker's X-CF-Cache-Age / X-CF-Vary-* debug headers, cache-safe by construction (a class token forwarded to origin renders empty unless the selected cache_key recipe 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 exclusionsedge { bypass_passes } (auto-derived) and edge { bypass PATTERN… } (operator-declared): paths the edge only ever passes are projected as CF routes that run no worker. Plus an X-Cadish-Edge response 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-existing cache_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 $N capture in a redirect target 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

Choose a tag to compare

@github-actions github-actions released this 27 Jun 09:34

[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 varyclassify {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
    explicit derives_from cookie NAME… forward (alias keep) 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 loud cookie-forward-uncollapsed
    warning flags the opt-in.
  • WebSocket / Upgrade passthroughupgrade @scope opts a route into end-to-end
    Connection: Upgrade tunnelling (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 controltls_insecure (skip origin certificate verification),
    ca_file <pem> (verify against a private CA), and alpn (pin origin
    ALPN). Default stays fully verifying; tls_insecure warns at cadish check; each upstream
    is isolated (one insecure origin never relaxes verification for another).
  • Origin-header-driven gracecache_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 with respond for an L4/DNS health endpoint that returns
    200/503 by pool liveness.
  • Configurable DNS resolution — per-upstream resolve [<interval>] [nameserver <ip:port>…]
    for dns:// 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
    redirect Location (the target host stays the validated host — the open-redirect defense
    is preserved). A redirect @scope PATH_REGEX CODE TARGET form combines a matcher scope
    and $N path-regex captures in one rule (e.g. language-conditioned bidirectional slug
    translation, couples↔parejas).
  • {proto} / {scheme} template tokenhttps when cadish terminated TLS, else http
    (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 denylistcache_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 of query_allow (mutually exclusive with it).
  • Response-header-scoped TTLcache_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 revalidationclient_cache_control ignore makes a site not
    honor a request Cache-Control: no-cache/max-age=0 (or Pragma: 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 requestscache_credentialed @scope
    opts a scope out of the safe-default credential bypass. Normally a request carrying a
    Cookie/Authorization the 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-scope cache_ttl … from_header NAME firing, or an origin max-age/
    s-maxage) — a static blanket TTL never authorizes it. When the signal fires, the response
    is stored under the shared key with Set-Cookie and the weak control headers
    (no-store/private/no-cache, Pragma, a past Expires) 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. A strip_cookies
    rule over the same scope is a compile error, cache_unsafe cannot open an alternate store
    path, and a cache-credentialed-origin-trust check warning flags the origin-trust
    requirement. Mirrored at the edge (fail-closed when the scope cannot be projected).
  • Reserved /.cadish/readyz warm-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
    :80 even in TLS-redirect mode (exempt from the HTTP→HTTPS redirect), so a Kubernetes
    httpGet readiness 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 both cadish check
    and cadish 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
    (previously cookie_allow/derives_from stripped them before the pass/cache decision,
    breaking auth/session on every passed 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_header TTL/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://host upstream 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 check also warns (tls-cert-uncovered-address)
    when a static cert does not cover every declared address.

Security

  • The sandboxed /api/validate admin path never reads files from disk: ca_file is
    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

Choose a tag to compare

@github-actions github-actions released this 25 Jun 21:37

Changelog

Others