Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bootstrap gossip encryption with Vault #811

Merged
merged 22 commits into from
Nov 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion acceptance/framework/consul/consul_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (h *HelmCluster) Create(t *testing.T) {
})

// Fail if there are any existing installations of the Helm chart.
helpers.CheckForPriorInstallations(t, h.kubernetesClient, h.helmOptions, "consul-helm")
helpers.CheckForPriorInstallations(t, h.kubernetesClient, h.helmOptions, "consul-helm", "chart=consul-helm")

helm.Install(t, h.helmOptions, config.HelmChartPath, h.releaseName)

Expand Down
4 changes: 2 additions & 2 deletions acceptance/framework/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func RandomName() string {

// CheckForPriorInstallations checks if there is an existing Helm release
// for this Helm chart already installed. If there is, it fails the tests.
func CheckForPriorInstallations(t *testing.T, client kubernetes.Interface, options *helm.Options, chartName string) {
func CheckForPriorInstallations(t *testing.T, client kubernetes.Interface, options *helm.Options, chartName, labelSelector string) {
t.Helper()

var helmListOutput string
Expand Down Expand Up @@ -57,7 +57,7 @@ func CheckForPriorInstallations(t *testing.T, client kubernetes.Interface, optio
// Wait for all pods in the "default" namespace to exit. A previous
// release may not be listed by Helm but its pods may still be terminating.
retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 60}, t, func(r *retry.R) {
pods, err := client.CoreV1().Pods(options.KubectlOptions.Namespace).List(context.Background(), metav1.ListOptions{LabelSelector: fmt.Sprintf("chart=%s", chartName)})
pods, err := client.CoreV1().Pods(options.KubectlOptions.Namespace).List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector})
require.NoError(r, err)
if len(pods.Items) > 0 {
var podNames []string
Expand Down
41 changes: 15 additions & 26 deletions acceptance/framework/vault/vault_cluster.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package vault

import (
"context"
"fmt"
"github.com/hashicorp/consul-k8s/acceptance/framework/helpers"
"github.com/hashicorp/consul-k8s/acceptance/framework/k8s"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"testing"
"time"

Expand All @@ -15,6 +10,8 @@ import (
terratestLogger "github.com/gruntwork-io/terratest/modules/logger"
"github.com/hashicorp/consul-k8s/acceptance/framework/config"
"github.com/hashicorp/consul-k8s/acceptance/framework/environment"
"github.com/hashicorp/consul-k8s/acceptance/framework/helpers"
"github.com/hashicorp/consul-k8s/acceptance/framework/k8s"
"github.com/hashicorp/consul-k8s/acceptance/framework/logger"
"github.com/hashicorp/consul/sdk/testutil/retry"
vapi "github.com/hashicorp/vault/api"
Expand All @@ -23,9 +20,9 @@ import (
)

const (
vaultNS = "default"
vaultChartVersion = "0.17.0"
vaultRootToken = "abcd1234"
vaultNS = "default"
vaultPodLabel = "app.kubernetes.io/instance="
vaultRootToken = "abcd1234"
)

// VaultCluster
Expand All @@ -35,7 +32,6 @@ type VaultCluster struct {

vaultHelmOptions *helm.Options
vaultReleaseName string
vaultChartName string
vaultClient *vapi.Client

kubectlOptions *terratestk8s.KubectlOptions
Expand Down Expand Up @@ -68,13 +64,14 @@ func NewVaultCluster(
SetValues: defaultVaultValues(),
KubectlOptions: kopts,
Logger: logger,
Version: vaultChartVersion,
}
// Add the vault helm repo in case it is missing, and do an update so we can utilise `vaultChartVersion` to install.
helm.AddRepo(t, &helm.Options{}, "hashicorp/vault", "https://helm.releases.hashicorp.com")
helm.AddRepo(t, vaultHelmOpts, "hashicorp", "https://helm.releases.hashicorp.com")
// Ignoring the error from `helm repo update` as it could fail due to stale cache or unreachable servers and we're
// asserting a chart version on Install which would fail in an obvious way should this not succeed.
_, _ = helm.RunHelmCommandAndGetOutputE(t, &helm.Options{}, "repo", "update")
_, err := helm.RunHelmCommandAndGetOutputE(t, &helm.Options{}, "repo", "update")
if err != nil {
logger.Logf(t, "Unable to update helm repository, proceeding anyway: %s.", err)
}

return &VaultCluster{
ctx: ctx,
Expand All @@ -89,7 +86,6 @@ func NewVaultCluster(
debugDirectory: cfg.DebugDirectory,
logger: logger,
vaultReleaseName: releaseName,
vaultChartName: fmt.Sprintf("vault-%s", vaultChartVersion),
}
}

Expand All @@ -115,7 +111,7 @@ func (v *VaultCluster) SetupVaultClient(t *testing.T) *vapi.Client {
v.logger)

// Retry creating the port forward since it can fail occasionally.
retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 3}, t, func(r *retry.R) {
retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 60}, t, func(r *retry.R) {
// NOTE: It's okay to pass in `t` to ForwardPortE despite being in a retry
// because we're using ForwardPortE (not ForwardPort) so the `t` won't
// get used to fail the test, just for logging.
Expand Down Expand Up @@ -158,7 +154,7 @@ func (v *VaultCluster) bootstrap(t *testing.T, ctx environment.TestContext) {
}
// We need to kubectl exec this one on the vault server:
// This is taken from https://learn.hashicorp.com/tutorials/vault/kubernetes-google-cloud-gke?in=vault/kubernetes#configure-kubernetes-authentication
cmdString := fmt.Sprintf("VAULT_TOKEN=%s vault write auth/kubernetes/config token_reviewer_jwt=\"$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\" kubernetes_host=\"https://${KUBERNETES_PORT_443_TCP_ADDR}:443\" kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", vaultRootToken)
cmdString := fmt.Sprintf("VAULT_TOKEN=%s vault write auth/kubernetes/config disable_iss_validation=\"true\" token_reviewer_jwt=\"$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\" kubernetes_host=\"https://${KUBERNETES_PORT_443_TCP_ADDR}:443\" kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", vaultRootToken)

v.logger.Logf(t, "updating vault kube auth config")
k8s.RunKubectl(t, ctx.KubectlOptions(t), "exec", "-i", fmt.Sprintf("%s-vault-0", v.vaultReleaseName), "--", "sh", "-c", cmdString)
Expand All @@ -175,19 +171,12 @@ func (v *VaultCluster) Create(t *testing.T, ctx environment.TestContext) {
})

// Fail if there are any existing installations of the Helm chart.
helpers.CheckForPriorInstallations(t, v.kubernetesClient, v.vaultHelmOptions, v.vaultChartName)
helpers.CheckForPriorInstallations(t, v.kubernetesClient, v.vaultHelmOptions, "", fmt.Sprintf("%s=%s", vaultPodLabel, v.vaultReleaseName))

// Install Vault.
helm.Install(t, v.vaultHelmOptions, "hashicorp/vault", v.vaultReleaseName)
// Wait for the injector pod to become Ready, but not the server.
helpers.WaitForAllPodsToBeReady(t, v.kubernetesClient, v.vaultHelmOptions.KubectlOptions.Namespace, "app.kubernetes.io/name=vault-agent-injector")
// Wait for the server pod to be PodRunning, it will not be Ready because it has not been Init+Unseal'd yet.
// The vault server has health checks bound to unseal status, and Unseal is done as part of bootstrap (below).
retry.RunWith(&retry.Counter{Wait: 1 * time.Second, Count: 30}, t, func(r *retry.R) {
pod, err := v.kubernetesClient.CoreV1().Pods(v.vaultHelmOptions.KubectlOptions.Namespace).Get(context.Background(), fmt.Sprintf("%s-vault-0", v.vaultReleaseName), metav1.GetOptions{})
require.NoError(r, err)
require.Equal(r, pod.Status.Phase, corev1.PodRunning)
})
// Wait for the injector and vault server pods to become Ready.
helpers.WaitForAllPodsToBeReady(t, v.kubernetesClient, v.vaultHelmOptions.KubectlOptions.Namespace, fmt.Sprintf("%s=%s", vaultPodLabel, v.vaultReleaseName))
// Now call bootstrap()
v.bootstrap(t, ctx)
}
Expand Down
109 changes: 92 additions & 17 deletions acceptance/tests/vault/vault_test.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,118 @@
package vault

import (
"testing"
"time"

"crypto/rand"
"encoding/base64"
"fmt"
"github.com/hashicorp/consul-k8s/acceptance/framework/consul"
"github.com/hashicorp/consul-k8s/acceptance/framework/helpers"
"github.com/hashicorp/consul-k8s/acceptance/framework/logger"
"github.com/hashicorp/consul-k8s/acceptance/framework/vault"
"github.com/stretchr/testify/require"
"testing"
)

// Installs Vault, bootstraps it with the kube auth method
// and then validates that the KV2 secrets engine is online
// and the Kube Auth Method is enabled.
func TestVault_Create(t *testing.T) {
// generateGossipSecret generates a random 32 byte secret returned as a base64 encoded string.
func generateGossipSecret() (string, error) {
// This code was copied from Consul's Keygen command:
// https://github.com/hashicorp/consul/blob/d652cc86e3d0322102c2b5e9026c6a60f36c17a5/command/keygen/keygen.go

key := make([]byte, 32)
n, err := rand.Reader.Read(key)
if err != nil {
return "", fmt.Errorf("error reading random data: %s", err)
}
if n != 32 {
return "", fmt.Errorf("couldn't read enough entropy")
}

return base64.StdEncoding.EncodeToString(key), nil
}

// Installs Vault, bootstraps it with secrets, policies, and Kube Auth Method
// then creates a gossip encryption secret and uses this to bootstrap Consul.
func TestVault_BootstrapConsulGossipEncryptionKey(t *testing.T) {
cfg := suite.Config()
ctx := suite.Environment().DefaultContext(t)

consulReleaseName := helpers.RandomName()
vaultReleaseName := helpers.RandomName()
consulClientServiceAccountName := fmt.Sprintf("%s-consul-client", consulReleaseName)
consulServerServiceAccountName := fmt.Sprintf("%s-consul-server", consulReleaseName)

vaultCluster := vault.NewVaultCluster(t, nil, ctx, cfg, vaultReleaseName)
vaultCluster.Create(t, ctx)
logger.Log(t, "Finished Installing and Bootstrapping")
// Vault is now installed in the cluster.

// Now fetch the Vault client so we can create the policies and secrets.
vaultClient := vaultCluster.VaultClient(t)

// Write to the KV2 engine succeeds.
logger.Log(t, "Creating a KV2 Secret")
// Create the Vault Policy for the gossip key.
logger.Log(t, "Creating the gossip policy")
rules := `
path "consul/data/secret/gossip" {
capabilities = ["read"]
}`
err := vaultClient.Sys().PutPolicy("consul-gossip", rules)
require.NoError(t, err)

// Create the Auth Roles for consul-server + consul-client.
logger.Log(t, "Creating the consul-server and consul-client-roles")
params := map[string]interface{}{
"bound_service_account_names": consulClientServiceAccountName,
"bound_service_account_namespaces": "default",
"policies": "consul-gossip",
"ttl": "24h",
}
_, err = vaultClient.Logical().Write("auth/kubernetes/role/consul-client", params)
require.NoError(t, err)

params = map[string]interface{}{
"bound_service_account_names": consulServerServiceAccountName,
"bound_service_account_namespaces": "default",
"policies": "consul-gossip",
"ttl": "24h",
}
_, err = vaultClient.Logical().Write("auth/kubernetes/role/consul-server", params)
require.NoError(t, err)

gossipKey, err := generateGossipSecret()
require.NoError(t, err)

// Create the gossip secret.
logger.Log(t, "Creating the gossip secret")
params = map[string]interface{}{
"data": map[string]interface{}{
"foo": "bar",
"gossip": gossipKey,
},
}
_, err := vaultClient.Logical().Write("consul/data/secret/test", params)
_, err = vaultClient.Logical().Write("consul/data/secret/gossip", params)
require.NoError(t, err)

// Validate that the Auth Method exists.
authList, err := vaultClient.Sys().ListAuth()
consulHelmValues := map[string]string{
"server.enabled": "true",
"server.replicas": "1",

"connectInject.enabled": "true",

"global.secretsBackend.vault.enabled": "true",
"global.secretsBackend.vault.consulServerRole": "consul-server",
"global.secretsBackend.vault.consulClientRole": "consul-client",

"global.acls.manageSystemACLs": "true",
"global.tls.enabled": "true",
"global.gossipEncryption.secretName": "consul/data/secret/gossip",
"global.gossipEncryption.secretKey": "gossip",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

}
logger.Log(t, "Installing Consul")
consulCluster := consul.NewHelmCluster(t, consulHelmValues, ctx, cfg, consulReleaseName)
consulCluster.Create(t)

// Validate that the gossip encryption key is set correctly.
logger.Log(t, "Validating the gossip key has been set correctly.")
consulClient := consulCluster.SetupConsulClient(t, true)
keys, err := consulClient.Operator().KeyringList(nil)
require.NoError(t, err)
logger.Log(t, "Auth List: ", authList)
require.NotNil(t, authList["kubernetes/"])
time.Sleep(time.Second * 60)
kschoche marked this conversation as resolved.
Show resolved Hide resolved
// We use keys[0] because KeyringList returns a list of keyrings for each dc, in this case there is only 1 dc.
require.Equal(t, 1, keys[0].PrimaryKeys[gossipKey])
}
6 changes: 6 additions & 0 deletions charts/consul/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ as well as the global.name setting.
{{- end -}}
{{- end -}}

{{- define "consul.vaultGossipTemplate" -}}
|
{{ "{{" }}- with secret "{{ .secretName }}" -{{ "}}" }}
{{ "{{" }}- {{ printf ".Data.data.%s" .secretKey }} -{{ "}}" }}
{{ "{{" }}- end -{{ "}}" }}
{{- end -}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️ this refactor! makes it look much cleaner

{{/*
Sets up the extra-from-values config file passed to consul and then uses sed to do any necessary
substitution for HOST_IP/POD_IP/HOSTNAME. Useful for dogstats telemetry. The output file
Expand Down
17 changes: 17 additions & 0 deletions charts/consul/templates/client-daemonset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{{- if (and (and .Values.global.tls.enabled .Values.global.tls.httpsOnly) (and .Values.global.metrics.enabled .Values.global.metrics.enableAgentMetrics))}}{{ fail "global.metrics.enableAgentMetrics cannot be enabled if TLS (HTTPS only) is enabled" }}{{ end -}}
{{- $serverEnabled := (or (and (ne (.Values.server.enabled | toString) "-") .Values.server.enabled) (and (eq (.Values.server.enabled | toString) "-") .Values.global.enabled)) -}}
{{- if (and .Values.global.adminPartitions.enabled $serverEnabled (ne .Values.global.adminPartitions.name "default"))}}{{ fail "global.adminPartitions.name has to be \"default\" in the server cluster" }}{{ end -}}
{{- if (and (not .Values.global.secretsBackend.vault.consulClientRole) .Values.global.secretsBackend.vault.enabled) }}{{ fail "global.secretsBackend.vault.consulClientRole must be provided if global.secretsBackend.vault.enabled=true." }}{{ end -}}
# DaemonSet to run the Consul clients on every node.
apiVersion: apps/v1
kind: DaemonSet
Expand Down Expand Up @@ -37,6 +38,16 @@ spec:
{{- toYaml .Values.client.extraLabels | nindent 8 }}
{{- end }}
annotations:
{{- if .Values.global.secretsBackend.vault.enabled }}
"vault.hashicorp.com/agent-inject": "true"
"vault.hashicorp.com/role": "{{ .Values.global.secretsBackend.vault.consulClientRole }}"
{{- if .Values.global.gossipEncryption.secretName }}
{{- with .Values.global.gossipEncryption }}
"vault.hashicorp.com/agent-inject-secret-gossip.txt": "{{ .secretName }}"
"vault.hashicorp.com/agent-inject-template-gossip.txt": {{ template "consul.vaultGossipTemplate" . }}
{{- end }}
{{- end }}
{{- end }}
"consul.hashicorp.com/connect-inject": "false"
"consul.hashicorp.com/config-checksum": {{ include (print $.Template.BasePath "/client-config-configmap.yaml") . | sha256sum }}
{{- if .Values.client.annotations }}
Expand Down Expand Up @@ -169,6 +180,7 @@ spec:
- name: CONSUL_DISABLE_PERM_MGMT
value: "true"
{{- if (or .Values.global.gossipEncryption.autoGenerate (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey)) }}
{{- if not .Values.global.secretsBackend.vault.enabled }}
- name: GOSSIP_KEY
valueFrom:
secretKeyRef:
Expand All @@ -180,6 +192,7 @@ spec:
key: {{ .Values.global.gossipEncryption.secretKey }}
{{- end }}
{{- end }}
{{- end }}
{{- if (and .Values.server.enterpriseLicense.secretName .Values.server.enterpriseLicense.secretKey .Values.server.enterpriseLicense.enableLicenseAutoload (not .Values.global.acls.manageSystemACLs)) }}
- name: CONSUL_LICENSE_PATH
value: /consul/license/{{ .Values.server.enterpriseLicense.secretKey }}
Expand All @@ -202,6 +215,10 @@ spec:
- |
CONSUL_FULLNAME="{{template "consul.fullname" . }}"

{{- if and .Values.global.secretsBackend.vault.enabled .Values.global.gossipEncryption.secretName }}
GOSSIP_KEY=`cat /vault/secrets/gossip.txt`
{{- end }}

{{ template "consul.extraconfig" }}

exec /usr/local/bin/docker-entrypoint.sh consul agent \
Expand Down
19 changes: 19 additions & 0 deletions charts/consul/templates/server-statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
{{- if .Values.server.disableFsGroupSecurityContext }}{{ fail "server.disableFsGroupSecurityContext has been removed. Please use global.openshift.enabled instead." }}{{ end }}
{{- if .Values.server.bootstrapExpect }}{{ if lt (int .Values.server.bootstrapExpect) (int .Values.server.replicas) }}{{ fail "server.bootstrapExpect cannot be less than server.replicas" }}{{ end }}{{ end }}
{{- if (and (and .Values.global.tls.enabled .Values.global.tls.httpsOnly) (and .Values.global.metrics.enabled .Values.global.metrics.enableAgentMetrics))}}{{ fail "global.metrics.enableAgentMetrics cannot be enabled if TLS (HTTPS only) is enabled" }}{{ end -}}
{{- if (and .Values.global.gossipEncryption.secretName (not .Values.global.gossipEncryption.secretKey)) }}{{fail "gossipEncryption.secretKey and secretName must both be specified." }}{{ end -}}
{{- if (and (not .Values.global.gossipEncryption.secretName) .Values.global.gossipEncryption.secretKey) }}{{fail "gossipEncryption.secretKey and secretName must both be specified." }}{{ end -}}
ishustava marked this conversation as resolved.
Show resolved Hide resolved
{{- if (and .Values.global.secretsBackend.vault.enabled (not .Values.global.secretsBackend.vault.consulServerRole)) }}{{ fail "global.secretsBackend.vault.consulServerRole must be provided if global.secretsBackend.vault.enabled=true." }}{{ end -}}
# StatefulSet to run the actual Consul server cluster.
apiVersion: apps/v1
kind: StatefulSet
Expand Down Expand Up @@ -46,6 +49,16 @@ spec:
{{- toYaml .Values.server.extraLabels | nindent 8 }}
{{- end }}
annotations:
{{- if .Values.global.secretsBackend.vault.enabled }}
"vault.hashicorp.com/agent-inject": "true"
"vault.hashicorp.com/role": "{{ .Values.global.secretsBackend.vault.consulServerRole }}"
{{- if .Values.global.gossipEncryption.secretName }}
{{- with .Values.global.gossipEncryption }}
"vault.hashicorp.com/agent-inject-secret-gossip.txt": "{{ .secretName }}"
"vault.hashicorp.com/agent-inject-template-gossip.txt": {{ template "consul.vaultGossipTemplate" . }}
{{- end }}
{{- end }}
{{- end }}
"consul.hashicorp.com/connect-inject": "false"
"consul.hashicorp.com/config-checksum": {{ include (print $.Template.BasePath "/server-config-configmap.yaml") . | sha256sum }}
{{- if .Values.server.annotations }}
Expand Down Expand Up @@ -157,6 +170,7 @@ spec:
- name: CONSUL_DISABLE_PERM_MGMT
value: "true"
{{- if (or .Values.global.gossipEncryption.autoGenerate (and .Values.global.gossipEncryption.secretName .Values.global.gossipEncryption.secretKey)) }}
{{- if not .Values.global.secretsBackend.vault.enabled }}
- name: GOSSIP_KEY
valueFrom:
secretKeyRef:
Expand All @@ -168,6 +182,7 @@ spec:
key: {{ .Values.global.gossipEncryption.secretKey }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.global.tls.enabled }}
- name: CONSUL_HTTP_ADDR
value: https://localhost:8501
Expand All @@ -192,6 +207,10 @@ spec:
- |
CONSUL_FULLNAME="{{template "consul.fullname" . }}"

{{- if .Values.global.secretsBackend.vault.enabled }}
GOSSIP_KEY=`cat /vault/secrets/gossip.txt`
{{- end }}

{{ template "consul.extraconfig" }}

exec /usr/local/bin/docker-entrypoint.sh consul agent \
Expand Down
Loading