Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f7387b3
use nextRefresh to set the credentials' expiration time
lauzadis Oct 5, 2022
98e9516
Allow use of expired credentials, add exception handling for IMDS ser…
lauzadis Oct 5, 2022
f35aaf6
Add static stability test cases
lauzadis Oct 5, 2022
0a20654
ktlint formatting
lauzadis Oct 5, 2022
86f4035
add changelog
lauzadis Oct 5, 2022
35283d7
Use sanitized nextRefresh field
lauzadis Oct 6, 2022
3365e41
Set expireCredentialsAfter longer than the credentials expirationTime
lauzadis Oct 6, 2022
d776b78
Add test for nextRefresh time taking priority over expiration time
lauzadis Oct 6, 2022
f25571d
Add mutex for previousCredentials
lauzadis Oct 6, 2022
cc9b26c
Merge branch 'main' of github.com:awslabs/aws-sdk-kotlin into feat-st…
lauzadis Oct 6, 2022
2a50708
Remove usage of nextRefresh in Credentials
lauzadis Oct 6, 2022
ec342cb
Replace Credentials' nextRefresh with a class member
lauzadis Oct 6, 2022
8cdff30
remove trailing comma
lauzadis Oct 6, 2022
3b0be19
Add SdkIOException for multiplatform support of IOException
lauzadis Oct 7, 2022
86b75f1
tickle CI
lauzadis Oct 7, 2022
a32db13
Remove microseconds from expiration time
lauzadis Oct 7, 2022
7c82e9f
Pass testClock into ImdsCredentialsProvider
lauzadis Oct 7, 2022
43a7af5
tickle CI
lauzadis Oct 7, 2022
6cced41
Address ktlint
lauzadis Oct 10, 2022
e4e35a1
Remove microseconds from IMDS response
lauzadis Oct 10, 2022
78eca44
Revert: remove microseconds from IMDS response
lauzadis Oct 10, 2022
a885300
Truncate expiration time to microseconds
lauzadis Oct 10, 2022
17d9585
Change SdkIOException visibility to `internal`
lauzadis Oct 10, 2022
004a2ee
Remove `previousCredentials` from constructor and refactor its usage,…
lauzadis Oct 10, 2022
d668d4d
ktlint
lauzadis Oct 10, 2022
9c4b276
Use .apply{} instead of .run{} to update `nextRefresh`
lauzadis Oct 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/8c2a1262-90c7-49a1-8c0a-29b269ef0f19.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "8c2a1262-90c7-49a1-8c0a-29b269ef0f19",
"type": "feature",
"description": "Support static stability for IMDS credentials",
"issues": ["#707"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ public class CachedCredentialsProvider(
logger.trace { "refreshing credentials cache" }
val providerCreds = source.getCredentials()
if (providerCreds.expiration != null) {
ExpiringValue(providerCreds, providerCreds.expiration!!)
val expiration = minOf(providerCreds.expiration!!, (clock.now() + expireCredentialsAfter))
ExpiringValue(providerCreds, expiration)
} else {
val expiration = clock.now() + expireCredentialsAfter
val creds = providerCreds.copy(expiration = expiration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,21 @@ import aws.smithy.kotlin.runtime.http.HttpStatusCode
import aws.smithy.kotlin.runtime.io.Closeable
import aws.smithy.kotlin.runtime.logging.Logger
import aws.smithy.kotlin.runtime.serde.json.JsonDeserializer
import aws.smithy.kotlin.runtime.time.Clock
import aws.smithy.kotlin.runtime.time.Instant
import aws.smithy.kotlin.runtime.util.Platform
import aws.smithy.kotlin.runtime.util.PlatformEnvironProvider
import aws.smithy.kotlin.runtime.util.asyncLazy
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
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"

internal expect class SdkIOException : Exception // FIXME move this to the proper place when we do the larger KMP Exception refactor

/**
* [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)
Expand All @@ -43,8 +50,16 @@ public class ImdsCredentialsProvider(
private val profileOverride: String? = null,
private val client: Lazy<InstanceMetadataProvider> = lazy { ImdsClient() },
private val platformProvider: PlatformEnvironProvider = Platform,
private val clock: Clock = Clock.System,
) : CredentialsProvider, Closeable {
private val logger = Logger.getLogger<ImdsCredentialsProvider>()
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()

private val profile = asyncLazy {
if (profileOverride != null) return@asyncLazy profileOverride
Expand All @@ -56,23 +71,52 @@ public class ImdsCredentialsProvider(
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 {
profile.get()
} catch (ex: Exception) {
throw CredentialsProviderException("failed to load instance profile", ex)
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 payload = client.value.get("$CREDENTIALS_BASE_PATH/$profileName")
val deserializer = JsonDeserializer(payload.encodeToByteArray())

return when (val resp = deserializeJsonCredentials(deserializer)) {
is JsonCredentialsResponse.SessionCredentials -> Credentials(
resp.accessKeyId,
resp.secretAccessKey,
resp.sessionToken,
resp.expiration,
PROVIDER_NAME,
)
is JsonCredentialsResponse.SessionCredentials -> {
nextRefresh = if (resp.expiration < clock.now()) {
logger.warn {
"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?")
Expand All @@ -98,4 +142,13 @@ public class ImdsCredentialsProvider(
throw ex
}
}

private suspend fun useCachedCredentials(ex: Exception): Credentials? = when {
ex is SdkIOException || ex is EC2MetadataError && ex.statusCode == HttpStatusCode.InternalServerError.value -> {
mu.withLock {
previousCredentials?.apply { nextRefresh = clock.now() + DEFAULT_CREDENTIALS_REFRESH_SECONDS.seconds }
}
}
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class CachedCredentialsProviderTest {
@Test
fun testRefreshBufferWindow() = runTest {
val source = TestCredentialsProvider(expiration = testExpiration)
val provider = CachedCredentialsProvider(source, clock = testClock)
val provider = CachedCredentialsProvider(source, clock = testClock, expireCredentialsAfter = 60.minutes)
val creds = provider.getCredentials()
val expected = Credentials("AKID", "secret", expiration = testExpiration)
assertEquals(expected, creds)
Expand Down
Loading