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

Add configuration for the vault Connect CA provider #872

Merged
merged 11 commits into from
Nov 24, 2021
4 changes: 3 additions & 1 deletion acceptance/framework/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/gruntwork-io/terratest/modules/helm"
"os"
"os/signal"
"strings"
"syscall"
"testing"
"time"

"github.com/gruntwork-io/terratest/modules/helm"

terratestk8s "github.com/gruntwork-io/terratest/modules/k8s"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/hashicorp/consul-k8s/acceptance/framework/logger"
Expand Down Expand Up @@ -85,6 +86,7 @@ func WaitForAllPodsToBeReady(t *testing.T, client kubernetes.Interface, namespac
retry.RunWith(counter, t, func(r *retry.R) {
pods, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: podLabelSelector})
require.NoError(r, err)
require.NotEmpty(r, pods.Items)
ishustava marked this conversation as resolved.
Show resolved Hide resolved

var notReadyPods []string
for _, pod := range pods.Items {
Expand Down
10 changes: 10 additions & 0 deletions acceptance/tests/fixtures/bases/intention/intention.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: consul.hashicorp.com/v1alpha1
kind: ServiceIntentions
metadata:
name: client-to-server
spec:
destination:
name: static-server
sources:
- name: static-client
action: allow
2 changes: 2 additions & 0 deletions acceptance/tests/fixtures/bases/intention/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
resources:
- intention.yaml
125 changes: 97 additions & 28 deletions acceptance/tests/vault/vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,50 @@ import (
"crypto/rand"
"encoding/base64"
"fmt"
"testing"

"github.com/hashicorp/consul-k8s/acceptance/framework/consul"
"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-k8s/acceptance/framework/vault"
"github.com/stretchr/testify/require"
"testing"
)

// generateGossipSecret generates a random 32 byte secret returned as a base64 encoded string.
func generateGossipSecret() (string, error) {
ishustava marked this conversation as resolved.
Show resolved Hide resolved
// This code was copied from Consul's Keygen command:
// https://github.com/hashicorp/consul/blob/d652cc86e3d0322102c2b5e9026c6a60f36c17a5/command/keygen/keygen.go
const (
gossipPolicy = `
path "consul/data/secret/gossip" {
capabilities = ["read"]
}`

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")
}
// connectCAPolicy allows Consul to bootstrap all certificates for the service mesh in Vault.
// Adapted from https://www.consul.io/docs/connect/ca/vault#consul-managed-pki-paths.
connectCAPolicy = `
path "/sys/mounts" {
capabilities = [ "read" ]
}

return base64.StdEncoding.EncodeToString(key), nil
path "/sys/mounts/connect_root" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}

path "/sys/mounts/connect_inter" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}

// 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) {
path "/connect_root/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}

path "/connect_inter/*" {
capabilities = [ "create", "read", "update", "delete", "list" ]
}
`
)

// 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) {
cfg := suite.Config()
ctx := suite.Environment().DefaultContext(t)

Expand All @@ -48,16 +64,20 @@ func TestVault_BootstrapConsulGossipEncryptionKey(t *testing.T) {
vaultClient := vaultCluster.VaultClient(t)

// 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)
logger.Log(t, "Creating policies")
err := vaultClient.Sys().PutPolicy("consul-gossip", gossipPolicy)
require.NoError(t, err)

err = vaultClient.Sys().PutPolicy("connect-ca", connectCAPolicy)
require.NoError(t, err)

// Create the Auth Roles for consul-server + consul-client.
logger.Log(t, "Creating the consul-server and consul-client-roles")
// Create the Auth Roles for consul-server and consul-client.
Copy link
Contributor

@ndhanushkodi ndhanushkodi Nov 23, 2021

Choose a reason for hiding this comment

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

Could we say "Create the Auth Roles for consul-server and consul-client which enables ___"? I wasn't sure what auth roles do until I read this (https://www.vaultproject.io/docs/auth/kubernetes#configuration) so maybe it'd be nice to add a tiny summary in the comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, great call-out! I've added more info to this comment. Let me know if that makes sense!

// Auth roles bind policies to Kubernetes service accounts, which
// then enables the Vault agent init container to call 'vault login'
// with the Kubernetes auth method to obtain a Vault token.
// Please see https://www.vaultproject.io/docs/auth/kubernetes#configuration
// for more details.
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",
Expand All @@ -70,7 +90,7 @@ path "consul/data/secret/gossip" {
params = map[string]interface{}{
"bound_service_account_names": consulServerServiceAccountName,
"bound_service_account_namespaces": "default",
"policies": "consul-gossip",
"policies": "consul-gossip,connect-ca",
"ttl": "24h",
}
_, err = vaultClient.Logical().Write("auth/kubernetes/role/consul-server", params)
Expand All @@ -88,17 +108,23 @@ path "consul/data/secret/gossip" {
}
Copy link
Contributor

Choose a reason for hiding this comment

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

why don't we need to generate anything for the connect ca like we generated the gossip secret? is the only info vault needs in this case just the address and two paths and it will generate the certs itself? maybe its worth a comment here but also if this is just something that would be documented in user facing docs that's fine too.

Copy link
Contributor Author

@ishustava ishustava Nov 23, 2021

Choose a reason for hiding this comment

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

is the only info vault needs in this case just the address and two paths and it will generate the certs itself?

Yep, except it's Consul that will generate it for us (as long as the policy allows it to do so).

From https://www.consul.io/docs/connect/ca/vault#root-and-intermediate-pki-paths:

If either path does not exist, then Consul will attempt to mount and initialize it. This requires additional privileges by the Vault token in use. If the paths already exist, Consul will use them as configured.

_, err = vaultClient.Logical().Write("consul/data/secret/gossip", params)
require.NoError(t, err)

consulHelmValues := map[string]string{
"global.image": "docker.mirror.hashicorp.services/hashicorpdev/consul:latest",

"server.enabled": "true",
"server.replicas": "1",

"connectInject.enabled": "true",
"controller.enabled": "true",

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

"global.secretsBackend.vault.connectCA.address": fmt.Sprintf("http://%s-vault:8200", vaultReleaseName),
"global.secretsBackend.vault.connectCA.rootPKIPath": "connect_root",
"global.secretsBackend.vault.connectCA.intermediatePKIPath": "connect_inter",

Copy link
Contributor

Choose a reason for hiding this comment

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

why are the connectCA keys under global.secretsBackend.vault but the gossip stuff is under global.gossipEncryption? Were we just trying to respect the previously used values for the gossipEncryption configuration? I guess I found it slightly unintuitive that you configure different vault things under different keys but this is not blocking the PR, just wanted to clarify if this is the case. cc @kschoche

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question. I actually suggested that we use existing configuration for gossip secret to make a consistent regardless of whether you use k8s secrets or vault secrets. I think this allows us to re-use existing configuration that is for the same purpose rather than duplicate it for vault specifically. So in this case, gossip key configuration is just a reference to some secret which could be in k8s or in vault.

Connect CA config is a bit different. First, it doesn't require any secrets since Consul can create all those secrets in Vault as long as the policy allows. Second, this configuration, unlike gossip key config, is specific to vault connect CA provider and so these options only make sense under secretsBackend.vault

Copy link
Contributor

Choose a reason for hiding this comment

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

that makes sense, thank you!!

"global.acls.manageSystemACLs": "true",
"global.tls.enabled": "true",
"global.gossipEncryption.secretName": "consul/data/secret/gossip",
Expand All @@ -113,6 +139,49 @@ path "consul/data/secret/gossip" {
consulClient := consulCluster.SetupConsulClient(t, true)
keys, err := consulClient.Operator().KeyringList(nil)
require.NoError(t, err)
// We use keys[0] because KeyringList returns a list of keyrings for each dc, in this case there is only 1 dc.
// There are two identical keys for LAN and WAN since there is only 1 dc.
kschoche marked this conversation as resolved.
Show resolved Hide resolved
require.Len(t, keys, 2)
require.Equal(t, 1, keys[0].PrimaryKeys[gossipKey])

// Confirm that the Vault Connect CA has been bootstrapped correctly.
caConfig, _, err := consulClient.Connect().CAGetConfig(nil)
require.NoError(t, err)
require.Equal(t, caConfig.Provider, "vault")

// Deploy two services and check that they can talk to each other.
logger.Log(t, "creating static-server and static-client deployments")
k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-server-inject")
if cfg.EnableTransparentProxy {
k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-tproxy")
} else {
k8s.DeployKustomize(t, ctx.KubectlOptions(t), cfg.NoCleanupOnFailure, cfg.DebugDirectory, "../fixtures/cases/static-client-inject")
}
helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() {
k8s.KubectlDeleteK(t, ctx.KubectlOptions(t), "../fixtures/bases/intention")
})
k8s.KubectlApplyK(t, ctx.KubectlOptions(t), "../fixtures/bases/intention")

logger.Log(t, "checking that connection is successful")
if cfg.EnableTransparentProxy {
k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), "http://static-server")
} else {
k8s.CheckStaticServerConnectionSuccessful(t, ctx.KubectlOptions(t), "http://localhost:1234")
}
}

// 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
}
30 changes: 30 additions & 0 deletions charts/consul/templates/server-config-configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,36 @@ metadata:
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
data:
{{- $vaultConnectCAEnabled := and .Values.global.secretsBackend.vault.connectCA.address .Values.global.secretsBackend.vault.connectCA.rootPKIPath .Values.global.secretsBackend.vault.connectCA.intermediatePKIPath -}}
{{- if and .Values.global.secretsBackend.vault.enabled $vaultConnectCAEnabled }}
Copy link
Contributor

Choose a reason for hiding this comment

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

so beautifully readable!!

{{- with .Values.global.secretsBackend.vault }}
connect-ca-config.json: |
{
"connect": [
{
"ca_config": [
{
"address": "{{ .connectCA.address }}",
"intermediate_pki_path": "{{ .connectCA.intermediatePKIPath }}",
"root_pki_path": "{{ .connectCA.rootPKIPath }}",
"auth_method": {
"type": "kubernetes",
"params": {
"role": "{{.consulServerRole}}"
}
}
}
],
"ca_provider": "vault"
}
]
}
{{- if .connectCA.additionalConfig }}
additional-connect-ca-config.json: |
{{ tpl .connectCA.additionalConfig $ | trimAll "\"" | indent 4 }}
kschoche marked this conversation as resolved.
Show resolved Hide resolved
{{- end }}
{{- end }}
{{- end }}
extra-from-values.json: |-
{{ tpl .Values.server.extraConfig . | trimAll "\"" | indent 4 }}
{{- if .Values.global.acls.manageSystemACLs }}
Expand Down
Loading