-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ECS Credential Provider Emulation (#11)
* ECS metadata service support * Handle credential expiry ; add readme * Minor readme change
- Loading branch information
Showing
8 changed files
with
222 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
||
|
@@ -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, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
||
|
@@ -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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters