Skip to content

Commit

Permalink
Use kube auth method to provision ACL token for the crd controller (#995
Browse files Browse the repository at this point in the history
)

* Use a Consul Kubernetes Auth Method to issue consul-login to mint ACL tokens and consul-logout to clean them up for the CRD controller.

Co-authored-by: Iryna Shustava <ishustava@users.noreply.github.com>
  • Loading branch information
2 people authored and jmurret committed Mar 16, 2022
1 parent 32d513d commit 906d826
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 34 deletions.
1 change: 0 additions & 1 deletion charts/consul/templates/connect-inject-clusterrole.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{{- if or (and (ne (.Values.connectInject.enabled | toString) "-") .Values.connectInject.enabled) (and (eq (.Values.connectInject.enabled | toString) "-") .Values.global.enabled) }}
# The ClusterRole to enable the Connect injector to get, list, watch and patch MutatingWebhookConfiguration.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{{- if or (and (ne (.Values.connectInject.enabled | toString) "-") .Values.connectInject.enabled) (and (eq (.Values.connectInject.enabled | toString) "-") .Values.global.enabled) }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
Expand All @@ -17,4 +16,4 @@ subjects:
- kind: ServiceAccount
name: {{ template "consul.fullname" . }}-connect-injector
namespace: {{ .Release.Namespace }}
{{- end }}
{{- end }}
2 changes: 0 additions & 2 deletions charts/consul/templates/connect-inject-serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{{- if or (and (ne (.Values.connectInject.enabled | toString) "-") .Values.connectInject.enabled) (and (eq (.Values.connectInject.enabled | toString) "-") .Values.global.enabled) }}
apiVersion: v1
kind: ServiceAccount
metadata:
Expand All @@ -20,4 +19,3 @@ imagePullSecrets:
- name: {{ .name }}
{{- end }}
{{- end }}
{{- end }}
27 changes: 0 additions & 27 deletions charts/consul/test/unit/connect-inject-clusterrole.bats
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,6 @@

load _helpers

@test "connectInject/ClusterRole: disabled by default" {
cd `chart_dir`
assert_empty helm template \
-s templates/connect-inject-clusterrole.yaml \
.
}

@test "connectInject/ClusterRole: enabled with global.enabled false" {
cd `chart_dir`
local actual=$(helm template \
-s templates/connect-inject-clusterrole.yaml \
--set 'global.enabled=false' \
--set 'client.enabled=true' \
--set 'connectInject.enabled=true' \
. | tee /dev/stderr |
yq -s 'length > 0' | tee /dev/stderr)
[ "${actual}" = "true" ]
}

@test "connectInject/ClusterRole: disabled with connectInject.enabled" {
cd `chart_dir`
assert_empty helm template \
-s templates/connect-inject-clusterrole.yaml \
--set 'connectInject.enabled=false' \
.
}

#--------------------------------------------------------------------
# global.enablePodSecurityPolicies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ load _helpers
-s templates/connect-inject-clusterrolebinding.yaml \
--set 'connectInject.enabled=false' \
.
}
}
7 changes: 7 additions & 0 deletions control-plane/subcommand/acl-init/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ type Command struct {
flagConsulCACert string
flagUseHTTPS bool

flagACLAuthMethod string // Auth Method to use for ACLs.
flagLogLevel string
flagLogJSON bool

bearerTokenFile string // Location of the bearer token. Default is defaultBearerTokenFile.
flagComponentName string // Name of the component to be used as metadata to ACL Login.

k8sClient kubernetes.Interface

once sync.Once
Expand Down
1 change: 0 additions & 1 deletion control-plane/subcommand/server-acl-init/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ func (c *Command) Run(args []string) int {
c.UI.Error(err.Error())
return 1
}

var aclReplicationToken string
if c.flagACLReplicationTokenFile != "" {
var err error
Expand Down
129 changes: 129 additions & 0 deletions control-plane/subcommand/server-acl-init/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,7 @@ func TestRun_AlreadyBootstrapped_ServerTokenExists(t *testing.T) {
func TestRun_SkipBootstrapping_WhenServersAreDisabled(t *testing.T) {
t.Parallel()
k8s := fake.NewSimpleClientset()
setUpK8sServiceAccount(t, k8s, ns)

bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
tokenFile := common.WriteTempFile(t, bootToken)
Expand Down Expand Up @@ -2901,6 +2902,134 @@ func replicatedSetup(t *testing.T, bootToken string) (*fake.Clientset, *api.Clie
}
}

// Test creating the correct ACL policies and Binding Rules for components that use the auth method.
// The test works by running the command and then ensuring that:
// * An ACLBindingRule exists which references the ACLRole.
// * An ACLRole exists and has the correct PolicyName in it's ACLPolicyLinkRule list.
// * The ACLPolicy exists.
func TestRun_PoliciesAndBindingRulesForACLLogin(t *testing.T) {
t.Parallel()

cases := []struct {
TestName string
TokenFlags []string
PolicyNames []string
Roles []string
}{
{
TestName: "Controller",
TokenFlags: []string{"-create-controller-token"},
PolicyNames: []string{"controller-policy"},
Roles: []string{resourcePrefix + "-controller-acl-role"},
},
}
for _, c := range cases {
t.Run(c.TestName, func(t *testing.T) {
k8s, testSvr := completeSetup(t)
defer testSvr.Stop()
setUpK8sServiceAccount(t, k8s, ns)

// Run the command.
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
clientset: k8s,
}
cmdArgs := append([]string{
"-timeout=500ms",
"-resource-prefix=" + resourcePrefix,
"-k8s-namespace=" + ns,
"-server-address", strings.Split(testSvr.HTTPAddr, ":")[0],
"-server-port", strings.Split(testSvr.HTTPAddr, ":")[1],
}, c.TokenFlags...)
cmd.init()
responseCode := cmd.Run(cmdArgs)
require.Equal(t, 0, responseCode, ui.ErrorWriter.String())

bootToken := getBootToken(t, k8s, resourcePrefix, ns)
consul, err := api.NewClient(&api.Config{
Address: testSvr.HTTPAddr,
Token: bootToken,
})
require.NoError(t, err)

// Check that the Role exists + has correct Policy and is associated with a BindingRule.
for i := range c.Roles {
// Check that the Policy exists.
policy, _, err := consul.ACL().PolicyReadByName(c.PolicyNames[i], &api.QueryOptions{})
require.NoError(t, err)
require.NotNil(t, policy)

// Check that the Role exists.
role, _, err := consul.ACL().RoleReadByName(c.Roles[i], &api.QueryOptions{})
require.NoError(t, err)
require.NotNil(t, role)

// Check that the Role references the Policy.
found := false
for x := range role.Policies {
if role.Policies[x].Name == policy.Name {
found = true
break
}
}
require.True(t, found)

// Check that there exists a BindingRule that references this Role.
rb, _, err := consul.ACL().BindingRuleList("release-name-"+componentAuthMethod, &api.QueryOptions{})
require.NoError(t, err)
require.NotNil(t, rb)
found = false
for x := range rb {
if rb[x].BindName == c.Roles[i] {
found = true
break
}
}
require.True(t, found)
}
})
}
}

// Test that the component auth method gets created.
func TestRun_ComponentAuthMethod(t *testing.T) {
t.Parallel()

k8s, testSvr := completeSetup(t)
setUpK8sServiceAccount(t, k8s, ns)
defer testSvr.Stop()
require := require.New(t)

// Run the command.
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
clientset: k8s,
}
cmd.init()
cmdArgs := []string{
"-timeout=1m",
"-k8s-namespace=" + ns,
"-server-address", strings.Split(testSvr.HTTPAddr, ":")[0],
"-server-port", strings.Split(testSvr.HTTPAddr, ":")[1],
"-resource-prefix=" + resourcePrefix}

responseCode := cmd.Run(cmdArgs)
require.Equal(0, responseCode, ui.ErrorWriter.String())

// Check that the expected policy was created.
bootToken := getBootToken(t, k8s, resourcePrefix, ns)
consulClient, err := api.NewClient(&api.Config{
Address: testSvr.HTTPAddr,
Token: bootToken,
})
require.NoError(err)
authMethod, _, err := consulClient.ACL().AuthMethodRead(resourcePrefix+"-k8s-component-auth-method", &api.QueryOptions{})
require.NoError(err)
require.NotNil(authMethod)
}

// getBootToken gets the bootstrap token from the Kubernetes secret. It will
// cause a test failure if the Secret doesn't exist or is malformed.
func getBootToken(t *testing.T, k8s *fake.Clientset, prefix string, k8sNamespace string) string {
Expand Down

0 comments on commit 906d826

Please sign in to comment.