Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client library doesn't support AWS IaM authentication. #315

Closed
tvishwanatharkin opened this issue Jul 17, 2018 · 19 comments
Closed

Client library doesn't support AWS IaM authentication. #315

tvishwanatharkin opened this issue Jul 17, 2018 · 19 comments
Labels
lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale.

Comments

@tvishwanatharkin
Copy link

With https://github.com/kubernetes-sigs/aws-iam-authenticator gaining more popularity and also being adopted by AWS to support EKS, we may need to have webhook based authentication implemented

@brendandburns
Copy link
Contributor

brendandburns commented Jul 19, 2018

@brendandburns brendandburns changed the title Does this client support Webhook based authentication? Client library doesn't support AWS IaM authentication. Jul 19, 2018
@christopherhein
Copy link

FYI is a dupe - #238 with the umbrella issue - kubernetes/kubernetes#62185

@CloudDevBe
Copy link

@christopherhein @brendandburns @tvishwanath-arkin Is anyone able to use AWS IAM authentication? I was able to connect to GKE cluster via GCP authenticator. I was looking for AWS authenticator which will connect to AWS EKS cluster, similar to GCP and Azure authenticator

@hluchej
Copy link

hluchej commented Mar 20, 2019

Isn't here someone from AWS who could contribute and make it work? We are now trying to switch to EKS and burning time on this limitation.

I would expect AWS contribution to allow its users to actually use EKS fully...

@jikuhn
Copy link

jikuhn commented Mar 20, 2019

I've successfully implemented EksAuthenticator with heavy reuse of code from this page: https://telegraphhillsoftware.com/awseksk8sclientauth/

Here is the code:

import com.amazonaws.DefaultRequest;
import com.amazonaws.Request;
import com.amazonaws.auth.AWS4Signer;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.DefaultAWSCredentialsProviderChain;
import com.amazonaws.http.HttpMethodName;
import com.google.common.io.BaseEncoding;
import io.kubernetes.client.util.authenticators.Authenticator;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Workaround for this issue:
 * https://github.com/kubernetes-client/java/issues/315
 */
class EksAuthenticator implements Authenticator {
    @Override
    public String getName() {
        return "eks";
    }

    @Override
    public String getToken(Map<String, Object> config) {
        return (String) config.get("token");
    }

    @Override
    public boolean isExpired(Map<String, Object> config) {
        if (config.get("token") == null) {
            return true;
        }

        // inspired by GCPAuthenticator
        Object expiryObj = config.get("expiry");
        if (expiryObj instanceof String) {
            Instant expiry = Instant.parse((String) expiryObj);
            return (expiry != null && expiry.compareTo(Instant.now()) <= 0);
        } else {
            throw new RuntimeException("Unexpected object type: " + expiryObj.getClass() + ", expiry value: " + expiryObj);
        }
    }

    /**
     * https://telegraphhillsoftware.com/awseksk8sclientauth/
     */
    @Override
    public Map<String, Object> refresh(Map<String, Object> config) {
        String clusterName = (String) config.get("cluster-name");
        if (clusterName == null) {
            throw new RuntimeException("cluster-name missing in auth-provider configuration in kube config file");
        }

        Request<Void> request = new DefaultRequest<Void>("sts");
        request.setHttpMethod(HttpMethodName.GET);
        request.setEndpoint(URI.create("https://sts.amazonaws.com/"));

        request.addParameter("Action", "GetCallerIdentity");
        request.addParameter("Version", "2011-06-15");
        request.addHeader("x-k8s-aws-id", clusterName);
        AWS4Signer signer = new AWS4Signer();
        signer.setServiceName("sts");

        Instant expiry = Instant.now().plusSeconds(60); // must be <= 60 seconds

        AWSCredentialsProvider credentials = new DefaultAWSCredentialsProviderChain();
        signer.presignRequest(request, credentials.getCredentials(), new Date(expiry.toEpochMilli()));

        StringBuilder sb = new StringBuilder("https://sts.amazonaws.com/");

        AtomicInteger count = new AtomicInteger(0);
        request.getParameters().forEach((k, v) -> {
            try {
                sb.append(count.getAndIncrement() == 0 ? "?" : "&");
                sb.append(URLEncoder.encode(k, "utf-8"));
                sb.append("=");
                sb.append(URLEncoder.encode(v.get(0), "utf-8"));
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        });

        config.put("token", "k8s-aws-v1." + BaseEncoding.base64Url().omitPadding().encode(sb.toString().getBytes()));
        config.put("expiry", expiry.toString());
        return config;
    }
}

I've pasted the code here because I don't consider it as a final solution of the problem. The only advantage is that it doesn't require any modification of client-java library itself (I've used v4.0.0). The drawback is, that it's incompatible with kube config file generated by aws eks update-kubeconfig. The typical content of the kube config file for user section is this:

kind: Config
preferences: {}
users:
- name: arn:aws:eks:eu-west-1:1234567890:cluster/my-cluster-name
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - token
      - -i
      - my-cluster-name
      command: aws-iam-authenticator

Whereas EksAuthenticator java code requires this content in the kube config file:

kind: Config
preferences: {}
users:
- name: aws
  user:
    auth-provider:
      name: eks
      config:
        cluster-name: my-cluster-name
        token:
        expiry:

The auth-provider section is required by the library, see KubeConfig.getAccessToken().

These two configurations cannot live together what requires you to keep two versions of the config file (one for kubectl, one for EksAuthenticator).

To support this solution it would be nice to have yet another environment variable apart of KUBECONFIG which is also used by kubectl.

But obviously, better solution would be to work out of the box just after calling aws eks update-kubeconfig.

@Raj725
Copy link

Raj725 commented May 27, 2019

@hluchej Thanks for sharing this.
Can you please share a basic program to use this. I tried this in several ways but no success. I am still getting {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}

@Raj725
Copy link

Raj725 commented May 27, 2019

I am not able to set two versions of the config file

@jikuhn
Copy link

jikuhn commented May 27, 2019

Did you use the KUBECONFIG environment variable to point to the manually created k8s configuration file? Something like:
export KUBECONFIG=/home/xyz/k8s-java.config

FYI: I've updated the code, the resulting base64 part of the token should have the padding omitted.

@pcj
Copy link

pcj commented Aug 22, 2019

Any update on this? My team is trying to determine the work involved in getting the java client (this codebase) to talk with an EKS cluster.

Can anyone of the core maintainers (or anyone else) comment on what would be the correct approach/solution and the level of effort required to have this working correctly? Happy to contribute a PR if needed.

@sillva
Copy link

sillva commented Sep 5, 2019

It took me a while but it finally worked.

A tip in case you're struggling to know why you're getting an Unauthorized response from AWS. Make sure you enable the logging in your EKS cluster so that you can see your authentication attempts in CloudTrail and find out why you got that.

For those who are using AWS java SDK2, here goes the code:

import com.google.common.base.Preconditions;
import com.google.common.io.BaseEncoding;

import io.kubernetes.client.util.authenticators.Authenticator;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.signer.Aws4Signer;
import software.amazon.awssdk.auth.signer.params.Aws4PresignerParams;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.regions.Region;

import java.time.Instant;
import java.util.Map;

/**
 * @author gui
 * Workaround for this issue:
 * https://github.com/kubernetes-client/java/issues/315
 */
public class EksAuthenticator implements Authenticator {

  private String clusterName;
  private AwsBasicCredentials awsCredentials;

  private EksAuthenticator() {}

  public EksAuthenticator(EksConfig config) {
    Preconditions.checkNotNull(config, "Config cannot be null");

    this.clusterName = config.getClusterName();
    this.awsCredentials = AwsBasicCredentials.create(config.getAccessKeyId(), config.getSecretAccessKey());
  }

  @Override
  public String getName() {
    return "eks";
  }

  @Override
  public String getToken(Map<String, Object> config) {
    return (String) config.get("token");
  }

  @Override
  public boolean isExpired(Map<String, Object> config) {
    if (config.get("token") == null) {
      return true;
    }

    // inspired by GCPAuthenticator
    Object expiryObj = config.get("expiry");
    if (expiryObj instanceof String) {
      Instant expiry = Instant.parse((String) expiryObj);
      return (expiry != null && expiry.compareTo(Instant.now()) <= 0);
    } else {
      throw new RuntimeException("Unexpected object type: " + expiryObj.getClass() + ", expiry value: " + expiryObj);
    }
  }

  /**
   * https://telegraphhillsoftware.com/awseksk8sclientauth/
   */
  @Override
  public Map<String, Object> refresh(Map<String, Object> config) {
    String clusterName = (String) config.get("cluster-name");
    if (clusterName == null) {
      throw new RuntimeException("cluster-name missing in auth-provider configuration in kube config file");
    }

    SdkHttpFullRequest request = SdkHttpFullRequest.builder()
        .protocol("https")
        .method(SdkHttpMethod.GET)
        .host("sts.amazonaws.com")
        .encodedPath("/")
        .appendHeader("x-k8s-aws-id", clusterName)
        .appendRawQueryParameter("Action", "GetCallerIdentity")
        .appendRawQueryParameter("Version", "2011-06-15").build();

    Instant expiry = Instant.now().plusSeconds(60); // must be <= 60 seconds

    Aws4PresignerParams params = Aws4PresignerParams
        .builder()
        .awsCredentials(awsCredentials)
        .expirationTime(expiry)
        .signingName("sts")
        .signingRegion(Region.US_EAST_1)
        .build();

    Aws4Signer signer = Aws4Signer.create();
    SdkHttpFullRequest presign = signer.presign(request, params);

    config.put("token", "k8s-aws-v1." + BaseEncoding.base64Url().omitPadding().encode(presign.getUri().toString().getBytes()));
    config.put("expiry", expiry.toString());

    return config;
  }
}

@mike-allcode
Copy link

Is there any progress on this?.

@sillva, would you mind to explain how you hooked this up so the API uses this authenticator?, Thanks in advance.

@saskiawagenaar
Copy link
Contributor

Looks like this issue is resolved via #512 which gives generic support for the exec construction in ~/.kube/config.
I've upgraded our client version to 5.0.0 and I can connect successfully using this syntax in my ~/.kube/config:

kind: Config
preferences: {}
users:
- name: arn:aws:eks:eu-west-1:1234567890:cluster/my-cluster-name
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - token
      - -i
      - my-cluster-name
      command: aws-iam-authenticator

@sillva
Copy link

sillva commented Oct 20, 2019

Is there any progress on this?.

@sillva, would you mind to explain how you hooked this up so the API uses this authenticator?, Thanks in advance.

Just register the Authenticator class using the static method KubeConfig.registerAuthenticator.

private void createAuthenticator(AwsCredentialsProvider credentialsProvider) {
    EksConfig eksConfig = EksConfig
        .builder()
        .clusterName(config.getString("kubewatcher.eks.clusterName"))
        .accessKeyId(credentialsProvider.resolveCredentials().accessKeyId())
        .secretAccessKey(credentialsProvider.resolveCredentials().secretAccessKey())
        .build();

    KubeConfig.registerAuthenticator(new EksAuthenticator(eksConfig));
  }

The object eksConfig is just a data object created by me.

@sillva
Copy link

sillva commented Oct 20, 2019

Looks like this issue is resolved via #512 which gives generic support for the exec construction in ~/.kube/config.
I've upgraded our client version to 5.0.0 and I can connect successfully using this syntax in my ~/.kube/config:

kind: Config
preferences: {}
users:
- name: arn:aws:eks:eu-west-1:1234567890:cluster/my-cluster-name
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - token
      - -i
      - my-cluster-name
      command: aws-iam-authenticator

Correct but in your example the docker image would need to have the aws-iam-authenticator on its path.

@rohinwork
Copy link

@sillva @jikuhn Thanks a lot for the resources! I was able to get it to work. I was wondering if the code handles scenarios when the token is expired automatically?

@jikuhn
Copy link

jikuhn commented Dec 17, 2019

@rohinwork, I don't know exactly what do you mean by automatical expiration, but the token contains validity time information and the code handles this.

On the other hand, as @sillva pointed out, the #512 should solve the problem. I gave it a try and it's really working! Thanks to it, this issue seems obsolete for me now.

@fejta-bot
Copy link

Issues go stale after 90d of inactivity.
Mark the issue as fresh with /remove-lifecycle stale.
Stale issues rot after an additional 30d of inactivity and eventually close.

If this issue is safe to close now please do so with /close.

Send feedback to sig-testing, kubernetes/test-infra and/or fejta.
/lifecycle stale

@k8s-ci-robot k8s-ci-robot added the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. label Mar 16, 2020
@brendandburns
Copy link
Contributor

Closing this issue since it is working via the exec implementation.

@srinivasev
Copy link

Hi @brendandburns ,
Can you please share the link of the code snipped for this -- Closing this issue since it is working via the exec implementation.

I am trying to understand whether a token can be fetched using AWS IAM access key ID and Secret access key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale.
Projects
None yet
Development

No branches or pull requests