15

How can I get a Kubernetes authentication token from AWS EKS using the AWS Java SDK v2? An authentication token that can then be used to authenticate with Kubernetes using a Kubernetes SDK. In other words I want to get an authentication token from EKS to use for authentication with Kubernetes so that I don't have to create a "kube config".

I actually got a solution working with AWS Java SDK v1 (not v2) looking at the code examples in the following open issue. There is also a Python code example here BUT I'm not having any success with AWS Java SDK v2. My attempt at doing it with AWS Java SDK v2:

public static String getAuthenticationToken(AwsCredentialsProvider awsAuth, Region awsRegion, String clusterName) {
    try {
        SdkHttpFullRequest requestToSign = SdkHttpFullRequest
                .builder()
                .method(SdkHttpMethod.GET)
                .uri(new URI("https", String.format("sts.%s.amazonaws.com", awsRegion.id()), null, null))
                .appendHeader("x-k8s-aws-id", clusterName)
                .appendRawQueryParameter("Action", "GetCallerIdentity")
                .appendRawQueryParameter("Version", "2011-06-15")
                .build();

        ZonedDateTime expirationDate = DateUtil.addSeconds(DateUtil.now(), 60);
        Aws4PresignerParams presignerParams = Aws4PresignerParams.builder()
                .awsCredentials(awsAuth.resolveCredentials())
                .expirationTime(expirationDate.toInstant())
                .signingName("sts")
                .signingRegion(awsRegion)
                .build();

        SdkHttpFullRequest signedRequest = Aws4Signer.create().presign(requestToSign, presignerParams);

        String encodedUrl = Base64.getUrlEncoder().withoutPadding().encodeToString(signedRequest.getUri().toString().getBytes(CharSet.UTF_8.getCharset()));
        return ("k8s-aws-v1." + encodedUrl);
    } catch (Exception e) {
        String errorMessage = "A problem occurred generating an Eks token";
        logger.error(errorMessage, e);
        throw new RuntimeException(errorMessage, e);
    }
}

It generates a token, but when I use the token in my Kubernetes client (the official Java Kubernetes SDK) I get back an "Unauthorized" response - so I'm missing something I can't put my finger on...

The AWS Java SDK v1 version looks something like this: (From the open issue mentioned earlier)

I got it working, but I'm struggling to get something similar to work in AWS Java SDK v2.

private String generateToken(String clusterName,
                                 Date expirationDate,
                                 String serviceName,
                                 String region,
                                 AWSSecurityTokenServiceClient awsSecurityTokenServiceClient,
                                 AWSCredentialsProvider credentialsProvider,
                                 String scheme,
                                 String host) throws URISyntaxException {
        try {
            DefaultRequest<GetCallerIdentityRequest> callerIdentityRequestDefaultRequest = new DefaultRequest<>(new GetCallerIdentityRequest(), serviceName);
            URI uri = new URI(scheme, host, null, null);
            callerIdentityRequestDefaultRequest.setResourcePath("/");
            callerIdentityRequestDefaultRequest.setEndpoint(uri);
            callerIdentityRequestDefaultRequest.setHttpMethod(HttpMethodName.GET);
            callerIdentityRequestDefaultRequest.addParameter("Action", "GetCallerIdentity");
            callerIdentityRequestDefaultRequest.addParameter("Version", "2011-06-15");
            callerIdentityRequestDefaultRequest.addHeader("x-k8s-aws-id", clusterName);

            Signer signer = SignerFactory.createSigner(SignerFactory.VERSION_FOUR_SIGNER, new SignerParams(serviceName, region));
            SignerProvider signerProvider = new DefaultSignerProvider(awsSecurityTokenServiceClient, signer);
            PresignerParams presignerParams = new PresignerParams(uri,
                    credentialsProvider,
                    signerProvider,
                    SdkClock.STANDARD);

            PresignerFacade presignerFacade = new PresignerFacade(presignerParams);
            URL url = presignerFacade.presign(callerIdentityRequestDefaultRequest, expirationDate);
            String encodedUrl = Base64.getUrlEncoder().withoutPadding().encodeToString(url.toString().getBytes());
            log.info("Token [{}]", encodedUrl);
            return "k8s-aws-v1." + encodedUrl;
        } catch (URISyntaxException e) {
            log.error("could not generate token", e);
            throw e;
        }
    }
ti7
  • 16,375
  • 6
  • 40
  • 68
dutoitns
  • 1,949
  • 1
  • 26
  • 32
  • As indicated in the AWS Java SDK v1 issue, the implementation is sensitive to specifying a too long expiration date. I did play around with the expiration date a bit, but it didn't resolve the issue. – dutoitns Feb 03 '20 at 07:58
  • did you tried using the aws-iam-authenticator utility to get tokens – Umesh Kumhar Feb 04 '20 at 17:00
  • I've used the aws-iam-authenticator before, but I need to be able to generate tokens from Java source code - without installing anything. And I have gotten this stuff to work with AWS Java SDK v1, just having problems with v2 of the SDK. – dutoitns Feb 05 '20 at 04:06
  • I am currently using the AWS Java SDK v1 to generate the token - but now I have to have it on my classpath :( As soon as I can figure this out I can refactor and remove v1 of the SDK from my dependencies :) – dutoitns Feb 05 '20 at 04:07
  • What Kubernetes version are you running? Where is this app meant to run (outside of cluster, inside of it)? – mewa Feb 05 '20 at 13:53
  • I'm using the latest version available on EKS, so that is Kubernetes version 1.14. The application needs to run outside of the cluster. – dutoitns Feb 05 '20 at 17:21

2 Answers2

8

Okay, I finally got it working.

The AWS Java SDK v2 version:

public static String getAuthenticationToken(AwsCredentialsProvider awsAuth, Region awsRegion, String clusterName) {
    try {    
        SdkHttpFullRequest requestToSign = SdkHttpFullRequest
                .builder()
                .method(SdkHttpMethod.GET)
                .uri(StsUtil.getStsRegionalEndpointUri(awsRegion))
                .appendHeader("x-k8s-aws-id", clusterName)
                .appendRawQueryParameter("Action", "GetCallerIdentity")
                .appendRawQueryParameter("Version", "2011-06-15")
                .build();

        ZonedDateTime expirationDate = DateUtil.addSeconds(DateUtil.now(), 60);
        Aws4PresignerParams presignerParams = Aws4PresignerParams.builder()
                .awsCredentials(awsAuth.resolveCredentials())
                .signingRegion(awsRegion)
                .signingName("sts")
                .signingClockOverride(Clock.systemUTC())
                .expirationTime(expirationDate.toInstant())
                .build();

        SdkHttpFullRequest signedRequest = Aws4Signer.create().presign(requestToSign, presignerParams);

        String encodedUrl = Base64.getUrlEncoder().withoutPadding().encodeToString(signedRequest.getUri().toString().getBytes(CharSet.UTF_8.getCharset()));
        return ("k8s-aws-v1." + encodedUrl);
    } catch (Exception e) {
        String errorMessage = "A problem occurred generating an Eks authentication token for cluster: " + clusterName;
        logger.error(errorMessage, e);
        throw new RuntimeException(errorMessage, e);
    }
}

The problem was in my STS endpoint Uri:

public static URI getStsRegionalEndpointUri(Region awsRegion) {
    try {
        return new URI("https", String.format("sts.%s.amazonaws.com", awsRegion.id()), "/", null);
    } catch (URISyntaxException shouldNotHappen) {
        String errorMessage = "An error occurred creating the STS regional endpoint Uri";
        logger.error(errorMessage, shouldNotHappen);
        throw new RuntimeException(errorMessage, shouldNotHappen);
    }
}

Note the / in the path (third) argument for the URI object. The AWS Java SDK v1 version didn't create the URI like that, but specified the / elsewhere. If I now print out the URI as a String I get https://sts.eu-west-1.amazonaws.com/, while the original version in the question just returned https://sts.eu-west-1.amazonaws.com

Interesting enough - the original version did also generate a token, but the token was rejected by Kubernetes. One should expect similar behavior if the expiration date is too far into the future - you'll get a token, but it will lead to an Unauthorized response from the Kubernetes service.

After changing the STS endpoint everything worked, but I made one more change:

I added the following line to my Aws4PresignerParams:

.signingClockOverride(Clock.systemUTC())

It wasn't required, but the original AWS Java SDK v1 did do something with a clock when it specified SdkClock.STANDARD, and the ZonedDateTime that I use in the AWS Java SDK v2 version does use the UTC timezone.

dutoitns
  • 1,949
  • 1
  • 26
  • 32
  • So yes, I got it working, but I don't have too much insight into the reasoning behind it. Even without the `/` I still got a token, but as indicated it just didn't work when I started to integrate with Kubernetes. – dutoitns Feb 13 '20 at 09:26
  • Just another interesting aside - the original AWS Java SDK v1 issue did indicate that the token is very short-lived. As soon as I try to set the expiration date to more than 60 seconds the same thing happens - I get a token, but it leads to an `Unauthorized` response. – dutoitns Feb 13 '20 at 10:06
  • Hi – how did you use the token to init the EKS client to connect to cluster? – Pankaj Agrawal Jun 15 '23 at 04:58
1

Just for completeness sake, in case you want to use the token with the Kubernetes client (io.kubernetes:client-java), you can add an interceptor that generates a valid token before every request:

  private ApiClient createClient(AwsCredentialsProvider awsAuth, Region awsRegion, String clusterName) {

    ApiClient client = Config.fromUrl(eksEndpoint, false);
    OkHttpClient httpClient = createHttpClient(client.getHttpClient(), awsAuth, awsRegion, clusterName);
    client.setHttpClient(httpClient);

    return client;
  }

  @NotNull
  private OkHttpClient createHttpClient(
      OkHttpClient baseHttpClient, AwsCredentialsProvider awsAuth, Region awsRegion, String clusterName
  ) {
    OkHttpClient.Builder builder = new OkHttpClient.Builder(baseHttpClient);
    builder.addInterceptor(chain -> {
      Request request = chain.request();
      String token = getAuthenticationToken(awsAuth, awsRegion, clusterName);
      Request newRequest = request
          .newBuilder()
          .header("Authorization", "Bearer " + token)
          .build();
      return chain.proceed(newRequest);
    });
    return builder.build();
  }
MosheElisha
  • 1,930
  • 2
  • 22
  • 27