From a08942ad1cef524424164232c64f3fb7b6b91619 Mon Sep 17 00:00:00 2001 From: Phil Calvin Date: Wed, 5 Aug 2026 15:01:02 +0000 Subject: [PATCH] Allow Docker Hub's CloudFront blob CDN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docker pull hello-world` in the guest fails: the manifest fetch succeeds, then the layer download is blocked with a proxy 418 and docker reports `error pulling image configuration: download failed after attempts=6`, which reads as a network fault rather than a missing allowlist rule. Docker Hub serves blobs from production.cloudfront.docker.com as well as the Cloudflare R2 bucket already allowed here, and picks per request, so allowing only one of them fails intermittently by design. Observed in .vm/blocked.jsonl as repeated GETs to production.cloudfront.docker.com/registry-v2/docker/registry/v2/blobs/... This is what was failing test_docker_hello_world on every branch — it is unrelated to the guest clock, and it fails in CI too, since CI uses this same allowlist. Adds a TestShippedAllowlist class that checks the real allowlist.txt against the URLs a pull actually requests. The existing filter tests all use synthetic rules, so nothing covered the shipped file, which is where upstream CDN drift shows up. Co-Authored-By: Claude Opus 5 (1M context) --- allowlist.txt | 8 ++++++-- tests/test_filter.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/allowlist.txt b/allowlist.txt index 731766d..4b1d7b0 100644 --- a/allowlist.txt +++ b/allowlist.txt @@ -70,9 +70,13 @@ GET https://registry-1.docker.io/v2/* # Auth tokens — the registry returns 401 with a token URL whose # query parameters vary per request (scope, service, etc.). GET https://auth.docker.io/token* -# Blob storage — the registry redirects layer downloads to this -# Cloudflare R2 bucket. Paths contain per-blob sha256 digests. +# Blob storage — the registry redirects layer downloads to a CDN. +# Paths contain per-blob sha256 digests, so they are wildcarded, but +# scoped to the /registry-v2/ prefix both CDNs serve them under. +# Docker Hub hands out both of these depending on the request; a pull +# fails with only one of them allowed. GET https://docker-images-prod.6aa30f8b08e16409b46e0173d6de2f56.r2.cloudflarestorage.com/registry-v2/* +GET https://production.cloudfront.docker.com/registry-v2/* # ── Debian cloud images (nested VM testing only) ────────────────── # Only needed when running the e2e test suite inside a VM (i.e. the diff --git a/tests/test_filter.py b/tests/test_filter.py index d53d58f..f31ae4b 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -12,6 +12,7 @@ import sys from pathlib import Path from unittest.mock import MagicMock +from urllib.parse import urlparse import pytest @@ -254,3 +255,41 @@ def test_allows_head_when_get_rule_matches(self, filter_env): flow = _make_flow("HEAD", "example.com", "https://example.com/page") addon.request(flow) assert flow.response is None # HEAD permitted by GET rule + + +# --------------------------------------------------------------------------- +# The shipped allowlist.txt (not synthetic rules) +# --------------------------------------------------------------------------- + +class TestShippedAllowlist: + """Checks the real allowlist.txt covers what the e2e suite needs. + + These are the URLs a `docker pull` actually requests. Docker Hub serves + blobs from more than one CDN and picks per request, so allowing only one of + them yields an intermittent-looking failure: the manifest fetch succeeds, + then the layer download dies with a proxy 418 and docker reports + `error pulling image configuration`, which reads as a network fault rather + than a missing allowlist rule. + """ + + @pytest.mark.parametrize("url", [ + # Registry API: manifests and config blobs. + "https://registry-1.docker.io/v2/library/hello-world/manifests/latest", + # Auth token for the anonymous pull. + "https://auth.docker.io/token?scope=repository%3Alibrary%2Fhello-world%3Apull", + # Blob CDNs — both are in play; paths carry per-blob sha256 digests. + "https://docker-images-prod.6aa30f8b08e16409b46e0173d6de2f56." + "r2.cloudflarestorage.com/registry-v2/docker/registry/v2/blobs/" + "sha256/58/58dee6a49ef1/data", + "https://production.cloudfront.docker.com/registry-v2/docker/registry/" + "v2/blobs/sha256/58/58dee6a49ef1/data", + ]) + def test_docker_pull_urls_allowed(self, url): + rules = fm.parse_allowlist(fm.ALLOWLIST_PATH) + host = urlparse(url).hostname + assert fm.is_allowed(rules, "GET", host, url), ( + f"allowlist.txt blocks a URL `docker pull hello-world` needs:\n" + f" {url}\n" + "test_docker_hello_world in the e2e suite will fail with a proxy " + "418 that looks like a network error." + )