Skip to content
Commits on Source (2)
......@@ -27,6 +27,7 @@ Generates a set of environment export variables from a 1password tag.
1. To add a prefix to each item, use the optional `--prefix PREFIX_` argument.
1. By default `pmv env` will create a temporary session if an MFA is registered. This behavior can be disabled with the `--skip-mfa` flag. **Warning**: this option is dangerous as it can circumvent MFA and could expose underlying credentials. Use with caution.
1. Use the `--assume-role` parameter to assume another role. This parameter will accept a role name, or a role ARN.
1. Use the `--validate-tokens` parameter to validate a token. Currently supports GitLab and AWS tokens. Other tokens will pass.
#### Usage
......
......@@ -52,7 +52,7 @@ var captureAwsCmd = &cobra.Command{
accessKey = strings.TrimSpace(accessKey)
secretAccessKey = strings.TrimSpace(secretAccessKey)
username, awsAccountID, accountAlias, err := aws.VerifyCallerIdentity(accessKey, secretAccessKey)
username, awsAccountID, accountAlias, err := aws.VerifyCallerIdentity(accessKey, secretAccessKey, "")
if err != nil {
log.Error().
Err(err).
......
......@@ -8,6 +8,8 @@ import (
"strings"
"time"
"gitlab.com/gitlab-com/gl-infra/pmv/internal/tokenvalidator"
log "github.com/rs/zerolog/log"
"github.com/spf13/cobra"
......@@ -24,6 +26,7 @@ var (
allowMultiple bool
assumeRole string
specifiedShell string
validateTokens bool
)
type environmentProcessor struct {
......@@ -63,47 +66,32 @@ func (c *environmentProcessor) item(title string, key string, value string) erro
}
func (c *environmentProcessor) done() error {
if !skipMFA && c.awsMFAArn != "" {
reader := bufio.NewReader(os.Stdin)
log.Info().
Str("title", c.awsMFATitle).
Msg("📲 Use your Authenticator app to provide a token code")
fmt.Fprintf(os.Stderr, "token: ") // This can't use the logger, as we don't want to write a newline
tokenCode, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read: %w", err)
}
tokenCode = strings.TrimSpace(tokenCode)
validateTokensUnneeded := false
credentials, err := aws.GetSessionToken(c.awsAccessKey, c.awsSecretAccessKey, c.awsMFAArn, tokenCode, region)
if !skipMFA && c.awsMFAArn != "" {
err := c.performMFAExchange()
if err != nil {
return fmt.Errorf("failed to perform MFA using the supplied code: %w", err)
return err
}
c.envMap["AWS_SESSION_TOKEN"] = *credentials.SessionToken
c.envMap["AWS_ACCESS_KEY"] = *credentials.AccessKeyId
c.envMap["AWS_ACCESS_KEY_ID"] = *credentials.AccessKeyId
c.envMap["AWS_SECRET_ACCESS_KEY"] = *credentials.SecretAccessKey
c.envMap["AWS_SESSION_EXPIRATION"] = credentials.Expiration.UTC().Format(time.RFC3339)
// Token is now validated since we've performed an MFA with it,
// no need to perform additional validation.
validateTokensUnneeded = true
}
if assumeRole != "" {
credentials, err := aws.AssumeRole(c.envMap["AWS_ACCESS_KEY"], c.envMap["AWS_SECRET_ACCESS_KEY"], c.envMap["AWS_SESSION_TOKEN"], assumeRole, region)
err := c.performAssumeRole()
if err != nil {
return fmt.Errorf("failed to assume role: %w", err)
return err
}
c.envMap["AWS_SESSION_TOKEN"] = *credentials.SessionToken
c.envMap["AWS_ACCESS_KEY"] = *credentials.AccessKeyId
c.envMap["AWS_ACCESS_KEY_ID"] = *credentials.AccessKeyId
c.envMap["AWS_SECRET_ACCESS_KEY"] = *credentials.SecretAccessKey
// Token is now validated since we've performed an assume role,
// no need to perform additional validation.
validateTokensUnneeded = true
}
c.envMap["AWS_SESSION_EXPIRATION"] = credentials.Expiration.UTC().Format(time.RFC3339)
if validateTokens && !validateTokensUnneeded {
c.validateToken()
}
err := export.Shell(c.envMap, c.calculateUnsetList(), c.writer, envPrefix, specifiedShell)
......@@ -114,6 +102,53 @@ func (c *environmentProcessor) done() error {
return nil
}
func (c *environmentProcessor) performAssumeRole() error {
credentials, err := aws.AssumeRole(c.envMap["AWS_ACCESS_KEY"], c.envMap["AWS_SECRET_ACCESS_KEY"], c.envMap["AWS_SESSION_TOKEN"], assumeRole, region)
if err != nil {
return fmt.Errorf("failed to assume role: %w", err)
}
c.envMap["AWS_SESSION_TOKEN"] = *credentials.SessionToken
c.envMap["AWS_ACCESS_KEY"] = *credentials.AccessKeyId
c.envMap["AWS_ACCESS_KEY_ID"] = *credentials.AccessKeyId
c.envMap["AWS_SECRET_ACCESS_KEY"] = *credentials.SecretAccessKey
c.envMap["AWS_SESSION_EXPIRATION"] = credentials.Expiration.UTC().Format(time.RFC3339)
return nil
}
func (c *environmentProcessor) performMFAExchange() error {
reader := bufio.NewReader(os.Stdin)
log.Info().
Str("title", c.awsMFATitle).
Msg("📲 Use your Authenticator app to provide a token code")
fmt.Fprintf(os.Stderr, "token: ") // This can't use the logger, as we don't want to write a newline
tokenCode, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read: %w", err)
}
tokenCode = strings.TrimSpace(tokenCode)
credentials, err := aws.GetSessionToken(c.awsAccessKey, c.awsSecretAccessKey, c.awsMFAArn, tokenCode, region)
if err != nil {
return fmt.Errorf("failed to perform MFA using the supplied code: %w", err)
}
c.envMap["AWS_SESSION_TOKEN"] = *credentials.SessionToken
c.envMap["AWS_ACCESS_KEY"] = *credentials.AccessKeyId
c.envMap["AWS_ACCESS_KEY_ID"] = *credentials.AccessKeyId
c.envMap["AWS_SECRET_ACCESS_KEY"] = *credentials.SecretAccessKey
c.envMap["AWS_SESSION_EXPIRATION"] = credentials.Expiration.UTC().Format(time.RFC3339)
return nil
}
func (c *environmentProcessor) calculateUnsetList() []string {
// For AWS, if AWS_SESSION_TOKEN is not set, but remains from a previous session,
// we should unclear it
......@@ -135,6 +170,14 @@ func (c *environmentProcessor) envMapContains(keys ...string) bool {
return false
}
func (c *environmentProcessor) validateToken() {
err := tokenvalidator.ValidateToken(c.envMap)
if err != nil {
log.Fatal().
Msg("Token validation failed. Token is invalid.")
}
}
var envCmd = &cobra.Command{
Use: "env",
ValidArgs: []string{"tags"},
......@@ -169,6 +212,8 @@ func init() {
"Dangerous as this option circumvents MFA and could expose underlying credentials.")
envCmd.PersistentFlags().StringVarP(&assumeRole, "assume-role", "A", "", "AWS role to assume after obtaining credentials.")
envCmd.PersistentFlags().StringVarP(&specifiedShell, "shell", "", "", "Create commands for a specific shell, defaults to $SHELL")
envCmd.PersistentFlags().BoolVar(&validateTokens, "validate-tokens", false,
"Use best-effort attempt to validate the tokens before exporting them. Only supported for GitLab and AWS tokens.")
rootCmd.AddCommand(envCmd)
}
......@@ -10,8 +10,9 @@ import (
// VerifyCallerIdentity will verify the caller identity given creds,
// and return some useful information about the account.
func VerifyCallerIdentity(accessKey string, secretAccessKey string) (username string, awsAccountID string, accountAlias string, err error) {
sess, err := createSession(accessKey, secretAccessKey, "", "")
func VerifyCallerIdentity(accessKey string, secretAccessKey string, sessionToken string) (username string,
awsAccountID string, accountAlias string, err error) {
sess, err := createSession(accessKey, secretAccessKey, sessionToken, "")
if err != nil {
return "", "", "", fmt.Errorf("failed to create session: %w", err)
}
......
package tokenvalidator
import (
"fmt"
"github.com/rs/zerolog/log"
"gitlab.com/gitlab-com/gl-infra/pmv/internal/aws"
)
func awsValidator(envMap map[string]string) (bool, error) {
accessKey, ok := envMap["AWS_ACCESS_KEY"]
if !ok {
return false, nil
}
secret := envMap["AWS_SECRET_ACCESS_KEY"]
sessionToken := envMap["AWS_SESSION_TOKEN"]
username, awsAccountID, accountAlias, err := aws.VerifyCallerIdentity(accessKey, secret, sessionToken)
if err != nil {
return true, fmt.Errorf("failed to verify aws token: %w", err)
}
log.Info().
Str("username", username).
Str("aws_account_id", awsAccountID).
Str("alias", accountAlias).
Msg("☁️ AWS Token validated")
return true, nil
}
package tokenvalidator
import (
"fmt"
"github.com/rs/zerolog/log"
"gitlab.com/gitlab-com/gl-infra/pmv/internal/gitlab"
)
func gitlabValidator(envMap map[string]string) (bool, error) {
token, ok := envMap["GITLAB_TOKEN"]
if !ok {
return false, nil
}
gitlabInstance, ok := envMap["GITLAB_INSTANCE"]
if !ok {
gitlabInstance = "https://gitlab.com"
}
username, err := gitlab.VerifyToken(gitlabInstance, token)
if err != nil {
return true, fmt.Errorf("failed to verify gitlab token: %w", err)
}
log.Info().
Str("instance", gitlabInstance).
Str("username", username).
Msg("🦊 GitLab token verified")
return true, nil
}
package tokenvalidator
import "fmt"
type validatorFunc func(envMap map[string]string) (bool, error)
// Add additional validators here...
var validators = []validatorFunc{
awsValidator,
gitlabValidator,
}
func ValidateToken(envMap map[string]string) error {
for _, v := range validators {
matched, err := v(envMap)
if err != nil {
return fmt.Errorf("failed to validate token: %w", err)
}
if matched {
return nil
}
}
// No matches
return nil
}