Support env vars for STS region (#6284)

This commit is contained in:
Becca Petrin 2019-02-28 09:31:06 -08:00 committed by GitHub
parent 81cfa79d02
commit 5829774e91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 314 additions and 38 deletions

View File

@ -7,6 +7,8 @@ CHANGES:
[JSONPointer](https://tools.ietf.org/html/rfc6901).
* auth/jwt: Roles now have a "role type" parameter with a default type of "oidc". To
configure new JWT roles, a role type of "jwt" must be explicitly specified.
* vault aws kms seal **breaking change**: user-configured regions will now be
preferred over regions set in the enclosing environment.
IMPROVEMENTS:

View File

@ -13,6 +13,7 @@ import (
"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"
)
@ -34,17 +35,17 @@ func stsSigningResolver(service, region string, optFns ...func(*endpoints.Option
// Generates 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, region 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
cfg := aws.Config{Credentials: creds}
if region != "" {
cfg.Region = &region
cfg.EndpointResolver = endpoints.ResolverFunc(stsSigningResolver)
}
region := awsutil.GetOrDefaultRegion(hclog.Default(), configuredRegion)
stsSession, err := session.NewSessionWithOptions(session.Options{
Config: cfg,
Config: aws.Config{
Credentials: creds,
Region: &region,
EndpointResolver: endpoints.ResolverFunc(stsSigningResolver),
},
})
if err != nil {
return nil, err

75
helper/awsutil/region.go Normal file
View 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
}

View File

@ -0,0 +1,225 @@
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 (
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) {
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()
}
}
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
}

View File

@ -8,7 +8,6 @@ import (
"sync/atomic"
"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"
@ -88,33 +87,7 @@ 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
}
k.region = awsutil.GetOrDefaultRegion(k.logger, config["region"])
// Check and set AWS access key, secret key, and session token
k.accessKey = config["access_key"]

View File

@ -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