/
imdsMsi.ts
176 lines (157 loc) · 5.68 KB
/
imdsMsi.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import {
AccessToken,
delay,
GetTokenOptions,
RequestPrepareOptions,
RestError
} from "@azure/core-http";
import { SpanStatusCode } from "@azure/core-tracing";
import { AuthenticationError } from "../../client/errors";
import { IdentityClient } from "../../client/identityClient";
import { credentialLogger } from "../../util/logging";
import { createSpan } from "../../util/tracing";
import { imdsApiVersion, imdsEndpoint } from "./constants";
import { MSI } from "./models";
import { msiGenericGetToken } from "./utils";
const logger = credentialLogger("ManagedIdentityCredential - IMDS");
function expiresInParser(requestBody: any): number {
if (requestBody.expires_on) {
// Use the expires_on timestamp if it's available
const expires = +requestBody.expires_on * 1000;
logger.info(`IMDS using expires_on: ${expires} (original value: ${requestBody.expires_on})`);
return expires;
} else {
// If these aren't possible, use expires_in and calculate a timestamp
const expires = Date.now() + requestBody.expires_in * 1000;
logger.info(`IMDS using expires_in: ${expires} (original value: ${requestBody.expires_in})`);
return expires;
}
}
function prepareRequestOptions(resource?: string, clientId?: string): RequestPrepareOptions {
const queryParameters: any = {
resource,
"api-version": imdsApiVersion
};
if (clientId) {
queryParameters.client_id = clientId;
}
return {
url: process.env.AZURE_POD_IDENTITY_TOKEN_URL ?? imdsEndpoint,
method: "GET",
queryParameters,
headers: {
Accept: "application/json",
Metadata: true
}
};
}
// 800ms -> 1600ms -> 3200ms
export const imdsMsiRetryConfig = {
maxRetries: 3,
startDelayInMs: 800,
intervalIncrement: 2
};
export const imdsMsi: MSI = {
async isAvailable(
identityClient: IdentityClient,
resource: string,
clientId?: string,
getTokenOptions?: GetTokenOptions
): Promise<boolean> {
const { span, updatedOptions } = createSpan(
"ManagedIdentityCredential-pingImdsEndpoint",
getTokenOptions
);
// if the PodIdenityEndpoint environment variable was set no need to probe the endpoint, it can be assumed to exist
if (process.env.AZURE_POD_IDENTITY_TOKEN_URL) {
return true;
}
const request = prepareRequestOptions(resource, clientId);
// This will always be populated, but let's make TypeScript happy
if (request.headers) {
// Remove the Metadata header to invoke a request error from
// IMDS endpoint
delete request.headers.Metadata;
}
request.spanOptions = updatedOptions?.tracingOptions?.spanOptions;
request.tracingContext = updatedOptions?.tracingOptions?.tracingContext;
try {
// Create a request with a timeout since we expect that
// not having a "Metadata" header should cause an error to be
// returned quickly from the endpoint, proving its availability.
const webResource = identityClient.createWebResource(request);
// In Kubernetes pods, node-fetch (used by core-http) takes longer than 2 seconds to begin sending the network request,
// So smaller timeouts will cause this credential to be immediately aborted.
// This won't be a problem once we move Identity to core-rest-pipeline.
webResource.timeout = updatedOptions?.requestOptions?.timeout || 3000;
try {
await identityClient.sendRequest(webResource);
} catch (err) {
if (
(err.name === "RestError" && err.code === RestError.REQUEST_SEND_ERROR) ||
err.name === "AbortError" ||
err.code === "ECONNREFUSED" || // connection refused
err.code === "EHOSTDOWN" // host is down
) {
// If the request failed, or NodeJS was unable to establish a connection,
// or the host was down, we'll assume the IMDS endpoint isn't available.
logger.info(`The Azure IMDS endpoint is unavailable`);
span.setStatus({
code: SpanStatusCode.ERROR,
message: err.message
});
return false;
}
}
// If we received any response, the endpoint is available
logger.info(`The Azure IMDS endpoint is available`);
// IMDS MSI available!
return true;
} catch (err) {
// createWebResource failed.
// This error should bubble up to the user.
logger.info(`Error when creating the WebResource for the IMDS endpoint: ${err.message}`);
span.setStatus({
code: SpanStatusCode.ERROR,
message: err.message
});
throw err;
} finally {
span.end();
}
},
async getToken(
identityClient: IdentityClient,
resource: string,
clientId?: string,
getTokenOptions: GetTokenOptions = {}
): Promise<AccessToken | null> {
logger.info(
`Using the IMDS endpoint coming form the environment variable MSI_ENDPOINT=${process.env.MSI_ENDPOINT}, and using the cloud shell to proceed with the authentication.`
);
let nextDelayInMs = imdsMsiRetryConfig.startDelayInMs;
for (let retries = 0; retries < imdsMsiRetryConfig.maxRetries; retries++) {
try {
return await msiGenericGetToken(
identityClient,
prepareRequestOptions(resource, clientId),
expiresInParser,
getTokenOptions
);
} catch (error) {
if (error.statusCode === 404) {
await delay(nextDelayInMs);
nextDelayInMs *= imdsMsiRetryConfig.intervalIncrement;
continue;
}
throw error;
}
}
throw new AuthenticationError(
404,
`Failed to retrieve IMDS token after ${imdsMsiRetryConfig.maxRetries} retries.`
);
}
};