Create new "previous-root" key purpose for root key migrations (#2639) (#2648)

* Update go-kms-wrapping dependency

* Apply changes suggested by Jeff

* Fix listener test

* Add note about config fix to CHANGELOG

* Review comments

Co-authored-by: Dan Heath <76443935+Dan-Heath@users.noreply.github.com>

Co-authored-by: Dan Heath <76443935+Dan-Heath@users.noreply.github.com>
pull/2651/head
Johan Brandhorst-Satzkorn 4 years ago committed by GitHub
parent 58f86936e7
commit df02501dcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -49,6 +49,12 @@ Canonical reference for changes, improvements, and bugfixes for Boundary.
sessions are ongoing ([PR](https://github.com/hashicorp/boundary/pull/2612))
* sessions: Fixed a panic in a worker when a user with an active
session is deleted ([PR](https://github.com/hashicorp/boundary/pull/2629))
* sessions: Fixed a bug where reading a session after its associated project
had been deleted would result in an error
([PR](https://github.com/hashicorp/boundary/pull/2615))
* config: Fixed a bug where supplying multiple KMS blocks with the same purpose
would silently ignore all but the last block
([PR](https://github.com/hashicorp/boundary/pull/2639))
### Deprecations/Changes

@ -2,6 +2,7 @@ package globals
const (
KmsPurposeRoot = "root"
KmsPurposePreviousRoot = "previous-root"
KmsPurposeWorkerAuth = "worker-auth"
KmsPurposeWorkerAuthStorage = "worker-auth-storage"
KmsPurposeRecovery = "recovery"

@ -30,7 +30,7 @@ require (
github.com/hashicorp/go-bexpr v0.1.10
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v1.2.2
github.com/hashicorp/go-kms-wrapping/v2 v2.0.6-0.20220722192355-a843f53fa48d
github.com/hashicorp/go-kms-wrapping/v2 v2.0.6-0.20221122211539-47c893099f13
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-retryablehttp v0.7.0
github.com/hashicorp/go-rootcerts v1.0.2
@ -92,7 +92,7 @@ require github.com/hashicorp/go-dbw v0.0.0-20220910135738-ed4505749995
require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/cenkalti/backoff/v4 v4.1.0
github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220914160710-1c6d04de2431
github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20221122211539-47c893099f13
github.com/hashicorp/nodeenrollment v0.1.17
github.com/kelseyhightower/envconfig v1.4.0
golang.org/x/exp v0.0.0-20220921164117-439092de6870

@ -679,12 +679,12 @@ github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjh
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g=
github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220914160710-1c6d04de2431 h1:2fmBl291s44mKzvBjF1TfqJgw1GHk97jxkGbalp4TEM=
github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20220914160710-1c6d04de2431/go.mod h1:qbLdiZhFd182vRSOlhBV/OarUcJVTOF2WklPwHs8mYQ=
github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20221122211539-47c893099f13 h1:486QccSsJAKonkWSZmBn5GWzZCW3UV3OwMoOGUXJBO4=
github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20221122211539-47c893099f13/go.mod h1:qbLdiZhFd182vRSOlhBV/OarUcJVTOF2WklPwHs8mYQ=
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.2 h1:it114Kjpk79JVh6AfxFMP5oi1gb+RPzcSLiG3zF/J0w=
github.com/hashicorp/go-kms-wrapping/plugin/v2 v2.0.2/go.mod h1:b9mq0bG5xJ10ZcTEQJLZ22O9y71hIt2MGTLqROA/SAM=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.6-0.20220722192355-a843f53fa48d h1:mOtPXWIp4cWKNt9S55IuYAdyUgNtCfUAEVIjcXDx59E=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.6-0.20220722192355-a843f53fa48d/go.mod h1:sDQAfwJGv25uGPZA04x87ERglCG6avnRcBT9wYoMII8=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.6-0.20221122211539-47c893099f13 h1:4YewFQjQJNAzXtahRax77IFi0emoYvPZGESfi+KCpGg=
github.com/hashicorp/go-kms-wrapping/v2 v2.0.6-0.20221122211539-47c893099f13/go.mod h1:sDQAfwJGv25uGPZA04x87ERglCG6avnRcBT9wYoMII8=
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=

@ -2,6 +2,7 @@ package base
import (
"context"
"fmt"
"sync"
"testing"
@ -10,6 +11,8 @@ import (
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/go-hclog"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/multi"
"github.com/hashicorp/go-secure-stdlib/configutil/v2"
"github.com/hashicorp/go-secure-stdlib/listenerutil"
"github.com/mitchellh/cli"
@ -31,7 +34,7 @@ func Test_NewServer(t *testing.T) {
})
}
func TestServer_SetupKMSes(t *testing.T) {
func TestServer_SetupKMSes_Purposes(t *testing.T) {
tests := []struct {
name string
purposes []string
@ -61,6 +64,41 @@ func TestServer_SetupKMSes(t *testing.T) {
globals.KmsPurposeWorkerAuthStorage, globals.KmsPurposeConfig,
},
},
{
name: "previous root without root",
purposes: []string{globals.KmsPurposePreviousRoot},
wantErrContains: fmt.Sprintf("KMS block contains '%s' without '%s'", globals.KmsPurposePreviousRoot, globals.KmsPurposeRoot),
},
{
name: "root and previous in the same stanza",
purposes: []string{globals.KmsPurposeRoot, globals.KmsPurposePreviousRoot},
wantErrContains: fmt.Sprintf("KMS blocks with purposes '%s' and '%s' must have different key IDs", globals.KmsPurposeRoot, globals.KmsPurposePreviousRoot),
},
{
name: "duplicate root purposes",
purposes: []string{globals.KmsPurposeRoot, globals.KmsPurposeRoot},
wantErrContains: fmt.Sprintf("Duplicate KMS block for purpose '%s'", globals.KmsPurposeRoot),
},
{
name: "duplicate previous root purposes",
purposes: []string{globals.KmsPurposePreviousRoot, globals.KmsPurposePreviousRoot},
wantErrContains: fmt.Sprintf("Duplicate KMS block for purpose '%s'", globals.KmsPurposePreviousRoot),
},
{
name: "duplicate worker auth purposes",
purposes: []string{globals.KmsPurposeWorkerAuth, globals.KmsPurposeWorkerAuth},
wantErrContains: fmt.Sprintf("Duplicate KMS block for purpose '%s'", globals.KmsPurposeWorkerAuth),
},
{
name: "duplicate worker auth storage purposes",
purposes: []string{globals.KmsPurposeWorkerAuthStorage, globals.KmsPurposeWorkerAuthStorage},
wantErrContains: fmt.Sprintf("Duplicate KMS block for purpose '%s'", globals.KmsPurposeWorkerAuthStorage),
},
{
name: "duplicate recovery purposes",
purposes: []string{globals.KmsPurposeRecovery, globals.KmsPurposeRecovery},
wantErrContains: fmt.Sprintf("Duplicate KMS block for purpose '%s'", globals.KmsPurposeRecovery),
},
}
logger := hclog.Default()
serLock := new(sync.Mutex)
@ -102,6 +140,104 @@ func TestServer_SetupKMSes(t *testing.T) {
}
}
func TestServer_SetupKMSes_RootMigration(t *testing.T) {
t.Parallel()
t.Run("correctly-pools-root-and-previous", func(t *testing.T) {
t.Parallel()
assert, require := assert.New(t), require.New(t)
logger := hclog.Default()
serLock := new(sync.Mutex)
conf := &configutil.SharedConfig{
Seals: []*configutil.KMS{
{
Type: "aead",
Purpose: []string{
globals.KmsPurposeRoot,
},
Config: map[string]string{
"key_id": "root",
},
},
{
Type: "aead",
Purpose: []string{
globals.KmsPurposePreviousRoot,
},
Config: map[string]string{
"key_id": "previous_root",
},
},
},
}
s := NewServer(&Command{Context: context.Background()})
require.NoError(s.SetupEventing(logger, serLock, "setup-kms-testing"))
err := s.SetupKMSes(s.Context, cli.NewMockUi(), &config.Config{SharedConfig: conf})
require.NoError(err)
require.NotNil(s.RootKms)
typ, err := s.RootKms.Type(s.Context)
require.NoError(err)
assert.Equal(wrapping.WrapperTypePooled, typ)
// Ensure that the root is the encryptor
keyId, err := s.RootKms.KeyId(s.Context)
require.NoError(err)
assert.Equal("root", keyId)
// Ensure that the previous root is in the wrapper too
assert.Equal([]string{"previous_root", "root"}, s.RootKms.(*multi.PooledWrapper).AllKeyIds())
})
t.Run("errors-on-previous-without-root", func(t *testing.T) {
t.Parallel()
require := require.New(t)
logger := hclog.Default()
serLock := new(sync.Mutex)
conf := &configutil.SharedConfig{
Seals: []*configutil.KMS{
{
Type: "aead",
Purpose: []string{
globals.KmsPurposePreviousRoot,
},
},
},
}
s := NewServer(&Command{Context: context.Background()})
require.NoError(s.SetupEventing(logger, serLock, "setup-kms-testing"))
err := s.SetupKMSes(s.Context, cli.NewMockUi(), &config.Config{SharedConfig: conf})
require.Error(err)
})
t.Run("errors-on-previous-and-root-with-same-key-id", func(t *testing.T) {
t.Parallel()
require := require.New(t)
logger := hclog.Default()
serLock := new(sync.Mutex)
conf := &configutil.SharedConfig{
Seals: []*configutil.KMS{
{
Type: "aead",
Purpose: []string{
globals.KmsPurposeRoot,
},
Config: map[string]string{
"key_id": "root",
},
},
{
Type: "aead",
Purpose: []string{
globals.KmsPurposePreviousRoot,
},
Config: map[string]string{
"key_id": "root",
},
},
},
}
s := NewServer(&Command{Context: context.Background()})
require.NoError(s.SetupEventing(logger, serLock, "setup-kms-testing"))
err := s.SetupKMSes(s.Context, cli.NewMockUi(), &config.Config{SharedConfig: conf})
require.Error(err)
})
}
func TestServer_SetupEventing(t *testing.T) {
// DO NOT run these test in parallel since they have a dependency on
// event.sysEventer

@ -27,11 +27,13 @@ import (
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/internal/util"
kms_plugin_assets "github.com/hashicorp/boundary/plugins/kms"
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/boundary/version"
"github.com/hashicorp/go-hclog"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/multi"
"github.com/hashicorp/go-multierror"
configutil "github.com/hashicorp/go-secure-stdlib/configutil/v2"
"github.com/hashicorp/go-secure-stdlib/gatedwriter"
@ -544,6 +546,7 @@ func (b *Server) SetupKMSes(ctx context.Context, ui cli.Ui, config *config.Confi
sharedConfig := config.SharedConfig
var pluginLogger hclog.Logger
var err error
var previousRootKms wrapping.Wrapper
for _, kms := range sharedConfig.Seals {
for _, purpose := range kms.Purpose {
purpose = strings.ToLower(purpose)
@ -554,7 +557,10 @@ func (b *Server) SetupKMSes(ctx context.Context, ui cli.Ui, config *config.Confi
if opts.withSkipWorkerAuthKmsInstantiation {
continue
}
case globals.KmsPurposeRoot, globals.KmsPurposeConfig, globals.KmsPurposeWorkerAuthStorage:
case globals.KmsPurposeRoot,
globals.KmsPurposePreviousRoot,
globals.KmsPurposeConfig,
globals.KmsPurposeWorkerAuthStorage:
case globals.KmsPurposeRecovery:
if config.Controller != nil && config.DevRecoveryKey != "" {
kms.Config["key"] = config.DevRecoveryKey
@ -615,22 +621,59 @@ func (b *Server) SetupKMSes(ctx context.Context, ui cli.Ui, config *config.Confi
kms.Purpose = origPurpose
switch purpose {
case globals.KmsPurposePreviousRoot:
if previousRootKms != nil {
return fmt.Errorf("Duplicate KMS block for purpose '%s'. You may need to remove all but the last KMS block for this purpose.", purpose)
}
previousRootKms = wrapper
case globals.KmsPurposeRoot:
if b.RootKms != nil {
return fmt.Errorf("Duplicate KMS block for purpose '%s'. You may need to remove all but the last KMS block for this purpose.", purpose)
}
b.RootKms = wrapper
case globals.KmsPurposeWorkerAuth:
if b.WorkerAuthKms != nil {
return fmt.Errorf("Duplicate KMS block for purpose '%s'. You may need to remove all but the last KMS block for this purpose.", purpose)
}
b.WorkerAuthKms = wrapper
case globals.KmsPurposeWorkerAuthStorage:
if b.WorkerAuthStorageKms != nil {
return fmt.Errorf("Duplicate KMS block for purpose '%s'. You may need to remove all but the last KMS block for this purpose.", purpose)
}
b.WorkerAuthStorageKms = wrapper
case globals.KmsPurposeRecovery:
if b.RecoveryKms != nil {
return fmt.Errorf("Duplicate KMS block for purpose '%s'. You may need to remove all but the last KMS block for this purpose.", purpose)
}
b.RecoveryKms = wrapper
case globals.KmsPurposeConfig:
// Do nothing, can be set in same file but not needed at runtime
continue
default:
return fmt.Errorf("KMS purpose of %q is unknown", purpose)
}
}
}
// Handle previous root KMS
if previousRootKms != nil {
if util.IsNil(b.RootKms) {
return fmt.Errorf("KMS block contains '%s' without '%s'", globals.KmsPurposePreviousRoot, globals.KmsPurposeRoot)
}
mw, err := multi.NewPooledWrapper(ctx, previousRootKms)
if err != nil {
return fmt.Errorf("failed to create multi wrapper: %w", err)
}
ok, err := mw.SetEncryptingWrapper(ctx, b.RootKms)
if err != nil {
return fmt.Errorf("failed to set root wrapper as active in multi wrapper: %w", err)
}
if !ok {
return fmt.Errorf("KMS blocks with purposes '%s' and '%s' must have different key IDs", globals.KmsPurposeRoot, globals.KmsPurposePreviousRoot)
}
b.RootKms = mw
}
// prepare a secure random reader
b.SecureRandomReader, err = configutil.CreateSecureRandomReaderFunc(config.SharedConfig, b.RootKms)
if err != nil {

@ -88,9 +88,7 @@ func TestServer_ReloadListener(t *testing.T) {
wd += "/test-fixtures/reload/"
td, err := os.MkdirTemp("", "boundary-test-")
if err != nil {
t.Fatal(err)
}
require.NoError(err)
defer os.RemoveAll(td)
controllerKey := config.DevKeyGeneration()
@ -103,13 +101,16 @@ func TestServer_ReloadListener(t *testing.T) {
UseDevAuthMethod: true,
UseDevTarget: true,
})
// Unset auto-created KMSes that are overwritten by config on startup
cmd.RootKms = nil
cmd.WorkerAuthKms = nil
cmd.RecoveryKms = nil
defer func() {
if cmd.DevDatabaseCleanupFunc != nil {
require.NoError(cmd.DevDatabaseCleanupFunc())
}
}()
require.NoError(err)
// Setup initial certs
inBytes, err := os.ReadFile(wd + "bundle1.pem")

@ -31,10 +31,10 @@ Following best practices of using different encryption keys for different
purposes, Boundary has a number of encryption keys generated within each scope.
The `root` KMS key acts as a KEK (Key Encrypting Key) for the scope-specific
KEKs (also referred to as the scope's `root` key). The scope's `root` KEK and
the various DEKs (Data Encryption Keys) are created when a scope is created. The
DEKs are encrypted with the scope's `root` KEK, and this is in turn encrypted
with the KMS key marked for `root` purpose.
KEKs (also referred to as the scope's `root` key). The scope's `root` KEK
and the various DEKs (Data Encryption Keys) are created when a scope is created.
The DEKs are encrypted with the scope's `root` KEK, and this is in turn
encrypted with the KMS key marked for the `root` purpose.
The current scoped DEKs and their purposes are detailed below:
@ -55,6 +55,60 @@ provided in this section is purely for informational purposes.
- `sessions`: This is used as a base key against which to derive
session-specific encryption keys.
### Key version lifecycle Management
You can control the lifecycles of the per-scope KEK and DEKs via the CLI or
API using the key endpoints under the scopes section. To rotate all the keys
in a scope, use the `rotate-keys` endpoint:
```shell-session
$ boundary scopes rotate-keys -scope-id p_A4jfDjZ9jf
```
This endpoint creates new key versions for all keys in the scope
`p_A4jfDjZ9jf`, and makes these key versions the active key versions for
encrypting new data. The previous key version(s) will continue to be used for
decrypting existing data. You can use the `-rewrap` flag to immediately rewrap
all DEK versions with the new KEK version. Otherwise, the DEK versions remain
encrypted by the prior KEK version from when they were created.
To list all keys in a scope and their versions, use the `list-keys` endpoint:
```shell-session
$ boundary scopes list-keys -scope-id p_A4jfDjZ9jf
```
To destroy a key version, use the `destroy-key-version` endpoint:
```shell-session
$ boundary scopes destroy-key-version -scope-id p_A4jfDjZ9jf -key-version-id kdkv_tr6ZN8opYr
```
The latest key version cannot be destroyed, so if you want to destroy the
latest key version, you will need to call the `rotate-keys` endpoint first,
to create a new set of key versions. The `oplog` purpose key versions also
cannot be destroyed at this time.
Destroying a key version sometimes requires background work before it can be
completed. This is because DEK versions that currently encrypt data
necessitate re-encrypting that data with the latest DEK version for each
purpose before they can be deleted. You can monitor the progress of this
re-encryption job via the `list-key-destruction-jobs` endpoint:
```shell-session
$ boundary scopes list-key-version-destruction-jobs -scope-id p_A4jfDjZ9jf
```
Once the job disappears from this list, the associated key version will have
been destroyed and any existing data will have been re-encrypted.
## The `previous-root` KMS key <sup>OSS Only</sup>
The `previous-root` KMS key is used when migrating to a new `root` key. Adding
the `previous-root` KMS key to your configuration informs the Controller to use
it for decrypting the existing information in the database, allowing you to
rotate and rewrap the KEKs to complete the migration to the new root key.
## The `worker-auth` KMS Key <sup>OSS Only</sup>
The `worker-auth` KMS key is a key shared by the Controller and Worker in order

@ -21,7 +21,8 @@ kms "aead" {
}
```
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `root`, `recovery`, or `config`.
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `worker-auth-storage`,
`root`, `previous-root`, `recovery`, or `config`.
- `aead_type` - The type of encryption this KMS uses. Currently only `aes-gcm` is implemented.

@ -31,7 +31,8 @@ kms "alicloudkms" {
These parameters apply to the `kms` stanza in the Boundary configuration file:
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `root`, `recovery`, or `config`.
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `worker-auth-storage`,
`root`, `previous-root`, `recovery`, or `config`.
- `region` `(string: <required> "us-east-1")`: The AliCloud region where the encryption key
lives. May also be specified by the `ALICLOUD_REGION`

@ -30,8 +30,8 @@ kms "awskms" {
These parameters apply to the `kms` stanza in the Boundary configuration file:
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `root`,
`recovery`, or `config`.
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `worker-auth-storage`,
`root`, `previous-root`, `recovery`, or `config`.
- `region` `(string: "us-east-1")`: The AWS region where the encryption key
lives. If not provided, may be populated from the `AWS_REGION` or

@ -32,7 +32,8 @@ kms "azurekeyvault" {
These parameters apply to the `kms` stanza in the Vault configuration file:
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `root`, `recovery`, or `config`.
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `worker-auth-storage`,
`root`, `previous-root`, `recovery`, or `config`.
- `tenant_id` `(string: <required>)`: The tenant id for the Azure Active Directory organization. May
also be specified by the `AZURE_TENANT_ID` environment variable.

@ -31,7 +31,8 @@ kms "gcpckms" {
These parameters apply to the `kms` stanza in the Boundary configuration file:
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `root`, `recovery`, or `config`.
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `worker-auth-storage`,
`root`, `previous-root`, `recovery`, or `config`.
- `credentials` `(string: <required>)`: The path to the credentials JSON file
to use. May be also specified by the `GOOGLE_CREDENTIALS` or

@ -30,7 +30,8 @@ kms "ocikms" {
These parameters apply to the `kms` stanza in the Boundary configuration file:
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `root`, `recovery`, or `config`.
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `worker-auth-storage`,
`root`, `previous-root`, `recovery`, or `config`.
- `key_id` `(string: <required>)`: The OCI KMS key ID to use.
- `crypto_endpoint` `(string: <required>)`: The OCI KMS cryptographic endpoint (or data plane endpoint)
to be used to make OCI KMS encryption/decryption requests.

@ -41,8 +41,8 @@ kms "transit" {
These parameters apply to the `kms` stanza in the Vault configuration file:
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `root`,
`recovery`, or `config`.
- `purpose` - Purpose of this KMS, acceptable values are: `worker-auth`, `worker-auth-storage`,
`root`, `previous-root`, `recovery`, or `config`.
- `address` `(string: <required>)`: The full address to the Vault cluster.
This may also be specified by the `VAULT_ADDR` environment variable.

Loading…
Cancel
Save