Skip to content

Conversation

@napalu
Copy link

@napalu napalu commented Feb 23, 2023

Take into account situations (such as proxies/load-balancers outside of your control) stripping Docker-Content-Digest header from responses when storing manifests. Closes #3848

@milosgajdos milosgajdos reopened this Jul 22, 2023
@codecov-commenter
Copy link

codecov-commenter commented Jul 22, 2023

Codecov Report

Patch coverage: 54.09% and project coverage change: -0.71% ⚠️

Comparison is base (e5d5810) 56.58% compared to head (f8afaf8) 55.87%.
Report is 126 commits behind head on main.

❗ Current head f8afaf8 differs from pull request most recent head 6b82655. Consider uploading reports for the commit 6b82655 to get more accurate results

❗ Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please install the Github App Integration for your organization. Read more.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3849      +/-   ##
==========================================
- Coverage   56.58%   55.87%   -0.71%     
==========================================
  Files         105      110       +5     
  Lines       10636    11092     +456     
==========================================
+ Hits         6018     6198     +180     
- Misses       3946     4204     +258     
- Partials      672      690      +18     
Files Changed Coverage Δ
registry/api/v2/descriptors.go 100.00% <ø> (ø)
registry/auth/token/token.go 57.89% <0.00%> (ø)
registry/proxy/proxyblobstore.go 52.08% <0.00%> (-1.44%) ⬇️
registry/proxy/proxymanifeststore.go 42.30% <0.00%> (-3.70%) ⬇️
registry/proxy/proxyregistry.go 0.00% <0.00%> (ø)
registry/storage/driver/storagedriver.go 0.00% <ø> (ø)
registry/storage/schema2manifesthandler.go 62.35% <0.00%> (ø)
registry/handlers/app.go 46.56% <14.28%> (-0.64%) ⬇️
registry/registry.go 45.56% <14.70%> (-2.44%) ⬇️
registry/client/repository.go 53.93% <44.44%> (-0.57%) ⬇️
... and 18 more

... and 3 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@milosgajdos
Copy link
Member

@napalu can you rebase to main, please

@napalu napalu force-pushed the fix/manifest_digest_on_put branch 2 times, most recently from f8afaf8 to 6b82655 Compare August 14, 2023 12:52
napalu added 3 commits August 14, 2023 14:58
Signed-off-by: Florent Heyworth <[email protected]>
Signed-off-by: Florent Heyworth <[email protected]>
@napalu napalu force-pushed the fix/manifest_digest_on_put branch from 6b82655 to 12325c2 Compare August 14, 2023 13:00
@napalu
Copy link
Author

napalu commented Aug 14, 2023

@milosgajdos done

Copy link
Collaborator

@davidspek davidspek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Although from reading this comment I think the Docker-* headers are legacy so I'm not sure what influence this change will have in the future.

@davidspek
Copy link
Collaborator

This is also related to #3793.

@milosgajdos
Copy link
Member

Stripping these Docker- prefixes will be very complicated if we want to make sure avoid breaking existing clients 🤔

@milosgajdos
Copy link
Member

Since this client is used in moby, it'd be good to get @thaJeztah and/or @corhere 's eyes on this as it does some rather interesting things to make up for possible stripping of the digest header

@milosgajdos
Copy link
Member

@thaJeztah do you mind having a look at this 🙇‍♂️

Comment on lines +667 to +677
// if we don't get a digest (do we really need one?) use a HEAD request since
// the spec mandates this should return a Docker-Content-Digest
if dgstHeader == "" {
req, _ := http.NewRequestWithContext(ctx, http.MethodHead, manifestURL, nil)
r, err := ms.client.Do(req)
if err != nil {
return "", err
}

dgstHeader = r.Header.Get("Docker-Content-Digest")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is a good idea to trigger a second request, as this could easily lead to a race condition with other clients pushing a version of the manifest, which may be a different digest as the one that was just pushed by this client.

I also doubt this will help in cases you described where the header would be stripped, as it it's stripped from the PUT request, it would just as well be stripped from the HEAD request?

Take into account situations (such as proxies/load-balancers outside of your control) stripping Docker-Content-Digest header from responses when storing manifests

Copy link
Author

@napalu napalu Aug 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Background for the pull request: in a setting with complex network topology (and many different teams/agencies involved) against a Nexus (Sonatype) registry we first thought some load-balancers/proxies were stripping the headers. The digest was however returned as part of the body. It later turned out the registry was not sending the digest header at all under certain circumstances (when using postgreSQL as an external metadata database but would return the header when using the embedded database - this behaviour has been fixed since). So in our case it turned out to be a misbehaving registry.

The client we are using (oc-mirror) only validates the digest if it is returned - the absence of a header is not considered an error. The distribution library however returns an error when no digest response is received on PUT. The HEAD request fallback was added as extra insurance in case the digest was not returned as part of the original headers or as part of the response body since the spec appears to mandate that a HEAD should return the digest.

Comment on lines +656 to +665
if mt != "" {
body, err := io.ReadAll(resp.Body)
if err == nil {
_, desc, err := distribution.UnmarshalManifest(mt, body)
if err != nil {
return "", err
}
dgstHeader = desc.Digest.String()
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct? Does the PUT response contain the uploaded data that was sent by the client? I think the response body would be either empty (only a static header) or an error-response in JSON format.

Copy link
Author

@napalu napalu Aug 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sonatype Nexus returns the digest as part of the body as well. I think some other registries do as well - see #3792 for context.

Comment on lines +639 to +641
// The specs are ambivalent on Docker-Content-Digest header: on the one hand, the docs at https://docs.docker.com/registry/spec/api/#digest-header
// mention that "any response may include a Docker-Content-Digest header" and on the other hand, the docs at
// https://docs.docker.com/registry/spec/api/#put-manifest say that a PUT request should return a Docker-Content-Digest header.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the docs at https://docs.docker.com/registry/spec/api/ are built from the files in this repository, and describe the "Docker Registry HTTP API V2" specification, which has been superseded by the OCI distribution spec.

From the OCI distribution spec for the PUT /v2/<name>/manifests/<reference> endpoint

<name> is the namespace of the repository, and the <reference> MUST be either a) a digest or b) a tag.

and

The Docker-Content-Digest header returns the canonical digest of the uploaded blob, and MUST be equal to the client provided digest. Clients MAY ignore the value but if it is used, the client SHOULD verify the value against the uploaded blob data.

So I guess there's 2 possible situations here;

  • when pushing the manifest with a digest as <reference>, the header MUST be the same as that digest
  • when pushing the manifest with a name as <reference>, the header must contain the canonical digest of the data

So, I'm not sure if it's possible to validate anything on the client side if the registry does not return a Docker-Content-Digest header (at least not with the current spec), other than the 201 status code, and trusting that the data was sent correctly;

The registry MUST store the manifest in the exact byte representation provided by the client. Upon a successful upload, the registry MUST return response code 201 Created,

With the current spec describing these headers as OPTIONAL;

Because of the origins this specification, the client MAY encounter Docker-specific headers, such as Docker-Content-Digest, or Docker-Distribution-API-Version. These headers are OPTIONAL and clients SHOULD NOT depend on them.

So the only correct option is to ignore missing Docker-Content-Digest headers, and to skip validation.

That said, it feels like an omission in the OCI spec that there's no option to validate, so perhaps this should be raised as (per this discussion) it looks like there's no real alternative to verify if the registry calculated the expected digest.

Perhaps this endpoint needs a ?digest=<digest> query-parameter, as is provided for PUT /v2/<name>/blobs/uploads/<reference>?digest=<digest>, which would allow the registry to reject the request if the calculated ("canonical") digest does not match the digest that's provided by the client.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your analysis that the absence of a digest should not result in an error - it should be up to the client to determine if a digest is expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Storing manifests fails for registries when Docker-Content-Digest header is stripped or not sent

5 participants