Support AWS EKS Pod Identity in IamAwsProvider - #1719
Conversation
EKS Pod Identity Agent exposes credentials over a container credential endpoint; it injects AWS_CONTAINER_CREDENTIALS_FULL_URI pointing to its link-local address and passes the authorization token in a file. Two gaps denied it. - AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE was not read at all, hence no authorization token was sent. It is read now and takes precedence over AWS_CONTAINER_AUTHORIZATION_TOKEN as done by AWS. Trailing newline of the file is removed as it is denied as HTTP header value. - The host of AWS_CONTAINER_CREDENTIALS_FULL_URI was denied unless it resolved to loopback addresses only. As done by AWS, the ECS and EKS Pod Identity link-local addresses 169.254.170.2, 169.254.170.23 and fd00:ec2::23 are allowed as well, and the check is skipped for HTTPS endpoints. Along with it, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is parsed as URI than added as path segments; AWS sets the value with leading slash which resulted in an empty path segment like http://169.254.170.2//v2/xxx. A malformed value of either variable raises ProviderException than NPE. Fixes minio#1717 Signed-off-by: Bala.FA <bala@minio.io>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthrough
ChangesAWS container credentials
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Feature Merge Risk: ⚪ Minimal · up to The container credential changes are mergeable after normal checks. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit reads each line, Comment |
|
None of this is covered by a test — Here's a full test file covering all four fixes — 8 of its 9 cases fail against the pre-fix code and pass on this branch: /*
* MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2026 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.minio.credentials;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.ProviderException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.junit.After;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
/**
* Covers the container-credentials paths of {@link IamAwsProvider} added for EKS Pod Identity: the
* ECS/EKS link-local + loopback + HTTPS-bypass host check, authorization-token-file precedence, and
* the relative-URI path building. Each test drives the class only through its public API ({@link
* IamAwsProvider#fetch} and the constructor's {@code customHttpClient} parameter): an OkHttp
* interceptor short-circuits the actual network call so the real link-local/loopback addresses are
* exercised without needing anything listening on them.
*/
public class IamAwsProviderTest {
private static final String SUCCESS_BODY =
"{\"AccessKeyID\":\"ak\",\"SecretAccessKey\":\"sk\",\"Token\":\"tok\",\"Code\":\"Success\"}";
private static final String[] PROPERTY_NAMES = {
"AWS_CONTAINER_CREDENTIALS_FULL_URI",
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"AWS_CONTAINER_AUTHORIZATION_TOKEN",
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE",
"AWS_WEB_IDENTITY_TOKEN_FILE",
};
@Rule public TemporaryFolder tmp = new TemporaryFolder();
private final Map<String, String> saved = new HashMap<>();
@After
public void restoreProperties() {
for (String name : PROPERTY_NAMES) {
set(name, saved.remove(name));
}
}
private void set(String name, String value) {
if (value == null) {
System.clearProperty(name);
} else {
System.setProperty(name, value);
}
}
private void withProperty(String name, String value) {
saved.putIfAbsent(name, System.getProperty(name));
set(name, value);
}
/** Returns a client that never touches the network; it records the request and replies. */
private OkHttpClient capturingClient(AtomicReference<Request> captured, String responseBody) {
Interceptor interceptor =
chain -> {
captured.set(chain.request());
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(200)
.message("OK")
.body(ResponseBody.create(responseBody, MediaType.parse("application/json")))
.build();
};
return new OkHttpClient.Builder().addInterceptor(interceptor).build();
}
@Test
public void fullUriAllowsEcsLinkLocalAddressOverHttp() {
withProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://169.254.170.2/v2/credentials/abc");
AtomicReference<Request> captured = new AtomicReference<>();
IamAwsProvider provider = new IamAwsProvider(null, capturingClient(captured, SUCCESS_BODY));
Credentials creds = provider.fetch();
Assert.assertEquals("ak", creds.accessKey());
Assert.assertEquals("http://169.254.170.2/v2/credentials/abc", captured.get().url().toString());
}
@Test
public void fullUriAllowsEksPodIdentityLinkLocalAddressOverHttp() {
withProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://169.254.170.23/v2/credentials/abc");
AtomicReference<Request> captured = new AtomicReference<>();
IamAwsProvider provider = new IamAwsProvider(null, capturingClient(captured, SUCCESS_BODY));
Credentials creds = provider.fetch();
Assert.assertEquals("ak", creds.accessKey());
}
@Test
public void fullUriAllowsLoopbackOverHttp() {
withProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://127.0.0.1:12345/creds");
AtomicReference<Request> captured = new AtomicReference<>();
IamAwsProvider provider = new IamAwsProvider(null, capturingClient(captured, SUCCESS_BODY));
Credentials creds = provider.fetch();
Assert.assertEquals("ak", creds.accessKey());
}
@Test
public void fullUriRejectsPublicHostOverHttp() {
// 203.0.113.10 is a documentation-only address (RFC 5737 TEST-NET-3): never routable, so this
// stays offline and deterministic while standing in for an arbitrary attacker-chosen host.
withProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://203.0.113.10/creds");
IamAwsProvider provider =
new IamAwsProvider(null, capturingClient(new AtomicReference<>(), SUCCESS_BODY));
ProviderException e = Assert.assertThrows(ProviderException.class, provider::fetch);
Assert.assertTrue(e.getMessage().contains("neither loopback"));
}
@Test
public void fullUriAllowsArbitraryHostOverHttps() {
withProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI", "https://arbitrary.example.invalid/creds");
AtomicReference<Request> captured = new AtomicReference<>();
IamAwsProvider provider = new IamAwsProvider(null, capturingClient(captured, SUCCESS_BODY));
Credentials creds = provider.fetch();
Assert.assertEquals("ak", creds.accessKey());
}
@Test
public void fullUriMalformedValueThrowsProviderExceptionNotNpe() {
withProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI", "not a valid uri ###");
IamAwsProvider provider =
new IamAwsProvider(null, capturingClient(new AtomicReference<>(), SUCCESS_BODY));
ProviderException e = Assert.assertThrows(ProviderException.class, provider::fetch);
Assert.assertTrue(e.getMessage().contains("AWS_CONTAINER_CREDENTIALS_FULL_URI"));
}
@Test
public void relativeUriBuildsSinglePathSeparator() {
withProperty("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/v2/credentials/abc-def");
AtomicReference<Request> captured = new AtomicReference<>();
IamAwsProvider provider = new IamAwsProvider(null, capturingClient(captured, SUCCESS_BODY));
provider.fetch();
Assert.assertEquals(
"http://169.254.170.2/v2/credentials/abc-def", captured.get().url().toString());
}
@Test
public void authorizationTokenFileTakesPrecedenceOverEnvVar() throws Exception {
Path tokenFile = tmp.newFile("token").toPath();
Files.write(tokenFile, "file-token\n".getBytes(StandardCharsets.UTF_8));
withProperty("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", tokenFile.toString());
withProperty("AWS_CONTAINER_AUTHORIZATION_TOKEN", "env-token");
withProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://169.254.170.2/v2/credentials/abc");
AtomicReference<Request> captured = new AtomicReference<>();
IamAwsProvider provider = new IamAwsProvider(null, capturingClient(captured, SUCCESS_BODY));
provider.fetch();
Assert.assertEquals("file-token", captured.get().header("Authorization"));
}
@Test
public void authorizationTokenFallsBackToEnvVarWhenFileUnset() {
withProperty("AWS_CONTAINER_AUTHORIZATION_TOKEN", "env-token");
withProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI", "http://169.254.170.2/v2/credentials/abc");
AtomicReference<Request> captured = new AtomicReference<>();
IamAwsProvider provider = new IamAwsProvider(null, capturingClient(captured, SUCCESS_BODY));
provider.fetch();
Assert.assertEquals("env-token", captured.get().header("Authorization"));
}
} |
These tests do not give values in real world. As it works only inside AWS environment, it is hard to guarantee by unit tests. |
EKS Pod Identity Agent exposes credentials over a container credential endpoint; it injects AWS_CONTAINER_CREDENTIALS_FULL_URI pointing to its link-local address and passes the authorization token in a file. Two gaps denied it.
Along with it, AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is parsed as URI than added as path segments; AWS sets the value with leading slash which resulted in an empty path segment like http://169.254.170.2//v2/xxx. A malformed value of either variable raises ProviderException than NPE.
Fixes #1717
Summary by CodeRabbit
New Features
Bug Fixes