From 2d38418e6e3100486fdb152e5780808a515ba41f Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 11 May 2026 13:06:48 -0700 Subject: [PATCH] C1: close forge package leak at the fly edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit forge.eblu.me's package registry (/api/packages/* and /api/v1/packages/*) served anonymous reads to the world even for private-repo releases — Forgejo's per-user visibility treats packages as world-readable when the owner's Visibility is Public, and we keep eblume Public so the profile page stays open. The sdist downloads include full source trees of private repos; that's the leak. The fix is to keep the user public but block /api/packages/* and /api/v1/packages/* at the proxy edge. forge.ops.eblu.me (tailnet) is untouched, so CI workflows + gilbert's uv + the nix-container-builder still work — they just need to use the tailnet hostname. Three consumers updated to forge.ops.eblu.me: - containers/shower/default.nix (the FOD pip --extra-index-url) - ansible/roles/cv/defaults/main.yml (cv_release_url for generic package) - chezmoi-tracked fish dotfiles (devpi.fish + conf.d/pypi.fish) — edited in chezmoi source, user will apply separately The blumeops repo had no other forge-pypi consumers (audited: workers, runner-job-image, ansible roles, container builds). Doc references in changelog fragments + comments left as-is — they describe history. The proper long-term fix is to move private packages to a Limited- visibility Forgejo org instead of relying on a proxy-side block (see queued Todoist for the migration plan). Edge block stays as defense in depth. Co-Authored-By: Claude Opus 4.7 (1M context) --- ansible/roles/cv/defaults/main.yml | 2 +- containers/shower/default.nix | 2 +- fly/nginx.conf | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ansible/roles/cv/defaults/main.yml b/ansible/roles/cv/defaults/main.yml index 734e52b..a18cc82 100644 --- a/ansible/roles/cv/defaults/main.yml +++ b/ansible/roles/cv/defaults/main.yml @@ -3,7 +3,7 @@ # Caddy serves cv_content_dir directly via the static-kind service block. cv_version: "v1.0.3" -cv_release_url: "https://forge.eblu.me/api/packages/eblume/generic/cv/{{ cv_version }}/cv-{{ cv_version }}.tar.gz" +cv_release_url: "https://forge.ops.eblu.me/api/packages/eblume/generic/cv/{{ cv_version }}/cv-{{ cv_version }}.tar.gz" cv_home: /Users/erichblume/blumeops/cv cv_content_dir: "{{ cv_home }}/content" diff --git a/containers/shower/default.nix b/containers/shower/default.nix index c968a7b..1b12649 100644 --- a/containers/shower/default.nix +++ b/containers/shower/default.nix @@ -49,7 +49,7 @@ let "$TMPDIR/venv/bin/pip" install \ --no-cache-dir \ --index-url=https://pypi.ops.eblu.me/root/pypi/+simple/ \ - --extra-index-url=https://forge.eblu.me/api/packages/eblume/pypi/simple/ \ + --extra-index-url=https://forge.ops.eblu.me/api/packages/eblume/pypi/simple/ \ "adelaide-baby-shower-app==${version}" \ gunicorn diff --git a/fly/nginx.conf b/fly/nginx.conf index 7a70167..089971c 100644 --- a/fly/nginx.conf +++ b/fly/nginx.conf @@ -184,6 +184,23 @@ http { return 200 "User-agent: *\nDisallow: /mirrors/\nDisallow: /user/\nDisallow: /users/\nDisallow: /*/archive/\nDisallow: /*/releases/download/\n"; } + # Block the package registry at the public edge. Forgejo's per-user + # visibility model treats packages as world-readable when the owner + # has Visibility=Public — which means anyone on the internet can + # enumerate and download every wheel/sdist/generic artifact, even + # for private-repo releases (the sdist contains full source). We + # like keeping eblume's profile public, so we close the hole here + # at the proxy instead: WAN sees 403, tailnet (forge.ops.eblu.me) + # stays open for legitimate consumers (CI workflows, gilbert). + # See docs/tutorials/expose-service-publicly.md for the broader + # threat model on this proxy. + location /api/packages/ { + return 403 "Package downloads are tailnet-only — use forge.ops.eblu.me.\n"; + } + location /api/v1/packages { + return 403 "Package enumeration is tailnet-only — use forge.ops.eblu.me.\n"; + } + # Block swagger API docs — use forge.ops.eblu.me from tailnet location /swagger { return 403 "API documentation is only available at forge.ops.eblu.me (tailnet).\n";