Skip to content

Commit 071a911

Browse files
stevedaustinvazquez
authored andcommitted
Trigger Docker auth on ECR token expiry
An expired token passed to ECR will return a 403, which lacks a Www-Authenticate header required to trigger the docker authorizer. This meant that credential helpers like amazon-ecr-credential-helper would not refresh the token. This change adds the proper header to fix this behavior. Signed-off-by: David Son <[email protected]>
1 parent 7a5056d commit 071a911

File tree

2 files changed

+38
-3
lines changed

2 files changed

+38
-3
lines changed

service/resolver/client.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,26 @@ func shouldAuthenticate(resp *http.Response) bool {
250250
for _, e := range errs {
251251
if err, ok := e.(docker.Error); ok {
252252
if err.Message == ecrTokenExpiredResponseMessage {
253+
// ECR's 403 doesn't return a Www-Authenticate and so doesn't trigger the
254+
// basic re-authentication in containerd's docker authorizer.
255+
// Ideally ECR would return the Www-Authenticate for expired tokens,
256+
// but until then we'll have to use this workaround.
257+
if resp.Header == nil {
258+
resp.Header = map[string][]string{}
259+
}
260+
261+
authenticateHeader := http.CanonicalHeaderKey("Www-Authenticate")
262+
if _, exists := resp.Header[authenticateHeader]; !exists {
263+
realm := ""
264+
if resp.Request != nil {
265+
realm = "https://" + resp.Request.URL.Host + "/"
266+
}
267+
service := "ecr.amazonaws.com"
268+
269+
authHeaderContent := fmt.Sprintf("Basic realm=\"%s\",service=\"%s\"", realm, service)
270+
resp.Header[authenticateHeader] = []string{authHeaderContent}
271+
}
272+
253273
return true
254274
}
255275
}

service/resolver/client_test.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,10 @@ func TestAuthentication(t *testing.T) {
167167
normalForbiddenResponse, _ := docker.Errors([]error{docker.ErrorCodeDenied}).MarshalJSON()
168168
unauthorizedResponse, _ := docker.Errors([]error{docker.ErrorCodeUnauthorized}).MarshalJSON()
169169
testCases := []struct {
170-
name string
171-
performAuth bool
172-
response *http.Response
170+
name string
171+
performAuth bool
172+
response *http.Response
173+
expectedAuthHeader string
173174
}{
174175
{
175176
name: "Authenticate on 403 with token expiry.",
@@ -178,6 +179,7 @@ func TestAuthentication(t *testing.T) {
178179
StatusCode: http.StatusForbidden,
179180
Body: io.NopCloser(bytes.NewReader(ecrForbiddenResponse)),
180181
},
182+
expectedAuthHeader: "Basic realm=\"\",service=\"ecr.amazonaws.com\"",
181183
},
182184
{
183185
name: "Do not authenticate on 403 without token expiry.",
@@ -210,5 +212,18 @@ func TestAuthentication(t *testing.T) {
210212
t.Fatalf("failed test case: %s: expected auth: %v; got auth: %v",
211213
tc.name, tc.performAuth, shouldPerformAuthentication)
212214
}
215+
216+
if tc.expectedAuthHeader != "" {
217+
authHeader, exists := tc.response.Header["Www-Authenticate"]
218+
if !exists {
219+
t.Fatalf("failed test case: %s: expected Www-Authenticate header: %s; got nothing",
220+
tc.name, tc.expectedAuthHeader)
221+
}
222+
if authHeader[0] != tc.expectedAuthHeader {
223+
t.Fatalf("failed test case: %s: expected Www-Authenticate header: %s; got %s",
224+
tc.name, tc.expectedAuthHeader, authHeader[0])
225+
}
226+
}
227+
213228
}
214229
}

0 commit comments

Comments
 (0)