Skip to content

Commit f325f4b

Browse files
authored
Handle Docker Desktop 403 response from IMDS endpoint (Azure#21432)
1 parent c4c059f commit f325f4b

File tree

3 files changed

+61
-25
lines changed

3 files changed

+61
-25
lines changed

sdk/azidentity/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
tokens by default or observe the environment variable "AZURE_IDENTITY_DISABLE_CP1".
1313

1414
### Bugs Fixed
15+
* Credential chains such as `DefaultAzureCredential` now try their next credential, if any, when
16+
managed identity authentication fails in a Docker Desktop container
17+
([#21417](https://github.com/Azure/azure-sdk-for-go/issues/21417))
1518

1619
### Other Changes
1720

sdk/azidentity/managed_identity_client.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,25 @@ func (c *managedIdentityClient) authenticate(ctx context.Context, id ManagedIDKi
175175
return c.createAccessToken(resp)
176176
}
177177

178-
if c.msiType == msiTypeIMDS && resp.StatusCode == 400 {
179-
if id != nil {
180-
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "the requested identity isn't assigned to this resource", resp, nil)
181-
}
182-
msg := "failed to authenticate a system assigned identity"
183-
if body, err := runtime.Payload(resp); err == nil && len(body) > 0 {
184-
msg += fmt.Sprintf(". The endpoint responded with %s", body)
178+
if c.msiType == msiTypeIMDS {
179+
switch resp.StatusCode {
180+
case http.StatusBadRequest:
181+
if id != nil {
182+
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "the requested identity isn't assigned to this resource", resp, nil)
183+
}
184+
msg := "failed to authenticate a system assigned identity"
185+
if body, err := runtime.Payload(resp); err == nil && len(body) > 0 {
186+
msg += fmt.Sprintf(". The endpoint responded with %s", body)
187+
}
188+
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, msg)
189+
case http.StatusForbidden:
190+
// Docker Desktop runs a proxy that responds 403 to IMDS token requests. If we get that response,
191+
// we return credentialUnavailableError so credential chains continue to their next credential
192+
body, err := runtime.Payload(resp)
193+
if err == nil && strings.Contains(string(body), "A socket operation was attempted to an unreachable network") {
194+
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, fmt.Sprintf("unexpected response %q", string(body)))
195+
}
185196
}
186-
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, msg)
187197
}
188198

189199
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "authentication failed", resp, nil)

sdk/azidentity/managed_identity_client_test.go

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ package azidentity
88

99
import (
1010
"context"
11+
"errors"
12+
"fmt"
1113
"net/http"
1214
"net/url"
1315
"strings"
@@ -76,23 +78,44 @@ func TestManagedIdentityClient_ApplicationID(t *testing.T) {
7678
}
7779
}
7880

79-
func TestManagedIdentityClient_IMDS400(t *testing.T) {
80-
srv, close := mock.NewServer(mock.WithTransformAllRequestsToTestServerUrl())
81-
defer close()
82-
body := `{"error":"invalid_request","error_description":"Identity not found"}`
83-
srv.SetResponse(mock.WithBody([]byte(body)), mock.WithStatusCode(http.StatusBadRequest))
84-
client, err := newManagedIdentityClient(&ManagedIdentityCredentialOptions{
85-
ClientOptions: azcore.ClientOptions{Transport: srv},
86-
})
87-
if err != nil {
88-
t.Fatal(err)
89-
}
90-
_, err = client.authenticate(context.Background(), nil, testTRO.Scopes)
91-
if err == nil {
92-
t.Fatal("expected an error")
93-
}
94-
if actual := err.Error(); !strings.Contains(actual, body) {
95-
t.Fatalf("expected response body in error, got %q", actual)
81+
func TestManagedIdentityClient_IMDSErrors(t *testing.T) {
82+
for _, test := range []struct {
83+
body, desc string
84+
code int
85+
}{
86+
{
87+
desc: "No identity assigned",
88+
code: http.StatusBadRequest,
89+
body: `{"error":"invalid_request","error_description":"Identity not found"}`,
90+
},
91+
{
92+
desc: "Docker Desktop",
93+
code: http.StatusForbidden,
94+
body: "connecting to 169.254.169.254:80: connecting to 169.254.169.254:80: dial tcp 169.254.169.254:80: connectex: A socket operation was attempted to an unreachable network.",
95+
},
96+
} {
97+
t.Run(fmt.Sprint(test.code), func(t *testing.T) {
98+
srv, close := mock.NewServer(mock.WithTransformAllRequestsToTestServerUrl())
99+
defer close()
100+
srv.SetResponse(mock.WithBody([]byte(test.body)), mock.WithStatusCode(test.code))
101+
client, err := newManagedIdentityClient(&ManagedIdentityCredentialOptions{
102+
ClientOptions: azcore.ClientOptions{Transport: srv},
103+
})
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
_, err = client.authenticate(context.Background(), nil, testTRO.Scopes)
108+
if err == nil {
109+
t.Fatal("expected an error")
110+
}
111+
if actual := err.Error(); !strings.Contains(actual, test.body) {
112+
t.Fatalf("expected response body in error, got %q", actual)
113+
}
114+
var unavailableErr *credentialUnavailableError
115+
if !errors.As(err, &unavailableErr) {
116+
t.Fatalf("expected %T, got %T", unavailableErr, err)
117+
}
118+
})
96119
}
97120
}
98121

0 commit comments

Comments
 (0)