mirror of
https://github.com/hashicorp/vault.git
synced 2025-11-28 22:21:30 +01:00
Merge pull request #6268 from hashicorp/6234-aws-region
Add region to CLI for generating AWS login data
This commit is contained in:
commit
936192c392
@ -1,3 +1,11 @@
|
||||
## Next
|
||||
|
||||
CHANGES:
|
||||
|
||||
* autoseal/aws: The user-configured regions on the AWSKMS seal stanza
|
||||
will now be preferred over regions set in the enclosing environment.
|
||||
This is a _breaking_ change.
|
||||
|
||||
## 1.1.1 (April 11th, 2019)
|
||||
|
||||
SECURITY:
|
||||
|
||||
@ -9,23 +9,43 @@ import (
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/hashicorp/errwrap"
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/helper/awsutil"
|
||||
)
|
||||
|
||||
type CLIHandler struct{}
|
||||
|
||||
// Generates the necessary data to send to the Vault server for generating a token
|
||||
// STS is a really weird service that used to only have global endpoints but now has regional endpoints as well.
|
||||
// For backwards compatibility, even if you request a region other than us-east-1, it'll still sign for us-east-1.
|
||||
// See, e.g., https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html#id_credentials_temp_enable-regions_writing_code
|
||||
// So we have to shim in this EndpointResolver to force it to sign for the right region
|
||||
func stsSigningResolver(service, region string, optFns ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
|
||||
defaultEndpoint, err := endpoints.DefaultResolver().EndpointFor(service, region, optFns...)
|
||||
if err != nil {
|
||||
return defaultEndpoint, err
|
||||
}
|
||||
defaultEndpoint.SigningRegion = region
|
||||
return defaultEndpoint, nil
|
||||
}
|
||||
|
||||
// GenerateLoginData populates the necessary data to send to the Vault server for generating a token
|
||||
// This is useful for other API clients to use
|
||||
func GenerateLoginData(creds *credentials.Credentials, headerValue string) (map[string]interface{}, error) {
|
||||
func GenerateLoginData(creds *credentials.Credentials, headerValue, configuredRegion string) (map[string]interface{}, error) {
|
||||
loginData := make(map[string]interface{})
|
||||
|
||||
// Use the credentials we've found to construct an STS session
|
||||
region := awsutil.GetOrDefaultRegion(hclog.Default(), configuredRegion)
|
||||
stsSession, err := session.NewSessionWithOptions(session.Options{
|
||||
Config: aws.Config{Credentials: creds},
|
||||
Config: aws.Config{
|
||||
Credentials: creds,
|
||||
Region: ®ion,
|
||||
EndpointResolver: endpoints.ResolverFunc(stsSigningResolver),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -79,7 +99,7 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loginData, err := GenerateLoginData(creds, headerValue)
|
||||
loginData, err := GenerateLoginData(creds, headerValue, m["region"])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -238,7 +238,7 @@ func (a *awsMethod) Authenticate(ctx context.Context, client *api.Client) (retTo
|
||||
defer a.credLock.Unlock()
|
||||
|
||||
var err error
|
||||
data, err = awsauth.GenerateLoginData(a.lastCreds, a.headerValue)
|
||||
data, err = awsauth.GenerateLoginData(a.lastCreds, a.headerValue, "")
|
||||
if err != nil {
|
||||
retErr = errwrap.Wrapf("error creating login value: {{err}}", err)
|
||||
return
|
||||
|
||||
75
helper/awsutil/region.go
Normal file
75
helper/awsutil/region.go
Normal file
@ -0,0 +1,75 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
)
|
||||
|
||||
// "us-east-1 is used because it's where AWS first provides support for new features,
|
||||
// is a widely used region, and is the most common one for some services like STS.
|
||||
const DefaultRegion = "us-east-1"
|
||||
|
||||
var ec2MetadataBaseURL = "http://169.254.169.254"
|
||||
|
||||
/*
|
||||
It's impossible to mimic "normal" AWS behavior here because it's not consistent
|
||||
or well-defined. For example, boto3, the Python SDK (which the aws cli uses),
|
||||
loads `~/.aws/config` by default and only reads the `AWS_DEFAULT_REGION` environment
|
||||
variable (and not `AWS_REGION`, while the golang SDK does _mostly_ the opposite -- it
|
||||
reads the region **only** from `AWS_REGION` and not at all `~/.aws/config`, **unless**
|
||||
the `AWS_SDK_LOAD_CONFIG` environment variable is set. So, we must define our own
|
||||
approach to walking AWS config and deciding what to use.
|
||||
|
||||
Our chosen approach is:
|
||||
|
||||
"More specific takes precedence over less specific."
|
||||
|
||||
1. User-provided configuration is the most explicit.
|
||||
2. Environment variables are potentially shared across many invocations and so they have less precedence.
|
||||
3. Configuration in `~/.aws/config` is shared across all invocations of a given user and so this has even less precedence.
|
||||
4. Configuration retrieved from the EC2 instance metadata service is shared by all invocations on a given machine, and so it has the lowest precedence.
|
||||
|
||||
This approach should be used in future updates to this logic.
|
||||
*/
|
||||
func GetOrDefaultRegion(logger hclog.Logger, configuredRegion string) string {
|
||||
if configuredRegion != "" {
|
||||
return configuredRegion
|
||||
}
|
||||
|
||||
sess, err := session.NewSessionWithOptions(session.Options{
|
||||
SharedConfigState: session.SharedConfigEnable,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn(fmt.Sprintf("unable to start session, defaulting region to %s", DefaultRegion))
|
||||
return DefaultRegion
|
||||
}
|
||||
|
||||
region := aws.StringValue(sess.Config.Region)
|
||||
if region != "" {
|
||||
return region
|
||||
}
|
||||
|
||||
metadata := ec2metadata.New(sess, &aws.Config{
|
||||
Endpoint: aws.String(ec2MetadataBaseURL + "/latest"),
|
||||
EC2MetadataDisableTimeoutOverride: aws.Bool(true),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: time.Second,
|
||||
},
|
||||
})
|
||||
if !metadata.Available() {
|
||||
return DefaultRegion
|
||||
}
|
||||
|
||||
region, err = metadata.Region()
|
||||
if err != nil {
|
||||
logger.Warn("unable to retrieve region from instance metadata, defaulting region to %s", DefaultRegion)
|
||||
return DefaultRegion
|
||||
}
|
||||
return region
|
||||
}
|
||||
237
helper/awsutil/region_test.go
Normal file
237
helper/awsutil/region_test.go
Normal file
@ -0,0 +1,237 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/user"
|
||||
"testing"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
)
|
||||
|
||||
const testConfigFile = `[default]
|
||||
region=%s
|
||||
output=json`
|
||||
|
||||
var (
|
||||
shouldTestFiles = os.Getenv("VAULT_ACC_AWS_FILES") == "1"
|
||||
|
||||
logger = hclog.NewNullLogger()
|
||||
expectedTestRegion = "us-west-2"
|
||||
unexpectedTestRegion = "us-east-2"
|
||||
regionEnvKeys = []string{"AWS_REGION", "AWS_DEFAULT_REGION"}
|
||||
)
|
||||
|
||||
func TestGetOrDefaultRegion_UserConfigPreferredFirst(t *testing.T) {
|
||||
configuredRegion := expectedTestRegion
|
||||
|
||||
cleanupEnv := setEnvRegion(t, unexpectedTestRegion)
|
||||
defer cleanupEnv()
|
||||
|
||||
cleanupFile := setConfigFileRegion(t, unexpectedTestRegion)
|
||||
defer cleanupFile()
|
||||
|
||||
cleanupMetadata := setInstanceMetadata(t, unexpectedTestRegion)
|
||||
defer cleanupMetadata()
|
||||
|
||||
result := GetOrDefaultRegion(logger, configuredRegion)
|
||||
if result != expectedTestRegion {
|
||||
t.Fatalf("expected: %s; actual: %s", expectedTestRegion, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrDefaultRegion_EnvVarsPreferredSecond(t *testing.T) {
|
||||
configuredRegion := ""
|
||||
|
||||
cleanupEnv := setEnvRegion(t, expectedTestRegion)
|
||||
defer cleanupEnv()
|
||||
|
||||
cleanupFile := setConfigFileRegion(t, unexpectedTestRegion)
|
||||
defer cleanupFile()
|
||||
|
||||
cleanupMetadata := setInstanceMetadata(t, unexpectedTestRegion)
|
||||
defer cleanupMetadata()
|
||||
|
||||
result := GetOrDefaultRegion(logger, configuredRegion)
|
||||
if result != expectedTestRegion {
|
||||
t.Fatalf("expected: %s; actual: %s", expectedTestRegion, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrDefaultRegion_ConfigFilesPreferredThird(t *testing.T) {
|
||||
if !shouldTestFiles {
|
||||
// In some test environments, like a CI environment, we may not have the
|
||||
// permissions to write to the ~/.aws/config file. Thus, this test is off
|
||||
// by default but can be set to on for local development.
|
||||
t.SkipNow()
|
||||
}
|
||||
configuredRegion := ""
|
||||
|
||||
cleanupEnv := setEnvRegion(t, "")
|
||||
defer cleanupEnv()
|
||||
|
||||
cleanupFile := setConfigFileRegion(t, expectedTestRegion)
|
||||
defer cleanupFile()
|
||||
|
||||
cleanupMetadata := setInstanceMetadata(t, unexpectedTestRegion)
|
||||
defer cleanupMetadata()
|
||||
|
||||
result := GetOrDefaultRegion(logger, configuredRegion)
|
||||
if result != expectedTestRegion {
|
||||
t.Fatalf("expected: %s; actual: %s", expectedTestRegion, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrDefaultRegion_ConfigFileUnfound(t *testing.T) {
|
||||
configuredRegion := ""
|
||||
|
||||
cleanupEnv := setEnvRegion(t, "")
|
||||
defer cleanupEnv()
|
||||
|
||||
if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "foo"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
result := GetOrDefaultRegion(logger, configuredRegion)
|
||||
if result != DefaultRegion {
|
||||
t.Fatalf("expected: %s; actual: %s", DefaultRegion, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrDefaultRegion_EC2InstanceMetadataPreferredFourth(t *testing.T) {
|
||||
configuredRegion := ""
|
||||
|
||||
cleanupEnv := setEnvRegion(t, "")
|
||||
defer cleanupEnv()
|
||||
|
||||
cleanupFile := setConfigFileRegion(t, "")
|
||||
defer cleanupFile()
|
||||
|
||||
cleanupMetadata := setInstanceMetadata(t, expectedTestRegion)
|
||||
defer cleanupMetadata()
|
||||
|
||||
result := GetOrDefaultRegion(logger, configuredRegion)
|
||||
if result != expectedTestRegion {
|
||||
t.Fatalf("expected: %s; actual: %s", expectedTestRegion, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrDefaultRegion_DefaultsToDefaultRegionWhenRegionUnavailable(t *testing.T) {
|
||||
configuredRegion := ""
|
||||
|
||||
cleanupEnv := setEnvRegion(t, "")
|
||||
defer cleanupEnv()
|
||||
|
||||
cleanupFile := setConfigFileRegion(t, "")
|
||||
defer cleanupFile()
|
||||
|
||||
result := GetOrDefaultRegion(logger, configuredRegion)
|
||||
if result != DefaultRegion {
|
||||
t.Fatalf("expected: %s; actual: %s", DefaultRegion, result)
|
||||
}
|
||||
}
|
||||
|
||||
func setEnvRegion(t *testing.T, region string) (cleanup func()) {
|
||||
for _, envKey := range regionEnvKeys {
|
||||
if err := os.Setenv(envKey, region); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
cleanup = func() {
|
||||
for _, envKey := range regionEnvKeys {
|
||||
if err := os.Unsetenv(envKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func setConfigFileRegion(t *testing.T, region string) (cleanup func()) {
|
||||
|
||||
var cleanupFuncs []func()
|
||||
|
||||
cleanup = func() {
|
||||
for _, f := range cleanupFuncs {
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
if !shouldTestFiles {
|
||||
return
|
||||
}
|
||||
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pathToAWSDir := usr.HomeDir + "/.aws"
|
||||
pathToConfig := pathToAWSDir + "/config"
|
||||
|
||||
preExistingConfig, err := ioutil.ReadFile(pathToConfig)
|
||||
if err != nil {
|
||||
// File simply doesn't exist.
|
||||
if err := os.Mkdir(pathToAWSDir, os.ModeDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupFuncs = append(cleanupFuncs, func() {
|
||||
if err := os.RemoveAll(pathToAWSDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
cleanupFuncs = append(cleanupFuncs, func() {
|
||||
if err := ioutil.WriteFile(pathToConfig, preExistingConfig, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
fileBody := fmt.Sprintf(testConfigFile, region)
|
||||
if err := ioutil.WriteFile(pathToConfig, []byte(fileBody), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", pathToConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanupFuncs = append(cleanupFuncs, func() {
|
||||
if err := os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func setInstanceMetadata(t *testing.T, region string) (cleanup func()) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reqPath := r.URL.String()
|
||||
switch reqPath {
|
||||
case "/latest/meta-data/instance-id":
|
||||
w.Write([]byte("i-1234567890abcdef0"))
|
||||
return
|
||||
case "/latest/meta-data/placement/availability-zone":
|
||||
// add a letter suffix, as a normal response is formatted like "us-east-1a"
|
||||
w.Write([]byte(region + "a"))
|
||||
return
|
||||
default:
|
||||
t.Fatalf("received unexpected request path: %s", reqPath)
|
||||
}
|
||||
}))
|
||||
originalMetadataBaseURL := ec2MetadataBaseURL
|
||||
ec2MetadataBaseURL = ts.URL
|
||||
cleanup = func() {
|
||||
ts.Close()
|
||||
ec2MetadataBaseURL = originalMetadataBaseURL
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -8,10 +8,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
|
||||
metrics "github.com/armon/go-metrics"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/kms"
|
||||
"github.com/aws/aws-sdk-go/service/kms/kmsiface"
|
||||
@ -91,33 +89,8 @@ func (k *AWSKMSSeal) SetConfig(config map[string]string) (map[string]string, err
|
||||
return nil, fmt.Errorf("'kms_key_id' not found for AWS KMS seal configuration")
|
||||
}
|
||||
|
||||
// Check and set region
|
||||
region, regionOk := config["region"]
|
||||
switch {
|
||||
case os.Getenv("AWS_REGION") != "":
|
||||
k.region = os.Getenv("AWS_REGION")
|
||||
case os.Getenv("AWS_DEFAULT_REGION") != "":
|
||||
k.region = os.Getenv("AWS_DEFAULT_REGION")
|
||||
case regionOk && region != "":
|
||||
k.region = region
|
||||
default:
|
||||
k.region = "us-east-1"
|
||||
|
||||
// If available, get the region from EC2 instance metadata
|
||||
sess, err := session.NewSession(nil)
|
||||
if err != nil {
|
||||
k.logger.Warn(fmt.Sprintf("unable to begin session: %s, defaulting region to %s", err, k.region))
|
||||
break
|
||||
}
|
||||
|
||||
// This will hang for ~10 seconds if the agent isn't running on an EC2 instance
|
||||
region, err := ec2metadata.New(sess).Region()
|
||||
if err != nil {
|
||||
k.logger.Warn(fmt.Sprintf("unable to retrieve region from ec2 instance metadata: %s, defaulting region to %s", err, k.region))
|
||||
break
|
||||
}
|
||||
k.region = region
|
||||
}
|
||||
// Please see GetOrDefaultRegion for an explanation of the order in which region is parsed.
|
||||
k.region = awsutil.GetOrDefaultRegion(k.logger, config["region"])
|
||||
|
||||
// Check and set AWS access key, secret key, and session token
|
||||
k.accessKey = config["access_key"]
|
||||
|
||||
@ -645,6 +645,13 @@ $ vault login -method=aws header_value=vault.example.com role=dev-role-iam \
|
||||
aws_security_token=<security_token>
|
||||
```
|
||||
|
||||
The region used defaults to `us-east-1`, but you can specify a custom region like so:
|
||||
```
|
||||
$ vault login -method=aws region=us-west-2 role=dev-role-iam
|
||||
```
|
||||
When using a custom region, be sure the designated region corresponds to that of the
|
||||
STS endpoint you're using.
|
||||
|
||||
An example of how to generate the required request values for the `login` method
|
||||
can be found found in the [vault cli
|
||||
source code](https://github.com/hashicorp/vault/blob/master/builtin/credential/aws/cli.go).
|
||||
|
||||
@ -40,9 +40,9 @@ seal "awskms" {
|
||||
These parameters apply to the `seal` stanza in the Vault configuration file:
|
||||
|
||||
- `region` `(string: "us-east-1")`: The AWS region where the encryption key
|
||||
lives. May also be specified by the `AWS_REGION` or `AWS_DEFAULT_REGION`
|
||||
environment variable or as part of the AWS profile from the AWS CLI or
|
||||
instance profile.
|
||||
lives. If not provided, may be populated from the `AWS_REGION` or
|
||||
`AWS_DEFAULT_REGION` environment variables, from your `~/.aws/config` file,
|
||||
or from instance metadata.
|
||||
|
||||
- `access_key` `(string: <required>)`: The AWS access key ID to use. May also be
|
||||
specified by the `AWS_ACCESS_KEY_ID` environment variable or as part of the
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user