Skip to content

Commit

Permalink
Merge pull request #230 from hashicorp/wan-federation-acl-anon-policy
Browse files Browse the repository at this point in the history
Configure anonymous token policy for connect
  • Loading branch information
lkysow authored Mar 19, 2020
2 parents 9cdda6c + 7fd044f commit ffdf97d
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 137 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 {
anonRules, 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: anonRules,
}

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
})
}
37 changes: 29 additions & 8 deletions subcommand/server-acl-init/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,10 @@ func (c *Command) Run(args []string) int {
c.UI.Error(fmt.Sprintf("Unable to read ACL replication token from file %q: %s", c.flagACLReplicationTokenFile, err))
return 1
}
if len(tokenBytes) == 0 {
c.UI.Error(fmt.Sprintf("ACL replication token file %q is empty", c.flagACLReplicationTokenFile))
return 1
}
aclReplicationToken = strings.TrimSpace(string(tokenBytes))
}

Expand Down Expand Up @@ -378,14 +382,8 @@ func (c *Command) Run(args []string) int {
}
}

// The DNS policy is attached to the anonymous token.
// 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.flagACLReplicationTokenFile == "" {
err := c.configureDNSPolicies(consulClient)
if c.createAnonymousPolicy() {
err := c.configureAnonymousPolicy(consulClient)
if err != nil {
c.Log.Error(err.Error())
return 1
Expand Down Expand Up @@ -581,3 +579,26 @@ func (c *Command) consulDatacenter(client *api.Client) (string, error) {
}
return dc, nil
}

// createAnonymousPolicy returns whether we should create a policy for the
// anonymous ACL token, i.e. queries without ACL tokens.
func (c *Command) createAnonymousPolicy() bool {
// If c.flagACLReplicationTokenFile is set then we're in a secondary DC.
// In this case we assume that the primary datacenter has already created
// the anonymous policy and attached it to the anonymous token.
// We don't want to modify the anonymous 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.
return c.flagACLReplicationTokenFile == "" &&
// Consul DNS requires the anonymous policy because DNS queries don't
// have ACL tokens.
(c.flagAllowDNS ||
// If the connect auth method and ACL replication token are being
// created then we know we're using multi-dc Connect.
// In this case the anonymous policy is required because Connect
// services in Kubernetes have local tokens which are stripped
// on cross-dc API calls. The cross-dc API calls thus use the anonymous
// token. Cross-dc API calls are needed by the Connect proxies to talk
// cross-dc.
(c.flagCreateInjectAuthMethod && c.flagCreateACLReplicationToken))
}
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
231 changes: 155 additions & 76 deletions subcommand/server-acl-init/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,56 +439,129 @@ func TestRun_TokensReplicatedDC(t *testing.T) {
}
}

func TestRun_AllowDNS(t *testing.T) {
// Test the conditions under which we should create the anonymous token
// policy.
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",
cases := map[string]struct {
Flags []string
SecondaryDC bool
ExpAnonymousPolicy bool
}{
"dns, primary dc": {
Flags: []string{"-allow-dns"},
SecondaryDC: false,
ExpAnonymousPolicy: true,
},
"dns, secondary dc": {
Flags: []string{"-allow-dns"},
SecondaryDC: true,
ExpAnonymousPolicy: false,
},
"auth method, primary dc, no replication": {
Flags: []string{"-create-inject-auth-method"},
SecondaryDC: false,
ExpAnonymousPolicy: false,
},
"auth method, primary dc, with replication": {
Flags: []string{"-create-inject-auth-method", "-create-acl-replication-token"},
SecondaryDC: false,
ExpAnonymousPolicy: true,
},
"auth method, secondary dc": {
Flags: []string{"-create-inject-auth-method"},
SecondaryDC: true,
ExpAnonymousPolicy: false,
},
}
responseCode := cmd.Run(cmdArgs)
require.Equal(0, responseCode, ui.ErrorWriter.String())
for name, c := range cases {
t.Run(name, func(t *testing.T) {
flags := c.Flags
var k8s *fake.Clientset
var consulHTTPAddr string
var consul *api.Client

if c.SecondaryDC {
var cleanup func()
bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
k8s, consul, cleanup = mockReplicatedSetup(t, resourcePrefix,
bootToken)
defer cleanup()

tmp, err := ioutil.TempFile("", "")
require.NoError(t, err)
_, err = tmp.WriteString(bootToken)
require.NoError(t, err)
flags = append(flags, "-acl-replication-token-file", tmp.Name())
} else {
var testSvr *testutil.TestServer
k8s, testSvr = completeSetup(t, resourcePrefix)
defer testSvr.Stop()
consulHTTPAddr = testSvr.HTTPAddr
}
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",
}, flags...)
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())
})
if !c.SecondaryDC {
bootToken := getBootToken(t, k8s, resourcePrefix, ns)
var err error
consul, err = api.NewClient(&api.Config{
Address: consulHTTPAddr,
Token: bootToken,
})
require.NoError(t, err)
}

anonPolicyName := "anonymous-token-policy"
if c.ExpAnonymousPolicy {
// Check that the anonymous token policy was created.
policy := policyExists(t, anonPolicyName, 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)
} else {
policies, _, err := consul.ACL().PolicyList(nil)
require.NoError(t, err)
for _, p := range policies {
if p.Name == anonPolicyName {
t.Error("anon policy was created")
}
}
}

// 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 @@ -1535,39 +1608,45 @@ func TestRun_ACLReplicationTokenValid(t *testing.T) {
})
}

// Test that if acl replication is enabled, we don't create a dns policy.
func TestRun_AllowDNSFlag_IgnoredWithReplication(t *testing.T) {
bootToken := "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
tokenFile, fileCleanup := writeTempFile(t, bootToken)
defer fileCleanup()
k8s, consul, cleanup := mockReplicatedSetup(t, resourcePrefix, bootToken)
defer cleanup()
// Test that if acl replication is enabled, we don't create an anonymous token policy.
func TestRun_AnonPolicy_IgnoredWithReplication(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 := mockReplicatedSetup(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",
"-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",
"-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 anonymous 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
Loading

0 comments on commit ffdf97d

Please sign in to comment.