-
Notifications
You must be signed in to change notification settings - Fork 45
/
ImdsCredentialsProvider.kt
154 lines (137 loc) · 7.11 KB
/
ImdsCredentialsProvider.kt
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
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.runtime.auth.credentials
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
import aws.sdk.kotlin.runtime.config.imds.EC2MetadataError
import aws.sdk.kotlin.runtime.config.imds.ImdsClient
import aws.sdk.kotlin.runtime.config.imds.InstanceMetadataProvider
import aws.smithy.kotlin.runtime.auth.awscredentials.CloseableCredentialsProvider
import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider
import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProviderException
import aws.smithy.kotlin.runtime.auth.awscredentials.DEFAULT_CREDENTIALS_REFRESH_SECONDS
import aws.smithy.kotlin.runtime.collections.Attributes
import aws.smithy.kotlin.runtime.config.resolve
import aws.smithy.kotlin.runtime.http.HttpStatusCode
import aws.smithy.kotlin.runtime.io.IOException
import aws.smithy.kotlin.runtime.serde.json.JsonDeserializer
import aws.smithy.kotlin.runtime.telemetry.logging.info
import aws.smithy.kotlin.runtime.telemetry.logging.warn
import aws.smithy.kotlin.runtime.time.Clock
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.util.PlatformEnvironProvider
import aws.smithy.kotlin.runtime.util.PlatformProvider
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.coroutineContext
import kotlin.time.Duration.Companion.seconds
private const val CREDENTIALS_BASE_PATH: String = "/latest/meta-data/iam/security-credentials"
private const val CODE_ASSUME_ROLE_UNAUTHORIZED_ACCESS: String = "AssumeRoleUnauthorizedAccess"
private const val PROVIDER_NAME = "IMDSv2"
/**
* [CredentialsProvider] that uses EC2 instance metadata service (IMDS) to provide credentials information.
* This provider requires that the EC2 instance has an [instance profile](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#ec2-instance-profile)
* configured.
*
* See [EC2 IAM Roles](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html) for more
* information.
*
* @param profileOverride override the instance profile name. When retrieving credentials, a call must first be made to
* `<IMDS_BASE_URL>/latest/meta-data/iam/security-credentials`. This returns the instance profile used. If
* [profileOverride] is set, the initial call to retrieve the profile is skipped and the provided value is used instead.
* @param client the IMDS client to use to resolve credentials information with. This provider takes ownership over
* the lifetime of the given [ImdsClient] and will close it when the provider is closed.
* @param platformProvider the [PlatformEnvironProvider] instance
*/
public class ImdsCredentialsProvider(
public val profileOverride: String? = null,
public val client: Lazy<InstanceMetadataProvider> = lazy { ImdsClient() },
public val platformProvider: PlatformEnvironProvider = PlatformProvider.System,
private val clock: Clock = Clock.System,
) : CloseableCredentialsProvider {
private var previousCredentials: Credentials? = null
// the time to refresh the Credentials. If set, it will take precedence over the Credentials' expiration time
private var nextRefresh: Instant? = null
// protects previousCredentials and nextRefresh
private val mu = Mutex()
override suspend fun resolve(attributes: Attributes): Credentials {
if (AwsSdkSetting.AwsEc2MetadataDisabled.resolve(platformProvider) == true) {
throw CredentialsNotLoadedException("AWS EC2 metadata is explicitly disabled; credentials not loaded")
}
// if we have previously served IMDS credentials and it's not time for a refresh, just return the previous credentials
mu.withLock {
previousCredentials?.run {
nextRefresh?.takeIf { clock.now() < it }?.run {
return previousCredentials!!
}
}
}
val profileName = try {
profileOverride ?: loadProfile()
} catch (ex: Exception) {
return useCachedCredentials(ex) ?: throw CredentialsProviderException("failed to load instance profile", ex)
}
val payload = try {
client.value.get("$CREDENTIALS_BASE_PATH/$profileName")
} catch (ex: Exception) {
return useCachedCredentials(ex) ?: throw CredentialsProviderException("failed to load credentials", ex)
}
val deserializer = JsonDeserializer(payload.encodeToByteArray())
return when (val resp = deserializeJsonCredentials(deserializer)) {
is JsonCredentialsResponse.SessionCredentials -> {
nextRefresh = if (resp.expiration != null && resp.expiration < clock.now()) {
coroutineContext.warn<ImdsCredentialsProvider> {
"Attempting credential expiration extension due to a credential service availability issue. " +
"A refresh of these credentials will be attempted again in " +
"${ DEFAULT_CREDENTIALS_REFRESH_SECONDS / 60 } minutes."
}
clock.now() + DEFAULT_CREDENTIALS_REFRESH_SECONDS.seconds
} else {
null
}
val creds = Credentials(
resp.accessKeyId,
resp.secretAccessKey,
resp.sessionToken,
resp.expiration,
PROVIDER_NAME,
)
creds.also {
mu.withLock { previousCredentials = it }
}
}
is JsonCredentialsResponse.Error -> {
when (resp.code) {
CODE_ASSUME_ROLE_UNAUTHORIZED_ACCESS -> throw ProviderConfigurationException("Incorrect IMDS/IAM configuration: [${resp.code}] ${resp.message}. Hint: Does this role have a trust relationship with EC2?")
else -> throw CredentialsProviderException("Error retrieving credentials from IMDS: code=${resp.code}; ${resp.message}")
}
}
}
}
override fun close() {
if (client.isInitialized()) {
client.value.close()
}
}
private suspend fun loadProfile() = try {
client.value.get(CREDENTIALS_BASE_PATH)
} catch (ex: EC2MetadataError) {
if (ex.statusCode == HttpStatusCode.NotFound.value) {
coroutineContext.info<ImdsCredentialsProvider> {
"Received 404 from IMDS when loading profile information. Hint: This instance may not have an " +
"IAM role associated."
}
}
throw ex
}
private suspend fun useCachedCredentials(ex: Exception): Credentials? = when {
ex is IOException || ex is EC2MetadataError && ex.statusCode == HttpStatusCode.InternalServerError.value -> {
mu.withLock {
previousCredentials?.apply { nextRefresh = clock.now() + DEFAULT_CREDENTIALS_REFRESH_SECONDS.seconds }
}
}
else -> null
}
}