From 4d1ab0e801b01ba89104d663c52eb38aeff76377 Mon Sep 17 00:00:00 2001 From: Paul Glass Date: Thu, 9 Feb 2023 09:10:11 -0600 Subject: [PATCH 1/6] NET-1721: Automatic ACL bootstrap with Vault secrets backend With the Vault secrets backend, server-acl-init now: * Runs the Vault agent as a sidecar * Bootstraps ACLs if the Vault bootstrap token is empty or not found, and writes the bootstrap token back to Vault via the Vault agent This adds the Vault SDK to the control-plane binary. This added 1 MB to the binary size (74MB to 75MB) --- acceptance/framework/vault/helpers.go | 14 +++ acceptance/tests/vault/vault_test.go | 60 +++++++++++- .../consul/templates/server-acl-init-job.yaml | 24 ++++- .../consul/test/unit/server-acl-init-job.bats | 77 ++++++++++++---- control-plane/go.mod | 29 +++++- control-plane/go.sum | 64 +++++++++++-- .../subcommand/server-acl-init/command.go | 92 +++++++++++++------ .../server-acl-init/k8s_secrets_backend.go | 60 ++++++++++++ .../server-acl-init/secrets_backend.go | 23 +++++ .../subcommand/server-acl-init/servers.go | 32 ++----- .../server-acl-init/vault_secrets_backend.go | 64 +++++++++++++ 11 files changed, 451 insertions(+), 88 deletions(-) create mode 100644 control-plane/subcommand/server-acl-init/k8s_secrets_backend.go create mode 100644 control-plane/subcommand/server-acl-init/secrets_backend.go create mode 100644 control-plane/subcommand/server-acl-init/vault_secrets_backend.go diff --git a/acceptance/framework/vault/helpers.go b/acceptance/framework/vault/helpers.go index 4726e246ae..2280aee8b2 100644 --- a/acceptance/framework/vault/helpers.go +++ b/acceptance/framework/vault/helpers.go @@ -167,6 +167,20 @@ func (config *KV2Secret) SaveSecretAndAddReadPolicy(t *testing.T, vaultClient *v path "%s" { capabilities = ["read"] }`, config.Path) + config.saveSecretAndAddPolicy(t, vaultClient, policy) +} + +// SaveSecretAndAddUpdatePolicy will create an update policy for the PolicyName +// on the KV2Secret and then will save the secret in the KV2 store. +func (config *KV2Secret) SaveSecretAndAddUpdatePolicy(t *testing.T, vaultClient *vapi.Client) { + policy := fmt.Sprintf(` + path "%s" { + capabilities = ["read", "update"] + }`, config.Path) + config.saveSecretAndAddPolicy(t, vaultClient, policy) +} + +func (config *KV2Secret) saveSecretAndAddPolicy(t *testing.T, vaultClient *vapi.Client, policy string) { // Create the Vault Policy for the secret. logger.Log(t, "Creating policy") err := vaultClient.Sys().PutPolicy(config.PolicyName, policy) diff --git a/acceptance/tests/vault/vault_test.go b/acceptance/tests/vault/vault_test.go index cf0c926b22..4d43d8bb5b 100644 --- a/acceptance/tests/vault/vault_test.go +++ b/acceptance/tests/vault/vault_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/consul-k8s/acceptance/framework/logger" "github.com/hashicorp/consul-k8s/acceptance/framework/portforward" "github.com/hashicorp/consul-k8s/acceptance/framework/vault" + "github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/go-uuid" "github.com/hashicorp/go-version" "github.com/stretchr/testify/require" @@ -31,6 +32,29 @@ const ( // TestVault installs Vault, bootstraps it with secrets, policies, and Kube Auth Method. // It then configures Consul to use vault as the backend and checks that it works. func TestVault(t *testing.T) { + cases := map[string]struct { + autoBootstrap bool + }{ + "manual ACL bootstrap": {}, + "automatic ACL bootstrap": { + autoBootstrap: true, + }, + } + for name, c := range cases { + c := c + t.Run(name, func(t *testing.T) { + testVault(t, c.autoBootstrap) + }) + } +} + +// testVault is the implementation for TestVault: +// +// - testAutoBootstrap = false. Test when ACL bootstrapping has already occurred. +// The test pre-populates a Vault secret with the bootstrap token. +// - testAutoBootstrap = true. Test that server-acl-init automatically ACL bootstraps +// consul and writes the bootstrap token to Vault. +func testVault(t *testing.T, testAutoBootstrap bool) { cfg := suite.Config() ctx := suite.Environment().DefaultContext(t) kubectlOptions := ctx.KubectlOptions(t) @@ -123,16 +147,22 @@ func TestVault(t *testing.T) { licenseSecret.SaveSecretAndAddReadPolicy(t, vaultClient) } - // Bootstrap Token - bootstrapToken, err := uuid.GenerateUUID() - require.NoError(t, err) bootstrapTokenSecret := &vault.KV2Secret{ Path: "consul/data/secret/bootstrap", Key: "token", - Value: bootstrapToken, + Value: "", PolicyName: "bootstrap", } - bootstrapTokenSecret.SaveSecretAndAddReadPolicy(t, vaultClient) + if testAutoBootstrap { + bootstrapTokenSecret.SaveSecretAndAddUpdatePolicy(t, vaultClient) + } else { + id, err := uuid.GenerateUUID() + require.NoError(t, err) + bootstrapTokenSecret.Value = id + bootstrapTokenSecret.SaveSecretAndAddReadPolicy(t, vaultClient) + } + + bootstrapToken := bootstrapTokenSecret.Value // ------------------------- // Additional Auth Roles @@ -265,6 +295,26 @@ func TestVault(t *testing.T) { logger.Logf(t, "Wait %d seconds for certificates to rotate....", expirationInSeconds) time.Sleep(time.Duration(expirationInSeconds) * time.Second) + if testAutoBootstrap { + logger.Logf(t, "Validating the ACL bootstrap token was stored in Vault.") + timer := &retry.Timer{Timeout: 10 * time.Second, Wait: 1 * time.Second} + retry.RunWith(timer, t, func(r *retry.R) { + secret, err := vaultClient.Logical().Read("consul/data/secret/bootstrap") + require.NoError(r, err) + + data, ok := secret.Data["data"].(map[string]interface{}) + require.True(r, ok) + require.NotNil(r, data) + + tok, ok := data["token"].(string) + require.True(r, ok) + require.NotEmpty(r, tok) + + // Set bootstrapToken for subsequent validations. + bootstrapToken = tok + }) + } + // Validate that the gossip encryption key is set correctly. logger.Log(t, "Validating the gossip key has been set correctly.") consulCluster.ACLToken = bootstrapToken diff --git a/charts/consul/templates/server-acl-init-job.yaml b/charts/consul/templates/server-acl-init-job.yaml index 440ab8bee0..9844844bec 100644 --- a/charts/consul/templates/server-acl-init-job.yaml +++ b/charts/consul/templates/server-acl-init-job.yaml @@ -47,7 +47,22 @@ spec: annotations: "consul.hashicorp.com/connect-inject": "false" {{- if .Values.global.secretsBackend.vault.enabled }} - "vault.hashicorp.com/agent-pre-populate-only": "true" + + {{- /* Run the Vault agent as both an init container and sidecar. + The Vault agent sidecar is needed when server-acl-init bootstraps ACLs + and writes the bootstrap token back to Vault. + * agent-prepopulate: true - Run the Vault agent init container. + * agent-pre-populate-only: false - Also, run the Vault agent sidecar. + * agent-cache-enable: true - Enable the Agent cache listener. + * agent-cache-listener-port: 8200 - (optional) Listen on 127.0.0.1:8200. + * agent-enable-quit: true - Enable a "quit" endpoint. server-acl-init + tells the Vault agent to stop (without this the Job will complete). + */}} + "vault.hashicorp.com/agent-pre-populate": "true" + "vault.hashicorp.com/agent-pre-populate-only": "false" + "vault.hashicorp.com/agent-cache-enable": "true" + "vault.hashicorp.com/agent-cache-listener-port": "8200" + "vault.hashicorp.com/agent-enable-quit": "true" "vault.hashicorp.com/agent-inject": "true" {{- if .Values.global.acls.bootstrapToken.secretName }} {{- with .Values.global.acls.bootstrapToken }} @@ -161,10 +176,13 @@ spec: -resource-prefix=${CONSUL_FULLNAME} \ -k8s-namespace={{ .Release.Namespace }} \ -set-server-tokens={{ $serverEnabled }} \ - + {{- if .Values.global.secretsBackend.vault.enabled }} + -secrets-backend=vault \ + {{- end }} {{- if .Values.global.acls.bootstrapToken.secretName }} {{- if .Values.global.secretsBackend.vault.enabled }} - -bootstrap-token-file=/vault/secrets/bootstrap-token \ + -bootstrap-token-secret-name={{ .Values.global.acls.bootstrapToken.secretName }} \ + -bootstrap-token-secret-key={{ .Values.global.acls.bootstrapToken.secretKey }} \ {{- else }} -bootstrap-token-file=/consul/acl/tokens/bootstrap-token \ {{- end }} diff --git a/charts/consul/test/unit/server-acl-init-job.bats b/charts/consul/test/unit/server-acl-init-job.bats index 63450aa4c2..70d58fdf92 100644 --- a/charts/consul/test/unit/server-acl-init-job.bats +++ b/charts/consul/test/unit/server-acl-init-job.bats @@ -634,7 +634,19 @@ load _helpers yq -r '.spec.template' | tee /dev/stderr) # Check annotations + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-pre-populate"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-pre-populate-only"]' | tee /dev/stderr) + [ "${actual}" = "false" ] + + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-cache-enable"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-cache-listener-port"]' | tee /dev/stderr) + [ "${actual}" = "8200" ] + + actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-enable-quit"]' | tee /dev/stderr) [ "${actual}" = "true" ] local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr) @@ -650,9 +662,9 @@ load _helpers local expected=$'{{- with secret \"foo\" -}}\n{{- .Data.data.bar -}}\n{{- end -}}' [ "${actual}" = "${expected}" ] - # Check that the bootstrap token flag is set to the path of the Vault secret. - local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-file=/vault/secrets/bootstrap-token"))') - [ "${actual}" = "true" ] + # Check that -bootstrap-token-file is not passed. + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-file"))') + [ "${actual}" = "false" ] # Check that no (secret) volumes are not attached local actual=$(echo $object | jq -r '.spec.volumes') @@ -682,20 +694,31 @@ load _helpers yq -r '.spec.template' | tee /dev/stderr) # Check annotations - local actual - actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-pre-populate-only"]' | tee /dev/stderr) + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-pre-populate"]' | tee /dev/stderr) [ "${actual}" = "true" ] - local actual - actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr) + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-pre-populate-only"]' | tee /dev/stderr) + [ "${actual}" = "false" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-cache-enable"]' | tee /dev/stderr) [ "${actual}" = "true" ] - local actual - actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-cache-listener-port"]' | tee /dev/stderr) + [ "${actual}" = "8200" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-enable-quit"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject"]' | tee /dev/stderr) + [ "${actual}" = "true" ] + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) [ "${actual}" = "aclrole" ] - local actual - actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr) + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-serverca.crt"]' | tee /dev/stderr) [ "${actual}" = "foo" ] - local actual - actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr) + + local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-serverca.crt"]' | tee /dev/stderr) [ "${actual}" = $'{{- with secret \"foo\" -}}\n{{- .Data.certificate -}}\n{{- end -}}' ] # Check that the consul-ca-cert volume is not attached @@ -895,12 +918,13 @@ load _helpers local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").volumeMounts') [ "${actual}" = "null" ] - # Check that the replication and bootstrap token flags are set to the path of the Vault secret. + # Replication token file is passed. local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-acl-replication-token-file=/vault/secrets/replication-token"))') [ "${actual}" = "true" ] - local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-file=/vault/secrets/bootstrap-token"))') - [ "${actual}" = "true" ] + # Bootstrap token file is not passed (server-acl-init reads the bootstrap token from the Vault API). + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-file"))') + [ "${actual}" = "false" ] } #-------------------------------------------------------------------- @@ -953,7 +977,7 @@ load _helpers #-------------------------------------------------------------------- # Vault agent annotations -@test "serverACLInit/Job: no vault agent annotations defined by default" { +@test "serverACLInit/Job: default vault agent annotations" { cd `chart_dir` local actual=$(helm template \ -s templates/server-acl-init-job.yaml \ @@ -967,8 +991,23 @@ load _helpers --set 'global.secretsBackend.vault.consulCARole=carole' \ --set 'global.secretsBackend.vault.manageSystemACLsRole=aclrole' \ . | tee /dev/stderr | - yq -r '.spec.template.metadata.annotations | del(."consul.hashicorp.com/connect-inject") | del(."vault.hashicorp.com/agent-inject") | del(."vault.hashicorp.com/agent-pre-populate-only") | del(."vault.hashicorp.com/role") | del(."vault.hashicorp.com/agent-inject-secret-bootstrap-token") | del(."vault.hashicorp.com/agent-inject-template-bootstrap-token")' | tee /dev/stderr) - [ "${actual}" = "{}" ] + yq -r .spec.template.metadata.annotations | tee /dev/stderr) + + local expected=$(echo '{ + "consul.hashicorp.com/connect-inject": "false", + "vault.hashicorp.com/agent-inject": "true", + "vault.hashicorp.com/agent-pre-populate": "true", + "vault.hashicorp.com/agent-pre-populate-only": "false", + "vault.hashicorp.com/agent-cache-enable": "true", + "vault.hashicorp.com/agent-cache-listener-port": "8200", + "vault.hashicorp.com/agent-enable-quit": "true", + "vault.hashicorp.com/agent-inject-secret-bootstrap-token": "foo", + "vault.hashicorp.com/agent-inject-template-bootstrap-token": "{{- with secret \"foo\" -}}\n{{- .Data.data.bar -}}\n{{- end -}}\n", + "vault.hashicorp.com/role": "aclrole" + }' | tee /dev/stderr) + + local equal=$(jq -n --argjson a "$actual" --argjson b "$expected" '$a == $b') + [ "$equal" = "true" ] } @test "serverACLInit/Job: vault agent annotations can be set" { diff --git a/control-plane/go.mod b/control-plane/go.mod index 51e4ad39a0..a60079cc53 100644 --- a/control-plane/go.mod +++ b/control-plane/go.mod @@ -19,14 +19,15 @@ require ( github.com/hashicorp/go-rootcerts v1.0.2 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/serf v0.10.1 + github.com/hashicorp/vault/api v1.8.3 github.com/kr/text v0.2.0 github.com/miekg/dns v1.1.41 github.com/mitchellh/cli v1.1.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/mapstructure v1.4.1 + github.com/mitchellh/mapstructure v1.5.0 github.com/stretchr/testify v1.7.2 go.uber.org/zap v1.19.0 - golang.org/x/text v0.3.7 + golang.org/x/text v0.3.8 golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 gomodules.xyz/jsonpatch/v2 v2.2.0 k8s.io/api v0.22.2 @@ -55,6 +56,7 @@ require ( github.com/aws/aws-sdk-go v1.25.41 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -68,6 +70,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/google/uuid v1.1.2 // indirect @@ -75,13 +78,22 @@ require ( github.com/googleapis/gnostic v0.5.5 // indirect github.com/gophercloud/gophercloud v0.1.0 // indirect github.com/hashicorp/consul/proto-public v0.1.0 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-immutable-radix v1.3.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-plugin v1.4.5 // indirect + github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/mdns v1.0.4 // indirect + github.com/hashicorp/vault/sdk v0.7.0 // indirect github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 // indirect + github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f // indirect @@ -90,10 +102,15 @@ require ( github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2 // indirect + github.com/oklog/run v1.0.0 // indirect github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c // indirect + github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/posener/complete v1.2.3 // indirect @@ -102,6 +119,7 @@ require ( github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -109,7 +127,7 @@ require ( github.com/tencentcloud/tencentcloud-sdk-go v3.0.83+incompatible // indirect github.com/vmware/govmomi v0.18.0 // indirect go.opencensus.io v0.23.0 // indirect - go.uber.org/atomic v1.7.0 // indirect + go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect @@ -125,6 +143,7 @@ require ( google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/resty.v1 v1.12.0 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.22.2 // indirect diff --git a/control-plane/go.sum b/control-plane/go.sum index 1fbb30b7ff..b4ed16e150 100644 --- a/control-plane/go.sum +++ b/control-plane/go.sum @@ -117,6 +117,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= +github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -197,10 +199,12 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= @@ -235,6 +239,7 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -276,6 +281,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= @@ -352,20 +359,22 @@ github.com/hashicorp/consul/proto-public v0.1.0 h1:O0LSmCqydZi363hsqc6n2v5sMz3us github.com/hashicorp/consul/proto-public v0.1.0/go.mod h1:vs2KkuWwtjkIgA5ezp4YKPzQp4GitV+q/+PvksrA92k= github.com/hashicorp/consul/sdk v0.4.1-0.20221021205723-cc843c4be892 h1:jw0NwPmNPr5CxAU04hACdj61JSaJBKZ0FdBo+kwfNp4= github.com/hashicorp/consul/sdk v0.4.1-0.20221021205723-cc843c4be892/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-discover v0.0.0-20200812215701-c4b85f6ed31f h1:7WFMVeuJQp6BkzuTv9O52pzwtEFVUJubKYN+zez8eTI= github.com/hashicorp/go-discover v0.0.0-20200812215701-c4b85f6ed31f/go.mod h1:D4eo8/CN92vm9/9UDG+ldX1/fMFa4kpl8qzyTolus8o= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.2.2 h1:ihRI7YFwcZdiSD7SIenIhHfQH3OuDvWerAUBZbeQS3M= github.com/hashicorp/go-hclog v1.2.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE= -github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +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-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqkZzoD4= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= @@ -374,12 +383,24 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-netaddrs v0.1.0 h1:TnlYvODD4C/wO+j7cX1z69kV5gOzI87u3OcUinANaW8= github.com/hashicorp/go-netaddrs v0.1.0/go.mod h1:33+a/emi5R5dqRspOuZKO0E+Tuz5WV1F84eRWALkedA= +github.com/hashicorp/go-plugin v1.4.5 h1:oTE/oQR4eghggRg8VY7PAz3dr++VwDNBGCcOfIvHpBo= +github.com/hashicorp/go-plugin v1.4.5/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= +github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -392,6 +413,7 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= @@ -404,8 +426,14 @@ github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hashicorp/vault/api v1.8.3 h1:cHQOLcMhBR+aVI0HzhPxO62w2+gJhIrKguQNONPzu6o= +github.com/hashicorp/vault/api v1.8.3/go.mod h1:4g/9lj9lmuJQMtT6CmVMHC5FW1yENaVv+Nv4ZfG8fAg= +github.com/hashicorp/vault/sdk v0.7.0 h1:2pQRO40R1etpKkia5fb4kjrdYMx3BHklPxl1pxpxDHg= +github.com/hashicorp/vault/sdk v0.7.0/go.mod h1:KyfArJkhooyba7gYCKSq8v66QdqJmnbAxtV/OX1+JTs= github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 h1:O/pT5C1Q3mVXMyuqg7yuAWUg/jMZR1/0QTzTRdNR6Uw= github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443/go.mod h1:bEpDU35nTu0ey1EXjwNwPjI9xErAsoOCmcMb9GKvyxo= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -419,6 +447,7 @@ github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGk github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da h1:FjHUJJ7oBW4G/9j1KzlHaXL09LyMVM9rupS39lncbXk= github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -449,6 +478,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -486,15 +516,23 @@ github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJys github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0 h1:tEElEatulEHDeedTxwckzyYMA5c86fbmNIUL1hBIiTg= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -516,6 +554,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -542,6 +582,8 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= +github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -588,6 +630,9 @@ github.com/rs/zerolog v1.4.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKk github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/conswriter v0.0.0-20180208195008-f5ae3917a627/go.mod h1:7zjs06qF79/FKAJpBvFx3P8Ww4UTIMAe+lpNXDHziac= github.com/sean-/pager v0.0.0-20180208200047-666be9bf53b5/go.mod h1:BeybITEsBEg6qbIiqJ6/Bqeq25bCLbL7YFmpaFfJDuM= @@ -683,8 +728,9 @@ go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4 go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -915,8 +961,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -982,8 +1028,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1121,6 +1167,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24 gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= diff --git a/control-plane/subcommand/server-acl-init/command.go b/control-plane/subcommand/server-acl-init/command.go index 698da2a25c..0177efc1fb 100644 --- a/control-plane/subcommand/server-acl-init/command.go +++ b/control-plane/subcommand/server-acl-init/command.go @@ -22,12 +22,11 @@ import ( "github.com/hashicorp/consul/api" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-netaddrs" + vaultApi "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" "github.com/mitchellh/mapstructure" "golang.org/x/text/cases" "golang.org/x/text/language" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) @@ -89,6 +88,10 @@ type Command struct { // Flag to support a custom bootstrap token. flagBootstrapTokenFile string + flagSecretsBackend string + flagBootstrapTokenSecretName string + flagBootstrapTokenSecretKey string + flagLogLevel string flagLogJSON bool flagTimeout time.Duration @@ -98,7 +101,8 @@ type Command struct { // flagFederation indicates if federation has been enabled in the cluster. flagFederation bool - clientset kubernetes.Interface + clientset kubernetes.Interface + vaultClient *vaultApi.Client watcher consul.ServerConnectionManager @@ -197,6 +201,12 @@ func (c *Command) init() { c.flags.StringVar(&c.flagBootstrapTokenFile, "bootstrap-token-file", "", "Path to file containing ACL token for creating policies and tokens. This token must have 'acl:write' permissions."+ "When provided, servers will not be bootstrapped and their policies and tokens will not be updated.") + c.flags.StringVar(&c.flagSecretsBackend, "secrets-backend", "kubernetes", + `The secrets backend to use. Either "vault" or "kubernetes". Defaults to "kubernetes"`) + c.flags.StringVar(&c.flagBootstrapTokenSecretName, "bootstrap-token-secret-name", "", + "The name of the Vault or Kuberenetes secret for the bootstrap token.") + c.flags.StringVar(&c.flagBootstrapTokenSecretKey, "bootstrap-token-secret-key", "", + "The key within the Vault or Kuberenetes containing the bootstrap token.") c.flags.DurationVar(&c.flagTimeout, "timeout", 10*time.Minute, "How long we'll try to bootstrap ACLs for before timing out, e.g. 1ms, 2s, 3m") @@ -231,6 +241,7 @@ func (c *Command) Help() string { // The function will retry its tasks indefinitely until they are complete. func (c *Command) Run(args []string) int { c.once.Do(c.init) + defer c.quitVaultAgent() if err := c.flags.Parse(args); err != nil { return 1 } @@ -306,8 +317,38 @@ func (c *Command) Run(args []string) int { c.UI.Error(err.Error()) } - var bootstrapToken string + var backend SecretsBackend + switch SecretsBackendType(c.flagSecretsBackend) { + case SecretsBackendKubernetes: + backend = &KubernetesSecretsBackend{ + ctx: c.ctx, + clientset: c.clientset, + k8sNamespace: c.flagK8sNamespace, + // TODO: should these use the global.acls.bootstrapToken.{secretName,secretKey}? + secretName: c.withPrefix("bootstrap-acl-token"), + secretKey: common.ACLTokenSecretKey, + } + case SecretsBackendVault: + cfg := vaultApi.DefaultConfig() + cfg.Address = "" + cfg.AgentAddress = "http://127.0.0.1:8200" + vaultClient, err := vaultApi.NewClient(cfg) + if err != nil { + c.log.Error("Error initializing Vault client: %w", "error", err) + return 1 + } + c.vaultClient = vaultClient // must set this for c.quitVaultAgent. + backend = &VaultSecretsBackend{ + vaultClient: c.vaultClient, + secretName: c.flagBootstrapTokenSecretName, + secretKey: c.flagBootstrapTokenSecretKey, + } + default: + c.log.Error(fmt.Sprintf("invalid value for -secrets-backend: %q", c.flagSecretsBackend)) + return 1 + } + var bootstrapToken string if c.flagACLReplicationTokenFile != "" && !c.flagCreateACLReplicationToken { // If ACL replication is enabled, we don't need to ACL bootstrap the servers // since they will be performing replication. @@ -317,20 +358,18 @@ func (c *Command) Run(args []string) int { bootstrapToken = aclReplicationToken } else { // Check if we've already been bootstrapped. - var bootTokenSecretName string if providedBootstrapToken != "" { c.log.Info("Using provided bootstrap token") bootstrapToken = providedBootstrapToken } else { - bootTokenSecretName = c.withPrefix("bootstrap-acl-token") - bootstrapToken, err = c.getBootstrapToken(bootTokenSecretName) + bootstrapToken, err = backend.BootstrapToken() if err != nil { c.log.Error(fmt.Sprintf("Unexpected error looking for preexisting bootstrap Secret: %s", err)) return 1 } } - bootstrapToken, err = c.bootstrapServers(ipAddrs, bootstrapToken, bootTokenSecretName) + bootstrapToken, err = c.bootstrapServers(ipAddrs, bootstrapToken, backend) if err != nil { c.log.Error(err.Error()) return 1 @@ -806,24 +845,6 @@ func (c *Command) configureGateway(gatewayParams ConfigureGatewayParams, consulC return nil } -// getBootstrapToken returns the existing bootstrap token if there is one by -// reading the Kubernetes Secret with name secretName. -// If there is no bootstrap token yet, then it returns an empty string (not an error). -func (c *Command) getBootstrapToken(secretName string) (string, error) { - secret, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Get(c.ctx, secretName, metav1.GetOptions{}) - if err != nil { - if k8serrors.IsNotFound(err) { - return "", nil - } - return "", err - } - token, ok := secret.Data[common.ACLTokenSecretKey] - if !ok { - return "", fmt.Errorf("secret %q does not have data key 'token'", secretName) - } - return string(token), nil -} - func (c *Command) configureKubeClient() error { config, err := subcommand.K8SConfig(c.k8s.KubeConfig()) if err != nil { @@ -977,6 +998,25 @@ func loadTokenFromFile(tokenFile string) (string, error) { return strings.TrimSpace(string(tokenBytes)), nil } +func (c *Command) quitVaultAgent() { + if c.vaultClient != nil { + // Tell the Vault agent to quit. + // TODO: RawRequest is deprecated, but there is also + // not a high level method for this in the Vault client. + //nolint:staticcheck // SA1004 ignore this! + _, err := c.vaultClient.RawRequest( + c.vaultClient.NewRequest("POST", "/agent/v1/quit"), + ) + if err != nil { + c.log.Warn(fmt.Sprintf("Unexpected error telling Vault agent to quit: %s", err)) + // proceed anyway. maybe the Vault agent sidecar is still running, + // but what could go wrong with a request to localhost that retrying would fix? + } else { + c.log.Debug("Success: told Vault agent to quit") + } + } +} + const ( consulDefaultNamespace = "default" consulDefaultPartition = "default" diff --git a/control-plane/subcommand/server-acl-init/k8s_secrets_backend.go b/control-plane/subcommand/server-acl-init/k8s_secrets_backend.go new file mode 100644 index 0000000000..d358dc6f20 --- /dev/null +++ b/control-plane/subcommand/server-acl-init/k8s_secrets_backend.go @@ -0,0 +1,60 @@ +package serveraclinit + +import ( + "context" + "fmt" + + "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" + apiv1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type KubernetesSecretsBackend struct { + ctx context.Context + clientset kubernetes.Interface + k8sNamespace string + secretName string + secretKey string +} + +var _ SecretsBackend = (*KubernetesSecretsBackend)(nil) + +// BootstrapToken returns the existing bootstrap token if there is one by +// reading the Kubernetes Secret. If there is no bootstrap token yet, then +// it returns an empty string (not an error). +func (b *KubernetesSecretsBackend) BootstrapToken() (string, error) { + secret, err := b.clientset.CoreV1().Secrets(b.k8sNamespace).Get(b.ctx, b.secretName, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + return "", nil + } + return "", err + } + token, ok := secret.Data[b.secretKey] + if !ok { + return "", fmt.Errorf("secret %q does not have data key %q", b.secretName, b.secretKey) + } + return string(token), nil + +} + +// WriteBootstrapToken writes the given bootstrap token to the Kubernetes Secret. +func (b *KubernetesSecretsBackend) WriteBootstrapToken(bootstrapToken string) error { + secret := &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: b.secretName, + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + Data: map[string][]byte{ + b.secretKey: []byte(bootstrapToken), + }, + } + _, err := b.clientset.CoreV1().Secrets(b.k8sNamespace).Create(b.ctx, secret, metav1.CreateOptions{}) + return err +} + +func (b *KubernetesSecretsBackend) BootstrapTokenSecretName() string { + return b.secretName +} diff --git a/control-plane/subcommand/server-acl-init/secrets_backend.go b/control-plane/subcommand/server-acl-init/secrets_backend.go new file mode 100644 index 0000000000..a8ef62c420 --- /dev/null +++ b/control-plane/subcommand/server-acl-init/secrets_backend.go @@ -0,0 +1,23 @@ +package serveraclinit + +type SecretsBackend interface { + // BootstrapToken fetches the bootstrap token from the backend. If the + // token is not found or empty, implementations should return an empty + // string (not an error). + BootstrapToken() (string, error) + + // WriteBootstrapToken writes the given bootstrap token to the backend. + // Implementations of this method do not need to retry the write until + // successful. + WriteBootstrapToken(string) error + + // BootstrapTokenSecretName returns the name of the bootstrap token secret. + BootstrapTokenSecretName() string +} + +type SecretsBackendType string + +const ( + SecretsBackendKubernetes SecretsBackendType = "kubernetes" + SecretsBackendVault SecretsBackendType = "vault" +) diff --git a/control-plane/subcommand/server-acl-init/servers.go b/control-plane/subcommand/server-acl-init/servers.go index 2dc8f8ab67..6869d97abf 100644 --- a/control-plane/subcommand/server-acl-init/servers.go +++ b/control-plane/subcommand/server-acl-init/servers.go @@ -9,16 +9,13 @@ import ( "time" "github.com/hashicorp/consul/api" - apiv1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/hashicorp/consul-k8s/control-plane/consul" - "github.com/hashicorp/consul-k8s/control-plane/subcommand/common" ) // bootstrapServers bootstraps ACLs and ensures each server has an ACL token. // If bootstrapToken is not empty then ACLs are already bootstrapped. -func (c *Command) bootstrapServers(serverAddresses []net.IPAddr, bootstrapToken, bootTokenSecretName string) (string, error) { +func (c *Command) bootstrapServers(serverAddresses []net.IPAddr, bootstrapToken string, backend SecretsBackend) (string, error) { // Pick the first server address to connect to for bootstrapping and set up connection. firstServerAddr := fmt.Sprintf("%s:%d", serverAddresses[0].IP.String(), c.consulFlags.HTTPPort) @@ -26,12 +23,12 @@ func (c *Command) bootstrapServers(serverAddresses []net.IPAddr, bootstrapToken, c.log.Info("No bootstrap token from previous installation found, continuing on to bootstrapping") var err error - bootstrapToken, err = c.bootstrapACLs(firstServerAddr, bootTokenSecretName) + bootstrapToken, err = c.bootstrapACLs(firstServerAddr, backend) if err != nil { return "", err } } else { - c.log.Info(fmt.Sprintf("ACLs already bootstrapped - retrieved bootstrap token from Secret %q", bootTokenSecretName)) + c.log.Info(fmt.Sprintf("ACLs already bootstrapped - retrieved bootstrap token from Secret %q", backend.BootstrapTokenSecretName())) } // We should only create and set server tokens when servers are running within this cluster. @@ -47,7 +44,7 @@ func (c *Command) bootstrapServers(serverAddresses []net.IPAddr, bootstrapToken, // bootstrapACLs makes the ACL bootstrap API call and writes the bootstrap token // to a kube secret. -func (c *Command) bootstrapACLs(firstServerAddr, bootTokenSecretName string) (string, error) { +func (c *Command) bootstrapACLs(firstServerAddr string, backend SecretsBackend) (string, error) { config := c.consulFlags.ConsulClientConfig().APIClientConfig config.Address = firstServerAddr // Exempting this particular use of the http client from using global.consulAPITimeout @@ -78,7 +75,7 @@ func (c *Command) bootstrapACLs(firstServerAddr, bootTokenSecretName string) (st // Check if already bootstrapped. if strings.Contains(err.Error(), "Unexpected response code: 403") { - unrecoverableErr = errors.New("ACLs already bootstrapped but the ACL token was not written to a Kubernetes secret." + + unrecoverableErr = errors.New("ACLs already bootstrapped but the ACL token was not written to a secret." + " We can't proceed because the bootstrap token is lost." + " You must reset ACLs.") return nil @@ -98,21 +95,12 @@ func (c *Command) bootstrapACLs(firstServerAddr, bootTokenSecretName string) (st return "", err } - // Write bootstrap token to a Kubernetes secret. - err = c.untilSucceeds(fmt.Sprintf("writing bootstrap Secret %q", bootTokenSecretName), + // Write bootstrap token to the secrets backend. + err = c.untilSucceeds(fmt.Sprintf("writing bootstrap Secret %q", backend.BootstrapTokenSecretName()), func() error { - secret := &apiv1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: bootTokenSecretName, - Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, - }, - Data: map[string][]byte{ - common.ACLTokenSecretKey: []byte(bootstrapToken), - }, - } - _, err := c.clientset.CoreV1().Secrets(c.flagK8sNamespace).Create(c.ctx, secret, metav1.CreateOptions{}) - return err - }) + return backend.WriteBootstrapToken(bootstrapToken) + }, + ) return bootstrapToken, err } diff --git a/control-plane/subcommand/server-acl-init/vault_secrets_backend.go b/control-plane/subcommand/server-acl-init/vault_secrets_backend.go new file mode 100644 index 0000000000..40cf214bb4 --- /dev/null +++ b/control-plane/subcommand/server-acl-init/vault_secrets_backend.go @@ -0,0 +1,64 @@ +package serveraclinit + +import ( + "fmt" + + "github.com/hashicorp/vault/api" +) + +type VaultSecretsBackend struct { + vaultClient *api.Client + secretName string + secretKey string +} + +var _ SecretsBackend = (*VaultSecretsBackend)(nil) + +// BootstrapToken returns the bootstrap token stored in Vault. +// If not found this returns an empty string (not an error). +func (b *VaultSecretsBackend) BootstrapToken() (string, error) { + secret, err := b.vaultClient.Logical().Read(b.secretName) + if err != nil { + return "", err + } + if secret == nil || secret.Data == nil { + // secret not found or empty. + return "", nil + } + // Grab secret.Data["data"][secretKey]. + dataRaw, found := secret.Data["data"] + if !found { + return "", nil + } + data, ok := dataRaw.(map[string]interface{}) + if !ok { + return "", nil + } + tokRaw, found := data[b.secretKey] + if !found { + return "", nil + } + if tok, ok := tokRaw.(string); ok { + return tok, nil + } + return "", fmt.Errorf("Unexpected data. To resolve this, "+ + "`vault kv put %[1]s=` if Consul is already ACL bootstrapped. "+ + "If not ACL bootstrapped, `vault kv put %[1]s=\"\"`", b.secretKey, b.secretKey) +} + +// BootstrapTokenSecretName returns the name of the bootstrap token secret. +func (b *VaultSecretsBackend) BootstrapTokenSecretName() string { + return b.secretName +} + +// WriteBootstrapToken writes the bootstrap token to Vault. +func (b *VaultSecretsBackend) WriteBootstrapToken(bootstrapToken string) error { + _, err := b.vaultClient.Logical().Write(b.secretName, + map[string]interface{}{ + "data": map[string]interface{}{ + b.secretKey: bootstrapToken, + }, + }, + ) + return err +} From a59a16a0e2afe221db5b238823526d84127e23de Mon Sep 17 00:00:00 2001 From: Paul Glass Date: Fri, 17 Feb 2023 13:19:24 -0600 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Chris Thain <32781396+cthain@users.noreply.github.com> --- charts/consul/templates/server-acl-init-job.yaml | 4 ++-- control-plane/subcommand/server-acl-init/command.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/consul/templates/server-acl-init-job.yaml b/charts/consul/templates/server-acl-init-job.yaml index 9844844bec..22dc14ea2b 100644 --- a/charts/consul/templates/server-acl-init-job.yaml +++ b/charts/consul/templates/server-acl-init-job.yaml @@ -51,12 +51,12 @@ spec: {{- /* Run the Vault agent as both an init container and sidecar. The Vault agent sidecar is needed when server-acl-init bootstraps ACLs and writes the bootstrap token back to Vault. - * agent-prepopulate: true - Run the Vault agent init container. + * agent-pre-populate: true - Run the Vault agent init container. * agent-pre-populate-only: false - Also, run the Vault agent sidecar. * agent-cache-enable: true - Enable the Agent cache listener. * agent-cache-listener-port: 8200 - (optional) Listen on 127.0.0.1:8200. * agent-enable-quit: true - Enable a "quit" endpoint. server-acl-init - tells the Vault agent to stop (without this the Job will complete). + tells the Vault agent to stop (without this the Job will not complete). */}} "vault.hashicorp.com/agent-pre-populate": "true" "vault.hashicorp.com/agent-pre-populate-only": "false" diff --git a/control-plane/subcommand/server-acl-init/command.go b/control-plane/subcommand/server-acl-init/command.go index 0177efc1fb..d485d1f564 100644 --- a/control-plane/subcommand/server-acl-init/command.go +++ b/control-plane/subcommand/server-acl-init/command.go @@ -206,7 +206,7 @@ func (c *Command) init() { c.flags.StringVar(&c.flagBootstrapTokenSecretName, "bootstrap-token-secret-name", "", "The name of the Vault or Kuberenetes secret for the bootstrap token.") c.flags.StringVar(&c.flagBootstrapTokenSecretKey, "bootstrap-token-secret-key", "", - "The key within the Vault or Kuberenetes containing the bootstrap token.") + "The key within the Vault or Kuberenetes secret containing the bootstrap token.") c.flags.DurationVar(&c.flagTimeout, "timeout", 10*time.Minute, "How long we'll try to bootstrap ACLs for before timing out, e.g. 1ms, 2s, 3m") From 9e701b87aef636afcd7c25c999ff3ec2d5f62cde Mon Sep 17 00:00:00 2001 From: Paul Glass Date: Tue, 21 Feb 2023 14:09:56 -0600 Subject: [PATCH 3/6] Improve Vault secrets backend * The Kubernetes backend will write the bootstrap token to the user-provided secret if that secret is empty. The Vault behavior is the same. * The Vault backend writes to a default secret name if the secretName and secretKey are not set in the helm chart values. * Pass the Vault namespace to server-acl-init server-acl-init reads the secret directly from k8s or Vault. * Remove -bootstrap-token-file flag from server-acl-init and remove the * Remove the volume/mount for that. And update all the tests for that. Remove the bootstrap token secret injection / template the Vault agent. --- .../consul/templates/server-acl-init-job.yaml | 35 +- .../consul/test/unit/server-acl-init-job.bats | 89 +--- charts/consul/values.yaml | 14 +- .../subcommand/server-acl-init/command.go | 150 +++--- .../server-acl-init/command_ent_test.go | 3 +- .../server-acl-init/command_test.go | 457 ++++++++---------- .../server-acl-init/k8s_secrets_backend.go | 2 + .../server-acl-init/secrets_backend.go | 9 +- .../subcommand/server-acl-init/servers.go | 28 +- .../test_fake_secrets_backend.go | 20 + .../server-acl-init/vault_secrets_backend.go | 2 + 11 files changed, 364 insertions(+), 445 deletions(-) create mode 100644 control-plane/subcommand/server-acl-init/test_fake_secrets_backend.go diff --git a/charts/consul/templates/server-acl-init-job.yaml b/charts/consul/templates/server-acl-init-job.yaml index 22dc14ea2b..e62db41ec2 100644 --- a/charts/consul/templates/server-acl-init-job.yaml +++ b/charts/consul/templates/server-acl-init-job.yaml @@ -64,12 +64,6 @@ spec: "vault.hashicorp.com/agent-cache-listener-port": "8200" "vault.hashicorp.com/agent-enable-quit": "true" "vault.hashicorp.com/agent-inject": "true" - {{- if .Values.global.acls.bootstrapToken.secretName }} - {{- with .Values.global.acls.bootstrapToken }} - "vault.hashicorp.com/agent-inject-secret-bootstrap-token": "{{ .secretName }}" - "vault.hashicorp.com/agent-inject-template-bootstrap-token": {{ template "consul.vaultSecretTemplate" . }} - {{- end }} - {{- end }} {{- if .Values.global.acls.partitionToken.secretName }} {{- with .Values.global.acls.partitionToken }} "vault.hashicorp.com/agent-inject-secret-partition-token": "{{ .secretName }}" @@ -116,14 +110,7 @@ spec: path: tls.crt {{- end }} {{- end }} - {{- if (and .Values.global.acls.bootstrapToken.secretName (not .Values.global.secretsBackend.vault.enabled)) }} - - name: bootstrap-token - secret: - secretName: {{ .Values.global.acls.bootstrapToken.secretName }} - items: - - key: {{ .Values.global.acls.bootstrapToken.secretKey }} - path: bootstrap-token - {{- else if and .Values.global.acls.replicationToken.secretName (not .Values.global.secretsBackend.vault.enabled) }} + {{- if and .Values.global.acls.replicationToken.secretName (not .Values.global.secretsBackend.vault.enabled) }} - name: acl-replication-token secret: secretName: {{ .Values.global.acls.replicationToken.secretName }} @@ -144,6 +131,13 @@ spec: valueFrom: fieldRef: fieldPath: metadata.name + # Extract the Vault namespace from the Vault agent annotations. + {{- if .Values.global.secretsBackend.vault.enabled }} + {{- if .Values.global.secretsBackend.vault.agentAnnotations }} + - name: VAULT_NAMESPACE + value: {{ get (tpl .Values.global.secretsBackend.vault.agentAnnotations . | fromYaml) "vault.hashicorp.com/namespace" }} + {{- end }} + {{- end }} {{- include "consul.consulK8sConsulServerEnvVars" . | nindent 8 }} {{- if (or .Values.global.tls.enabled .Values.global.acls.replicationToken.secretName .Values.global.acls.bootstrapToken.secretName) }} volumeMounts: @@ -154,11 +148,7 @@ spec: readOnly: true {{- end }} {{- end }} - {{- if (and .Values.global.acls.bootstrapToken.secretName (not .Values.global.secretsBackend.vault.enabled)) }} - - name: bootstrap-token - mountPath: /consul/acl/tokens - readOnly: true - {{- else if and .Values.global.acls.replicationToken.secretName (not .Values.global.secretsBackend.vault.enabled) }} + {{- if and .Values.global.acls.replicationToken.secretName (not .Values.global.secretsBackend.vault.enabled) }} - name: acl-replication-token mountPath: /consul/acl/tokens readOnly: true @@ -178,14 +168,13 @@ spec: -set-server-tokens={{ $serverEnabled }} \ {{- if .Values.global.secretsBackend.vault.enabled }} -secrets-backend=vault \ + {{- else }} + -secrets-backend=kubernetes \ {{- end }} + {{- if .Values.global.acls.bootstrapToken.secretName }} - {{- if .Values.global.secretsBackend.vault.enabled }} -bootstrap-token-secret-name={{ .Values.global.acls.bootstrapToken.secretName }} \ -bootstrap-token-secret-key={{ .Values.global.acls.bootstrapToken.secretKey }} \ - {{- else }} - -bootstrap-token-file=/consul/acl/tokens/bootstrap-token \ - {{- end }} {{- end }} {{- if .Values.syncCatalog.enabled }} diff --git a/charts/consul/test/unit/server-acl-init-job.bats b/charts/consul/test/unit/server-acl-init-job.bats index 70d58fdf92..81064c95eb 100644 --- a/charts/consul/test/unit/server-acl-init-job.bats +++ b/charts/consul/test/unit/server-acl-init-job.bats @@ -655,16 +655,14 @@ load _helpers local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/role"]' | tee /dev/stderr) [ "${actual}" = "aclrole" ] - local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-secret-bootstrap-token"]' | tee /dev/stderr) - [ "${actual}" = "foo" ] + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-secrets-backend=vault"))') + [ "${actual}" = "true" ] - local actual=$(echo $object | jq -r '.metadata.annotations["vault.hashicorp.com/agent-inject-template-bootstrap-token"]' | tee /dev/stderr) - local expected=$'{{- with secret \"foo\" -}}\n{{- .Data.data.bar -}}\n{{- end -}}' - [ "${actual}" = "${expected}" ] + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-secret-name=foo"))') + [ "${actual}" = "true" ] - # Check that -bootstrap-token-file is not passed. - local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-file"))') - [ "${actual}" = "false" ] + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-secret-key=bar"))') + [ "${actual}" = "true" ] # Check that no (secret) volumes are not attached local actual=$(echo $object | jq -r '.spec.volumes') @@ -904,12 +902,14 @@ load _helpers local expected=$'{{- with secret \"/vault/replication\" -}}\n{{- .Data.data.token -}}\n{{- end -}}' [ "${actual}" = "${expected}" ] - local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-inject-secret-bootstrap-token"') - [ "${actual}" = "/vault/bootstrap" ] + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-secrets-backend=vault"))') + [ "${actual}" = "true" ] - local actual=$(echo $object | yq -r '.metadata.annotations."vault.hashicorp.com/agent-inject-template-bootstrap-token"') - local expected=$'{{- with secret \"/vault/bootstrap\" -}}\n{{- .Data.data.token -}}\n{{- end -}}' - [ "${actual}" = "${expected}" ] + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-secret-name=/vault/bootstrap"))') + [ "${actual}" = "true" ] + + local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-secret-key=token"))') + [ "${actual}" = "true" ] # Check that replication token Kubernetes secret volumes and volumeMounts are not attached. local actual=$(echo $object | jq -r '.spec.volumes') @@ -921,10 +921,6 @@ load _helpers # Replication token file is passed. local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-acl-replication-token-file=/vault/secrets/replication-token"))') [ "${actual}" = "true" ] - - # Bootstrap token file is not passed (server-acl-init reads the bootstrap token from the Vault API). - local actual=$(echo $object | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-file"))') - [ "${actual}" = "false" ] } #-------------------------------------------------------------------- @@ -1001,8 +997,6 @@ load _helpers "vault.hashicorp.com/agent-cache-enable": "true", "vault.hashicorp.com/agent-cache-listener-port": "8200", "vault.hashicorp.com/agent-enable-quit": "true", - "vault.hashicorp.com/agent-inject-secret-bootstrap-token": "foo", - "vault.hashicorp.com/agent-inject-template-bootstrap-token": "{{- with secret \"foo\" -}}\n{{- .Data.data.bar -}}\n{{- end -}}\n", "vault.hashicorp.com/role": "aclrole" }' | tee /dev/stderr) @@ -1846,55 +1840,23 @@ load _helpers [[ "$output" =~ "both global.acls.bootstrapToken.secretKey and global.acls.bootstrapToken.secretName must be set if one of them is provided" ]] } -@test "serverACLInit/Job: -bootstrap-token-file is not set by default" { - cd `chart_dir` - local object=$(helm template \ - -s templates/server-acl-init-job.yaml \ - --set 'global.acls.manageSystemACLs=true' \ - . | tee /dev/stderr) - - # Test the flag is not set. - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].command | any(contains("-bootstrap-token-file"))' | tee /dev/stderr) - [ "${actual}" = "false" ] - - # Test the volume doesn't exist - local actual=$(echo "$object" | - yq '.spec.template.spec.volumes | length == 0' | tee /dev/stderr) - [ "${actual}" = "true" ] - - # Test the volume mount doesn't exist - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].volumeMounts | length == 0' | tee /dev/stderr) - [ "${actual}" = "true" ] -} - -@test "serverACLInit/Job: -bootstrap-token-file is set when acls.bootstrapToken.secretKey and secretName are set" { +@test "serverACLInit/Job: bootstrap token secret is passed when acls.bootstrapToken.secretKey and secretName are set" { cd `chart_dir` local object=$(helm template \ -s templates/server-acl-init-job.yaml \ --set 'global.acls.manageSystemACLs=true' \ --set 'global.acls.bootstrapToken.secretName=name' \ --set 'global.acls.bootstrapToken.secretKey=key' \ - . | tee /dev/stderr) - - # Test the -bootstrap-token-file flag is set. - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].command | any(contains("-bootstrap-token-file=/consul/acl/tokens/bootstrap-token"))' | tee /dev/stderr) - [ "${actual}" = "true" ] + . | yq .spec.template | tee /dev/stderr) - # Test the volume exists - local actual=$(echo "$object" | - yq '.spec.template.spec.volumes | map(select(.name == "bootstrap-token")) | length == 1' | tee /dev/stderr) + local actual=$(echo "$object" | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-secret-name=name"))') [ "${actual}" = "true" ] - # Test the volume mount exists - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].volumeMounts | map(select(.name == "bootstrap-token")) | length == 1' | tee /dev/stderr) + local actual=$(echo "$object" | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-secret-key=key"))') [ "${actual}" = "true" ] } -@test "serverACLInit/Job: -bootstrap-token-file is preferred when both acls.bootstrapToken and acls.replicationToken are set" { +@test "serverACLInit/Job: bootstrap token secret is passed when both acl.bootstrapToken and acls.replicationToken are set" { cd `chart_dir` local object=$(helm template \ -s templates/server-acl-init-job.yaml \ @@ -1903,21 +1865,12 @@ load _helpers --set 'global.acls.bootstrapToken.secretKey=key' \ --set 'global.acls.replicationToken.secretName=replication' \ --set 'global.acls.replicationToken.secretKey=token' \ - . | tee /dev/stderr) - - # Test the -bootstrap-token-file flag is set. - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].command | any(contains("-bootstrap-token-file=/consul/acl/tokens/bootstrap-token"))' | tee /dev/stderr) - [ "${actual}" = "true" ] + . | yq .spec.template | tee /dev/stderr) - # Test the volume exists - local actual=$(echo "$object" | - yq '.spec.template.spec.volumes | map(select(.name == "bootstrap-token")) | length == 1' | tee /dev/stderr) + local actual=$(echo "$object" | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-secret-name=name"))') [ "${actual}" = "true" ] - # Test the volume mount exists - local actual=$(echo "$object" | - yq '.spec.template.spec.containers[0].volumeMounts | map(select(.name == "bootstrap-token")) | length == 1' | tee /dev/stderr) + local actual=$(echo "$object" | jq -r '.spec.containers[] | select(.name=="server-acl-init-job").command | any(contains("-bootstrap-token-secret-key=key"))') [ "${actual}" = "true" ] } diff --git a/charts/consul/values.yaml b/charts/consul/values.yaml index dbc5935211..009e851e1b 100644 --- a/charts/consul/values.yaml +++ b/charts/consul/values.yaml @@ -392,14 +392,20 @@ global: # This requires Consul >= 1.4. manageSystemACLs: false - # A Kubernetes or Vault secret containing the bootstrap token to use for - # creating policies and tokens for all Consul and consul-k8s-control-plane components. - # If set, we will skip ACL bootstrapping of the servers and will only - # initialize ACLs for the Consul clients and consul-k8s-control-plane system components. + # A Kubernetes or Vault secret containing the bootstrap token to use for creating policies and + # tokens for all Consul and consul-k8s-control-plane components. If `secretName` and `secretKey` + # are unset, a default secret name and secret key are used. If the secret is populated, then + # we will skip ACL bootstrapping of the servers and will only initialize ACLs for the Consul + # clients and consul-k8s-control-plane system components. + # If the secret is empty, then we will bootstrap ACLs on the Consul servers, and write the + # bootstrap token to this secret. If ACLs are already bootstrapped on the servers, then the + # secret must contain the bootstrap token. bootstrapToken: # The name of the Kubernetes or Vault secret that holds the bootstrap token. + # If unset, this defaults to `{{ global.name }}-bootstrap-acl-token`. secretName: null # The key within the Kubernetes or Vault secret that holds the bootstrap token. + # If unset, this defaults to `token`. secretKey: null # If true, an ACL token will be created that can be used in secondary diff --git a/control-plane/subcommand/server-acl-init/command.go b/control-plane/subcommand/server-acl-init/command.go index d485d1f564..a444f65aaa 100644 --- a/control-plane/subcommand/server-acl-init/command.go +++ b/control-plane/subcommand/server-acl-init/command.go @@ -85,10 +85,8 @@ type Command struct { flagEnableInjectK8SNSMirroring bool // Enables mirroring of k8s namespaces into Consul for Connect inject flagInjectK8SNSMirroringPrefix string // Prefix added to Consul namespaces created when mirroring injected services - // Flag to support a custom bootstrap token. - flagBootstrapTokenFile string - - flagSecretsBackend string + // Flags for the secrets backend. + flagSecretsBackend SecretsBackendType flagBootstrapTokenSecretName string flagBootstrapTokenSecretKey string @@ -101,6 +99,7 @@ type Command struct { // flagFederation indicates if federation has been enabled in the cluster. flagFederation bool + backend SecretsBackend // for unit testing. clientset kubernetes.Interface vaultClient *vaultApi.Client @@ -198,13 +197,12 @@ func (c *Command) init() { c.flags.BoolVar(&c.flagFederation, "federation", false, "Toggle for when federation has been enabled.") - c.flags.StringVar(&c.flagBootstrapTokenFile, "bootstrap-token-file", "", - "Path to file containing ACL token for creating policies and tokens. This token must have 'acl:write' permissions."+ - "When provided, servers will not be bootstrapped and their policies and tokens will not be updated.") - c.flags.StringVar(&c.flagSecretsBackend, "secrets-backend", "kubernetes", + c.flags.StringVar((*string)(&c.flagSecretsBackend), "secrets-backend", "kubernetes", `The secrets backend to use. Either "vault" or "kubernetes". Defaults to "kubernetes"`) c.flags.StringVar(&c.flagBootstrapTokenSecretName, "bootstrap-token-secret-name", "", - "The name of the Vault or Kuberenetes secret for the bootstrap token.") + "The name of the Vault or Kuberenetes secret for the bootstrap token. This token must have `ac::write` permission "+ + "in order to create policies and tokens. If not provided or if the secret is empty, then this command will "+ + "bootstrap ACLs and write the bootstrap token to this secret.") c.flags.StringVar(&c.flagBootstrapTokenSecretKey, "bootstrap-token-secret-key", "", "The key within the Vault or Kuberenetes secret containing the bootstrap token.") @@ -275,16 +273,6 @@ func (c *Command) Run(args []string) int { } } - var providedBootstrapToken string - if c.flagBootstrapTokenFile != "" { - var err error - providedBootstrapToken, err = loadTokenFromFile(c.flagBootstrapTokenFile) - if err != nil { - c.UI.Error(err.Error()) - return 1 - } - } - var cancel context.CancelFunc c.ctx, cancel = context.WithTimeout(context.Background(), c.flagTimeout) // The context will only ever be intentionally ended by the timeout. @@ -317,34 +305,8 @@ func (c *Command) Run(args []string) int { c.UI.Error(err.Error()) } - var backend SecretsBackend - switch SecretsBackendType(c.flagSecretsBackend) { - case SecretsBackendKubernetes: - backend = &KubernetesSecretsBackend{ - ctx: c.ctx, - clientset: c.clientset, - k8sNamespace: c.flagK8sNamespace, - // TODO: should these use the global.acls.bootstrapToken.{secretName,secretKey}? - secretName: c.withPrefix("bootstrap-acl-token"), - secretKey: common.ACLTokenSecretKey, - } - case SecretsBackendVault: - cfg := vaultApi.DefaultConfig() - cfg.Address = "" - cfg.AgentAddress = "http://127.0.0.1:8200" - vaultClient, err := vaultApi.NewClient(cfg) - if err != nil { - c.log.Error("Error initializing Vault client: %w", "error", err) - return 1 - } - c.vaultClient = vaultClient // must set this for c.quitVaultAgent. - backend = &VaultSecretsBackend{ - vaultClient: c.vaultClient, - secretName: c.flagBootstrapTokenSecretName, - secretKey: c.flagBootstrapTokenSecretKey, - } - default: - c.log.Error(fmt.Sprintf("invalid value for -secrets-backend: %q", c.flagSecretsBackend)) + if err := c.configureSecretsBackend(); err != nil { + c.log.Error(err.Error()) return 1 } @@ -357,19 +319,7 @@ func (c *Command) Run(args []string) int { c.log.Info("ACL replication is enabled so skipping Consul server ACL bootstrapping") bootstrapToken = aclReplicationToken } else { - // Check if we've already been bootstrapped. - if providedBootstrapToken != "" { - c.log.Info("Using provided bootstrap token") - bootstrapToken = providedBootstrapToken - } else { - bootstrapToken, err = backend.BootstrapToken() - if err != nil { - c.log.Error(fmt.Sprintf("Unexpected error looking for preexisting bootstrap Secret: %s", err)) - return 1 - } - } - - bootstrapToken, err = c.bootstrapServers(ipAddrs, bootstrapToken, backend) + bootstrapToken, err = c.bootstrapServers(ipAddrs, c.backend) if err != nil { c.log.Error(err.Error()) return 1 @@ -857,6 +807,55 @@ func (c *Command) configureKubeClient() error { return nil } +// configureSecretsBackend configures either the Kubernetes or Vault +// secrets backend based on flags. +func (c *Command) configureSecretsBackend() error { + if c.backend != nil { + // support a fake backend in unit tests + return nil + } + secretName := c.flagBootstrapTokenSecretName + if secretName == "" { + secretName = c.withPrefix("bootstrap-acl-token") + } + + secretKey := c.flagBootstrapTokenSecretKey + if secretKey == "" { + secretKey = common.ACLTokenSecretKey + } + + switch c.flagSecretsBackend { + case SecretsBackendTypeKubernetes: + c.backend = &KubernetesSecretsBackend{ + ctx: c.ctx, + clientset: c.clientset, + k8sNamespace: c.flagK8sNamespace, + secretName: secretName, + secretKey: secretKey, + } + return nil + case SecretsBackendTypeVault: + cfg := vaultApi.DefaultConfig() + cfg.Address = "" + cfg.AgentAddress = "http://127.0.0.1:8200" + vaultClient, err := vaultApi.NewClient(cfg) + if err != nil { + return fmt.Errorf("Error initializing Vault client: %w", err) + } + + c.vaultClient = vaultClient // must set this for c.quitVaultAgent. + c.backend = &VaultSecretsBackend{ + vaultClient: c.vaultClient, + secretName: secretName, + secretKey: secretKey, + } + return nil + default: + validValues := []SecretsBackendType{SecretsBackendTypeKubernetes, SecretsBackendTypeVault} + return fmt.Errorf("Invalid value for -secrets-backend: %q. Valid values are %v.", c.flagSecretsBackend, validValues) + } +} + // untilSucceeds runs op until it returns a nil error. // If c.cmdTimeout is cancelled it will exit. func (c *Command) untilSucceeds(opName string, op func() error) error { @@ -983,6 +982,10 @@ func (c *Command) validateFlags() error { return errors.New("-consul-api-timeout must be set to a value greater than 0") } + //if c.flagVaultNamespace != "" && c.flagSecretsBackend != SecretsBackendTypeVault { + // return fmt.Errorf("-vault-namespace not supported for -secrets-backend=%q", c.flagSecretsBackend) + //} + return nil } @@ -999,21 +1002,24 @@ func loadTokenFromFile(tokenFile string) (string, error) { } func (c *Command) quitVaultAgent() { - if c.vaultClient != nil { - // Tell the Vault agent to quit. - // TODO: RawRequest is deprecated, but there is also - // not a high level method for this in the Vault client. - //nolint:staticcheck // SA1004 ignore this! + if c.vaultClient == nil { + return + } + + // Tell the Vault agent sidecar to quit. Without this, the Job does not + // complete because the Vault agent does not stop. This retries because it + // does not know exactly when the Vault agent sidecar will start. + err := c.untilSucceeds("tell Vault agent to quit", func() error { + // TODO: RawRequest is deprecated, but there is also not a high level + // method for this in the Vault client. + // nolint:staticcheck // SA1004 ignore _, err := c.vaultClient.RawRequest( c.vaultClient.NewRequest("POST", "/agent/v1/quit"), ) - if err != nil { - c.log.Warn(fmt.Sprintf("Unexpected error telling Vault agent to quit: %s", err)) - // proceed anyway. maybe the Vault agent sidecar is still running, - // but what could go wrong with a request to localhost that retrying would fix? - } else { - c.log.Debug("Success: told Vault agent to quit") - } + return err + }) + if err != nil { + c.log.Error("Error telling Vault agent to quit", "error", err) } } diff --git a/control-plane/subcommand/server-acl-init/command_ent_test.go b/control-plane/subcommand/server-acl-init/command_ent_test.go index e31b787e4b..fa56f3bb3a 100644 --- a/control-plane/subcommand/server-acl-init/command_ent_test.go +++ b/control-plane/subcommand/server-acl-init/command_ent_test.go @@ -222,7 +222,6 @@ func TestRun_ConnectInject_NamespaceMirroring(t *testing.T) { // a non-default partition. func TestRun_AnonymousToken_CreatedFromNonDefaultPartition(t *testing.T) { bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - tokenFile := common.WriteTempFile(t, bootToken) server := partitionedSetup(t, bootToken, "test") k8s := fake.NewSimpleClientset() setUpK8sServiceAccount(t, k8s, ns) @@ -231,6 +230,7 @@ func TestRun_AnonymousToken_CreatedFromNonDefaultPartition(t *testing.T) { cmd := Command{ UI: ui, clientset: k8s, + backend: &FakeSecretsBackend{bootstrapToken: bootToken}, } cmd.init() args := []string{ @@ -239,7 +239,6 @@ func TestRun_AnonymousToken_CreatedFromNonDefaultPartition(t *testing.T) { "-grpc-port=" + strings.Split(server.GRPCAddr, ":")[1], "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, - "-bootstrap-token-file", tokenFile, "-allow-dns", "-partition=test", "-enable-namespaces", diff --git a/control-plane/subcommand/server-acl-init/command_test.go b/control-plane/subcommand/server-acl-init/command_test.go index 3111f58820..ffe60af593 100644 --- a/control-plane/subcommand/server-acl-init/command_test.go +++ b/control-plane/subcommand/server-acl-init/command_test.go @@ -60,13 +60,6 @@ func TestRun_FlagValidation(t *testing.T) { "-resource-prefix=prefix"}, ExpErr: "unable to read token from file \"/notexist\": open /notexist: no such file or directory", }, - { - Flags: []string{ - "-bootstrap-token-file=/notexist", - "-addresses=localhost", - "-resource-prefix=prefix"}, - ExpErr: "unable to read token from file \"/notexist\": open /notexist: no such file or directory", - }, { Flags: []string{ "-addresses=localhost", @@ -407,7 +400,6 @@ func TestRun_TokensWithProvidedBootstrapToken(t *testing.T) { for _, c := range cases { t.Run(c.TestName, func(t *testing.T) { bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - tokenFile := common.WriteTempFile(t, bootToken) k8s, testAgent := completeBootstrappedSetup(t, bootToken) setUpK8sServiceAccount(t, k8s, ns) @@ -417,11 +409,11 @@ func TestRun_TokensWithProvidedBootstrapToken(t *testing.T) { cmd := Command{ UI: ui, clientset: k8s, + backend: &FakeSecretsBackend{bootstrapToken: bootToken}, } cmdArgs := append([]string{ "-timeout=1m", "-k8s-namespace", ns, - "-bootstrap-token-file", tokenFile, "-addresses", strings.Split(testAgent.TestServer.HTTPAddr, ":")[0], "-http-port", strings.Split(testAgent.TestServer.HTTPAddr, ":")[1], "-grpc-port", strings.Split(testAgent.TestServer.GRPCAddr, ":")[1], @@ -915,7 +907,6 @@ func TestRun_ErrorsOnDuplicateACLPolicy(t *testing.T) { // Create Consul with ACLs already bootstrapped so that we can // then seed it with our manually created policy. bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - tokenFile := common.WriteTempFile(t, bootToken) k8s, testAgent := completeBootstrappedSetup(t, bootToken) setUpK8sServiceAccount(t, k8s, ns) @@ -938,11 +929,11 @@ func TestRun_ErrorsOnDuplicateACLPolicy(t *testing.T) { cmd := Command{ UI: ui, clientset: k8s, + backend: &FakeSecretsBackend{bootstrapToken: bootToken}, } cmdArgs := []string{ "-timeout=1s", "-k8s-namespace", ns, - "-bootstrap-token-file", tokenFile, "-resource-prefix=" + resourcePrefix, "-k8s-namespace=" + ns, "-addresses", strings.Split(testAgent.TestServer.HTTPAddr, ":")[0], @@ -1453,272 +1444,223 @@ func TestRun_ClientPolicyAndBindingRuleRetry(t *testing.T) { // Test if there is an old bootstrap Secret we still try to create and set // server tokens. func TestRun_AlreadyBootstrapped(t *testing.T) { - t.Parallel() - cases := map[string]bool{ - "token saved in k8s secret": true, - "token provided via file": false, - } - - for name, tokenFromK8sSecret := range cases { - t.Run(name, func(t *testing.T) { - k8s := fake.NewSimpleClientset() - - type APICall struct { - Method string - Path string - } - var consulAPICalls []APICall - - // Start the Consul server. - consulServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Record all the API calls made. - consulAPICalls = append(consulAPICalls, APICall{ - Method: r.Method, - Path: r.URL.Path, - }) - switch r.URL.Path { - case "/v1/agent/self": - fmt.Fprintln(w, `{"Config": {"Datacenter": "dc1", "PrimaryDatacenter": "dc1"}}`) - case "/v1/acl/tokens": - fmt.Fprintln(w, `[]`) - case "/v1/acl/token": - fmt.Fprintln(w, `{}`) - case "/v1/acl/policy": - fmt.Fprintln(w, `{}`) - case "/v1/agent/token/acl_agent_token": - fmt.Fprintln(w, `{}`) - case "/v1/acl/auth-method": - fmt.Fprintln(w, `{}`) - case "/v1/acl/role/name/release-name-consul-client-acl-role": - w.WriteHeader(404) - case "/v1/acl/role": - fmt.Fprintln(w, `{}`) - case "/v1/acl/binding-rules": - fmt.Fprintln(w, `[]`) - case "/v1/acl/binding-rule": - fmt.Fprintln(w, `{}`) - default: - w.WriteHeader(500) - fmt.Fprintln(w, "Mock Server not configured for this route: "+r.URL.Path) - } - })) - defer consulServer.Close() + k8s := fake.NewSimpleClientset() - serverURL, err := url.Parse(consulServer.URL) - require.NoError(t, err) - port, err := strconv.Atoi(serverURL.Port()) - require.NoError(t, err) - setUpK8sServiceAccount(t, k8s, ns) + type APICall struct { + Method string + Path string + } + var consulAPICalls []APICall - cmdArgs := []string{ - "-timeout=500ms", - "-resource-prefix=" + resourcePrefix, - "-k8s-namespace=" + ns, - "-addresses=" + serverURL.Hostname(), - "-http-port=" + serverURL.Port(), - } + // Start the Consul server. + consulServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Record all the API calls made. + consulAPICalls = append(consulAPICalls, APICall{ + Method: r.Method, + Path: r.URL.Path, + }) + switch r.URL.Path { + case "/v1/agent/self": + fmt.Fprintln(w, `{"Config": {"Datacenter": "dc1", "PrimaryDatacenter": "dc1"}}`) + case "/v1/acl/tokens": + fmt.Fprintln(w, `[]`) + case "/v1/acl/token": + fmt.Fprintln(w, `{}`) + case "/v1/acl/policy": + fmt.Fprintln(w, `{}`) + case "/v1/agent/token/acl_agent_token": + fmt.Fprintln(w, `{}`) + case "/v1/acl/auth-method": + fmt.Fprintln(w, `{}`) + case "/v1/acl/role/name/release-name-consul-client-acl-role": + w.WriteHeader(404) + case "/v1/acl/role": + fmt.Fprintln(w, `{}`) + case "/v1/acl/binding-rules": + fmt.Fprintln(w, `[]`) + case "/v1/acl/binding-rule": + fmt.Fprintln(w, `{}`) + default: + w.WriteHeader(500) + fmt.Fprintln(w, "Mock Server not configured for this route: "+r.URL.Path) + } + })) + defer consulServer.Close() - // Create the bootstrap secret. - if tokenFromK8sSecret { - _, err = k8s.CoreV1().Secrets(ns).Create( - context.Background(), - &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourcePrefix + "-bootstrap-acl-token", - Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, - }, - Data: map[string][]byte{ - "token": []byte("old-token"), - }, - }, - metav1.CreateOptions{}) - require.NoError(t, err) - } else { - // Write token to a file. - bootTokenFile, err := os.CreateTemp("", "") - require.NoError(t, err) - defer os.RemoveAll(bootTokenFile.Name()) + serverURL, err := url.Parse(consulServer.URL) + require.NoError(t, err) + port, err := strconv.Atoi(serverURL.Port()) + require.NoError(t, err) + setUpK8sServiceAccount(t, k8s, ns) - _, err = bootTokenFile.WriteString("old-token") - require.NoError(t, err) + cmdArgs := []string{ + "-timeout=500ms", + "-resource-prefix=" + resourcePrefix, + "-k8s-namespace=" + ns, + "-addresses=" + serverURL.Hostname(), + "-http-port=" + serverURL.Port(), + } - require.NoError(t, err) - cmdArgs = append(cmdArgs, "-bootstrap-token-file", bootTokenFile.Name()) - } + // Create the bootstrap secret. + _, err = k8s.CoreV1().Secrets(ns).Create( + context.Background(), + &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourcePrefix + "-bootstrap-acl-token", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + Data: map[string][]byte{ + "token": []byte("old-token"), + }, + }, + metav1.CreateOptions{}) + require.NoError(t, err) - // Run the command. - ui := cli.NewMockUi() - cmd := Command{ - UI: ui, - clientset: k8s, - watcher: test.MockConnMgrForIPAndPort(serverURL.Hostname(), port), - } + // Run the command. + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + clientset: k8s, + watcher: test.MockConnMgrForIPAndPort(serverURL.Hostname(), port), + } - responseCode := cmd.Run(cmdArgs) - require.Equal(t, 0, responseCode, ui.ErrorWriter.String()) + responseCode := cmd.Run(cmdArgs) + require.Equal(t, 0, responseCode, ui.ErrorWriter.String()) - // Test that the Secret is the same. - if tokenFromK8sSecret { - secret, err := k8s.CoreV1().Secrets(ns).Get(context.Background(), resourcePrefix+"-bootstrap-acl-token", metav1.GetOptions{}) - require.NoError(t, err) - require.Contains(t, secret.Data, "token") - require.Equal(t, "old-token", string(secret.Data["token"])) - } + // Test that the Secret is the same. + secret, err := k8s.CoreV1().Secrets(ns).Get(context.Background(), resourcePrefix+"-bootstrap-acl-token", metav1.GetOptions{}) + require.NoError(t, err) + require.Contains(t, secret.Data, "token") + require.Equal(t, "old-token", string(secret.Data["token"])) - // Test that the expected API calls were made. - require.Equal(t, []APICall{ - // We expect calls for updating the server policy, setting server tokens, - // and updating client policy. - { - "PUT", - "/v1/acl/policy", - }, - { - "GET", - "/v1/acl/tokens", - }, - { - "PUT", - "/v1/acl/token", - }, - { - "PUT", - "/v1/agent/token/agent", - }, - { - "PUT", - "/v1/agent/token/acl_agent_token", - }, - { - "GET", - "/v1/agent/self", - }, - { - "PUT", - "/v1/acl/auth-method", - }, - { - "PUT", - "/v1/acl/policy", - }, - { - "GET", - "/v1/acl/role/name/release-name-consul-client-acl-role", - }, - { - "PUT", - "/v1/acl/role", - }, - { - "GET", - "/v1/acl/binding-rules", - }, - { - "PUT", - "/v1/acl/binding-rule", - }, - }, consulAPICalls) - }) - } + // Test that the expected API calls were made. + require.Equal(t, []APICall{ + // We expect calls for updating the server policy, setting server tokens, + // and updating client policy. + { + "PUT", + "/v1/acl/policy", + }, + { + "GET", + "/v1/acl/tokens", + }, + { + "PUT", + "/v1/acl/token", + }, + { + "PUT", + "/v1/agent/token/agent", + }, + { + "PUT", + "/v1/agent/token/acl_agent_token", + }, + { + "GET", + "/v1/agent/self", + }, + { + "PUT", + "/v1/acl/auth-method", + }, + { + "PUT", + "/v1/acl/policy", + }, + { + "GET", + "/v1/acl/role/name/release-name-consul-client-acl-role", + }, + { + "PUT", + "/v1/acl/role", + }, + { + "GET", + "/v1/acl/binding-rules", + }, + { + "PUT", + "/v1/acl/binding-rule", + }, + }, consulAPICalls) } // Test if there is an old bootstrap Secret and the server token exists // that we don't try and recreate the token. func TestRun_AlreadyBootstrapped_ServerTokenExists(t *testing.T) { - t.Parallel() - cases := map[string]bool{ - "token saved in k8s secret": true, - "token provided via file": false, - } - - for name, tokenInK8sSecret := range cases { - t.Run(name, func(t *testing.T) { - - // First set everything up with ACLs bootstrapped. - bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - k8s, testAgent := completeBootstrappedSetup(t, bootToken) - setUpK8sServiceAccount(t, k8s, ns) - - cmdArgs := []string{ - "-timeout=1m", - "-k8s-namespace", ns, - "-addresses", strings.Split(testAgent.TestServer.HTTPAddr, ":")[0], - "-http-port", strings.Split(testAgent.TestServer.HTTPAddr, ":")[1], - "-grpc-port", strings.Split(testAgent.TestServer.GRPCAddr, ":")[1], - "-resource-prefix", resourcePrefix, - } - - if tokenInK8sSecret { - _, err := k8s.CoreV1().Secrets(ns).Create(context.Background(), &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourcePrefix + "-bootstrap-acl-token", - }, - Data: map[string][]byte{ - "token": []byte(bootToken), - }, - }, metav1.CreateOptions{}) - require.NoError(t, err) - } else { - // Write token to a file. - bootTokenFile, err := os.CreateTemp("", "") - require.NoError(t, err) - defer os.RemoveAll(bootTokenFile.Name()) + // First set everything up with ACLs bootstrapped. + bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + k8s, testAgent := completeBootstrappedSetup(t, bootToken) + setUpK8sServiceAccount(t, k8s, ns) - _, err = bootTokenFile.WriteString(bootToken) - require.NoError(t, err) + cmdArgs := []string{ + "-timeout=1m", + "-k8s-namespace", ns, + "-addresses", strings.Split(testAgent.TestServer.HTTPAddr, ":")[0], + "-http-port", strings.Split(testAgent.TestServer.HTTPAddr, ":")[1], + "-grpc-port", strings.Split(testAgent.TestServer.GRPCAddr, ":")[1], + "-resource-prefix", resourcePrefix, + } - require.NoError(t, err) - cmdArgs = append(cmdArgs, "-bootstrap-token-file", bootTokenFile.Name()) - } + _, err := k8s.CoreV1().Secrets(ns).Create(context.Background(), &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourcePrefix + "-bootstrap-acl-token", + }, + Data: map[string][]byte{ + "token": []byte(bootToken), + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) - consulClient, err := api.NewClient(&api.Config{ - Address: testAgent.TestServer.HTTPAddr, - Token: bootToken, - }) - require.NoError(t, err) - ui := cli.NewMockUi() - cmd := Command{ - UI: ui, - clientset: k8s, - } + consulClient, err := api.NewClient(&api.Config{ + Address: testAgent.TestServer.HTTPAddr, + Token: bootToken, + }) + require.NoError(t, err) + ui := cli.NewMockUi() + cmd := Command{ + UI: ui, + clientset: k8s, + } - cmd.init() - // Create the server policy and token _before_ we run the command. - agentPolicyRules, err := cmd.agentRules() - require.NoError(t, err) - policy, _, err := consulClient.ACL().PolicyCreate(&api.ACLPolicy{ - Name: "agent-token", - Description: "Agent Token Policy", - Rules: agentPolicyRules, - }, nil) - require.NoError(t, err) - _, _, err = consulClient.ACL().TokenCreate(&api.ACLToken{ - Description: fmt.Sprintf("Server Token for %s", strings.Split(testAgent.TestServer.HTTPAddr, ":")[0]), - Policies: []*api.ACLTokenPolicyLink{ - { - Name: policy.Name, - }, - }, - }, nil) - require.NoError(t, err) + cmd.init() + // Create the server policy and token _before_ we run the command. + agentPolicyRules, err := cmd.agentRules() + require.NoError(t, err) + policy, _, err := consulClient.ACL().PolicyCreate(&api.ACLPolicy{ + Name: "agent-token", + Description: "Agent Token Policy", + Rules: agentPolicyRules, + }, nil) + require.NoError(t, err) + _, _, err = consulClient.ACL().TokenCreate(&api.ACLToken{ + Description: fmt.Sprintf("Server Token for %s", strings.Split(testAgent.TestServer.HTTPAddr, ":")[0]), + Policies: []*api.ACLTokenPolicyLink{ + { + Name: policy.Name, + }, + }, + }, nil) + require.NoError(t, err) - // Run the command. - responseCode := cmd.Run(cmdArgs) - require.Equal(t, 0, responseCode, ui.ErrorWriter.String()) + // Run the command. + responseCode := cmd.Run(cmdArgs) + require.Equal(t, 0, responseCode, ui.ErrorWriter.String()) - // Check that only one server token exists, i.e. it didn't create an - // extra token. - tokens, _, err := consulClient.ACL().TokenList(nil) - require.NoError(t, err) - count := 0 - for _, token := range tokens { - if len(token.Policies) == 1 && token.Policies[0].Name == policy.Name { - count++ - } - } - require.Equal(t, 1, count) - }) + // Check that only one server token exists, i.e. it didn't create an + // extra token. + tokens, _, err := consulClient.ACL().TokenList(nil) + require.NoError(t, err) + count := 0 + for _, token := range tokens { + if len(token.Policies) == 1 && token.Policies[0].Name == policy.Name { + count++ + } } + require.Equal(t, 1, count) } // Test if -set-server-tokens is false (i.e. servers are disabled), we skip bootstrapping of the servers @@ -1728,7 +1670,6 @@ func TestRun_SkipBootstrapping_WhenServersAreDisabled(t *testing.T) { k8s := fake.NewSimpleClientset() bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" - tokenFile := common.WriteTempFile(t, bootToken) type APICall struct { Method string @@ -1766,6 +1707,7 @@ func TestRun_SkipBootstrapping_WhenServersAreDisabled(t *testing.T) { UI: ui, clientset: k8s, watcher: test.MockConnMgrForIPAndPort(serverURL.Hostname(), port), + backend: &FakeSecretsBackend{bootstrapToken: bootToken}, } responseCode := cmd.Run([]string{ "-timeout=500ms", @@ -1773,7 +1715,6 @@ func TestRun_SkipBootstrapping_WhenServersAreDisabled(t *testing.T) { "-k8s-namespace=" + ns, "-addresses=" + serverURL.Hostname(), "-http-port=" + serverURL.Port(), - "-bootstrap-token-file=" + tokenFile, "-set-server-tokens=false", "-client=false", // disable client token, so there are fewer calls }) diff --git a/control-plane/subcommand/server-acl-init/k8s_secrets_backend.go b/control-plane/subcommand/server-acl-init/k8s_secrets_backend.go index d358dc6f20..4e3efcf65b 100644 --- a/control-plane/subcommand/server-acl-init/k8s_secrets_backend.go +++ b/control-plane/subcommand/server-acl-init/k8s_secrets_backend.go @@ -11,6 +11,8 @@ import ( "k8s.io/client-go/kubernetes" ) +const SecretsBackendTypeKubernetes SecretsBackendType = "kubernetes" + type KubernetesSecretsBackend struct { ctx context.Context clientset kubernetes.Interface diff --git a/control-plane/subcommand/server-acl-init/secrets_backend.go b/control-plane/subcommand/server-acl-init/secrets_backend.go index a8ef62c420..4b4d5c2fc4 100644 --- a/control-plane/subcommand/server-acl-init/secrets_backend.go +++ b/control-plane/subcommand/server-acl-init/secrets_backend.go @@ -1,5 +1,7 @@ package serveraclinit +type SecretsBackendType string + type SecretsBackend interface { // BootstrapToken fetches the bootstrap token from the backend. If the // token is not found or empty, implementations should return an empty @@ -14,10 +16,3 @@ type SecretsBackend interface { // BootstrapTokenSecretName returns the name of the bootstrap token secret. BootstrapTokenSecretName() string } - -type SecretsBackendType string - -const ( - SecretsBackendKubernetes SecretsBackendType = "kubernetes" - SecretsBackendVault SecretsBackendType = "vault" -) diff --git a/control-plane/subcommand/server-acl-init/servers.go b/control-plane/subcommand/server-acl-init/servers.go index 6869d97abf..01e9a58145 100644 --- a/control-plane/subcommand/server-acl-init/servers.go +++ b/control-plane/subcommand/server-acl-init/servers.go @@ -1,7 +1,6 @@ package serveraclinit import ( - "errors" "fmt" "net" "net/http" @@ -14,21 +13,25 @@ import ( ) // bootstrapServers bootstraps ACLs and ensures each server has an ACL token. -// If bootstrapToken is not empty then ACLs are already bootstrapped. -func (c *Command) bootstrapServers(serverAddresses []net.IPAddr, bootstrapToken string, backend SecretsBackend) (string, error) { +// If a bootstrap is found in the secrets backend, then skip ACL bootstrapping. +// Otherwise, bootstrap ACLs and write the bootstrap token to the secrets backend. +func (c *Command) bootstrapServers(serverAddresses []net.IPAddr, backend SecretsBackend) (string, error) { // Pick the first server address to connect to for bootstrapping and set up connection. firstServerAddr := fmt.Sprintf("%s:%d", serverAddresses[0].IP.String(), c.consulFlags.HTTPPort) - if bootstrapToken == "" { - c.log.Info("No bootstrap token from previous installation found, continuing on to bootstrapping") + bootstrapToken, err := backend.BootstrapToken() + if err != nil { + return "", fmt.Errorf("Unexpected error fetching bootstrap token secret: %w", err) + } - var err error + if bootstrapToken != "" { + c.log.Info("Found bootstrap token in secrets backend", "secret", backend.BootstrapTokenSecretName()) + } else { + c.log.Info("No bootstrap token found in secrets backend, continuing to ACL bootstrapping", "secret", backend.BootstrapTokenSecretName()) bootstrapToken, err = c.bootstrapACLs(firstServerAddr, backend) if err != nil { return "", err } - } else { - c.log.Info(fmt.Sprintf("ACLs already bootstrapped - retrieved bootstrap token from Secret %q", backend.BootstrapTokenSecretName())) } // We should only create and set server tokens when servers are running within this cluster. @@ -75,9 +78,12 @@ func (c *Command) bootstrapACLs(firstServerAddr string, backend SecretsBackend) // Check if already bootstrapped. if strings.Contains(err.Error(), "Unexpected response code: 403") { - unrecoverableErr = errors.New("ACLs already bootstrapped but the ACL token was not written to a secret." + - " We can't proceed because the bootstrap token is lost." + - " You must reset ACLs.") + unrecoverableErr = fmt.Errorf( + "ACLs already bootstrapped but unable to find the bootstrap token in the secrets backend."+ + " We can't proceed without a bootstrap token."+ + " Store a token with `acl:write` permission in the secret %q.", + backend.BootstrapTokenSecretName(), + ) return nil } diff --git a/control-plane/subcommand/server-acl-init/test_fake_secrets_backend.go b/control-plane/subcommand/server-acl-init/test_fake_secrets_backend.go new file mode 100644 index 0000000000..5c9a63f1a5 --- /dev/null +++ b/control-plane/subcommand/server-acl-init/test_fake_secrets_backend.go @@ -0,0 +1,20 @@ +package serveraclinit + +type FakeSecretsBackend struct { + bootstrapToken string +} + +func (b *FakeSecretsBackend) BootstrapToken() (string, error) { + return b.bootstrapToken, nil +} + +func (*FakeSecretsBackend) BootstrapTokenSecretName() string { + return "fake-bootstrap-token" +} + +func (b *FakeSecretsBackend) WriteBootstrapToken(token string) error { + b.bootstrapToken = token + return nil +} + +var _ SecretsBackend = (*FakeSecretsBackend)(nil) diff --git a/control-plane/subcommand/server-acl-init/vault_secrets_backend.go b/control-plane/subcommand/server-acl-init/vault_secrets_backend.go index 40cf214bb4..2706567878 100644 --- a/control-plane/subcommand/server-acl-init/vault_secrets_backend.go +++ b/control-plane/subcommand/server-acl-init/vault_secrets_backend.go @@ -6,6 +6,8 @@ import ( "github.com/hashicorp/vault/api" ) +const SecretsBackendTypeVault SecretsBackendType = "vault" + type VaultSecretsBackend struct { vaultClient *api.Client secretName string From 583d5457c18fd1a5197b011d0d72b07bad26f994 Mon Sep 17 00:00:00 2001 From: Paul Glass Date: Tue, 28 Feb 2023 12:53:10 -0600 Subject: [PATCH 4/6] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81edcde7c2..39f00b404d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ IMPROVEMENTS: * Add the `envoyExtensions` field to the `ProxyDefaults` and `ServiceDefaults` CRD. [[GH-1823]](https://github.com/hashicorp/consul-k8s/pull/1823) * Add the `balanceInboundConnections` field to the `ServiceDefaults` CRD. [[GH-1823]](https://github.com/hashicorp/consul-k8s/pull/1823) * Add the `upstreamConfig.overrides[].peer` field to the `ServiceDefaults` CRD. [[GH-1853]](https://github.com/hashicorp/consul-k8s/pull/1853) + * When the `global.acls.bootstrapToken` field is set and the content of the secret is empty, the bootstrap ACL token is written to that secret after bootstrapping ACLs. This applies to both the Vault and Consul secrets backends. [[GH-1920](https://github.com/hashicorp/consul-k8s/pull/1920)] * Control-Plane * Update minimum go version for project to 1.20 [[GH-1908](https://github.com/hashicorp/consul-k8s/pull/1908)] * Add support for the annotation `consul.hashicorp.com/use-proxy-health-check`. When this annotation is used by a service, it configures a readiness endpoint on Consul Dataplane and queries it instead of the proxy's inbound port which forwards requests to the application. [[GH-1824](https://github.com/hashicorp/consul-k8s/pull/1824)], [[GH-1841](https://github.com/hashicorp/consul-k8s/pull/1841)] From 2ae60da23b29911cac9fe04e6796e73b1fde4889 Mon Sep 17 00:00:00 2001 From: Paul Glass Date: Mon, 6 Mar 2023 09:33:24 -0600 Subject: [PATCH 5/6] Fix changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f00b404d..2f6a722b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## UNRELEASED +IMPROVEMENTS: + +* Helm: + * When the `global.acls.bootstrapToken` field is set and the content of the secret is empty, the bootstrap ACL token is written to that secret after bootstrapping ACLs. This applies to both the Vault and Consul secrets backends. [[GH-1920](https://github.com/hashicorp/consul-k8s/pull/1920)] + ## 1.1.0 (February 27, 2023) BREAKING CHANGES: @@ -26,7 +31,6 @@ IMPROVEMENTS: * Add the `envoyExtensions` field to the `ProxyDefaults` and `ServiceDefaults` CRD. [[GH-1823]](https://github.com/hashicorp/consul-k8s/pull/1823) * Add the `balanceInboundConnections` field to the `ServiceDefaults` CRD. [[GH-1823]](https://github.com/hashicorp/consul-k8s/pull/1823) * Add the `upstreamConfig.overrides[].peer` field to the `ServiceDefaults` CRD. [[GH-1853]](https://github.com/hashicorp/consul-k8s/pull/1853) - * When the `global.acls.bootstrapToken` field is set and the content of the secret is empty, the bootstrap ACL token is written to that secret after bootstrapping ACLs. This applies to both the Vault and Consul secrets backends. [[GH-1920](https://github.com/hashicorp/consul-k8s/pull/1920)] * Control-Plane * Update minimum go version for project to 1.20 [[GH-1908](https://github.com/hashicorp/consul-k8s/pull/1908)] * Add support for the annotation `consul.hashicorp.com/use-proxy-health-check`. When this annotation is used by a service, it configures a readiness endpoint on Consul Dataplane and queries it instead of the proxy's inbound port which forwards requests to the application. [[GH-1824](https://github.com/hashicorp/consul-k8s/pull/1824)], [[GH-1841](https://github.com/hashicorp/consul-k8s/pull/1841)] From 8ebd3e756931576d1d17c0bedd04418a123b58e0 Mon Sep 17 00:00:00 2001 From: Paul Glass Date: Mon, 6 Mar 2023 09:35:31 -0600 Subject: [PATCH 6/6] Fix changelog again --- .changelog/1920.txt | 3 +++ CHANGELOG.md | 5 ----- 2 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 .changelog/1920.txt diff --git a/.changelog/1920.txt b/.changelog/1920.txt new file mode 100644 index 0000000000..4b1f151fe4 --- /dev/null +++ b/.changelog/1920.txt @@ -0,0 +1,3 @@ +```release-note:improvement +helm: When the `global.acls.bootstrapToken` field is set and the content of the secret is empty, the bootstrap ACL token is written to that secret after bootstrapping ACLs. This applies to both the Vault and Consul secrets backends. +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f6a722b6e..81edcde7c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,5 @@ ## UNRELEASED -IMPROVEMENTS: - -* Helm: - * When the `global.acls.bootstrapToken` field is set and the content of the secret is empty, the bootstrap ACL token is written to that secret after bootstrapping ACLs. This applies to both the Vault and Consul secrets backends. [[GH-1920](https://github.com/hashicorp/consul-k8s/pull/1920)] - ## 1.1.0 (February 27, 2023) BREAKING CHANGES: