connect: Support auth methods for the vault connect CA provider (#11573)
* Support vault auth methods for the Vault connect CA provider * Rotate the token (re-authenticate to vault using auth method) when the token can no longer be renewed
This commit is contained in:
parent
07e7fdcd09
commit
bd3fb0d0e9
|
@ -0,0 +1,5 @@
|
||||||
|
```release-note:improvement
|
||||||
|
connect: Support Vault auth methods for the Connect CA Vault provider. Currently, we support any non-deprecated auth methods
|
||||||
|
the latest version of Vault supports (v1.8.5), which include AppRole, AliCloud, AWS, Azure, Cloud Foundry, GitHub, Google Cloud,
|
||||||
|
JWT/OIDC, Kerberos, Kubernetes, LDAP, Oracle Cloud Infrastructure, Okta, Radius, TLS Certificates, and Username & Password.
|
||||||
|
```
|
|
@ -8,9 +8,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/lib/decode"
|
||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
vaultapi "github.com/hashicorp/vault/api"
|
vaultapi "github.com/hashicorp/vault/api"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
|
@ -19,7 +21,29 @@ import (
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VaultCALeafCertRole = "leaf-cert"
|
const (
|
||||||
|
VaultCALeafCertRole = "leaf-cert"
|
||||||
|
|
||||||
|
VaultAuthMethodTypeAliCloud = "alicloud"
|
||||||
|
VaultAuthMethodTypeAppRole = "approle"
|
||||||
|
VaultAuthMethodTypeAWS = "aws"
|
||||||
|
VaultAuthMethodTypeAzure = "azure"
|
||||||
|
VaultAuthMethodTypeCloudFoundry = "cf"
|
||||||
|
VaultAuthMethodTypeGitHub = "github"
|
||||||
|
VaultAuthMethodTypeGCP = "gcp"
|
||||||
|
VaultAuthMethodTypeJWT = "jwt"
|
||||||
|
VaultAuthMethodTypeKerberos = "kerberos"
|
||||||
|
VaultAuthMethodTypeKubernetes = "kubernetes"
|
||||||
|
VaultAuthMethodTypeLDAP = "ldap"
|
||||||
|
VaultAuthMethodTypeOCI = "oci"
|
||||||
|
VaultAuthMethodTypeOkta = "okta"
|
||||||
|
VaultAuthMethodTypeRadius = "radius"
|
||||||
|
VaultAuthMethodTypeTLS = "cert"
|
||||||
|
VaultAuthMethodTypeToken = "token"
|
||||||
|
VaultAuthMethodTypeUserpass = "userpass"
|
||||||
|
|
||||||
|
defaultK8SServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||||
|
)
|
||||||
|
|
||||||
var ErrBackendNotMounted = fmt.Errorf("backend not mounted")
|
var ErrBackendNotMounted = fmt.Errorf("backend not mounted")
|
||||||
var ErrBackendNotInitialized = fmt.Errorf("backend not initialized")
|
var ErrBackendNotInitialized = fmt.Errorf("backend not initialized")
|
||||||
|
@ -74,6 +98,13 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.AuthMethod != nil {
|
||||||
|
loginResp, err := vaultLogin(client, config.AuthMethod)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.Token = loginResp.Auth.ClientToken
|
||||||
|
}
|
||||||
client.SetToken(config.Token)
|
client.SetToken(config.Token)
|
||||||
|
|
||||||
// We don't want to set the namespace if it's empty to prevent potential
|
// We don't want to set the namespace if it's empty to prevent potential
|
||||||
|
@ -94,7 +125,7 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if secret == nil {
|
} else if secret == nil {
|
||||||
return fmt.Errorf("Could not look up Vault provider token: not found")
|
return fmt.Errorf("could not look up Vault provider token: not found")
|
||||||
}
|
}
|
||||||
var token struct {
|
var token struct {
|
||||||
Renewable bool
|
Renewable bool
|
||||||
|
@ -105,7 +136,7 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up a renewer to renew the token automatically, if supported.
|
// Set up a renewer to renew the token automatically, if supported.
|
||||||
if token.Renewable {
|
if token.Renewable || config.AuthMethod != nil {
|
||||||
lifetimeWatcher, err := client.NewLifetimeWatcher(&vaultapi.LifetimeWatcherInput{
|
lifetimeWatcher, err := client.NewLifetimeWatcher(&vaultapi.LifetimeWatcherInput{
|
||||||
Secret: &vaultapi.Secret{
|
Secret: &vaultapi.Secret{
|
||||||
Auth: &vaultapi.SecretAuth{
|
Auth: &vaultapi.SecretAuth{
|
||||||
|
@ -118,10 +149,10 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
||||||
RenewBehavior: vaultapi.RenewBehaviorIgnoreErrors,
|
RenewBehavior: vaultapi.RenewBehaviorIgnoreErrors,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error beginning Vault provider token renewal: %v", err)
|
return fmt.Errorf("error beginning Vault provider token renewal: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
v.shutdown = cancel
|
v.shutdown = cancel
|
||||||
go v.renewToken(ctx, lifetimeWatcher)
|
go v.renewToken(ctx, lifetimeWatcher)
|
||||||
}
|
}
|
||||||
|
@ -129,7 +160,9 @@ func (v *VaultProvider) Configure(cfg ProviderConfig) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// renewToken uses a vaultapi.Renewer to repeatedly renew our token's lease.
|
// renewToken uses a vaultapi.LifetimeWatcher to repeatedly renew our token's lease.
|
||||||
|
// If the token can no longer be renewed and auth method is set,
|
||||||
|
// it will re-authenticate to Vault using the auth method and restart the renewer with the new token.
|
||||||
func (v *VaultProvider) renewToken(ctx context.Context, watcher *vaultapi.LifetimeWatcher) {
|
func (v *VaultProvider) renewToken(ctx context.Context, watcher *vaultapi.LifetimeWatcher) {
|
||||||
go watcher.Start()
|
go watcher.Start()
|
||||||
defer watcher.Stop()
|
defer watcher.Stop()
|
||||||
|
@ -144,7 +177,35 @@ func (v *VaultProvider) renewToken(ctx context.Context, watcher *vaultapi.Lifeti
|
||||||
v.logger.Error("Error renewing token for Vault provider", "error", err)
|
v.logger.Error("Error renewing token for Vault provider", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watcher routine has finished, so start it again.
|
// If the watcher has exited and auth method is enabled,
|
||||||
|
// re-authenticate using the auth method and set up a new watcher.
|
||||||
|
if v.config.AuthMethod != nil {
|
||||||
|
// Login to Vault using the auth method.
|
||||||
|
loginResp, err := vaultLogin(v.client, v.config.AuthMethod)
|
||||||
|
if err != nil {
|
||||||
|
v.logger.Error("Error login in to Vault with %q auth method", v.config.AuthMethod.Type)
|
||||||
|
// Restart the watcher.
|
||||||
|
go watcher.Start()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the new token for the vault client.
|
||||||
|
v.client.SetToken(loginResp.Auth.ClientToken)
|
||||||
|
v.logger.Info("Successfully re-authenticated with Vault using auth method")
|
||||||
|
|
||||||
|
// Start the new watcher for the new token.
|
||||||
|
watcher, err = v.client.NewLifetimeWatcher(&vaultapi.LifetimeWatcherInput{
|
||||||
|
Secret: loginResp,
|
||||||
|
RenewBehavior: vaultapi.RenewBehaviorIgnoreErrors,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
v.logger.Error("Error starting token renewal process")
|
||||||
|
go watcher.Start()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the watcher.
|
||||||
go watcher.Start()
|
go watcher.Start()
|
||||||
|
|
||||||
case <-watcher.RenewCh():
|
case <-watcher.RenewCh():
|
||||||
|
@ -599,7 +660,10 @@ func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderCon
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeConf := &mapstructure.DecoderConfig{
|
decodeConf := &mapstructure.DecoderConfig{
|
||||||
DecodeHook: structs.ParseDurationFunc(),
|
DecodeHook: mapstructure.ComposeDecodeHookFunc(
|
||||||
|
structs.ParseDurationFunc(),
|
||||||
|
decode.HookTranslateKeys,
|
||||||
|
),
|
||||||
Result: &config,
|
Result: &config,
|
||||||
WeaklyTypedInput: true,
|
WeaklyTypedInput: true,
|
||||||
}
|
}
|
||||||
|
@ -613,8 +677,12 @@ func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderCon
|
||||||
return nil, fmt.Errorf("error decoding config: %s", err)
|
return nil, fmt.Errorf("error decoding config: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Token == "" {
|
if config.Token == "" && config.AuthMethod == nil {
|
||||||
return nil, fmt.Errorf("must provide a Vault token")
|
return nil, fmt.Errorf("must provide a Vault token or configure a Vault auth method")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Token != "" && config.AuthMethod != nil {
|
||||||
|
return nil, fmt.Errorf("only one of Vault token or Vault auth method can be provided, but not both")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.RootPKIPath == "" {
|
if config.RootPKIPath == "" {
|
||||||
|
@ -637,3 +705,76 @@ func ParseVaultCAConfig(raw map[string]interface{}) (*structs.VaultCAProviderCon
|
||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func vaultLogin(client *vaultapi.Client, authMethod *structs.VaultAuthMethod) (*vaultapi.Secret, error) {
|
||||||
|
// Adapted from https://www.vaultproject.io/docs/auth/kubernetes#code-example
|
||||||
|
loginPath, err := configureVaultAuthMethod(authMethod)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Logical().Write(loginPath, authMethod.Params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp == nil || resp.Auth == nil || resp.Auth.ClientToken == "" {
|
||||||
|
return nil, fmt.Errorf("login response did not return client token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureVaultAuthMethod(authMethod *structs.VaultAuthMethod) (loginPath string, err error) {
|
||||||
|
if authMethod.MountPath == "" {
|
||||||
|
authMethod.MountPath = authMethod.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
switch authMethod.Type {
|
||||||
|
case VaultAuthMethodTypeKubernetes:
|
||||||
|
// For the Kubernetes Auth method, we will try to read the JWT token
|
||||||
|
// from the default service account file location if jwt was not provided.
|
||||||
|
if jwt, ok := authMethod.Params["jwt"]; !ok || jwt == "" {
|
||||||
|
serviceAccountToken, err := os.ReadFile(defaultK8SServiceAccountTokenPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
authMethod.Params["jwt"] = string(serviceAccountToken)
|
||||||
|
}
|
||||||
|
loginPath = fmt.Sprintf("auth/%s/login", authMethod.MountPath)
|
||||||
|
// These auth methods require a username for the login API path.
|
||||||
|
case VaultAuthMethodTypeLDAP, VaultAuthMethodTypeUserpass, VaultAuthMethodTypeOkta, VaultAuthMethodTypeRadius:
|
||||||
|
// Get username from the params.
|
||||||
|
if username, ok := authMethod.Params["username"]; ok {
|
||||||
|
loginPath = fmt.Sprintf("auth/%s/login/%s", authMethod.MountPath, username)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("failed to get 'username' from auth method params")
|
||||||
|
}
|
||||||
|
// This auth method requires a role for the login API path.
|
||||||
|
case VaultAuthMethodTypeOCI:
|
||||||
|
if role, ok := authMethod.Params["role"]; ok {
|
||||||
|
loginPath = fmt.Sprintf("auth/%s/login/%s", authMethod.MountPath, role)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("failed to get 'role' from auth method params")
|
||||||
|
}
|
||||||
|
case VaultAuthMethodTypeToken:
|
||||||
|
return "", fmt.Errorf("'token' auth method is not supported via auth method configuration; " +
|
||||||
|
"please provide the token with the 'token' parameter in the CA configuration")
|
||||||
|
// The rest of the auth methods use auth/<auth method path> login API path.
|
||||||
|
case VaultAuthMethodTypeAliCloud,
|
||||||
|
VaultAuthMethodTypeAppRole,
|
||||||
|
VaultAuthMethodTypeAWS,
|
||||||
|
VaultAuthMethodTypeAzure,
|
||||||
|
VaultAuthMethodTypeCloudFoundry,
|
||||||
|
VaultAuthMethodTypeGitHub,
|
||||||
|
VaultAuthMethodTypeGCP,
|
||||||
|
VaultAuthMethodTypeJWT,
|
||||||
|
VaultAuthMethodTypeKerberos,
|
||||||
|
VaultAuthMethodTypeTLS:
|
||||||
|
loginPath = fmt.Sprintf("auth/%s/login", authMethod.MountPath)
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("auth method %q is not supported", authMethod.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,94 @@ import (
|
||||||
"github.com/hashicorp/consul/sdk/testutil/retry"
|
"github.com/hashicorp/consul/sdk/testutil/retry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestVaultCAProvider_ParseVaultCAConfig(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
rawConfig map[string]interface{}
|
||||||
|
expConfig *structs.VaultCAProviderConfig
|
||||||
|
expError string
|
||||||
|
}{
|
||||||
|
"no token and no auth method provided": {
|
||||||
|
rawConfig: map[string]interface{}{},
|
||||||
|
expError: "must provide a Vault token or configure a Vault auth method",
|
||||||
|
},
|
||||||
|
"both token and auth method provided": {
|
||||||
|
rawConfig: map[string]interface{}{"Token": "test", "AuthMethod": map[string]interface{}{"Type": "test"}},
|
||||||
|
expError: "only one of Vault token or Vault auth method can be provided, but not both",
|
||||||
|
},
|
||||||
|
"no root PKI path": {
|
||||||
|
rawConfig: map[string]interface{}{"Token": "test"},
|
||||||
|
expError: "must provide a valid path to a root PKI backend",
|
||||||
|
},
|
||||||
|
"no root intermediate path": {
|
||||||
|
rawConfig: map[string]interface{}{"Token": "test", "RootPKIPath": "test"},
|
||||||
|
expError: "must provide a valid path for the intermediate PKI backend",
|
||||||
|
},
|
||||||
|
"adds a slash to RootPKIPath and IntermediatePKIPath": {
|
||||||
|
rawConfig: map[string]interface{}{"Token": "test", "RootPKIPath": "test", "IntermediatePKIPath": "test"},
|
||||||
|
expConfig: &structs.VaultCAProviderConfig{
|
||||||
|
CommonCAProviderConfig: defaultCommonConfig(),
|
||||||
|
Token: "test",
|
||||||
|
RootPKIPath: "test/",
|
||||||
|
IntermediatePKIPath: "test/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, c := range cases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
config, err := ParseVaultCAConfig(c.rawConfig)
|
||||||
|
if c.expError != "" {
|
||||||
|
require.EqualError(t, err, c.expError)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, c.expConfig, config)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultCAProvider_configureVaultAuthMethod(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
expLoginPath string
|
||||||
|
params map[string]interface{}
|
||||||
|
expError string
|
||||||
|
}{
|
||||||
|
"alicloud": {expLoginPath: "auth/alicloud/login"},
|
||||||
|
"approle": {expLoginPath: "auth/approle/login"},
|
||||||
|
"aws": {expLoginPath: "auth/aws/login"},
|
||||||
|
"azure": {expLoginPath: "auth/azure/login"},
|
||||||
|
"cf": {expLoginPath: "auth/cf/login"},
|
||||||
|
"github": {expLoginPath: "auth/github/login"},
|
||||||
|
"gcp": {expLoginPath: "auth/gcp/login"},
|
||||||
|
"jwt": {expLoginPath: "auth/jwt/login"},
|
||||||
|
"kerberos": {expLoginPath: "auth/kerberos/login"},
|
||||||
|
"kubernetes": {expLoginPath: "auth/kubernetes/login", params: map[string]interface{}{"jwt": "fake"}},
|
||||||
|
"ldap": {expLoginPath: "auth/ldap/login/foo", params: map[string]interface{}{"username": "foo"}},
|
||||||
|
"oci": {expLoginPath: "auth/oci/login/foo", params: map[string]interface{}{"role": "foo"}},
|
||||||
|
"okta": {expLoginPath: "auth/okta/login/foo", params: map[string]interface{}{"username": "foo"}},
|
||||||
|
"radius": {expLoginPath: "auth/radius/login/foo", params: map[string]interface{}{"username": "foo"}},
|
||||||
|
"cert": {expLoginPath: "auth/cert/login"},
|
||||||
|
"token": {expError: "'token' auth method is not supported via auth method configuration; please provide the token with the 'token' parameter in the CA configuration"},
|
||||||
|
"userpass": {expLoginPath: "auth/userpass/login/foo", params: map[string]interface{}{"username": "foo"}},
|
||||||
|
"unsupported": {expError: "auth method \"unsupported\" is not supported"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for authMethodType, c := range cases {
|
||||||
|
t.Run(authMethodType, func(t *testing.T) {
|
||||||
|
loginPath, err := configureVaultAuthMethod(&structs.VaultAuthMethod{
|
||||||
|
Type: authMethodType,
|
||||||
|
Params: c.params,
|
||||||
|
})
|
||||||
|
if c.expError == "" {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, c.expLoginPath, loginPath)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, c.expError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVaultCAProvider_VaultTLSConfig(t *testing.T) {
|
func TestVaultCAProvider_VaultTLSConfig(t *testing.T) {
|
||||||
config := &structs.VaultCAProviderConfig{
|
config := &structs.VaultCAProviderConfig{
|
||||||
CAFile: "/capath/ca.pem",
|
CAFile: "/capath/ca.pem",
|
||||||
|
@ -507,6 +595,138 @@ func TestVaultProvider_Cleanup(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVaultProvider_ConfigureWithAuthMethod(t *testing.T) {
|
||||||
|
|
||||||
|
SkipIfVaultNotPresent(t)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
authMethodType string
|
||||||
|
configureAuthMethodFunc func(t *testing.T, vaultClient *vaultapi.Client) map[string]interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
authMethodType: "userpass",
|
||||||
|
configureAuthMethodFunc: func(t *testing.T, vaultClient *vaultapi.Client) map[string]interface{} {
|
||||||
|
_, err := vaultClient.Logical().Write("/auth/userpass/users/test",
|
||||||
|
map[string]interface{}{"password": "foo", "policies": "admins"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return map[string]interface{}{
|
||||||
|
"Type": "userpass",
|
||||||
|
"Params": map[string]interface{}{
|
||||||
|
"username": "test",
|
||||||
|
"password": "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
authMethodType: "approle",
|
||||||
|
configureAuthMethodFunc: func(t *testing.T, vaultClient *vaultapi.Client) map[string]interface{} {
|
||||||
|
_, err := vaultClient.Logical().Write("auth/approle/role/my-role", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := vaultClient.Logical().Read("auth/approle/role/my-role/role-id")
|
||||||
|
require.NoError(t, err)
|
||||||
|
roleID := resp.Data["role_id"]
|
||||||
|
|
||||||
|
resp, err = vaultClient.Logical().Write("auth/approle/role/my-role/secret-id", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
secretID := resp.Data["secret_id"]
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"Type": "approle",
|
||||||
|
"Params": map[string]interface{}{
|
||||||
|
"role_id": roleID,
|
||||||
|
"secret_id": secretID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.authMethodType, func(t *testing.T) {
|
||||||
|
testVault := NewTestVaultServer(t)
|
||||||
|
|
||||||
|
err := testVault.Client().Sys().EnableAuthWithOptions(c.authMethodType, &vaultapi.EnableAuthOptions{Type: c.authMethodType})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
authMethodConf := c.configureAuthMethodFunc(t, testVault.Client())
|
||||||
|
|
||||||
|
conf := map[string]interface{}{
|
||||||
|
"Address": testVault.Addr,
|
||||||
|
"RootPKIPath": "pki-root/",
|
||||||
|
"IntermediatePKIPath": "pki-intermediate/",
|
||||||
|
"AuthMethod": authMethodConf,
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewVaultProvider(hclog.New(nil))
|
||||||
|
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
ClusterID: connect.TestClusterID,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
IsPrimary: true,
|
||||||
|
RawConfig: conf,
|
||||||
|
}
|
||||||
|
t.Cleanup(provider.Stop)
|
||||||
|
err = provider.Configure(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, provider.client.Token())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultProvider_RotateAuthMethodToken(t *testing.T) {
|
||||||
|
|
||||||
|
SkipIfVaultNotPresent(t)
|
||||||
|
|
||||||
|
testVault := NewTestVaultServer(t)
|
||||||
|
|
||||||
|
err := testVault.Client().Sys().EnableAuthWithOptions("approle", &vaultapi.EnableAuthOptions{Type: "approle"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = testVault.Client().Logical().Write("auth/approle/role/my-role",
|
||||||
|
map[string]interface{}{"token_ttl": "2s", "token_explicit_max_ttl": "2s"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := testVault.Client().Logical().Read("auth/approle/role/my-role/role-id")
|
||||||
|
require.NoError(t, err)
|
||||||
|
roleID := resp.Data["role_id"]
|
||||||
|
|
||||||
|
resp, err = testVault.Client().Logical().Write("auth/approle/role/my-role/secret-id", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
secretID := resp.Data["secret_id"]
|
||||||
|
|
||||||
|
conf := map[string]interface{}{
|
||||||
|
"Address": testVault.Addr,
|
||||||
|
"RootPKIPath": "pki-root/",
|
||||||
|
"IntermediatePKIPath": "pki-intermediate/",
|
||||||
|
"AuthMethod": map[string]interface{}{
|
||||||
|
"Type": "approle",
|
||||||
|
"Params": map[string]interface{}{
|
||||||
|
"role_id": roleID,
|
||||||
|
"secret_id": secretID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := NewVaultProvider(hclog.New(nil))
|
||||||
|
|
||||||
|
cfg := ProviderConfig{
|
||||||
|
ClusterID: connect.TestClusterID,
|
||||||
|
Datacenter: "dc1",
|
||||||
|
IsPrimary: true,
|
||||||
|
RawConfig: conf,
|
||||||
|
}
|
||||||
|
t.Cleanup(provider.Stop)
|
||||||
|
err = provider.Configure(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
token := provider.client.Token()
|
||||||
|
require.NotEmpty(t, token)
|
||||||
|
|
||||||
|
// Check that the token is rotated after max_ttl time has passed.
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return provider.client.Token() != token
|
||||||
|
}, 10*time.Second, 100*time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
func getIntermediateCertTTL(t *testing.T, caConf *structs.CAConfiguration) time.Duration {
|
func getIntermediateCertTTL(t *testing.T, caConf *structs.CAConfiguration) time.Duration {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
@ -526,10 +746,6 @@ func getIntermediateCertTTL(t *testing.T, caConf *structs.CAConfiguration) time.
|
||||||
return dur
|
return dur
|
||||||
}
|
}
|
||||||
|
|
||||||
func testVaultProvider(t *testing.T) (*VaultProvider, *TestVaultServer) {
|
|
||||||
return testVaultProviderWithConfig(t, true, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testVaultProviderWithConfig(t *testing.T, isPrimary bool, rawConf map[string]interface{}) (*VaultProvider, *TestVaultServer) {
|
func testVaultProviderWithConfig(t *testing.T, isPrimary bool, rawConf map[string]interface{}) (*VaultProvider, *TestVaultServer) {
|
||||||
testVault, err := runTestVault(t)
|
testVault, err := runTestVault(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -573,6 +789,7 @@ func createVaultProvider(t *testing.T, isPrimary bool, addr, token string, rawCo
|
||||||
cfg.Datacenter = "dc2"
|
cfg.Datacenter = "dc2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Cleanup(provider.Stop)
|
||||||
require.NoError(t, provider.Configure(cfg))
|
require.NoError(t, provider.Configure(cfg))
|
||||||
if isPrimary {
|
if isPrimary {
|
||||||
require.NoError(t, provider.GenerateRoot())
|
require.NoError(t, provider.GenerateRoot())
|
||||||
|
|
|
@ -481,6 +481,14 @@ type VaultCAProviderConfig struct {
|
||||||
KeyFile string
|
KeyFile string
|
||||||
TLSServerName string
|
TLSServerName string
|
||||||
TLSSkipVerify bool
|
TLSSkipVerify bool
|
||||||
|
|
||||||
|
AuthMethod *VaultAuthMethod `alias:"auth_method"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VaultAuthMethod struct {
|
||||||
|
Type string
|
||||||
|
MountPath string `alias:"mount_path"`
|
||||||
|
Params map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AWSCAProviderConfig struct {
|
type AWSCAProviderConfig struct {
|
||||||
|
|
|
@ -88,13 +88,33 @@ The configuration options are listed below.
|
||||||
- `Address` / `address` (`string: <required>`) - The address of the Vault
|
- `Address` / `address` (`string: <required>`) - The address of the Vault
|
||||||
server.
|
server.
|
||||||
|
|
||||||
- `Token` / `token` (`string: <required>`) - A token for accessing Vault.
|
- `Token` / `token` (`string: ""`) - A token for accessing Vault.
|
||||||
This is write-only and will not be exposed when reading the CA configuration.
|
This is write-only and will not be exposed when reading the CA configuration.
|
||||||
This token must have [proper privileges](#vault-acl-policies) for the PKI
|
This token must have [proper privileges](#vault-acl-policies) for the PKI
|
||||||
paths configured. In Consul 1.8.5 and later, if the token has the [renewable](https://www.vaultproject.io/api-docs/auth/token#renewable)
|
paths configured. In Consul 1.8.5 and later, if the token has the [renewable](https://www.vaultproject.io/api-docs/auth/token#renewable)
|
||||||
flag set, Consul will attempt to renew its lease periodically after half the
|
flag set, Consul will attempt to renew its lease periodically after half the
|
||||||
duration has expired.
|
duration has expired.
|
||||||
|
|
||||||
|
!> **Warning:** You must either provide a token or configure an auth method below.
|
||||||
|
|
||||||
|
- `AuthMethod` / `auth_method` (`map: nil`) - Vault auth method to use for logging in to Vault.
|
||||||
|
Please see [Vault Auth Methods](https://www.vaultproject.io/docs/auth) for more information
|
||||||
|
on how to configure individual auth methods. If auth method is provided, Consul will obtain a
|
||||||
|
a new token from Vault when the token can no longer be renewed.
|
||||||
|
|
||||||
|
- `Type`/ `type` (`string: ""`) - The type of Vault auth method.
|
||||||
|
|
||||||
|
- `MountPath`/ `mount_path` (`string: <AuthMethod.Type>`) - The mount path of the auth method.
|
||||||
|
If not provided the auth method type will be used as the mount path.
|
||||||
|
|
||||||
|
- `Params`/`params` (`map: nil`) - The parameters to configure the auth method. Please see
|
||||||
|
[Vault Auth Methods](https://www.vaultproject.io/docs/auth) for information on how to configure the
|
||||||
|
auth method you wish to use. If using the Kubernetes auth method,
|
||||||
|
Consul will read the service account token from the
|
||||||
|
default mount path `/var/run/secrets/kubernetes.io/serviceaccount/token` if the `jwt` parameter
|
||||||
|
is not provided.
|
||||||
|
|
||||||
|
|
||||||
- `RootPKIPath` / `root_pki_path` (`string: <required>`) - The path to
|
- `RootPKIPath` / `root_pki_path` (`string: <required>`) - The path to
|
||||||
a PKI secrets engine for the root certificate. If the path does not
|
a PKI secrets engine for the root certificate. If the path does not
|
||||||
exist, Consul will mount a new PKI secrets engine at the specified path with the
|
exist, Consul will mount a new PKI secrets engine at the specified path with the
|
||||||
|
|
Loading…
Reference in New Issue