diff --git a/command/agent.go b/command/agent.go index 57e0551d73..b9f49c79a1 100644 --- a/command/agent.go +++ b/command/agent.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/errwrap" log "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agent/auth/alicloud" "github.com/hashicorp/vault/command/agent/auth/aws" "github.com/hashicorp/vault/command/agent/auth/azure" "github.com/hashicorp/vault/command/agent/auth/gcp" @@ -284,6 +285,8 @@ func (c *AgentCommand) Run(args []string) int { Config: config.AutoAuth.Method.Config, } switch config.AutoAuth.Method.Type { + case "alicloud": + method, err = alicloud.NewAliCloudAuthMethod(authConfig) case "aws": method, err = aws.NewAWSAuthMethod(authConfig) case "azure": diff --git a/command/agent/alicloud_end_to_end_test.go b/command/agent/alicloud_end_to_end_test.go new file mode 100644 index 0000000000..78d79fda22 --- /dev/null +++ b/command/agent/alicloud_end_to_end_test.go @@ -0,0 +1,213 @@ +package agent + +import ( + "bytes" + "context" + "encoding/json" + "io/ioutil" + "os" + "strings" + "testing" + "time" + + "github.com/aliyun/alibaba-cloud-sdk-go/sdk" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials/providers" + "github.com/aliyun/alibaba-cloud-sdk-go/services/sts" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-uuid" + vaultalicloud "github.com/hashicorp/vault-plugin-auth-alicloud" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/command/agent/auth" + agentalicloud "github.com/hashicorp/vault/command/agent/auth/alicloud" + "github.com/hashicorp/vault/command/agent/sink" + "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/helper/logging" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/vault" +) + +const ( + envVarRunAccTests = "VAULT_ACC" + envVarAccessKey = "ALICLOUD_TEST_ACCESS_KEY" + envVarSecretKey = "ALICLOUD_TEST_SECRET_KEY" + envVarRoleArn = "ALICLOUD_TEST_ROLE_ARN" +) + +var runAcceptanceTests = os.Getenv(envVarRunAccTests) == "1" + +func TestAliCloudEndToEnd(t *testing.T) { + if !runAcceptanceTests { + t.SkipNow() + } + + logger := logging.NewVaultLogger(hclog.Trace) + coreConfig := &vault.CoreConfig{ + Logger: logger, + CredentialBackends: map[string]logical.Factory{ + "alicloud": vaultalicloud.Factory, + }, + } + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + vault.TestWaitActive(t, cluster.Cores[0].Core) + client := cluster.Cores[0].Client + + // Setup Vault + if err := client.Sys().EnableAuthWithOptions("alicloud", &api.EnableAuthOptions{ + Type: "alicloud", + }); err != nil { + t.Fatal(err) + } + + if _, err := client.Logical().Write("auth/alicloud/role/test", map[string]interface{}{ + "arn": os.Getenv(envVarRoleArn), + }); err != nil { + t.Fatal(err) + } + + ctx, cancelFunc := context.WithCancel(context.Background()) + timer := time.AfterFunc(30*time.Second, func() { + cancelFunc() + }) + defer timer.Stop() + + // We're going to feed alicloud auth creds via env variables. + if err := setAliCloudEnvCreds(); err != nil { + t.Fatal(err) + } + defer func() { + if err := unsetAliCloudEnvCreds(); err != nil { + t.Fatal(err) + } + }() + + am, err := agentalicloud.NewAliCloudAuthMethod(&auth.AuthConfig{ + Logger: logger.Named("auth.alicloud"), + MountPath: "auth/alicloud", + Config: map[string]interface{}{ + "role": "test", + "region": "us-west-1", + "cred_check_freq_seconds": 1, + }, + }) + if err != nil { + t.Fatal(err) + } + + ahConfig := &auth.AuthHandlerConfig{ + Logger: logger.Named("auth.handler"), + Client: client, + } + + ah := auth.NewAuthHandler(ahConfig) + go ah.Run(ctx, am) + defer func() { + <-ah.DoneCh + }() + + tmpFile, err := ioutil.TempFile("", "auth.tokensink.test.") + if err != nil { + t.Fatal(err) + } + tokenSinkFileName := tmpFile.Name() + tmpFile.Close() + os.Remove(tokenSinkFileName) + t.Logf("output: %s", tokenSinkFileName) + + config := &sink.SinkConfig{ + Logger: logger.Named("sink.file"), + Config: map[string]interface{}{ + "path": tokenSinkFileName, + }, + WrapTTL: 10 * time.Second, + } + + fs, err := file.NewFileSink(config) + if err != nil { + t.Fatal(err) + } + config.Sink = fs + + ss := sink.NewSinkServer(&sink.SinkServerConfig{ + Logger: logger.Named("sink.server"), + Client: client, + }) + go ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config}) + defer func() { + <-ss.DoneCh + }() + + if stat, err := os.Lstat(tokenSinkFileName); err == nil { + t.Fatalf("expected err but got %s", stat) + } else if !os.IsNotExist(err) { + t.Fatal("expected notexist err") + } + + // Wait 2 seconds for the env variables to be detected and an auth to be generated. + time.Sleep(time.Second * 2) + + token, err := readToken(tokenSinkFileName) + if err != nil { + t.Fatal(err) + } + + if token.Token == "" { + t.Fatal("expected token but didn't receive it") + } +} + +func readToken(fileName string) (*logical.HTTPWrapInfo, error) { + b, err := ioutil.ReadFile(fileName) + if err != nil { + return nil, err + } + wrapper := &logical.HTTPWrapInfo{} + if err := json.NewDecoder(bytes.NewReader(b)).Decode(wrapper); err != nil { + return nil, err + } + return wrapper, nil +} + +func setAliCloudEnvCreds() error { + config := sdk.NewConfig() + config.Scheme = "https" + client, err := sts.NewClientWithOptions("us-west-1", config, credentials.NewAccessKeyCredential(os.Getenv(envVarAccessKey), os.Getenv(envVarSecretKey))) + if err != nil { + return err + } + roleSessionName, err := uuid.GenerateUUID() + if err != nil { + return err + } + assumeRoleReq := sts.CreateAssumeRoleRequest() + assumeRoleReq.RoleArn = os.Getenv(envVarRoleArn) + assumeRoleReq.RoleSessionName = strings.Replace(roleSessionName, "-", "", -1) + assumeRoleResp, err := client.AssumeRole(assumeRoleReq) + if err != nil { + return err + } + + if err := os.Setenv(providers.EnvVarAccessKeyID, assumeRoleResp.Credentials.AccessKeyId); err != nil { + return err + } + if err := os.Setenv(providers.EnvVarAccessKeySecret, assumeRoleResp.Credentials.AccessKeySecret); err != nil { + return err + } + return os.Setenv(providers.EnvVarAccessKeyStsToken, assumeRoleResp.Credentials.SecurityToken) +} + +func unsetAliCloudEnvCreds() error { + if err := os.Unsetenv(providers.EnvVarAccessKeyID); err != nil { + return err + } + if err := os.Unsetenv(providers.EnvVarAccessKeySecret); err != nil { + return err + } + return os.Unsetenv(providers.EnvVarAccessKeyStsToken) +} diff --git a/command/agent/auth/alicloud/alicloud.go b/command/agent/auth/alicloud/alicloud.go new file mode 100644 index 0000000000..a41e3ff2b7 --- /dev/null +++ b/command/agent/auth/alicloud/alicloud.go @@ -0,0 +1,233 @@ +package alicloud + +import ( + "context" + "errors" + "fmt" + "reflect" + "sync" + "time" + + aliCloudAuth "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials/providers" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault-plugin-auth-alicloud/tools" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/command/agent/auth" +) + +/* + + Creds can be inferred from instance metadata, and those creds + expire every 60 minutes, so we're going to need to poll for new + creds. Since we're polling anyways, let's poll once a minute so + all changes can be picked up rather quickly. This is configurable, + however. + +*/ +const defaultCredCheckFreqSeconds = 60 + +func NewAliCloudAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) { + if conf == nil { + return nil, errors.New("empty config") + } + if conf.Config == nil { + return nil, errors.New("empty config data") + } + + a := &alicloudMethod{ + logger: conf.Logger, + mountPath: conf.MountPath, + credsFound: make(chan struct{}), + stopCh: make(chan struct{}), + } + + // Build the required information we'll need to create a client. + if roleRaw, ok := conf.Config["role"]; !ok { + return nil, errors.New("'role' is required but is not provided") + } else { + if a.role, ok = roleRaw.(string); !ok { + return nil, errors.New("could not convert 'role' config value to string") + } + } + if regionRaw, ok := conf.Config["region"]; !ok { + return nil, errors.New("'region' is required but is not provided") + } else { + if a.region, ok = regionRaw.(string); !ok { + return nil, errors.New("could not convert 'region' config value to string") + } + } + + // Check for an optional custom frequency at which we should poll for creds. + credCheckFreqSec := defaultCredCheckFreqSeconds + if checkFreqRaw, ok := conf.Config["cred_check_freq_seconds"]; ok { + if credFreq, ok := checkFreqRaw.(int); ok { + credCheckFreqSec = credFreq + } + } + + // Build the optional, configuration-based piece of the credential chain. + credConfig := &providers.Configuration{} + + if accessKeyRaw, ok := conf.Config["access_key"]; ok { + if credConfig.AccessKeyID, ok = accessKeyRaw.(string); !ok { + return nil, errors.New("could not convert 'access_key' config value to string") + } + } + + if accessSecretRaw, ok := conf.Config["access_secret"]; ok { + if credConfig.AccessKeySecret, ok = accessSecretRaw.(string); !ok { + return nil, errors.New("could not convert 'access_secret' config value to string") + } + } + + if accessTokenRaw, ok := conf.Config["access_token"]; ok { + if credConfig.AccessKeyStsToken, ok = accessTokenRaw.(string); !ok { + return nil, errors.New("could not convert 'access_token' config value to string") + } + } + + if roleArnRaw, ok := conf.Config["role_arn"]; ok { + if credConfig.RoleArn, ok = roleArnRaw.(string); !ok { + return nil, errors.New("could not convert 'role_arn' config value to string") + } + } + + if roleSessionNameRaw, ok := conf.Config["role_session_name"]; ok { + if credConfig.RoleSessionName, ok = roleSessionNameRaw.(string); !ok { + return nil, errors.New("could not convert 'role_session_name' config value to string") + } + } + + if roleSessionExpirationRaw, ok := conf.Config["role_session_expiration"]; ok { + if roleSessionExpiration, ok := roleSessionExpirationRaw.(int); !ok { + return nil, errors.New("could not convert 'role_session_expiration' config value to int") + } else { + credConfig.RoleSessionExpiration = &roleSessionExpiration + } + } + + if privateKeyRaw, ok := conf.Config["private_key"]; ok { + if credConfig.PrivateKey, ok = privateKeyRaw.(string); !ok { + return nil, errors.New("could not convert 'private_key' config value to string") + } + } + + if publicKeyIdRaw, ok := conf.Config["public_key_id"]; ok { + if credConfig.PublicKeyID, ok = publicKeyIdRaw.(string); !ok { + return nil, errors.New("could not convert 'public_key_id' config value to string") + } + } + + if sessionExpirationRaw, ok := conf.Config["session_expiration"]; ok { + if sessionExpiration, ok := sessionExpirationRaw.(int); !ok { + return nil, errors.New("could not convert 'session_expiration' config value to int") + } else { + credConfig.SessionExpiration = &sessionExpiration + } + } + + if roleNameRaw, ok := conf.Config["role_name"]; ok { + if credConfig.RoleName, ok = roleNameRaw.(string); !ok { + return nil, errors.New("could not convert 'role_name' config value to string") + } + } + + credentialChain := []providers.Provider{ + providers.NewEnvCredentialProvider(), + providers.NewConfigurationCredentialProvider(credConfig), + providers.NewInstanceMetadataProvider(), + } + credProvider := providers.NewChainProvider(credentialChain) + + // Do an initial population of the creds because we want to err right away if we can't + // even get a first set. + lastCreds, err := credProvider.Retrieve() + if err != nil { + return nil, err + } + a.lastCreds = lastCreds + + go a.pollForCreds(credProvider, credCheckFreqSec) + + return a, nil +} + +type alicloudMethod struct { + logger hclog.Logger + mountPath string + + // These parameters are fed into building login data. + role string + region string + + // These are used to share the latest creds safely across goroutines. + credLock sync.Mutex + lastCreds aliCloudAuth.Credential + + // Notifies the outer environment that it should call Authenticate again. + credsFound chan struct{} + + // Detects that the outer environment is closing. + stopCh chan struct{} +} + +func (a *alicloudMethod) Authenticate(context.Context, *api.Client) (string, map[string]interface{}, error) { + a.credLock.Lock() + defer a.credLock.Unlock() + + a.logger.Trace("beginning authentication") + data, err := tools.GenerateLoginData(a.role, a.lastCreds, a.region) + if err != nil { + return "", nil, err + } + return fmt.Sprintf("%s/login", a.mountPath), data, nil +} + +func (a *alicloudMethod) NewCreds() chan struct{} { + return a.credsFound +} + +func (a *alicloudMethod) CredSuccess() {} + +func (a *alicloudMethod) Shutdown() { + close(a.credsFound) + close(a.stopCh) +} + +func (a *alicloudMethod) pollForCreds(credProvider providers.Provider, frequencySeconds int) { + ticker := time.NewTicker(time.Duration(frequencySeconds) * time.Second) + defer ticker.Stop() + for { + select { + case <-a.stopCh: + a.logger.Trace("shutdown triggered, stopping alicloud auth handler") + return + case <-ticker.C: + if err := a.checkCreds(credProvider); err != nil { + a.logger.Warn("unable to retrieve current creds, retaining last creds", err) + } + } + } +} + +func (a *alicloudMethod) checkCreds(credProvider providers.Provider) error { + a.credLock.Lock() + defer a.credLock.Unlock() + + a.logger.Trace("checking for new credentials") + currentCreds, err := credProvider.Retrieve() + if err != nil { + return err + } + // These will always have different pointers regardless of whether their + // values are identical, hence the use of DeepEqual. + if reflect.DeepEqual(currentCreds, a.lastCreds) { + a.logger.Trace("credentials are unchanged") + return nil + } + a.lastCreds = currentCreds + a.logger.Trace("new credentials detected, triggering Authenticate") + a.credsFound <- struct{}{} + return nil +} diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-alicloud/path_login.go b/vendor/github.com/hashicorp/vault-plugin-auth-alicloud/path_login.go index 3ec7d0b20a..8af904bee1 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-alicloud/path_login.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-alicloud/path_login.go @@ -114,7 +114,7 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat "identity_type": callerIdentity.IdentityType, "principal_id": callerIdentity.PrincipalId, "request_id": callerIdentity.RequestId, - "role_name": parsedARN.RoleName, + "role_name": roleName, }, DisplayName: callerIdentity.PrincipalId, LeaseOptions: logical.LeaseOptions{ diff --git a/vendor/vendor.json b/vendor/vendor.json index 20256925cf..c5071e3319 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1351,10 +1351,10 @@ "revisionTime": "2018-05-30T15:59:58Z" }, { - "checksumSHA1": "76udfjuAEmd4JFZP8LhTLTKZ6gk=", + "checksumSHA1": "pqkqaBRFKL2P/64xpuxj/3J/+sQ=", "path": "github.com/hashicorp/vault-plugin-auth-alicloud", - "revision": "90acf238c385792939aade0286fcb941d9899435", - "revisionTime": "2018-08-22T21:26:04Z" + "revision": "1a078292f70a4c9e366a13d3c725d105bd5be1af", + "revisionTime": "2018-09-04T20:26:51Z" }, { "checksumSHA1": "xdrSQoX7B7Hr4iWm9T2+5wHVpHQ=", diff --git a/website/source/docs/agent/autoauth/methods/alicloud.html.md b/website/source/docs/agent/autoauth/methods/alicloud.html.md new file mode 100644 index 0000000000..d5667f89f3 --- /dev/null +++ b/website/source/docs/agent/autoauth/methods/alicloud.html.md @@ -0,0 +1,62 @@ +--- +layout: "docs" +page_title: "Vault Agent Auto-Auth AliCloud Method" +sidebar_current: "docs-agent-autoauth-methods-alicloud" +description: |- + AliCloud Method for Vault Agent Auto-Auth +--- + +# Vault Agent Auto-Auth AliCloud Method + +The `alicloud` method performs authentication against the [AliCloud Auth +method](https://www.vaultproject.io/docs/auth/alicloud.html). + +## Credentials + +The Vault agent will use the first credential it can successfully obtain in the following order: + +1. [Env variables](https://github.com/aliyun/alibaba-cloud-sdk-go/blob/master/sdk/auth/credentials/providers/env.go) +2. A static credential configuration +3. Instance metadata (recommended) + +Wherever possible, we recommend using instance metadata for credentials. These rotate every hour +and require no effort on your part to provision, making instance metadata the most secure of the three methods. If +using instance metadata _and_ a custom `cred_check_freq_seconds`, be sure the frequency is set for +less than an hour, because instance metadata credentials expire every hour. + +Environment variables are given first precedence to provide the ability to quickly override your +configuration. + +## Configuration + +### General + +- `role` `(string: required)` - The role to authenticate against on Vault. + +- `region` `(string: required)` - The AliCloud region in which the Vault agent resides. Example: "us-west-1". + +- `cred_check_freq_seconds` `(integer: optional)` - In seconds, how frequently the Vault agent should check for new credentials. + +### Optional Static Credential Configuration (Not Preferred) + +If instance metadata is not available, you may provide credential information through the parameters below. + +- `access_key` `(string: optional)` - The access key to use. + +- `secret_key` `(string: optional)` - The secret key to use. + +- `access_token` `(string: optional)` - The access token to use. + +- `role_arn` `(string: optional)` - The role ARN to use. + +- `role_session_name` `(string: optional)` - The role session name to use. + +- `role_session_expiration` `(string: optional)` - The role session expiration to use. + +- `private_key` `(string: optional)` - The private key to use. + +- `public_key_id` `(string: optional)` - The public key ID to use. + +- `session_expiration` `(string: optional)` - The session expiration to use. + +- `role_name` `(string: optional)` - The role name to use. \ No newline at end of file diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 3241d73bab..ddd1d1c5b9 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -383,6 +383,9 @@