cloudflare / cloudflare/serverless-registry

Pull-through fallback fails with ghcr.io: token scope is double-prefixed and includes push permission

Open Beginner friendly
#148 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
1.5k
Forks
122
PR merge metrics
No merged PRs in 30d

Description

## Description

When configuring ghcr.io as a fallback (upstream) registry via `REGISTRIES_JSON`, pull-through fallback always fails with `manifest unknown` (404). The same configuration works fine for Docker Hub, GCR, quay.io and registry.k8s.io.

## Root cause

Two issues in `src/registry/http.ts` break token-based authentication for registries like ghcr.io:

### 1. Scope returned by upstream `/v2/` is used as-is and double-prefixed

`authenticate()` only overwrites the scope when it's empty:

```ts
const authCtx = authHeaderIntoAuthContext(this.url, authenticateHeader);
if (!authCtx.scope) authCtx.scope = namespace;
```

This assumes the upstream `/v2/` endpoint returns a `WWW-Authenticate` header **without** a `scope` attribute (this is what Docker Hub does). However, ghcr.io returns a **placeholder scope**:

```
WWW-Authenticate: Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"
```

So `authCtx.scope` becomes `repository:user/image:pull`, and `authenticateBearer()` then builds:

```
scope: `repository:${ctx.scope}:pull,push`
=> "repository:repository:user/image:pull:pull,push"
```

which the token endpoint rejects.

### 2. Requesting `:pull,push` is rejected by ghcr.io

Even with the scope prefix fixed, ghcr.io's token endpoint returns `403 DENIED` for combined `:pull,push` scope:

```
GET https://ghcr.io/token?service=ghcr.io&scope=repository:actions/actions-runner:pull,push
=> {"errors":[{"code":"DENIED","message":"requested access to the resource is denied"}]}

GET https://ghcr.io/token?service=ghcr.io&scope=repository:actions/actions-runner:pull
=> {"token":"..."} (200 OK)
```

Since fallback is a read-only operation, requesting push permission is unnecessary anyway.

## Proposed fix

Two one-line changes in `src/registry/http.ts`:

1. Always use the request namespace for the scope (ignore the upstream placeholder):
```ts
authCtx.scope = namespace;
```
2. Request only `:pull` (fallback never pushes):
```ts
scope: `repository:${ctx.scope}:pull`,
```

## Verification

After the fix, with `REGISTRIES_JSON` containing Docker Hub + ghcr.io + registry.k8s.io + quay.io + gcr.io:

| Registry | Image | Before | After |
|---|---|---|---|
| ghcr.io | actions/actions-runner | 404 | 200 |
| ghcr.io | home-assistant/home-assistant | 404 | 200 |
| ghcr.io | open-telemetry/opentelemetry-collector-releases/opentelemetry-collector | 404 | 200 |
| docker.io | library/alpine | 200 | 200 |
| registry.k8s.io | pause | 200 | 200 |
| quay.io | prometheus/node-exporter | 200 | 200 |
| gcr.io | google-containers/pause | 200 | 200 |

## Environment

- serverless-registry @ main (20cd590)
- wrangler 4.118.0

Contributor guide

Open the contributing guide

Research direction

Start in src/registry/http.ts, focusing on authenticate() and authenticateBearer() and the scope values they construct. Apply the proposed authentication changes, then verify the REGISTRIES_JSON fallback matrix: ghcr.io pulls return 200 while Docker Hub, registry.k8s.io, quay.io, and gcr.io continue to work.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, typescript
Domain
authentication, backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
88/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.