Add AliCloud auth to the Vault Agent (#5179)

This commit is contained in:
Becca Petrin
2018-09-05 08:56:30 -07:00
committed by Jeff Mitchell
parent f5c712f52a
commit d69c674c8e
7 changed files with 518 additions and 4 deletions

View File

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

View File

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

View File

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

View File

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

6
vendor/vendor.json vendored
View File

@@ -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=",

View File

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

View File

@@ -383,6 +383,9 @@
<li<%= sidebar_current("docs-agent-autoauth-methods") %>>
<a href="/docs/agent/autoauth/methods/index.html">Methods</a>
<ul class="nav">
<li<%= sidebar_current("docs-agent-autoauth-methods-alicloud") %>>
<a href="/docs/agent/autoauth/methods/alicloud.html">AliCloud</a>
</li>
<li<%= sidebar_current("docs-agent-autoauth-methods-aws") %>>
<a href="/docs/agent/autoauth/methods/aws.html">AWS</a>
</li>