-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow checking credentials for image imports against repo url, not just passed url #14851
Conversation
[test] |
Evaluated for origin test up to d236486 |
@soltysh need unit test |
continuous-integration/openshift-jenkins/test FAILURE (https://ci.openshift.redhat.com/jenkins/job/test_pull_request_origin/2547/) (Base Commit: c3c286b) (PR Branch Commit: d236486) |
I know, but I don't want to spend time working on it, unless I get an approval from Michal and Clayton about the approach. |
pkg/image/importer/credentials.go
Outdated
@@ -102,14 +102,15 @@ func NewLazyCredentialsForSecrets(secretsFn func() ([]kapi.Secret, error)) *Secr | |||
|
|||
type SecretCredentialStore struct { | |||
lock sync.Mutex | |||
origin *url.URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a map[string /* origin */]string /* realm */
.
With the single value, the origin
may be overwritten by other concurrent auth routines.
pkg/image/importer/credentials.go
Outdated
return basicCredentialsFromKeyring(keyring, &url.URL{Host: "docker.io"}, nil) | ||
} | ||
// try origin url, if it's different than target | ||
if origin != nil && origin.String() != target.String() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no guarantee that origin
really corresponds to the target
realm. In docker-registry, single context can be used for different registries, each having possibly different auth server. The association origin <-> target
needs to be verified first. Otherwise, credentials belonging to one server may be sent to another.
The verification may be simple like extracting auth realm from a simple http GET request to the origin
:
curl -I https://index.docker.io/v2/
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Docker-Distribution-Api-Version: registry/2.0
Www-Authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"
Date: Thu, 29 Jun 2017 11:53:49 GMT
Thread-safety needs to be ensured though. I would welcome the verification being done lazily. However, I'm not sure about polling remote registries in this function which is supposed to be fast.
@liggitt this is your domain, do you have better idea?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also concerned about secret exposure. We would need to be really sure that we aren't vulnerable to handing out a secret to a malicious registry.
Fairly concerned about merging this this close to release. If this is only usability, would prefer to discuss for 3.6 |
3.7 |
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: soltysh No associated issue. Update pull-request body to add a reference to an issue, or get approval with The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these OWNERS Files:
You can indicate your approval by writing |
Workaround documented: |
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: soltysh No associated issue. Update pull-request body to add a reference to an issue, or get approval with The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these OWNERS Files:
You can indicate your approval by writing |
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: soltysh No associated issue. Update pull-request body to add a reference to an issue, or get approval with The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these OWNERS Files:
You can indicate your approval by writing |
@smarterclayton all in all, is this approach you're ok with or we need to discuss this? |
@miminar @smarterclayton I've went with a realm cache and based on that I've added a fallback logic authing against the url from before the realm one. |
/test unit |
Flake: #13966 /test unit |
/retest |
Fixed that failing unit test and added another for the realm part. |
/retest |
pkg/image/importer/client.go
Outdated
if realm, ok := ch.Parameters["realm"]; ok { | ||
if url, err := url.Parse(realm); err == nil { | ||
if scs, ok := r.credentials.(*SecretCredentialStore); ok { | ||
scs.AddRealm(®istry, url) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should log at v(4) if we encounter this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
pkg/image/importer/client_test.go
Outdated
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Docker-Distribution-API-Version", "registry/2.0") | ||
w.Header().Set("WWW-Authenticate", `Bearer realm="https://auth.example.com/token"`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So assume I'm a malicious docker registry at https://badserver.com
. You hit me, I say, "oh, i need credentials for www.redhat.com
". Will we then send credentials for www.redhat.com
to https://badserver.com
, just because badserver.com
told us?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we won't send credentials. The flow looks like this:
- OpenShift sends ping to
https://badserver.com
, in response we MUST get401 Unauthorized
with headerWWW-Authenticate: <scheme> realm="https://www.redhat.com",service="<servicename>"
. - OpenShift sends credentials TO realm, here
https://www.redhat.com
with information about service.https://www.redhat.com
verifies service (needs to be known for it, otherwise you'll get rejected) and credentials. In response sends back a token (usually short lived one). - OpenShift uses token against
https://baserver.com
to perform necessary actions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is all described in details in RFC 7235.
@smarterclayton any other doubts, or we're good to ship it. @miminar is out still |
pkg/image/importer/client_test.go
Outdated
} | ||
if ru.registry.String() != server.URL { | ||
t.Errorf("Unexpected registry url, expected %s, got %v", server.URL, ru.registry) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this test do more to prove the credentials for the realm would not be offered to the endpoint?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not within this test. I'd have to create the entire client flow to verify if the original server is not getting those creds. Seems like a lot of effort for such a reasonable small thing. Is it worth it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small security thing sometimes turns out to be a big nightmare. I'd like having this covered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sigh... will update.
pkg/image/importer/client.go
Outdated
if challenges, err := r.context.Challenges.GetChallenges(*resp.Request.URL); err == nil { | ||
for _, ch := range challenges { | ||
// TODO: support multiple realm headers | ||
if realm, ok := ch.Parameters["realm"]; ok { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Challenges with ch.Scheme
other than basic
and bearer
should be skipped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See here:
origin/pkg/image/importer/client.go
Line 114 in 724c7d2
auth.NewBasicHandler(r.credentials), |
We don't support any other schemes but bearer
and basic
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Roger that, will update.
pkg/image/importer/credentials.go
Outdated
func (s *SecretCredentialStore) AddRealm(registry, realm *url.URL) { | ||
ru := &realmURL{ | ||
realm: *realm, | ||
registry: *registry, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are parallel requests using the same credential store to different registries having the same realm
, one entry will overwrite the other. To avoid the race, the registry should be a list and access to these entries needs to be guarded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will update.
pkg/image/importer/client_test.go
Outdated
} | ||
if ru.registry.String() != server.URL { | ||
t.Errorf("Unexpected registry url, expected %s, got %v", server.URL, ru.registry) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small security thing sometimes turns out to be a big nightmare. I'd like having this covered.
pkg/image/importer/client_test.go
Outdated
if !ok { | ||
t.Errorf("Unexpected object type, expected realmURL, got %T", obj) | ||
} | ||
if ru.realm.String() != "https://auth.example.com/token" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: a good candidate for const
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't bother since this is just test.
@soltysh PR needs rebase |
724c7d2
to
788a5f9
Compare
@smarterclayton I've spent great deal of time to think through this solution (especially after your recent changes) and I think this approach is not good. I've additionally confirmed my suspicions wrt to the insecure nature of this approach with @miminar, while in Brno yesterday. Mostly wrt to having the authorization decoupled from the credential store, like we have right now. We need to implement appropriate authorization handlers in image imports that will have the full access to the both the request and the credential store so that it matches appropriate secrets when needed. Additionally, having a proper authhandler in place will allow us to simplify the dockerhub credentials matching code which will also be supported when we have a proper auth delegation implemented. Having said that, I've created a trello card in devexp board (@bparees fyi) to have a proper fix instead of hacky approach. |
@soltysh: The following tests failed, say
Full PR test history. Your PR dashboard. Please help us cut down on flakes by linking to an open issue when you hit one in your PR. Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here. |
Fixes #9584 and https://bugzilla.redhat.com/show_bug.cgi?id=1462606.
Here's the story. Currently our credentials store (during image import) is passed a url, which we then match against secrets. Unfortunately, in some cases the url of the repository and the one passed to that credentials store are different, which causes frustration, because people first of all need to know the 2nd url and most importantly create it. This situation happens when authentication is redirecting to a different url. Whereas pull secrets don't require this, because they are matching secrets against repository url, not the authn (even if it's different). This simple fix passes the repository url down to the credentials store, which can then try to get the proper credentials using either one or the other url.
@smarterclayton @miminar wdyt of this approach? I guess some tests are needed, but I don't want to spend time working on tests when the approach will be bad. I couldn't find a better way of passing the original repo url, other than this somehow nasty hack.