Skip to content

Commit

Permalink
Add aws-vault export command to output creds in a variety of formats
Browse files Browse the repository at this point in the history
  • Loading branch information
mtibben committed Feb 16, 2023
1 parent 0126e8c commit f18ec54
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 57 deletions.
86 changes: 29 additions & 57 deletions cli/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cli

import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
Expand All @@ -23,30 +22,30 @@ import (
)

type ExecCommandInput struct {
ProfileName string
Command string
Args []string
StartEc2Server bool
StartEcsServer bool
Lazy bool
CredentialHelper bool
Config vault.Config
SessionDuration time.Duration
NoSession bool
UseStdout bool
ProfileName string
Command string
Args []string
StartEc2Server bool
StartEcsServer bool
Lazy bool
JSONDeprecated bool
Config vault.Config
SessionDuration time.Duration
NoSession bool
UseStdout bool
}

func (input ExecCommandInput) validate() error {
if input.StartEc2Server && input.StartEcsServer {
return fmt.Errorf("Can't use --server with --ecs-server")
}
if input.StartEc2Server && input.CredentialHelper {
if input.StartEc2Server && input.JSONDeprecated {
return fmt.Errorf("Can't use --server with --json")
}
if input.StartEc2Server && input.NoSession {
return fmt.Errorf("Can't use --server with --no-session")
}
if input.StartEcsServer && input.CredentialHelper {
if input.StartEcsServer && input.JSONDeprecated {
return fmt.Errorf("Can't use --ecs-server with --json")
}
if input.StartEcsServer && input.NoSession {
Expand Down Expand Up @@ -84,7 +83,8 @@ func ConfigureExecCommand(app *kingpin.Application, a *AwsVault) {

cmd.Flag("json", "Output credentials in JSON that can be used by credential_process").
Short('j').
BoolVar(&input.CredentialHelper)
Hidden().
BoolVar(&input.JSONDeprecated)

cmd.Flag("server", "Alias for --ec2-server. Run a EC2 metadata server in the background for credentials").
Short('s').
Expand Down Expand Up @@ -129,7 +129,20 @@ func ConfigureExecCommand(app *kingpin.Application, a *AwsVault) {
return err
}

err = ExecCommand(input, f, keyring)
if input.JSONDeprecated {
exportCommandInput := ExportCommandInput{
ProfileName: input.ProfileName,
Format: "json",
Config: input.Config,
SessionDuration: input.SessionDuration,
NoSession: input.NoSession,
}

err = ExportCommand(exportCommandInput, f, keyring)
} else {
err = ExecCommand(input, f, keyring)
}

app.FatalIfError(err, "exec")
return nil
})
Expand Down Expand Up @@ -171,10 +184,6 @@ func ExecCommand(input ExecCommandInput, f *vault.ConfigFile, keyring keyring.Ke
return execEcsServer(input, config, credsProvider)
}

if input.CredentialHelper {
return execCredentialHelper(input, credsProvider)
}

return execEnvironment(input, config, credsProvider)
}

Expand Down Expand Up @@ -231,43 +240,6 @@ func execEcsServer(input ExecCommandInput, config *vault.Config, credsProvider a
return execCmd(input.Command, input.Args, env)
}

func execCredentialHelper(input ExecCommandInput, credsProvider aws.CredentialsProvider) error {
// AwsCredentialHelperData is metadata for AWS CLI credential process
// See https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes
type AwsCredentialHelperData struct {
Version int `json:"Version"`
AccessKeyID string `json:"AccessKeyId"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken,omitempty"`
Expiration string `json:"Expiration,omitempty"`
}

creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err)
}

credentialData := AwsCredentialHelperData{
Version: 1,
AccessKeyID: creds.AccessKeyID,
SecretAccessKey: creds.SecretAccessKey,
SessionToken: creds.SessionToken,
}

if creds.CanExpire {
credentialData.Expiration = iso8601.Format(creds.Expires)
}

json, err := json.Marshal(&credentialData)
if err != nil {
return fmt.Errorf("Error creating credential json: %w", err)
}

fmt.Print(string(json))

return nil
}

func execEnvironment(input ExecCommandInput, config *vault.Config, credsProvider aws.CredentialsProvider) error {
creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
Expand Down
192 changes: 192 additions & 0 deletions cli/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package cli

import (
"context"
"encoding/json"
"fmt"
"os"
"time"

"github.com/99designs/aws-vault/v7/iso8601"
"github.com/99designs/aws-vault/v7/vault"
"github.com/99designs/keyring"
"github.com/alecthomas/kingpin"
"github.com/aws/aws-sdk-go-v2/aws"
)

type ExportCommandInput struct {
ProfileName string
Format string
Config vault.Config
SessionDuration time.Duration
NoSession bool
UseStdout bool
}

var (
FormatTypeEnv = "env"
FormatTypeExportEnv = "export-env"
FormatTypeExportJSON = "json"
FormatTypeExportINI = "ini"
)

func ConfigureExportCommand(app *kingpin.Application, a *AwsVault) {
input := ExportCommandInput{}

cmd := app.Command("export", "Export AWS credentials in env, export-env, json or ini format")

cmd.Flag("duration", "Duration of the temporary or assume-role session. Defaults to 1h").
Short('d').
DurationVar(&input.SessionDuration)

cmd.Flag("no-session", "Skip creating STS session with GetSessionToken").
Short('n').
BoolVar(&input.NoSession)

cmd.Flag("region", "The AWS region").
StringVar(&input.Config.Region)

cmd.Flag("mfa-token", "The MFA token to use").
Short('t').
StringVar(&input.Config.MfaToken)

cmd.Flag("format", fmt.Sprintf("Format to output credentials. Valid values are %s, %s and %s", FormatTypeEnv, FormatTypeExportEnv, FormatTypeExportJSON)).
Default(FormatTypeEnv).
EnumVar(&input.Format, FormatTypeEnv, FormatTypeExportEnv, FormatTypeExportJSON, FormatTypeExportINI)

cmd.Flag("stdout", "Print the SSO link to the terminal without automatically opening the browser").
BoolVar(&input.UseStdout)

cmd.Arg("profile", "Name of the profile").
Required().
HintAction(a.MustGetProfileNames).
StringVar(&input.ProfileName)

cmd.Action(func(c *kingpin.ParseContext) (err error) {
input.Config.MfaPromptMethod = a.PromptDriver
input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration
input.Config.AssumeRoleDuration = input.SessionDuration
input.Config.SSOUseStdout = input.UseStdout

f, err := a.AwsConfigFile()
if err != nil {
return err
}
keyring, err := a.Keyring()
if err != nil {
return err
}

err = ExportCommand(input, f, keyring)
app.FatalIfError(err, "exec")
return nil
})
}

func ExportCommand(input ExportCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) error {
if os.Getenv("AWS_VAULT") != "" {
return fmt.Errorf("aws-vault sessions should be nested with care, unset AWS_VAULT to force")
}

vault.UseSession = !input.NoSession

configLoader := vault.ConfigLoader{
File: f,
BaseConfig: input.Config,
ActiveProfile: input.ProfileName,
}
config, err := configLoader.LoadFromProfile(input.ProfileName)
if err != nil {
return fmt.Errorf("Error loading config: %w", err)
}

ckr := &vault.CredentialKeyring{Keyring: keyring}
credsProvider, err := vault.NewTempCredentialsProvider(config, ckr)
if err != nil {
return fmt.Errorf("Error getting temporary credentials: %w", err)
}

if input.Format == FormatTypeExportJSON {
return printJSON(input, credsProvider)
} else if input.Format == FormatTypeExportINI {
return printINI(input, credsProvider)
} else if input.Format == FormatTypeExportEnv {
return printEnv(input, credsProvider, "export ")
} else {
return printEnv(input, credsProvider, "")
}
}

func printJSON(input ExportCommandInput, credsProvider aws.CredentialsProvider) error {
// AwsCredentialHelperData is metadata for AWS CLI credential process
// See https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes
type AwsCredentialHelperData struct {
Version int `json:"Version"`
AccessKeyID string `json:"AccessKeyId"`
SecretAccessKey string `json:"SecretAccessKey"`
SessionToken string `json:"SessionToken,omitempty"`
Expiration string `json:"Expiration,omitempty"`
}

creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err)
}

credentialData := AwsCredentialHelperData{
Version: 1,
AccessKeyID: creds.AccessKeyID,
SecretAccessKey: creds.SecretAccessKey,
SessionToken: creds.SessionToken,
}

if creds.CanExpire {
credentialData.Expiration = iso8601.Format(creds.Expires)
}

json, err := json.MarshalIndent(&credentialData, "", " ")
if err != nil {
return fmt.Errorf("Error creating credential json: %w", err)
}

fmt.Print(string(json) + "\n")

return nil
}

func printINI(input ExportCommandInput, credsProvider aws.CredentialsProvider) error {
creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err)
}

fmt.Printf("[%s]\n", input.ProfileName)
fmt.Printf("aws_access_key_id=%s\n", creds.AccessKeyID)
fmt.Printf("aws_secret_access_key=%s\n", creds.SecretAccessKey)

if creds.SessionToken != "" {
fmt.Printf("aws_session_token=%s\n", creds.SessionToken)
}
if creds.CanExpire {
fmt.Printf(";aws_credential_expiration=%s\n", iso8601.Format(creds.Expires))
}
return nil
}

func printEnv(input ExportCommandInput, credsProvider aws.CredentialsProvider, prefix string) error {
creds, err := credsProvider.Retrieve(context.TODO())
if err != nil {
return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err)
}

fmt.Printf("%sAWS_ACCESS_KEY_ID=%s\n", prefix, creds.AccessKeyID)
fmt.Printf("%sAWS_SECRET_ACCESS_KEY=%s\n", prefix, creds.SecretAccessKey)

if creds.SessionToken != "" {
fmt.Printf("%sAWS_SESSION_TOKEN=%s\n", prefix, creds.SessionToken)
}
if creds.CanExpire {
fmt.Printf("%sAWS_CREDENTIAL_EXPIRATION=%s\n", prefix, iso8601.Format(creds.Expires))
}
return nil
}
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func main() {
cli.ConfigureListCommand(app, a)
cli.ConfigureRotateCommand(app, a)
cli.ConfigureExecCommand(app, a)
cli.ConfigureExportCommand(app, a)
cli.ConfigureClearCommand(app, a)
cli.ConfigureRemoveCommand(app, a)
cli.ConfigureLoginCommand(app, a)
Expand Down

0 comments on commit f18ec54

Please sign in to comment.