From cdf488fe959f9e517ae31f8f348197113985454d Mon Sep 17 00:00:00 2001 From: Ashwin Venkatesh Date: Wed, 27 Jul 2022 13:12:14 -0400 Subject: [PATCH] Peering ACL support (#1343) --- CHANGELOG.md | 5 +- .../peering_connect_namespaces_test.go | 97 +++++++++---------- .../tests/peering/peering_connect_test.go | 66 +++++-------- .../consul/templates/server-acl-init-job.yaml | 3 + .../consul/test/unit/server-acl-init-job.bats | 31 ++++++ .../subcommand/server-acl-init/command.go | 6 ++ .../subcommand/server-acl-init/rules.go | 5 + .../subcommand/server-acl-init/rules_test.go | 44 ++++++++- 8 files changed, 165 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2813f44cf1..a9c1c8353f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## UNRELEASED +FEATURES: +* [Experimental] Cluster Peering: + * Add support for ACLs and TLS. [[GH-1343](https://github.com/hashicorp/consul-k8s/pull/1343)] [[GH-1366](https://github.com/hashicorp/consul-k8s/pull/1366)] + ## 0.46.1 (July 26, 2022) IMPROVEMENTS: @@ -13,7 +17,6 @@ IMPROVEMENTS: * CLI * Update minimum go version for project to 1.18 [[GH-1292](https://github.com/hashicorp/consul-k8s/pull/1292)] - FEATURES: * [Experimental] Cluster Peering: * Add support for secret watchers on the Peering Acceptor and Peering Dialer controllers. [[GH-1284](https://github.com/hashicorp/consul-k8s/pull/1284)] diff --git a/acceptance/tests/peering/peering_connect_namespaces_test.go b/acceptance/tests/peering/peering_connect_namespaces_test.go index ec932f2fe8..78a1c58436 100644 --- a/acceptance/tests/peering/peering_connect_namespaces_test.go +++ b/acceptance/tests/peering/peering_connect_namespaces_test.go @@ -66,6 +66,24 @@ func TestPeering_ConnectNamespaces(t *testing.T) { true, false, }, + { + "default destination namespace", + defaultNamespace, + false, + true, + }, + { + "single destination namespace", + staticServerNamespace, + false, + true, + }, + { + "mirror k8s namespaces", + staticServerNamespace, + true, + true, + }, } for _, c := range cases { @@ -77,7 +95,7 @@ func TestPeering_ConnectNamespaces(t *testing.T) { "global.peering.enabled": "true", "global.enableConsulNamespaces": "true", - "global.image": "thisisnotashwin/consul@sha256:446aad6e02f66e3027756dfc0d34e8e6e2b11ac6ec5637b134b34644ca7cda64", + "global.image": "thisisnotashwin/consul@sha256:b1d3f59406adf5fb9a3bee4ded058e619d3a186e83b2e2dc14d6da3f28a7073d", "global.tls.enabled": "true", "global.tls.httpsOnly": strconv.FormatBool(c.ACLsAndAutoEncryptEnabled), @@ -266,61 +284,40 @@ func TestPeering_ConnectNamespaces(t *testing.T) { }) } - logger.Log(t, "checking that connection is successful") - if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.%s.consul", c.destinationNamespace, staticServerPeer)) - } else { - k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, staticClientName, "http://localhost:1234") - } + if c.ACLsAndAutoEncryptEnabled { + logger.Log(t, "checking that the connection is not successful because there's no allow intention") + if cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, staticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", fmt.Sprintf("curl: (7) Failed to connect to static-server.%s port 80: Connection refused", c.destinationNamespace)}, "", fmt.Sprintf("http://static-server.virtual.%s.%s.consul", c.destinationNamespace, staticServerPeer)) + } else { + k8s.CheckStaticServerConnectionFailing(t, staticClientOpts, staticClientName, "http://localhost:1234") + } - denyAllIntention := &api.ServiceIntentionsConfigEntry{ - Name: "*", - Kind: api.ServiceIntentions, - Namespace: "*", - Sources: []*api.SourceIntention{ - { - Name: "*", - Namespace: "*", - Action: api.IntentionActionDeny, - Peer: staticClientPeer, + intention := &api.ServiceIntentionsConfigEntry{ + Name: staticServerName, + Kind: api.ServiceIntentions, + Namespace: staticServerNamespace, + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Namespace: staticClientNamespace, + Action: api.IntentionActionAllow, + Peer: staticClientPeer, + }, }, - }, - } - _, _, err = staticServerPeerClient.ConfigEntries().Set(denyAllIntention, &api.WriteOptions{}) - require.NoError(t, err) - - logger.Log(t, "checking that the connection is not successful because there's no allow intention") - if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionMultipleFailureMessages(t, staticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.virtual.%s.%s.consul", c.destinationNamespace, staticServerPeer)) - } else { - k8s.CheckStaticServerConnectionFailing(t, staticClientOpts, staticClientName, "http://localhost:1234") - } + } - intention := &api.ServiceIntentionsConfigEntry{ - Name: staticServerName, - Kind: api.ServiceIntentions, - Namespace: staticServerNamespace, - Sources: []*api.SourceIntention{ - { - Name: staticClientName, - Namespace: staticClientNamespace, - Action: api.IntentionActionAllow, - Peer: staticClientPeer, - }, - }, - } + // Set the destination namespace to be the same + // unless mirrorK8S is true. + if !c.mirrorK8S { + intention.Namespace = c.destinationNamespace + intention.Sources[0].Namespace = c.destinationNamespace + } - // Set the destination namespace to be the same - // unless mirrorK8S is true. - if !c.mirrorK8S { - intention.Namespace = c.destinationNamespace - intention.Sources[0].Namespace = c.destinationNamespace + logger.Log(t, "creating intentions in server peer") + _, _, err = staticServerPeerClient.ConfigEntries().Set(intention, &api.WriteOptions{}) + require.NoError(t, err) } - logger.Log(t, "creating intentions in server peer") - _, _, err = staticServerPeerClient.ConfigEntries().Set(intention, &api.WriteOptions{}) - require.NoError(t, err) - logger.Log(t, "checking that connection is successful") if cfg.EnableTransparentProxy { k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.%s.consul", c.destinationNamespace, staticServerPeer)) diff --git a/acceptance/tests/peering/peering_connect_test.go b/acceptance/tests/peering/peering_connect_test.go index 0749b2cdc0..d18d6bee70 100644 --- a/acceptance/tests/peering/peering_connect_test.go +++ b/acceptance/tests/peering/peering_connect_test.go @@ -40,6 +40,10 @@ func TestPeering_Connect(t *testing.T) { "default installation", false, }, + { + "secure installation", + true, + }, } for _, c := range cases { @@ -50,7 +54,7 @@ func TestPeering_Connect(t *testing.T) { commonHelmValues := map[string]string{ "global.peering.enabled": "true", - "global.image": "thisisnotashwin/consul@sha256:446aad6e02f66e3027756dfc0d34e8e6e2b11ac6ec5637b134b34644ca7cda64", + "global.image": "thisisnotashwin/consul@sha256:b1d3f59406adf5fb9a3bee4ded058e619d3a186e83b2e2dc14d6da3f28a7073d", "global.tls.enabled": "true", "global.tls.httpsOnly": strconv.FormatBool(c.ACLsAndAutoEncryptEnabled), @@ -210,50 +214,32 @@ func TestPeering_Connect(t *testing.T) { helpers.Cleanup(t, cfg.NoCleanupOnFailure, func() { k8s.KubectlDeleteK(t, staticServerPeerClusterContext.KubectlOptions(t), "../fixtures/cases/crd-peers/default") }) - logger.Log(t, "checking that connection is successful") - if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.consul", staticServerPeer)) - } else { - k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, staticClientName, "http://localhost:1234") - } - denyAllIntention := &api.ServiceIntentionsConfigEntry{ - Name: "*", - Kind: api.ServiceIntentions, - Sources: []*api.SourceIntention{ - { - Name: "*", - Action: api.IntentionActionDeny, - Peer: staticClientPeer, + if c.ACLsAndAutoEncryptEnabled { + logger.Log(t, "checking that the connection is not successful because there's no allow intention") + if cfg.EnableTransparentProxy { + k8s.CheckStaticServerConnectionMultipleFailureMessages(t, staticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.virtual.%s.consul", staticServerPeer)) + } else { + k8s.CheckStaticServerConnectionFailing(t, staticClientOpts, staticClientName, "http://localhost:1234") + } + + intention := &api.ServiceIntentionsConfigEntry{ + Name: staticServerName, + Kind: api.ServiceIntentions, + Sources: []*api.SourceIntention{ + { + Name: staticClientName, + Action: api.IntentionActionAllow, + Peer: staticClientPeer, + }, }, - }, - } - _, _, err = staticServerPeerClient.ConfigEntries().Set(denyAllIntention, &api.WriteOptions{}) - require.NoError(t, err) - - logger.Log(t, "checking that the connection is not successful because there's no allow intention") - if cfg.EnableTransparentProxy { - k8s.CheckStaticServerConnectionMultipleFailureMessages(t, staticClientOpts, staticClientName, false, []string{"curl: (56) Recv failure: Connection reset by peer", "curl: (52) Empty reply from server", "curl: (7) Failed to connect to static-server.ns1 port 80: Connection refused"}, "", fmt.Sprintf("http://static-server.virtual.%s.consul", staticServerPeer)) - } else { - k8s.CheckStaticServerConnectionFailing(t, staticClientOpts, staticClientName, "http://localhost:1234") - } + } - intention := &api.ServiceIntentionsConfigEntry{ - Name: staticServerName, - Kind: api.ServiceIntentions, - Sources: []*api.SourceIntention{ - { - Name: staticClientName, - Action: api.IntentionActionAllow, - Peer: staticClientPeer, - }, - }, + logger.Log(t, "creating intentions in server peer") + _, _, err = staticServerPeerClient.ConfigEntries().Set(intention, &api.WriteOptions{}) + require.NoError(t, err) } - logger.Log(t, "creating intentions in server peer") - _, _, err = staticServerPeerClient.ConfigEntries().Set(intention, &api.WriteOptions{}) - require.NoError(t, err) - logger.Log(t, "checking that connection is successful") if cfg.EnableTransparentProxy { k8s.CheckStaticServerConnectionSuccessful(t, staticClientOpts, staticClientName, fmt.Sprintf("http://static-server.virtual.%s.consul", staticServerPeer)) diff --git a/charts/consul/templates/server-acl-init-job.yaml b/charts/consul/templates/server-acl-init-job.yaml index c736384a4e..8709b82af7 100644 --- a/charts/consul/templates/server-acl-init-job.yaml +++ b/charts/consul/templates/server-acl-init-job.yaml @@ -185,6 +185,9 @@ spec: -enable-partitions=true \ -partition={{ .Values.global.adminPartitions.name }} \ {{- end }} + {{- if .Values.global.peering.enabled }} + -enable-peering=true \ + {{- end }} {{- if (or (and (ne (.Values.dns.enabled | toString) "-") .Values.dns.enabled) (and (eq (.Values.dns.enabled | toString) "-") .Values.global.enabled)) }} -allow-dns=true \ {{- end }} diff --git a/charts/consul/test/unit/server-acl-init-job.bats b/charts/consul/test/unit/server-acl-init-job.bats index 8259ec1c36..7b442a2ef8 100644 --- a/charts/consul/test/unit/server-acl-init-job.bats +++ b/charts/consul/test/unit/server-acl-init-job.bats @@ -1386,6 +1386,37 @@ load _helpers [ "${actual}" = "true" ] } +#-------------------------------------------------------------------- +# cluster peering + +@test "serverACLInit/Job: cluster peering disabled by default" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual=$(echo $object | + yq 'any(contains("enable-peering"))' | tee /dev/stderr) + [ "${actual}" = "false" ] +} + +@test "serverACLInit/Job: cluster peering enabled when peering is enabled" { + cd `chart_dir` + local object=$(helm template \ + -s templates/server-acl-init-job.yaml \ + --set 'global.acls.manageSystemACLs=true' \ + --set 'global.peering.enabled=true' \ + --set 'connectInject.enabled=true' \ + . | tee /dev/stderr | + yq '.spec.template.spec.containers[0].command' | tee /dev/stderr) + + local actual=$(echo $object | + yq 'any(contains("enable-peering"))' | tee /dev/stderr) + [ "${actual}" = "true" ] +} + #-------------------------------------------------------------------- # admin partitions diff --git a/control-plane/subcommand/server-acl-init/command.go b/control-plane/subcommand/server-acl-init/command.go index 93a6fefc31..ddf9b32ba3 100644 --- a/control-plane/subcommand/server-acl-init/command.go +++ b/control-plane/subcommand/server-acl-init/command.go @@ -79,6 +79,9 @@ type Command struct { flagPartitionName string // name of the Admin Partition flagPartitionTokenFile string + // Flags to support peering. + flagEnablePeering bool // true if Cluster Peering is enabled + // Flags to support namespaces. flagEnableNamespaces bool // Use namespacing on all components flagConsulSyncDestinationNamespace string // Consul namespace to register all catalog sync services into if not mirroring @@ -181,6 +184,9 @@ func (c *Command) init() { c.flags.StringVar(&c.flagPartitionTokenFile, "partition-token-file", "", "[Enterprise Only] Path to file containing ACL token to be used in non-default partitions.") + c.flags.BoolVar(&c.flagEnablePeering, "enable-peering", false, + "Enables Cluster Peering.") + c.flags.BoolVar(&c.flagEnableNamespaces, "enable-namespaces", false, "[Enterprise Only] Enables namespaces, in either a single Consul namespace or mirrored [Enterprise only feature]") c.flags.StringVar(&c.flagConsulSyncDestinationNamespace, "consul-sync-destination-namespace", consulDefaultNamespace, diff --git a/control-plane/subcommand/server-acl-init/rules.go b/control-plane/subcommand/server-acl-init/rules.go index eb24b6138e..8b2dec7a14 100644 --- a/control-plane/subcommand/server-acl-init/rules.go +++ b/control-plane/subcommand/server-acl-init/rules.go @@ -8,6 +8,7 @@ import ( type rulesData struct { EnablePartitions bool + EnablePeering bool PartitionName string EnableNamespaces bool SyncConsulDestNS string @@ -315,6 +316,9 @@ partition "{{ .PartitionName }}" { {{- if .EnableNamespaces }} operator = "write" {{- end }} +{{- end }} +{{- if .EnablePeering }} + peering = "write" {{- end }} node_prefix "" { policy = "write" @@ -416,6 +420,7 @@ partition "{{ .PartitionName }}" { func (c *Command) rulesData() rulesData { return rulesData{ EnablePartitions: c.flagEnablePartitions, + EnablePeering: c.flagEnablePeering, PartitionName: c.flagPartitionName, EnableNamespaces: c.flagEnableNamespaces, SyncConsulDestNS: c.flagConsulSyncDestinationNamespace, diff --git a/control-plane/subcommand/server-acl-init/rules_test.go b/control-plane/subcommand/server-acl-init/rules_test.go index 723ab55f4e..1e736f9a95 100644 --- a/control-plane/subcommand/server-acl-init/rules_test.go +++ b/control-plane/subcommand/server-acl-init/rules_test.go @@ -848,12 +848,14 @@ func TestInjectRules(t *testing.T) { cases := []struct { EnableNamespaces bool EnablePartitions bool + EnablePeering bool PartitionName string Expected string }{ { EnableNamespaces: false, EnablePartitions: false, + EnablePeering: false, Expected: ` node_prefix "" { policy = "write" @@ -866,6 +868,7 @@ func TestInjectRules(t *testing.T) { { EnableNamespaces: true, EnablePartitions: false, + EnablePeering: false, Expected: ` operator = "write" node_prefix "" { @@ -877,13 +880,51 @@ func TestInjectRules(t *testing.T) { policy = "write" } }`, + }, + { + EnableNamespaces: true, + EnablePartitions: false, + EnablePeering: true, + Expected: ` + operator = "write" + peering = "write" + node_prefix "" { + policy = "write" + } + namespace_prefix "" { + acl = "write" + service_prefix "" { + policy = "write" + } + }`, + }, + { + EnableNamespaces: true, + EnablePartitions: true, + EnablePeering: false, + PartitionName: "part-1", + Expected: ` +partition "part-1" { + node_prefix "" { + policy = "write" + } + namespace_prefix "" { + policy = "write" + acl = "write" + service_prefix "" { + policy = "write" + } + } +}`, }, { EnableNamespaces: true, EnablePartitions: true, + EnablePeering: true, PartitionName: "part-1", Expected: ` partition "part-1" { + peering = "write" node_prefix "" { policy = "write" } @@ -899,13 +940,14 @@ partition "part-1" { } for _, tt := range cases { - caseName := fmt.Sprintf("ns=%t, partition=%t", tt.EnableNamespaces, tt.EnablePartitions) + caseName := fmt.Sprintf("ns=%t, partition=%t, peering=%t", tt.EnableNamespaces, tt.EnablePartitions, tt.EnablePeering) t.Run(caseName, func(t *testing.T) { cmd := Command{ flagEnablePartitions: tt.EnablePartitions, flagPartitionName: tt.PartitionName, flagEnableNamespaces: tt.EnableNamespaces, + flagEnablePeering: tt.EnablePeering, } injectorRules, err := cmd.injectRules()