Skip to content

Commit

Permalink
ECS Credential Provider Emulation (#11)
Browse files Browse the repository at this point in the history
* ECS metadata service support

* Handle credential expiry ; add readme

* Minor readme change
  • Loading branch information
castrapel authored Oct 28, 2020
1 parent 83a6731 commit 56e5696
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 47 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,43 @@ sudo /sbin/iptables-restore < <path_to_file>.txt

## Usage

### ECS Credential Provider (Recommended)

Weep supports emulating the ECS credential provider to provide credentials to your AWS SDK. This solution can be
minimally configured by setting the `AWS_CONTAINER_CREDENTIALS_FULL_URI` environment variable for your process. There's
no need for iptables or routing rules with this approach, and each different shell or process can use weep to request
credentials for different roles. Weep will cache the credentials you request in-memory, and will refresh them on-demand
when they are within 10 minutes of expiring.

In one shell, run weep:

```bash
weep ecs_credential_provider
```

In your favorite IDE or shell, set the `AWS_CONTAINER_CREDENTIALS_FULL_URI` environment variable and run AWS commands.

```bash
AWS_CONTAINER_CREDENTIALS_FULL_URI=http://localhost:9090/ecs/consoleme_oss_1 aws sts get-caller-identity
{
"UserId": "AROA4JEFLERSKVPFT4INI:[email protected]",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/consoleme_oss_1_test_user/[email protected]"
}

AWS_CONTAINER_CREDENTIALS_FULL_URI=http://localhost:9090/ecs/consoleme_oss_2 aws sts get-caller-identity
{
"UserId": "AROA6KW3MOV2F7J6AT4PC:[email protected]",
"Account": "223456789012",
"Arn": "arn:aws:sts::223456789012:assumed-role/consoleme_oss_2_test_user/[email protected]"
}
```

### Metadata Proxy

Weep supports emulating the instance metadata service. This requires that you have iptables DNAT rules configured, and
it only serves one role per weep process.

```bash
# You can use a full ARN
weep metadata arn:aws:iam::123456789012:role/exampleRole
Expand Down
56 changes: 56 additions & 0 deletions cmd/ecs_credential_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package cmd

import (
"fmt"
"net"
"net/http"
"os"
"os/signal"
"syscall"

"github.com/gorilla/mux"
"github.com/netflix/weep/handlers"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func init() {
ecsCredentialProvider.PersistentFlags().StringVarP(&metadataListenAddr, "listen-address", "a", "127.0.0.1", "IP address for the ECS credential provider to listen on")
ecsCredentialProvider.PersistentFlags().IntVarP(&metadataListenPort, "port", "p", 9090, "port for the ECS credential provider service to listen on")
rootCmd.AddCommand(ecsCredentialProvider)
}

var ecsCredentialProvider = &cobra.Command{
Use: "ecs_credential_provider",
Short: "Run a local ECS Credential Provider endpoint that serves and caches credentials for roles on demand",
RunE: runEcsMetadata,
}

func runEcsMetadata(cmd *cobra.Command, args []string) error {
ipaddress := net.ParseIP(metadataListenAddr)

if ipaddress == nil {
fmt.Println("Invalid IP: ", metadataListenAddr)
os.Exit(1)
}

listenAddr := fmt.Sprintf("%s:%d", ipaddress, metadataListenPort)

router := mux.NewRouter()
router.HandleFunc("/ecs/{role:.*}", handlers.MetaDataServiceMiddleware(handlers.ECSMetadataServiceCredentialsHandler))
router.HandleFunc("/{path:.*}", handlers.MetaDataServiceMiddleware(handlers.CustomHandler))

go func() {
log.Info("Starting weep ECS meta-data service...")
log.Info("Server started on: ", listenAddr)
log.Info(http.ListenAndServe(listenAddr, router))
}()

// Check for interrupt signal and exit cleanly
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Print("Shutdown signal received, exiting weep...")

return nil
}
52 changes: 52 additions & 0 deletions consoleme/consoleme.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
AwsSdkCredentials "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/netflix/weep/util"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"runtime"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -217,9 +224,54 @@ func (c *Client) GetRoleCredentials(role string, ipRestrict bool) (AwsCredential
return credentials.Credentials, errors.Wrap(err, "failed to unmarshal JSON")
}

credentials.Credentials.RoleArn, err = getRoleArnFromCredentials(credentials.Credentials)

return credentials.Credentials, nil
}

func getRoleArnFromCredentials(credentials AwsCredentials) (string, error) {
sess, err := session.NewSession(&aws.Config{
Credentials: AwsSdkCredentials.NewStaticCredentials(
credentials.AccessKeyId,
credentials.SecretAccessKey,
credentials.SessionToken),
})
util.CheckError(err)
svc := sts.New(sess)
input := &sts.GetCallerIdentityInput{}

result, err := svc.GetCallerIdentity(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return "", err
}
// Replace assumed role ARN with role ARN, if possible
// arn:aws:sts::123456789012:assumed-role/exampleInstanceProfile/[email protected] ->
// arn:aws:iam::123456789012:role/exampleInstanceProfile
Role := strings.Replace(*result.Arn, ":sts:", ":iam:", 1)
Role = strings.Replace(Role, ":assumed-role/", ":role/", 1)
// result.UserId looks like AROAIEBAVBLAH:[email protected]
splittedUserId := strings.Split(*result.UserId, ":")
if len(splittedUserId) > 1 {
sessionName := splittedUserId[1]
Role = strings.Replace(
Role,
fmt.Sprintf("/%s", sessionName),
"",
1)
}
return Role, nil
}

func defaultTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
Expand Down
1 change: 1 addition & 0 deletions consoleme/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type AwsCredentials struct {
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken"`
Expiration int64 `json:"Expiration"`
RoleArn string `json:"RoleArn"`
}

type CredentialProcess struct {
Expand Down
68 changes: 68 additions & 0 deletions handlers/ecsCredentialsHandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package handlers

import (
"bytes"
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"github.com/netflix/weep/consoleme"
"github.com/netflix/weep/metadata"
log "github.com/sirupsen/logrus"
"net/http"
"time"
)

var credentialMap = make(map[string]consoleme.AwsCredentials)

func ECSMetadataServiceCredentialsHandler(w http.ResponseWriter, r *http.Request) {
var client, err = consoleme.GetClient()
if err != nil {
log.Error(err)
return
}
vars := mux.Vars(r)
requestedRole := vars["role"]
var Credentials consoleme.AwsCredentials

val, ok := credentialMap[requestedRole]
if ok {
Credentials = val

// Refresh credentials on demand if expired or within 10 minutes of expiry
currentTime := time.Now()
tm := time.Unix(Credentials.Expiration, 0)
timeToRenew := tm.Add(-10 * time.Minute)
if currentTime.After(timeToRenew) {
Credentials, err = client.GetRoleCredentials(requestedRole, false)
if err != nil {
log.Error(err)
return
}
}
} else {
Credentials, err = client.GetRoleCredentials(requestedRole, false)
if err != nil {
log.Error(err)
return
}
credentialMap[requestedRole] = Credentials
}

tm := time.Unix(Credentials.Expiration, 0)

credentials := metadata.ECSMetaDataCredentialResponse{
AccessKeyId: fmt.Sprintf("%s", Credentials.AccessKeyId),
Expiration: tm.UTC().Format("2006-01-02T15:04:05Z"),
RoleArn: Credentials.RoleArn,
SecretAccessKey: fmt.Sprintf("%s", Credentials.SecretAccessKey),
Token: fmt.Sprintf("%s", Credentials.SessionToken),
}

b, err := json.Marshal(credentials)
if err != nil {
log.Error(err)
}
var out bytes.Buffer
json.Indent(&out, b, "", " ")
fmt.Fprintln(w, out.String())
}
2 changes: 1 addition & 1 deletion handlers/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func MetaDataServiceMiddleware(next http.HandlerFunc) http.HandlerFunc {
"user-agent": ua,
"path": r.URL.Path,
"metadata_version": metadataVersion,
}).Info("You are using a SDK that does not support User-Agents that Netflix wants")
}).Info("You are using a SDK that is not passing an appropriate AWS User-Agent")
} else {
log.WithFields(log.Fields{
"user-agent": ua,
Expand Down
47 changes: 1 addition & 46 deletions metadata/metadata.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
package metadata

import (
"fmt"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/netflix/weep/util"
log "github.com/sirupsen/logrus"

Expand All @@ -32,45 +25,7 @@ func StartMetaDataRefresh(client *consoleme.Client) {
// TODO: If 403 response,
MetaDataCredentials, err = client.GetRoleCredentials(Role, NoIpRestrict)
util.CheckError(err)
sess, err := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials(
MetaDataCredentials.AccessKeyId,
MetaDataCredentials.SecretAccessKey,
MetaDataCredentials.SessionToken),
})
util.CheckError(err)
svc := sts.New(sess)
input := &sts.GetCallerIdentityInput{}

result, err := svc.GetCallerIdentity(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
default:
fmt.Println(aerr.Error())
}
} else {
// Print the error, cast err to awserr.Error to get the Code and
// Message from an error.
fmt.Println(err.Error())
}
return
}
// Replace assumed role ARN with role ARN, if possible
// arn:aws:sts::123456789012:assumed-role/exampleInstanceProfile/[email protected] ->
// arn:aws:iam::123456789012:role/exampleInstanceProfile
Role = strings.Replace(*result.Arn, ":sts:", ":iam:", 1)
Role = strings.Replace(Role, ":assumed-role/", ":role/", 1)
// result.UserId looks like AROAIEBAVBLAH:[email protected]
splittedUserId := strings.Split(*result.UserId, ":")
if len(splittedUserId) > 1 {
sessionName := splittedUserId[1]
Role = strings.Replace(
Role,
fmt.Sprintf("/%s", sessionName),
"",
1)
}
Role = MetaDataCredentials.RoleArn
if err != nil {
log.Error(err)
time.Sleep(retryDelay)
Expand Down
8 changes: 8 additions & 0 deletions metadata/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ type MetaDataCredentialResponse struct {
Expiration string
}

type ECSMetaDataCredentialResponse struct {
AccessKeyId string
SecretAccessKey string
Token string
Expiration string
RoleArn string
}

type MetaDataIamInfoResponse struct {
Code string `json:"Code"`
LastUpdated string `json:"LastUpdated"`
Expand Down

0 comments on commit 56e5696

Please sign in to comment.