Skip to content

Commit

Permalink
Configure anonymous token policy for connect
Browse files Browse the repository at this point in the history
When running Consul Connect, cross-dc calls require that the anonymous
token has read permissions on all services. This change updates the
server-acl-init command to give the anonymous token those permissions if
connect is enabled.

Since we already set those permissions in the case of dns being enabled,
the change was to also set those permissions in the case of connect
being enabled. To detect connect being enabled, we used the presence of
the -create-inject-auth-method flag since that's set when connect is
enabled.

The policy was renamed from dns-policy to anonymous-token-policy since
it applies for more than just dns now. In existing installations, a new
policy with that name will be created and attached to the anonymous
token that will duplicate the old dns-policy but will have no
detrimental effects.
  • Loading branch information
lkysow committed Mar 10, 2020
1 parent e60bd68 commit 2ee7dfe
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 134 deletions.
43 changes: 43 additions & 0 deletions subcommand/server-acl-init/anonymous_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package serveraclinit

import (
"github.com/hashicorp/consul/api"
)

// configureAnonymousPolicy sets up policies and tokens so that Consul DNS and
// cross-datacenter Consul connect calls will work.
func (c *Command) configureAnonymousPolicy(consulClient *api.Client) error {
dnsRules, err := c.anonymousTokenRules()
if err != nil {
c.Log.Error("Error templating anonymous token rules", "err", err)
return err
}

// Create policy for the anonymous token
anonPolicy := api.ACLPolicy{
Name: "anonymous-token-policy",
Description: "Anonymous token Policy",
Rules: dnsRules,
}

err = c.untilSucceeds("creating anonymous token policy - PUT /v1/acl/policy",
func() error {
return c.createOrUpdateACLPolicy(anonPolicy, consulClient)
})
if err != nil {
return err
}

// Create token to get sent to TokenUpdate
aToken := api.ACLToken{
AccessorID: "00000000-0000-0000-0000-000000000002",
Policies: []*api.ACLTokenPolicyLink{{Name: anonPolicy.Name}},
}

// Update anonymous token to include this policy
return c.untilSucceeds("updating anonymous token with policy",
func() error {
_, _, err := consulClient.ACL().TokenUpdate(&aToken, &api.WriteOptions{})
return err
})
}
7 changes: 4 additions & 3 deletions subcommand/server-acl-init/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,14 +386,15 @@ func (c *Command) Run(args []string) int {
}
}

// The DNS policy is attached to the anonymous token.
// The anonymous token policy needs to be configured if using Consul DNS
// or Consul Connect.
// If performing ACL replication, we assume that the primary datacenter
// has already created the DNS policy and attached it to the anonymous
// token. We don't want to modify the DNS policy in secondary datacenters
// because it is global and we can't create separate tokens for each
// secondary datacenter because the anonymous token is global.
if c.flagAllowDNS && !c.flagEnableACLReplication {
err := c.configureDNSPolicies(consulClient)
if (c.flagAllowDNS || c.flagCreateInjectAuthMethod) && !c.flagEnableACLReplication {
err := c.configureAnonymousPolicy(consulClient)
if err != nil {
c.Log.Error(err.Error())
return 1
Expand Down
4 changes: 2 additions & 2 deletions subcommand/server-acl-init/command_ent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ func TestRun_ACLPolicyUpdates(t *testing.T) {

// Check that the expected policies were created.
firstRunExpectedPolicies := []string{
"dns-policy",
"anonymous-token-policy",
"client-token",
"catalog-sync-token",
"connect-inject-token",
Expand Down Expand Up @@ -295,7 +295,7 @@ func TestRun_ACLPolicyUpdates(t *testing.T) {

// Check that the policies have all been updated.
secondRunExpectedPolicies := []string{
"dns-policy",
"anonymous-token-policy",
"client-token",
"catalog-sync-token",
"connect-inject-token",
Expand Down
168 changes: 90 additions & 78 deletions subcommand/server-acl-init/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,56 +444,61 @@ func TestRun_TokensReplicatedDC(t *testing.T) {
}
}

func TestRun_AllowDNS(t *testing.T) {
func TestRun_AnonymousTokenPolicy(t *testing.T) {
t.Parallel()
k8s, testSvr := completeSetup(t, resourcePrefix)
defer testSvr.Stop()
require := require.New(t)

// Run the command.
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
clientset: k8s,
}
cmd.init()
cmdArgs := []string{
"-server-label-selector=component=server,app=consul,release=" + releaseName,
"-resource-prefix=" + resourcePrefix,
"-k8s-namespace=" + ns,
"-expected-replicas=1",
"-allow-dns",
}
responseCode := cmd.Run(cmdArgs)
require.Equal(0, responseCode, ui.ErrorWriter.String())
cases := []string{"-allow-dns", "-create-inject-auth-method"}
for _, flag := range cases {
t.Run(flag, func(t *testing.T) {
k8s, testSvr := completeSetup(t, resourcePrefix)
defer testSvr.Stop()
setUpK8sServiceAccount(t, k8s)

// Check that the dns policy was created.
bootToken := getBootToken(t, k8s, resourcePrefix, ns)
consul, err := api.NewClient(&api.Config{
Address: testSvr.HTTPAddr,
Token: bootToken,
})
require.NoError(err)
policy := policyExists(t, "dns-policy", consul)
// Should be a global policy.
require.Len(policy.Datacenters, 0)
// Run the command.
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
clientset: k8s,
}
cmd.init()
cmdArgs := append([]string{
"-server-label-selector=component=server,app=consul,release=" + releaseName,
"-resource-prefix=" + resourcePrefix,
"-k8s-namespace=" + ns,
"-expected-replicas=1",
}, flag)
responseCode := cmd.Run(cmdArgs)
require.Equal(t, 0, responseCode, ui.ErrorWriter.String())

// Check that the anonymous token has the DNS policy.
tokenData, _, err := consul.ACL().TokenReadSelf(&api.QueryOptions{Token: "anonymous"})
require.NoError(err)
require.Equal("dns-policy", tokenData.Policies[0].Name)

// Test that if the same command is re-run it doesn't error.
t.Run("retried", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
clientset: k8s,
}
cmd.init()
responseCode := cmd.Run(cmdArgs)
require.Equal(0, responseCode, ui.ErrorWriter.String())
})
// Check that the anonymous token policy was created.
bootToken := getBootToken(t, k8s, resourcePrefix, ns)
consul, err := api.NewClient(&api.Config{
Address: testSvr.HTTPAddr,
Token: bootToken,
})
require.NoError(t, err)
policy := policyExists(t, "anonymous-token-policy", consul)
// Should be a global policy.
require.Len(t, policy.Datacenters, 0)

// Check that the anonymous token has the policy.
tokenData, _, err := consul.ACL().TokenReadSelf(&api.QueryOptions{Token: "anonymous"})
require.NoError(t, err)
require.Equal(t, "anonymous-token-policy", tokenData.Policies[0].Name)

// Test that if the same command is re-run it doesn't error.
t.Run("retried", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
clientset: k8s,
}
cmd.init()
responseCode := cmd.Run(cmdArgs)
require.Equal(t, 0, responseCode, ui.ErrorWriter.String())
})
})
}
}

func TestRun_ConnectInjectAuthMethod(t *testing.T) {
Expand Down Expand Up @@ -1601,40 +1606,47 @@ func TestRun_ACLReplicationTokenValid(t *testing.T) {
})
}

// Test that if acl replication is enabled, we don't create a dns policy.
func TestRun_DNSFlagIgnoredReplication(t *testing.T) {
bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
tokenFile, fileCleanup := writeTempFile(t, bootToken)
defer fileCleanup()
k8s, consul, cleanup := completeReplicatedSetup(t, resourcePrefix, bootToken)
defer cleanup()
// Test that if acl replication is enabled, we don't create an anonymous token policy.
func TestRun_AnonPolicyIgnoredReplication(t *testing.T) {
// The anonymous policy is configured when one of these flags is set.
cases := []string{"-allow-dns", "-create-inject-auth-method"}
for _, flag := range cases {
t.Run(flag, func(t *testing.T) {
bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
tokenFile, fileCleanup := writeTempFile(t, bootToken)
defer fileCleanup()
k8s, consul, cleanup := completeReplicatedSetup(t, resourcePrefix, bootToken)
setUpK8sServiceAccount(t, k8s)
defer cleanup()

// Run the command.
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
clientset: k8s,
}
cmd.init()
cmdArgs := []string{
"-k8s-namespace=" + ns,
"-expected-replicas=1",
"-enable-acl-replication",
"-acl-replication-token-file", tokenFile,
"-server-label-selector=component=server,app=consul,release=" + releaseName,
"-resource-prefix=" + resourcePrefix,
"-allow-dns",
}
responseCode := cmd.Run(cmdArgs)
require.Equal(t, 0, responseCode, ui.ErrorWriter.String())
// Run the command.
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
clientset: k8s,
}
cmd.init()
cmdArgs := append([]string{
"-k8s-namespace=" + ns,
"-expected-replicas=1",
"-enable-acl-replication",
"-acl-replication-token-file", tokenFile,
"-server-label-selector=component=server,app=consul,release=" + releaseName,
"-resource-prefix=" + resourcePrefix,
}, flag)
responseCode := cmd.Run(cmdArgs)
require.Equal(t, 0, responseCode, ui.ErrorWriter.String())

// The DNS policy should not have been created.
policies, _, err := consul.ACL().PolicyList(nil)
require.NoError(t, err)
for _, p := range policies {
if p.Name == "dns-policy" {
require.Fail(t, "dns-policy exists")
}
// The anonymout token policy should not have been created.
policies, _, err := consul.ACL().PolicyList(nil)
require.NoError(t, err)
for _, p := range policies {
if p.Name == "anonymous-token-policy" {
require.Fail(t, "anonymous-token-policy exists")
}
}

})
}
}

Expand Down
43 changes: 0 additions & 43 deletions subcommand/server-acl-init/dns.go

This file was deleted.

22 changes: 17 additions & 5 deletions subcommand/server-acl-init/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,22 @@ namespace_prefix "" {
return c.renderRules(agentRulesTpl)
}

func (c *Command) dnsRules() (string, error) {
// DNS rules need to have access to all namespaces
// to be able to resolve services in any namespace.
dnsRulesTpl := `
func (c *Command) anonymousTokenRules() (string, error) {
// For Consul DNS and cross-datacenter Consul Connect,
// the anonymous token needs to have read access to
// services in all namespaces.
// For Consul DNS this is needed because in a DNS request
// no token can be presented so the anonymous policy will
// be used and DNS needs to be able to resolve all services.
// For cross-dc Consul Connect, each Kubernetes pod has a
// local ACL token returned from the Kubernetes auth method.
// When making cross-dc requests, the sidecar proxies need read
// access to services in the other dc. When the API call
// to read cross-dc services is forwarded to the remote dc, the
// local ACL token is stripped and the request continues without
// ACL token. Thus the anonymous policy must
// allow reading all services.
anonTokenRulesTpl := `
{{- if .EnableNamespaces }}
namespace_prefix "" {
{{- end }}
Expand All @@ -72,7 +84,7 @@ namespace_prefix "" {
{{- end }}
`

return c.renderRules(dnsRulesTpl)
return c.renderRules(anonTokenRulesTpl)
}

// This assumes users are using the default name for the service, i.e.
Expand Down
6 changes: 3 additions & 3 deletions subcommand/server-acl-init/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ namespace_prefix "" {
}
}

func TestDNSRules(t *testing.T) {
func TestAnonymousTokenRules(t *testing.T) {
cases := []struct {
Name string
EnableNamespaces bool
Expand Down Expand Up @@ -92,10 +92,10 @@ namespace_prefix "" {
flagEnableNamespaces: tt.EnableNamespaces,
}

dnsRules, err := cmd.dnsRules()
rules, err := cmd.anonymousTokenRules()

require.NoError(err)
require.Equal(tt.Expected, dnsRules)
require.Equal(tt.Expected, rules)
})
}
}
Expand Down

0 comments on commit 2ee7dfe

Please sign in to comment.