Cache snap packages and assertions (snap-store reverse-relay proxy)
Goal
Cache .snap package downloads and snap assertions for a fleet of snapd
clients, the snap-store analog of what pkgproxy already does for apt
archives.
Why this needs a different shape than the apt proxy
The apt side is a forward proxy: apt keeps real mirror hostnames and is
pointed at pkgproxy via Acquire::http::Proxy, with CONNECT blindly
tunneled (uncached) for any https:// source — HTTPS content caching/MITM
is an explicit non-goal (docs/design.md §1).
The snap store (api.snapcraft.io + its download CDN) is HTTPS-only with
no plain-HTTP fallback anywhere in the protocol. A blind CONNECT tunnel
can't cache it, and TLS-terminating MITM was ruled out both on principle
(reverses the documented non-goal) and in practice — snapd is known to
detect and reject TLS interception on its store connection.
Decision
pkgproxy exposes its own explicit HTTP endpoint implementing a
relaying/caching subset of the Snap Store API v2, and snapd is pointed
directly at it (not tunneled) via the SNAPPY_FORCE_API_URL environment
override. This makes pkgproxy a genuine reverse-proxy/relay for snap
traffic (internal/snapproxy, built on httputil.ReverseProxy), a
different serving model from the apt forward proxy (internal/proxy), not
a variant of it.
Live-verified 2026-08-19 against this machine's real snapd (2.77,
series 16): SNAPPY_FORCE_API_URL does redirect all real snapd store
traffic — GET /v2/snaps/find, POST /v2/snaps/refresh, and
GET /v2/assertions/<type>/... fallback fetches — to an arbitrary plain
http:// URL, and a transparent relay to the real store round-trips
snap find and sudo snap refresh successfully end-to-end. This
de-risks the core mechanism the whole design depends on.
Design summary
- Cache:
.snapblobs (content-addressed bysha3-384, cache forever — the hash is known before the fetch, real CAS unlike apt's path-normalization compromise) and assertions (account-key/snap-revisionimmutable;account/snap-declaration/store/validation-setrevalidate with TTL, or immutable when pinned via?sequence=N— verify the mutability classification against live behavior during implementation, it's an assumption not a confirmed fact).GET /v2/snaps/info/<name>gets a short-TTL revalidate cache (no per-client context, same reasoning as apt'sdists/**). - Relay only, no caching:
POST /v2/snaps/refresh(per-clientcontext[], not cacheable across machines) andGET /v2/snaps/find(broad query surface, low repeat value). - Response rewriting: since pkgproxy is a real origin here (not
transparent), every relayed JSON response has its embedded
download.urlfields rewritten to/v2/download/sha3-384/<hex>so the client's blob fetch lands back on pkgproxy, andassertion-stream-urlsre-based to pkgproxy's own host.deltas[]is stripped so every download is a full-file fetch pkgproxy can see and cache. Decode asmap[string]anyand recurse structurally (not typed structs) so unknown fields and minor API drift survive untouched. - Persisted hash→origin-URL map: the rewritten download URL is opaque
(arbitrary CDN path, not derivable from the hash), so pkgproxy must
remember what it originally pointed at. Persisted through the existing
store.Storeinterface as a smallpending/sha3-384/<hex>record (no new persistence layer), not in-memory-only, so a client holding a previously rewritten URL across a pkgproxy restart still resolves. - New packages:
internal/snap(protocol constants + assertion mutability table, no typed request/response structs) andinternal/snapproxy(the handler). Reusesinternal/store/fslocalunchanged (new instance rooted at<cache-dir>/snap). New CLI flags:-snap-addr(default empty/disabled until this lands),-snap-store-url(defaulthttps://api.snapcraft.io),-snap-external-url,-snap-revalidate-ttl.
Explicit v1 scope boundaries
Private/authenticated snaps, find's full field surface, delta downloads,
the legacy /api/v1/snaps/assertions/... path, GC/eviction (already a
project-wide deferred non-goal), official Snap Store Proxy registration
(Canonical store-id + signed store assertion), and multi-origin/mirror-alias
handling (there's exactly one real store URL) are all out of scope.
Verification plan
- Unit tests for the rewrite walker, assertion mutability table, and hash-verify-then-commit path (no network).
httptest-based handler tests analogous tointernal/proxy/proxy_test.go.- Exploratory: point a real snapd at a running pkgproxy snap listener via
SNAPPY_FORCE_API_URL, runsnap find/install/refresh, confirm cache hits on repeat runs and successful assertion/trust verification. Real end-to-end snapd testing likely needs a VM-based harness (systemd, squashfs, seccomp) rather than a bare podman container, unlike the apt integration test.
Full design notes will land in docs/design.md as part of the same change
that implements this, per AGENTS.md.