Add default intention policy (#20544)

This commit is contained in:
Chris S. Kim 2024-02-08 15:25:42 -05:00 committed by GitHub
parent 7c3a379e48
commit 26661a1c3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 401 additions and 268 deletions

3
.changelog/20544.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
agent: Introduces a new agent config default_intention_policy to decouple the default intention behavior from ACLs
```

View File

@ -93,6 +93,10 @@ type Authorizer interface {
// IntentionDefaultAllow determines the default authorized behavior // IntentionDefaultAllow determines the default authorized behavior
// when no intentions match a Connect request. // when no intentions match a Connect request.
//
// Deprecated: Use DefaultIntentionPolicy under agent configuration.
// Moving forwards, intentions will not inherit default allow behavior
// from the ACL system.
IntentionDefaultAllow(*AuthorizerContext) EnforcementDecision IntentionDefaultAllow(*AuthorizerContext) EnforcementDecision
// IntentionRead determines if a specific intention can be read. // IntentionRead determines if a specific intention can be read.
@ -297,17 +301,6 @@ func (a AllowAuthorizer) IdentityWriteAnyAllowed(ctx *AuthorizerContext) error {
return nil return nil
} }
// IntentionDefaultAllowAllowed determines the default authorized behavior
// when no intentions match a Connect request.
func (a AllowAuthorizer) IntentionDefaultAllowAllowed(ctx *AuthorizerContext) error {
if a.Authorizer.IntentionDefaultAllow(ctx) != Allow {
// This is a bit nuanced, in that this isn't set by a rule, but inherited globally
// TODO(acl-error-enhancements) revisit when we have full accessor info
return PermissionDeniedError{Cause: "Denied by intention default"}
}
return nil
}
// IntentionReadAllowed determines if a specific intention can be read. // IntentionReadAllowed determines if a specific intention can be read.
func (a AllowAuthorizer) IntentionReadAllowed(name string, ctx *AuthorizerContext) error { func (a AllowAuthorizer) IntentionReadAllowed(name string, ctx *AuthorizerContext) error {
if a.Authorizer.IntentionRead(name, ctx) != Allow { if a.Authorizer.IntentionRead(name, ctx) != Allow {

View File

@ -113,6 +113,7 @@ func (c *ChainedAuthorizer) IdentityWriteAny(entCtx *AuthorizerContext) Enforcem
// when no intentions match a Connect request. // when no intentions match a Connect request.
func (c *ChainedAuthorizer) IntentionDefaultAllow(entCtx *AuthorizerContext) EnforcementDecision { func (c *ChainedAuthorizer) IntentionDefaultAllow(entCtx *AuthorizerContext) EnforcementDecision {
return c.executeChain(func(authz Authorizer) EnforcementDecision { return c.executeChain(func(authz Authorizer) EnforcementDecision {
//nolint:staticcheck
return authz.IntentionDefaultAllow(entCtx) return authz.IntentionDefaultAllow(entCtx)
}) })
} }

View File

@ -813,6 +813,15 @@ func (a *Agent) Start(ctx context.Context) error {
return fmt.Errorf("unexpected ACL default policy value of %q", a.config.ACLResolverSettings.ACLDefaultPolicy) return fmt.Errorf("unexpected ACL default policy value of %q", a.config.ACLResolverSettings.ACLDefaultPolicy)
} }
// If DefaultIntentionPolicy is defined, it should override
// the values inherited from ACLDefaultPolicy.
switch a.config.DefaultIntentionPolicy {
case "allow":
intentionDefaultAllow = true
case "deny":
intentionDefaultAllow = false
}
go a.baseDeps.ViewStore.Run(&lib.StopChannelContext{StopCh: a.shutdownCh}) go a.baseDeps.ViewStore.Run(&lib.StopChannelContext{StopCh: a.shutdownCh})
// Start the proxy config manager. // Start the proxy config manager.
@ -4747,8 +4756,8 @@ func (a *Agent) proxyDataSources(server *consul.Server) proxycfg.DataSources {
sources.Health = proxycfgglue.ServerHealthBlocking(deps, proxycfgglue.ClientHealth(a.rpcClientHealth)) sources.Health = proxycfgglue.ServerHealthBlocking(deps, proxycfgglue.ClientHealth(a.rpcClientHealth))
sources.HTTPChecks = proxycfgglue.ServerHTTPChecks(deps, a.config.NodeName, proxycfgglue.CacheHTTPChecks(a.cache), a.State) sources.HTTPChecks = proxycfgglue.ServerHTTPChecks(deps, a.config.NodeName, proxycfgglue.CacheHTTPChecks(a.cache), a.State)
sources.Intentions = proxycfgglue.ServerIntentions(deps) sources.Intentions = proxycfgglue.ServerIntentions(deps)
sources.IntentionUpstreams = proxycfgglue.ServerIntentionUpstreams(deps) sources.IntentionUpstreams = proxycfgglue.ServerIntentionUpstreams(deps, a.config.DefaultIntentionPolicy)
sources.IntentionUpstreamsDestination = proxycfgglue.ServerIntentionUpstreamsDestination(deps) sources.IntentionUpstreamsDestination = proxycfgglue.ServerIntentionUpstreamsDestination(deps, a.config.DefaultIntentionPolicy)
sources.InternalServiceDump = proxycfgglue.ServerInternalServiceDump(deps, proxycfgglue.CacheInternalServiceDump(a.cache)) sources.InternalServiceDump = proxycfgglue.ServerInternalServiceDump(deps, proxycfgglue.CacheInternalServiceDump(a.cache))
sources.PeeringList = proxycfgglue.ServerPeeringList(deps) sources.PeeringList = proxycfgglue.ServerPeeringList(deps)
sources.PeeredUpstreams = proxycfgglue.ServerPeeredUpstreams(deps) sources.PeeredUpstreams = proxycfgglue.ServerPeeredUpstreams(deps)

View File

@ -1760,8 +1760,12 @@ func (s *HTTPHandlers) AgentConnectAuthorize(resp http.ResponseWriter, req *http
// This is an L7 intention, so DENY. // This is an L7 intention, so DENY.
authorized = false authorized = false
} }
} else if s.agent.config.DefaultIntentionPolicy != "" {
reason = "Default intention policy"
authorized = s.agent.config.DefaultIntentionPolicy == structs.IntentionDefaultPolicyAllow
} else { } else {
reason = "Default behavior configured by ACLs" reason = "Default behavior configured by ACLs"
//nolint:staticcheck
authorized = authz.IntentionDefaultAllow(nil) == acl.Allow authorized = authz.IntentionDefaultAllow(nil) == acl.Allow
} }

View File

@ -8015,76 +8015,104 @@ func TestAgentConnectAuthorize_serviceWrite(t *testing.T) {
assert.Equal(t, http.StatusForbidden, resp.Code) assert.Equal(t, http.StatusForbidden, resp.Code)
} }
// Test when no intentions match w/ a default deny policy func TestAgentConnectAuthorize_DefaultIntentionPolicy(t *testing.T) {
func TestAgentConnectAuthorize_defaultDeny(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("too slow for testing.Short") t.Skip("too slow for testing.Short")
} }
t.Parallel() t.Parallel()
a := NewTestAgent(t, TestACLConfig()) agentConfig := `primary_datacenter = "dc1"
defer a.Shutdown() default_intention_policy = "%s"
testrpc.WaitForLeader(t, a.RPC, "dc1") `
aclBlock := `acl {
args := &structs.ConnectAuthorizeRequest{ enabled = true
Target: "foo", default_policy = "%s"
ClientCertURI: connect.TestSpiffeIDService(t, "web").URI().String(), tokens {
initial_management = "root"
agent = "root"
agent_recovery = "towel"
} }
req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args))
req.Header.Add("X-Consul-Token", "root")
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
assert.Equal(t, 200, resp.Code)
dec := json.NewDecoder(resp.Body)
obj := &connectAuthorizeResp{}
require.NoError(t, dec.Decode(obj))
assert.False(t, obj.Authorized)
assert.Contains(t, obj.Reason, "Default behavior")
} }
`
// Test when no intentions match w/ a default allow policy type testcase struct {
func TestAgentConnectAuthorize_defaultAllow(t *testing.T) { aclsEnabled bool
if testing.Short() { defaultACL string
t.Skip("too slow for testing.Short") defaultIxn string
expectAuthz bool
expectReason string
} }
tcs := map[string]testcase{
"no ACLs, default intention allow": {
aclsEnabled: false,
defaultIxn: "allow",
expectAuthz: true,
expectReason: "Default intention policy",
},
"no ACLs, default intention deny": {
aclsEnabled: false,
defaultIxn: "deny",
expectAuthz: false,
expectReason: "Default intention policy",
},
"ACL deny, no intention policy": {
aclsEnabled: true,
defaultACL: "deny",
expectAuthz: false,
expectReason: "Default behavior configured by ACLs",
},
"ACL allow, no intention policy": {
aclsEnabled: true,
defaultACL: "allow",
expectAuthz: true,
expectReason: "Default behavior configured by ACLs",
},
"ACL deny, default intentions allow": {
aclsEnabled: true,
defaultACL: "deny",
defaultIxn: "allow",
expectAuthz: true,
expectReason: "Default intention policy",
},
"ACL allow, default intentions deny": {
aclsEnabled: true,
defaultACL: "allow",
defaultIxn: "deny",
expectAuthz: false,
expectReason: "Default intention policy",
},
}
for name, tc := range tcs {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
t.Parallel() conf := fmt.Sprintf(agentConfig, tc.defaultIxn)
if tc.aclsEnabled {
dc1 := "dc1" conf += fmt.Sprintf(aclBlock, tc.defaultACL)
a := NewTestAgent(t, `
primary_datacenter = "`+dc1+`"
acl {
enabled = true
default_policy = "allow"
tokens {
initial_management = "root"
agent = "root"
agent_recovery = "towel"
} }
} a := NewTestAgent(t, conf)
`)
defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, dc1)
args := &structs.ConnectAuthorizeRequest{ testrpc.WaitForLeader(t, a.RPC, "dc1")
Target: "foo",
ClientCertURI: connect.TestSpiffeIDService(t, "web").URI().String(), args := &structs.ConnectAuthorizeRequest{
Target: "foo",
ClientCertURI: connect.TestSpiffeIDService(t, "web").URI().String(),
}
req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args))
req.Header.Add("X-Consul-Token", "root")
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
assert.Equal(t, 200, resp.Code)
dec := json.NewDecoder(resp.Body)
obj := &connectAuthorizeResp{}
require.NoError(t, dec.Decode(obj))
assert.Equal(t, tc.expectAuthz, obj.Authorized)
assert.Contains(t, obj.Reason, tc.expectReason)
})
} }
req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args))
req.Header.Add("X-Consul-Token", "root")
resp := httptest.NewRecorder()
a.srv.h.ServeHTTP(resp, req)
assert.Equal(t, 200, resp.Code)
dec := json.NewDecoder(resp.Body)
obj := &connectAuthorizeResp{}
require.NoError(t, dec.Decode(obj))
assert.True(t, obj.Authorized)
assert.Contains(t, obj.Reason, "Default behavior")
} }
func TestAgent_Host(t *testing.T) { func TestAgent_Host(t *testing.T) {

View File

@ -1000,6 +1000,7 @@ func (b *builder) build() (rt RuntimeConfig, err error) {
DataDir: dataDir, DataDir: dataDir,
Datacenter: datacenter, Datacenter: datacenter,
DefaultQueryTime: b.durationVal("default_query_time", c.DefaultQueryTime), DefaultQueryTime: b.durationVal("default_query_time", c.DefaultQueryTime),
DefaultIntentionPolicy: stringVal(c.DefaultIntentionPolicy),
DevMode: boolVal(b.opts.DevMode), DevMode: boolVal(b.opts.DevMode),
DisableAnonymousSignature: boolVal(c.DisableAnonymousSignature), DisableAnonymousSignature: boolVal(c.DisableAnonymousSignature),
DisableCoordinates: boolVal(c.DisableCoordinates), DisableCoordinates: boolVal(c.DisableCoordinates),

View File

@ -165,6 +165,7 @@ type Config struct {
DataDir *string `mapstructure:"data_dir" json:"data_dir,omitempty"` DataDir *string `mapstructure:"data_dir" json:"data_dir,omitempty"`
Datacenter *string `mapstructure:"datacenter" json:"datacenter,omitempty"` Datacenter *string `mapstructure:"datacenter" json:"datacenter,omitempty"`
DefaultQueryTime *string `mapstructure:"default_query_time" json:"default_query_time,omitempty"` DefaultQueryTime *string `mapstructure:"default_query_time" json:"default_query_time,omitempty"`
DefaultIntentionPolicy *string `mapstructure:"default_intention_policy" json:"default_intention_policy,omitempty"`
DisableAnonymousSignature *bool `mapstructure:"disable_anonymous_signature" json:"disable_anonymous_signature,omitempty"` DisableAnonymousSignature *bool `mapstructure:"disable_anonymous_signature" json:"disable_anonymous_signature,omitempty"`
DisableCoordinates *bool `mapstructure:"disable_coordinates" json:"disable_coordinates,omitempty"` DisableCoordinates *bool `mapstructure:"disable_coordinates" json:"disable_coordinates,omitempty"`
DisableHostNodeID *bool `mapstructure:"disable_host_node_id" json:"disable_host_node_id,omitempty"` DisableHostNodeID *bool `mapstructure:"disable_host_node_id" json:"disable_host_node_id,omitempty"`

View File

@ -564,6 +564,15 @@ type RuntimeConfig struct {
// flag: -data-dir string // flag: -data-dir string
DataDir string DataDir string
// DefaultIntentionPolicy is used to define a default intention action for all
// sources and destinations. Possible values are "allow", "deny", or "" (blank).
// For compatibility, falls back to ACLResolverSettings.ACLDefaultPolicy (which
// itself has a default of "allow") if left blank. Future versions of Consul
// will default this field to "deny" to be secure by default.
//
// hcl: default_intention_policy = string
DefaultIntentionPolicy string
// DefaultQueryTime is the amount of time a blocking query will wait before // DefaultQueryTime is the amount of time a blocking query will wait before
// Consul will force a response. This value can be overridden by the 'wait' // Consul will force a response. This value can be overridden by the 'wait'
// query parameter. // query parameter.

View File

@ -134,11 +134,11 @@
"ClientSecret": "hidden", "ClientSecret": "hidden",
"Hostname": "", "Hostname": "",
"ManagementToken": "hidden", "ManagementToken": "hidden",
"NodeID": "",
"NodeName": "",
"ResourceID": "cluster1", "ResourceID": "cluster1",
"ScadaAddress": "", "ScadaAddress": "",
"TLSConfig": null, "TLSConfig": null
"NodeID": "",
"NodeName": ""
}, },
"ConfigEntryBootstrap": [], "ConfigEntryBootstrap": [],
"ConnectCAConfig": {}, "ConnectCAConfig": {},
@ -185,6 +185,7 @@
"DNSUseCache": false, "DNSUseCache": false,
"DataDir": "", "DataDir": "",
"Datacenter": "", "Datacenter": "",
"DefaultIntentionPolicy": "",
"DefaultQueryTime": "0s", "DefaultQueryTime": "0s",
"DevMode": false, "DevMode": false,
"DisableAnonymousSignature": false, "DisableAnonymousSignature": false,

View File

@ -221,6 +221,23 @@ type ACLResolverSettings struct {
ACLDefaultPolicy string ACLDefaultPolicy string
} }
func (a ACLResolverSettings) CheckACLs() error {
switch a.ACLDefaultPolicy {
case "allow":
case "deny":
default:
return fmt.Errorf("Unsupported default ACL policy: %s", a.ACLDefaultPolicy)
}
switch a.ACLDownPolicy {
case "allow":
case "deny":
case "async-cache", "extend-cache":
default:
return fmt.Errorf("Unsupported down ACL policy: %s", a.ACLDownPolicy)
}
return nil
}
func (s ACLResolverSettings) IsDefaultAllow() (bool, error) { func (s ACLResolverSettings) IsDefaultAllow() (bool, error) {
switch s.ACLDefaultPolicy { switch s.ACLDefaultPolicy {
case "allow": case "allow":

View File

@ -107,7 +107,7 @@ func NewClient(config *Config, deps Deps) (*Client, error) {
if config.DataDir == "" { if config.DataDir == "" {
return nil, fmt.Errorf("Config must provide a DataDir") return nil, fmt.Errorf("Config must provide a DataDir")
} }
if err := config.CheckACL(); err != nil { if err := config.CheckEnumStrings(); err != nil {
return nil, err return nil, err
} }

View File

@ -411,6 +411,13 @@ type Config struct {
// datacenters should exclusively traverse mesh gateways. // datacenters should exclusively traverse mesh gateways.
ConnectMeshGatewayWANFederationEnabled bool ConnectMeshGatewayWANFederationEnabled bool
// DefaultIntentionPolicy is used to define a default intention action for all
// sources and destinations. Possible values are "allow", "deny", or "" (blank).
// For compatibility, falls back to ACLResolverSettings.ACLDefaultPolicy (which
// itself has a default of "allow") if left blank. Future versions of Consul
// will default this field to "deny" to be secure by default.
DefaultIntentionPolicy string
// DisableFederationStateAntiEntropy solely exists for use in unit tests to // DisableFederationStateAntiEntropy solely exists for use in unit tests to
// disable a background routine. // disable a background routine.
DisableFederationStateAntiEntropy bool DisableFederationStateAntiEntropy bool
@ -470,21 +477,17 @@ func (c *Config) CheckProtocolVersion() error {
return nil return nil
} }
// CheckACL validates the ACL configuration. // CheckEnumStrings validates string configuration which must be specific values.
// TODO: move this to ACLResolverSettings func (c *Config) CheckEnumStrings() error {
func (c *Config) CheckACL() error { if err := c.ACLResolverSettings.CheckACLs(); err != nil {
switch c.ACLResolverSettings.ACLDefaultPolicy { return err
case "allow":
case "deny":
default:
return fmt.Errorf("Unsupported default ACL policy: %s", c.ACLResolverSettings.ACLDefaultPolicy)
} }
switch c.ACLResolverSettings.ACLDownPolicy { switch c.DefaultIntentionPolicy {
case "allow": case structs.IntentionDefaultPolicyAllow:
case "deny": case structs.IntentionDefaultPolicyDeny:
case "async-cache", "extend-cache": case "":
default: default:
return fmt.Errorf("Unsupported down ACL policy: %s", c.ACLResolverSettings.ACLDownPolicy) return fmt.Errorf("Unsupported default intention policy: %s", c.DefaultIntentionPolicy)
} }
return nil return nil
} }

View File

@ -752,19 +752,7 @@ func (s *Intention) Check(args *structs.IntentionQueryRequest, reply *structs.In
} }
} }
// Note: the default intention policy is like an intention with a defaultAllow := DefaultIntentionAllow(authz, s.srv.config.DefaultIntentionPolicy)
// wildcarded destination in that it is limited to L4-only.
// No match, we need to determine the default behavior. We do this by
// fetching the default intention behavior from the resolved authorizer.
// The default behavior if ACLs are disabled is to allow connections
// to mimic the behavior of Consul itself: everything is allowed if
// ACLs are disabled.
//
// NOTE(mitchellh): This is the same behavior as the agent authorize
// endpoint. If this behavior is incorrect, we should also change it there
// which is much more important.
defaultDecision := authz.IntentionDefaultAllow(nil)
store := s.srv.fsm.State() store := s.srv.fsm.State()
@ -784,7 +772,7 @@ func (s *Intention) Check(args *structs.IntentionQueryRequest, reply *structs.In
Partition: query.DestinationPartition, Partition: query.DestinationPartition,
Intentions: intentions, Intentions: intentions,
MatchType: structs.IntentionMatchDestination, MatchType: structs.IntentionMatchDestination,
DefaultDecision: defaultDecision, DefaultAllow: defaultAllow,
AllowPermissions: false, AllowPermissions: false,
} }
decision, err := store.IntentionDecision(opts) decision, err := store.IntentionDecision(opts)

View File

@ -1960,106 +1960,89 @@ func TestIntentionMatch_acl(t *testing.T) {
} }
} }
// Test the Check method defaults to allow with no ACL set. func TestIntentionCheck(t *testing.T) {
func TestIntentionCheck_defaultNoACL(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("too slow for testing.Short") t.Skip("too slow for testing.Short")
} }
t.Parallel() t.Parallel()
dir1, s1 := testServer(t) type testcase struct {
defer os.RemoveAll(dir1) aclsEnabled bool
defer s1.Shutdown() defaultACL string
codec := rpcClient(t, s1) defaultIxn string
defer codec.Close() expectAllowed bool
}
waitForLeaderEstablishment(t, s1) tcs := map[string]testcase{
"acls disabled, no default intention policy": {
// Test aclsEnabled: false,
req := &structs.IntentionQueryRequest{ expectAllowed: true,
Datacenter: "dc1", },
Check: &structs.IntentionQueryCheck{ "acls disabled, default intention allow": {
SourceName: "bar", aclsEnabled: false,
DestinationName: "qux", defaultIxn: "allow",
SourceType: structs.IntentionSourceConsul, expectAllowed: true,
},
"acls disabled, default intention deny": {
aclsEnabled: false,
defaultIxn: "deny",
expectAllowed: false,
},
"acls deny, no default intention policy": {
aclsEnabled: true,
defaultACL: "deny",
expectAllowed: false,
},
"acls allow, no default intention policy": {
aclsEnabled: true,
defaultACL: "allow",
expectAllowed: true,
},
"acls deny, default intention allow": {
aclsEnabled: true,
defaultACL: "deny",
defaultIxn: "allow",
expectAllowed: true,
},
"acls allow, default intention deny": {
aclsEnabled: true,
defaultACL: "allow",
defaultIxn: "deny",
expectAllowed: false,
}, },
} }
var resp structs.IntentionQueryCheckResponse for name, tc := range tcs {
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp)) tc := tc
require.True(t, resp.Allowed) t.Run(name, func(t *testing.T) {
} t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
if tc.aclsEnabled {
c.PrimaryDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLInitialManagementToken = "root"
c.ACLResolverSettings.ACLDefaultPolicy = tc.defaultACL
}
c.DefaultIntentionPolicy = tc.defaultIxn
})
codec := rpcClient(t, s1)
// Test the Check method defaults to deny with allowlist ACLs. waitForLeaderEstablishment(t, s1)
func TestIntentionCheck_defaultACLDeny(t *testing.T) {
if testing.Short() { req := &structs.IntentionQueryRequest{
t.Skip("too slow for testing.Short") Datacenter: "dc1",
Check: &structs.IntentionQueryCheck{
SourceName: "bar",
DestinationName: "qux",
SourceType: structs.IntentionSourceConsul,
},
}
req.Token = "root"
var resp structs.IntentionQueryCheckResponse
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
require.Equal(t, tc.expectAllowed, resp.Allowed)
})
} }
t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.PrimaryDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLInitialManagementToken = "root"
c.ACLResolverSettings.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
waitForLeaderEstablishment(t, s1)
// Check
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Check: &structs.IntentionQueryCheck{
SourceName: "bar",
DestinationName: "qux",
SourceType: structs.IntentionSourceConsul,
},
}
req.Token = "root"
var resp structs.IntentionQueryCheckResponse
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
require.False(t, resp.Allowed)
}
// Test the Check method defaults to deny with denylist ACLs.
func TestIntentionCheck_defaultACLAllow(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.PrimaryDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLInitialManagementToken = "root"
c.ACLResolverSettings.ACLDefaultPolicy = "allow"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
waitForLeaderEstablishment(t, s1)
// Check
req := &structs.IntentionQueryRequest{
Datacenter: "dc1",
Check: &structs.IntentionQueryCheck{
SourceName: "bar",
DestinationName: "qux",
SourceType: structs.IntentionSourceConsul,
},
}
req.Token = "root"
var resp structs.IntentionQueryCheckResponse
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Check", req, &resp))
require.True(t, resp.Allowed)
} }
// Test the Check method requires service:read permission. // Test the Check method requires service:read permission.

View File

@ -306,7 +306,7 @@ func (m *Internal) ServiceTopology(args *structs.ServiceSpecificRequest, reply *
&args.QueryOptions, &args.QueryOptions,
&reply.QueryMeta, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error { func(ws memdb.WatchSet, state *state.Store) error {
defaultAllow := authz.IntentionDefaultAllow(nil) defaultAllow := DefaultIntentionAllow(authz, m.srv.config.DefaultIntentionPolicy)
index, topology, err := state.ServiceTopology(ws, args.Datacenter, args.ServiceName, args.ServiceKind, defaultAllow, &args.EnterpriseMeta) index, topology, err := state.ServiceTopology(ws, args.Datacenter, args.ServiceName, args.ServiceKind, defaultAllow, &args.EnterpriseMeta)
if err != nil { if err != nil {
@ -375,10 +375,10 @@ func (m *Internal) internalUpstreams(args *structs.ServiceSpecificRequest, reply
&args.QueryOptions, &args.QueryOptions,
&reply.QueryMeta, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error { func(ws memdb.WatchSet, state *state.Store) error {
defaultDecision := authz.IntentionDefaultAllow(nil) defaultAllow := DefaultIntentionAllow(authz, m.srv.config.DefaultIntentionPolicy)
sn := structs.NewServiceName(args.ServiceName, &args.EnterpriseMeta) sn := structs.NewServiceName(args.ServiceName, &args.EnterpriseMeta)
index, services, err := state.IntentionTopology(ws, sn, false, defaultDecision, intentionTarget) index, services, err := state.IntentionTopology(ws, sn, false, defaultAllow, intentionTarget)
if err != nil { if err != nil {
return err return err
} }

View File

@ -2384,14 +2384,12 @@ func TestInternal_ServiceTopology_ACL(t *testing.T) {
} }
t.Parallel() t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) { _, s1 := testServerWithConfig(t, func(c *Config) {
c.PrimaryDatacenter = "dc1" c.PrimaryDatacenter = "dc1"
c.ACLsEnabled = true c.ACLsEnabled = true
c.ACLInitialManagementToken = TestDefaultInitialManagementToken c.ACLInitialManagementToken = TestDefaultInitialManagementToken
c.ACLResolverSettings.ACLDefaultPolicy = "deny" c.ACLResolverSettings.ACLDefaultPolicy = "deny"
}) })
defer os.RemoveAll(dir1)
defer s1.Shutdown()
testrpc.WaitForLeader(t, s1.RPC, "dc1") testrpc.WaitForLeader(t, s1.RPC, "dc1")
@ -2473,6 +2471,40 @@ service "web" { policy = "read" }
}) })
} }
// Tests that default intention deny policy overrides the ACL allow policy.
// More comprehensive tests are done at the state store so this is minimal
// coverage to be confident that the override happens.
func TestInternal_ServiceTopology_DefaultIntentionPolicy(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, s1 := testServerWithConfig(t, func(c *Config) {
c.PrimaryDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLInitialManagementToken = TestDefaultInitialManagementToken
c.ACLResolverSettings.ACLDefaultPolicy = "allow"
c.DefaultIntentionPolicy = "deny"
})
testrpc.WaitForLeader(t, s1.RPC, "dc1")
codec := rpcClient(t, s1)
registerTestTopologyEntries(t, codec, TestDefaultInitialManagementToken)
args := structs.ServiceSpecificRequest{
Datacenter: "dc1",
ServiceName: "redis",
QueryOptions: structs.QueryOptions{Token: TestDefaultInitialManagementToken},
}
var out structs.IndexedServiceTopology
require.NoError(t, msgpackrpc.CallWithCodec(codec, "Internal.ServiceTopology", &args, &out))
webSN := structs.NewServiceName("web", acl.DefaultEnterpriseMeta())
require.False(t, out.ServiceTopology.DownstreamDecisions[webSN.String()].DefaultAllow)
}
func TestInternal_IntentionUpstreams(t *testing.T) { func TestInternal_IntentionUpstreams(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("too slow for testing.Short") t.Skip("too slow for testing.Short")

View File

@ -526,7 +526,7 @@ func NewServer(config *Config, flat Deps, externalGRPCServer *grpc.Server,
if config.DataDir == "" && !config.DevMode { if config.DataDir == "" && !config.DevMode {
return nil, fmt.Errorf("Config must provide a DataDir") return nil, fmt.Errorf("Config must provide a DataDir")
} }
if err := config.CheckACL(); err != nil { if err := config.CheckEnumStrings(); err != nil {
return nil, err return nil, err
} }

View File

@ -4315,7 +4315,7 @@ func (s *Store) ServiceTopology(
ws memdb.WatchSet, ws memdb.WatchSet,
dc, service string, dc, service string,
kind structs.ServiceKind, kind structs.ServiceKind,
defaultAllow acl.EnforcementDecision, defaultAllow bool,
entMeta *acl.EnterpriseMeta, entMeta *acl.EnterpriseMeta,
) (uint64, *structs.ServiceTopology, error) { ) (uint64, *structs.ServiceTopology, error) {
tx := s.db.ReadTxn() tx := s.db.ReadTxn()
@ -4466,7 +4466,7 @@ func (s *Store) ServiceTopology(
Partition: un.PartitionOrDefault(), Partition: un.PartitionOrDefault(),
Intentions: srcIntentions, Intentions: srcIntentions,
MatchType: structs.IntentionMatchDestination, MatchType: structs.IntentionMatchDestination,
DefaultDecision: defaultAllow, DefaultAllow: defaultAllow,
AllowPermissions: false, AllowPermissions: false,
} }
decision, err := s.IntentionDecision(opts) decision, err := s.IntentionDecision(opts)
@ -4590,7 +4590,7 @@ func (s *Store) ServiceTopology(
Partition: dn.PartitionOrDefault(), Partition: dn.PartitionOrDefault(),
Intentions: dstIntentions, Intentions: dstIntentions,
MatchType: structs.IntentionMatchSource, MatchType: structs.IntentionMatchSource,
DefaultDecision: defaultAllow, DefaultAllow: defaultAllow,
AllowPermissions: false, AllowPermissions: false,
} }
decision, err := s.IntentionDecision(opts) decision, err := s.IntentionDecision(opts)

View File

@ -743,7 +743,7 @@ type IntentionDecisionOpts struct {
Peer string Peer string
Intentions structs.SimplifiedIntentions Intentions structs.SimplifiedIntentions
MatchType structs.IntentionMatchType MatchType structs.IntentionMatchType
DefaultDecision acl.EnforcementDecision DefaultAllow bool
AllowPermissions bool AllowPermissions bool
} }
@ -763,7 +763,7 @@ func (s *Store) IntentionDecision(opts IntentionDecisionOpts) (structs.Intention
} }
resp := structs.IntentionDecisionSummary{ resp := structs.IntentionDecisionSummary{
DefaultAllow: opts.DefaultDecision == acl.Allow, DefaultAllow: opts.DefaultAllow,
} }
if ixnMatch == nil { if ixnMatch == nil {
// No intention found, fall back to default // No intention found, fall back to default
@ -1029,13 +1029,13 @@ func (s *Store) IntentionTopology(
ws memdb.WatchSet, ws memdb.WatchSet,
target structs.ServiceName, target structs.ServiceName,
downstreams bool, downstreams bool,
defaultDecision acl.EnforcementDecision, defaultAllow bool,
intentionTarget structs.IntentionTargetType, intentionTarget structs.IntentionTargetType,
) (uint64, structs.ServiceList, error) { ) (uint64, structs.ServiceList, error) {
tx := s.db.ReadTxn() tx := s.db.ReadTxn()
defer tx.Abort() defer tx.Abort()
idx, services, err := s.intentionTopologyTxn(tx, ws, target, downstreams, defaultDecision, intentionTarget) idx, services, err := s.intentionTopologyTxn(tx, ws, target, downstreams, defaultAllow, intentionTarget)
if err != nil { if err != nil {
requested := "upstreams" requested := "upstreams"
if downstreams { if downstreams {
@ -1055,7 +1055,7 @@ func (s *Store) intentionTopologyTxn(
tx ReadTxn, ws memdb.WatchSet, tx ReadTxn, ws memdb.WatchSet,
target structs.ServiceName, target structs.ServiceName,
downstreams bool, downstreams bool,
defaultDecision acl.EnforcementDecision, defaultAllow bool,
intentionTarget structs.IntentionTargetType, intentionTarget structs.IntentionTargetType,
) (uint64, []ServiceWithDecision, error) { ) (uint64, []ServiceWithDecision, error) {
@ -1163,7 +1163,7 @@ func (s *Store) intentionTopologyTxn(
Partition: candidate.PartitionOrDefault(), Partition: candidate.PartitionOrDefault(),
Intentions: intentions, Intentions: intentions,
MatchType: decisionMatchType, MatchType: decisionMatchType,
DefaultDecision: defaultDecision, DefaultAllow: defaultAllow,
AllowPermissions: true, AllowPermissions: true,
} }
decision, err := s.IntentionDecision(opts) decision, err := s.IntentionDecision(opts)

View File

@ -1966,27 +1966,27 @@ func TestStore_IntentionDecision(t *testing.T) {
src string src string
dst string dst string
matchType structs.IntentionMatchType matchType structs.IntentionMatchType
defaultDecision acl.EnforcementDecision defaultAllow bool
allowPermissions bool allowPermissions bool
expect structs.IntentionDecisionSummary expect structs.IntentionDecisionSummary
}{ }{
{ {
name: "no matching intention and default deny", name: "no matching intention and default deny",
src: "does-not-exist", src: "does-not-exist",
dst: "ditto", dst: "ditto",
matchType: structs.IntentionMatchDestination, matchType: structs.IntentionMatchDestination,
defaultDecision: acl.Deny, defaultAllow: false,
expect: structs.IntentionDecisionSummary{ expect: structs.IntentionDecisionSummary{
Allowed: false, Allowed: false,
DefaultAllow: false, DefaultAllow: false,
}, },
}, },
{ {
name: "no matching intention and default allow", name: "no matching intention and default allow",
src: "does-not-exist", src: "does-not-exist",
dst: "ditto", dst: "ditto",
matchType: structs.IntentionMatchDestination, matchType: structs.IntentionMatchDestination,
defaultDecision: acl.Allow, defaultAllow: true,
expect: structs.IntentionDecisionSummary{ expect: structs.IntentionDecisionSummary{
Allowed: true, Allowed: true,
DefaultAllow: true, DefaultAllow: true,
@ -2079,7 +2079,7 @@ func TestStore_IntentionDecision(t *testing.T) {
Partition: acl.DefaultPartitionName, Partition: acl.DefaultPartitionName,
Intentions: intentions, Intentions: intentions,
MatchType: tc.matchType, MatchType: tc.matchType,
DefaultDecision: tc.defaultDecision, DefaultAllow: tc.defaultAllow,
AllowPermissions: tc.allowPermissions, AllowPermissions: tc.allowPermissions,
} }
decision, err := s.IntentionDecision(opts) decision, err := s.IntentionDecision(opts)
@ -2161,7 +2161,7 @@ func TestStore_IntentionTopology(t *testing.T) {
} }
tests := []struct { tests := []struct {
name string name string
defaultDecision acl.EnforcementDecision defaultAllow bool
intentions []structs.ServiceIntentionsConfigEntry intentions []structs.ServiceIntentionsConfigEntry
discoveryChains []structs.ConfigEntry discoveryChains []structs.ConfigEntry
target structs.ServiceName target structs.ServiceName
@ -2169,8 +2169,8 @@ func TestStore_IntentionTopology(t *testing.T) {
expect expect expect expect
}{ }{
{ {
name: "(upstream) acl allow all but intentions deny one", name: "(upstream) default allow all but intentions deny one",
defaultDecision: acl.Allow, defaultAllow: true,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2196,8 +2196,8 @@ func TestStore_IntentionTopology(t *testing.T) {
}, },
}, },
{ {
name: "(upstream) acl allow includes virtual service", name: "(upstream) default allow includes virtual service",
defaultDecision: acl.Allow, defaultAllow: true,
discoveryChains: []structs.ConfigEntry{ discoveryChains: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{ &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver, Kind: structs.ServiceResolver,
@ -2225,8 +2225,8 @@ func TestStore_IntentionTopology(t *testing.T) {
}, },
}, },
{ {
name: "(upstream) acl deny all intentions allow virtual service", name: "(upstream) default deny intentions allow virtual service",
defaultDecision: acl.Deny, defaultAllow: false,
discoveryChains: []structs.ConfigEntry{ discoveryChains: []structs.ConfigEntry{
&structs.ServiceResolverConfigEntry{ &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver, Kind: structs.ServiceResolver,
@ -2258,8 +2258,8 @@ func TestStore_IntentionTopology(t *testing.T) {
}, },
}, },
{ {
name: "(upstream) acl deny all intentions allow one", name: "(upstream) default deny intentions allow one",
defaultDecision: acl.Deny, defaultAllow: false,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2285,8 +2285,8 @@ func TestStore_IntentionTopology(t *testing.T) {
}, },
}, },
{ {
name: "(downstream) acl allow all but intentions deny one", name: "(downstream) default allow but intentions deny one",
defaultDecision: acl.Allow, defaultAllow: true,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2316,8 +2316,8 @@ func TestStore_IntentionTopology(t *testing.T) {
}, },
}, },
{ {
name: "(downstream) acl deny all intentions allow one", name: "(downstream) default deny all intentions allow one",
defaultDecision: acl.Deny, defaultAllow: false,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2343,8 +2343,8 @@ func TestStore_IntentionTopology(t *testing.T) {
}, },
}, },
{ {
name: "acl deny but intention allow all overrides it", name: "default deny but intention allow all overrides it",
defaultDecision: acl.Deny, defaultAllow: false,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2374,8 +2374,8 @@ func TestStore_IntentionTopology(t *testing.T) {
}, },
}, },
{ {
name: "acl allow but intention deny all overrides it", name: "default allow but intention deny all overrides it",
defaultDecision: acl.Allow, defaultAllow: true,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2396,8 +2396,8 @@ func TestStore_IntentionTopology(t *testing.T) {
}, },
}, },
{ {
name: "acl deny but intention allow all overrides it", name: "default deny but intention allow all overrides it",
defaultDecision: acl.Deny, defaultAllow: false,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2448,7 +2448,7 @@ func TestStore_IntentionTopology(t *testing.T) {
idx++ idx++
} }
idx, got, err := s.IntentionTopology(nil, tt.target, tt.downstreams, tt.defaultDecision, structs.IntentionTargetService) idx, got, err := s.IntentionTopology(nil, tt.target, tt.downstreams, tt.defaultAllow, structs.IntentionTargetService)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.expect.idx, idx) require.Equal(t, tt.expect.idx, idx)
@ -2502,16 +2502,16 @@ func TestStore_IntentionTopology_Destination(t *testing.T) {
services structs.ServiceList services structs.ServiceList
} }
tests := []struct { tests := []struct {
name string name string
defaultDecision acl.EnforcementDecision defaultAllow bool
intentions []structs.ServiceIntentionsConfigEntry intentions []structs.ServiceIntentionsConfigEntry
target structs.ServiceName target structs.ServiceName
downstreams bool downstreams bool
expect expect expect expect
}{ }{
{ {
name: "(upstream) acl allow all but intentions deny one, destination target", name: "(upstream) default allow all but intentions deny one, destination target",
defaultDecision: acl.Allow, defaultAllow: true,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2537,8 +2537,8 @@ func TestStore_IntentionTopology_Destination(t *testing.T) {
}, },
}, },
{ {
name: "(upstream) acl deny all intentions allow one, destination target", name: "(upstream) default deny intentions allow one, destination target",
defaultDecision: acl.Deny, defaultAllow: false,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2564,8 +2564,8 @@ func TestStore_IntentionTopology_Destination(t *testing.T) {
}, },
}, },
{ {
name: "(upstream) acl deny all check only destinations show, service target", name: "(upstream) default deny check only destinations show, service target",
defaultDecision: acl.Deny, defaultAllow: false,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2586,8 +2586,8 @@ func TestStore_IntentionTopology_Destination(t *testing.T) {
}, },
}, },
{ {
name: "(upstream) acl allow all check only destinations show, service target", name: "(upstream) default allow check only destinations show, service target",
defaultDecision: acl.Allow, defaultAllow: true,
intentions: []structs.ServiceIntentionsConfigEntry{ intentions: []structs.ServiceIntentionsConfigEntry{
{ {
Kind: structs.ServiceIntentions, Kind: structs.ServiceIntentions,
@ -2638,7 +2638,7 @@ func TestStore_IntentionTopology_Destination(t *testing.T) {
idx++ idx++
} }
idx, got, err := s.IntentionTopology(nil, tt.target, tt.downstreams, tt.defaultDecision, structs.IntentionTargetDestination) idx, got, err := s.IntentionTopology(nil, tt.target, tt.downstreams, tt.defaultAllow, structs.IntentionTargetDestination)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.expect.idx, idx) require.Equal(t, tt.expect.idx, idx)
@ -2665,7 +2665,7 @@ func TestStore_IntentionTopology_Watches(t *testing.T) {
target := structs.NewServiceName("web", structs.DefaultEnterpriseMetaInDefaultPartition()) target := structs.NewServiceName("web", structs.DefaultEnterpriseMetaInDefaultPartition())
ws := memdb.NewWatchSet() ws := memdb.NewWatchSet()
index, got, err := s.IntentionTopology(ws, target, false, acl.Deny, structs.IntentionTargetService) index, got, err := s.IntentionTopology(ws, target, false, false, structs.IntentionTargetService)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(0), index) require.Equal(t, uint64(0), index)
require.Empty(t, got) require.Empty(t, got)
@ -2687,7 +2687,7 @@ func TestStore_IntentionTopology_Watches(t *testing.T) {
// Reset the WatchSet // Reset the WatchSet
ws = memdb.NewWatchSet() ws = memdb.NewWatchSet()
index, got, err = s.IntentionTopology(ws, target, false, acl.Deny, structs.IntentionTargetService) index, got, err = s.IntentionTopology(ws, target, false, false, structs.IntentionTargetService)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(2), index) require.Equal(t, uint64(2), index)
// Because API is a virtual service, it is included in this output. // Because API is a virtual service, it is included in this output.
@ -2709,7 +2709,7 @@ func TestStore_IntentionTopology_Watches(t *testing.T) {
// require.False(t, watchFired(ws)) // require.False(t, watchFired(ws))
// Result should not have changed // Result should not have changed
index, got, err = s.IntentionTopology(ws, target, false, acl.Deny, structs.IntentionTargetService) index, got, err = s.IntentionTopology(ws, target, false, false, structs.IntentionTargetService)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(3), index) require.Equal(t, uint64(3), index)
require.Equal(t, structs.ServiceList{structs.NewServiceName("api", nil)}, got) require.Equal(t, structs.ServiceList{structs.NewServiceName("api", nil)}, got)
@ -2724,7 +2724,7 @@ func TestStore_IntentionTopology_Watches(t *testing.T) {
require.True(t, watchFired(ws)) require.True(t, watchFired(ws))
// Reset the WatchSet // Reset the WatchSet
index, got, err = s.IntentionTopology(nil, target, false, acl.Deny, structs.IntentionTargetService) index, got, err = s.IntentionTopology(nil, target, false, false, structs.IntentionTargetService)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(4), index) require.Equal(t, uint64(4), index)

View File

@ -11,7 +11,9 @@ import (
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
"github.com/hashicorp/serf/serf" "github.com/hashicorp/serf/serf"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/metadata" "github.com/hashicorp/consul/agent/metadata"
"github.com/hashicorp/consul/agent/structs"
) )
// CanServersUnderstandProtocol checks to see if all the servers in the given // CanServersUnderstandProtocol checks to see if all the servers in the given
@ -171,3 +173,14 @@ func isSerfMember(s *serf.Serf, nodeName string) bool {
} }
return false return false
} }
func DefaultIntentionAllow(authz acl.Authorizer, defaultIntentionPolicy string) bool {
// The default intention policy inherits from ACLs but
// is overridden by the agent's DefaultIntentionPolicy.
//nolint:staticcheck
defaultAllow := authz.IntentionDefaultAllow(nil) == acl.Allow
if defaultIntentionPolicy != "" {
defaultAllow = defaultIntentionPolicy == structs.IntentionDefaultPolicyAllow
}
return defaultAllow
}

View File

@ -43,7 +43,7 @@ type Store interface {
FederationStateList(ws memdb.WatchSet) (uint64, []*structs.FederationState, error) FederationStateList(ws memdb.WatchSet) (uint64, []*structs.FederationState, error)
GatewayServices(ws memdb.WatchSet, gateway string, entMeta *acl.EnterpriseMeta) (uint64, structs.GatewayServices, error) GatewayServices(ws memdb.WatchSet, gateway string, entMeta *acl.EnterpriseMeta) (uint64, structs.GatewayServices, error)
IntentionMatchOne(ws memdb.WatchSet, entry structs.IntentionMatchEntry, matchType structs.IntentionMatchType, destinationType structs.IntentionTargetType) (uint64, structs.SimplifiedIntentions, error) IntentionMatchOne(ws memdb.WatchSet, entry structs.IntentionMatchEntry, matchType structs.IntentionMatchType, destinationType structs.IntentionTargetType) (uint64, structs.SimplifiedIntentions, error)
IntentionTopology(ws memdb.WatchSet, target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision, intentionTarget structs.IntentionTargetType) (uint64, structs.ServiceList, error) IntentionTopology(ws memdb.WatchSet, target structs.ServiceName, downstreams bool, defaultDecision bool, intentionTarget structs.IntentionTargetType) (uint64, structs.ServiceList, error)
ReadResolvedServiceConfigEntries(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, upstreamIDs []structs.ServiceID, proxyMode structs.ProxyMode) (uint64, *configentry.ResolvedServiceConfigSet, error) ReadResolvedServiceConfigEntries(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, upstreamIDs []structs.ServiceID, proxyMode structs.ProxyMode) (uint64, *configentry.ResolvedServiceConfigSet, error)
ServiceDiscoveryChain(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, req discoverychain.CompileRequest) (uint64, *structs.CompiledDiscoveryChain, *configentry.DiscoveryChainSet, error) ServiceDiscoveryChain(ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta, req discoverychain.CompileRequest) (uint64, *structs.CompiledDiscoveryChain, *configentry.DiscoveryChainSet, error)
ServiceDump(ws memdb.WatchSet, kind structs.ServiceKind, useKind bool, entMeta *acl.EnterpriseMeta, peerName string) (uint64, structs.CheckServiceNodes, error) ServiceDump(ws memdb.WatchSet, kind structs.ServiceKind, useKind bool, entMeta *acl.EnterpriseMeta, peerName string) (uint64, structs.CheckServiceNodes, error)

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/cache"
cachetype "github.com/hashicorp/consul/agent/cache-types" cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/consul"
"github.com/hashicorp/consul/agent/consul/watch" "github.com/hashicorp/consul/agent/consul/watch"
"github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
@ -33,20 +34,21 @@ func CacheIntentionUpstreamsDestination(c *cache.Cache) proxycfg.IntentionUpstre
// ServerIntentionUpstreams satisfies the proxycfg.IntentionUpstreams interface // ServerIntentionUpstreams satisfies the proxycfg.IntentionUpstreams interface
// by sourcing upstreams for the given service, inferred from intentions, from // by sourcing upstreams for the given service, inferred from intentions, from
// the server's state store. // the server's state store.
func ServerIntentionUpstreams(deps ServerDataSourceDeps) proxycfg.IntentionUpstreams { func ServerIntentionUpstreams(deps ServerDataSourceDeps, defaultIntentionPolicy string) proxycfg.IntentionUpstreams {
return serverIntentionUpstreams{deps, structs.IntentionTargetService} return serverIntentionUpstreams{deps, structs.IntentionTargetService, defaultIntentionPolicy}
} }
// ServerIntentionUpstreamsDestination satisfies the proxycfg.IntentionUpstreams // ServerIntentionUpstreamsDestination satisfies the proxycfg.IntentionUpstreams
// interface by sourcing upstreams for the given destination, inferred from // interface by sourcing upstreams for the given destination, inferred from
// intentions, from the server's state store. // intentions, from the server's state store.
func ServerIntentionUpstreamsDestination(deps ServerDataSourceDeps) proxycfg.IntentionUpstreams { func ServerIntentionUpstreamsDestination(deps ServerDataSourceDeps, defaultIntentionPolicy string) proxycfg.IntentionUpstreams {
return serverIntentionUpstreams{deps, structs.IntentionTargetDestination} return serverIntentionUpstreams{deps, structs.IntentionTargetDestination, defaultIntentionPolicy}
} }
type serverIntentionUpstreams struct { type serverIntentionUpstreams struct {
deps ServerDataSourceDeps deps ServerDataSourceDeps
target structs.IntentionTargetType target structs.IntentionTargetType
defaultIntentionPolicy string
} }
func (s serverIntentionUpstreams) Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error { func (s serverIntentionUpstreams) Notify(ctx context.Context, req *structs.ServiceSpecificRequest, correlationID string, ch chan<- proxycfg.UpdateEvent) error {
@ -58,9 +60,10 @@ func (s serverIntentionUpstreams) Notify(ctx context.Context, req *structs.Servi
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
defaultDecision := authz.IntentionDefaultAllow(nil)
index, services, err := store.IntentionTopology(ws, target, false, defaultDecision, s.target) defaultAllow := consul.DefaultIntentionAllow(authz, s.defaultIntentionPolicy)
index, services, err := store.IntentionTopology(ws, target, false, defaultAllow, s.target)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }

View File

@ -66,7 +66,7 @@ func TestServerIntentionUpstreams(t *testing.T) {
dataSource := ServerIntentionUpstreams(ServerDataSourceDeps{ dataSource := ServerIntentionUpstreams(ServerDataSourceDeps{
ACLResolver: newStaticResolver(authz), ACLResolver: newStaticResolver(authz),
GetStore: func() Store { return store }, GetStore: func() Store { return store },
}) }, "")
ch := make(chan proxycfg.UpdateEvent) ch := make(chan proxycfg.UpdateEvent)
err := dataSource.Notify(ctx, &structs.ServiceSpecificRequest{ServiceName: serviceName}, "", ch) err := dataSource.Notify(ctx, &structs.ServiceSpecificRequest{ServiceName: serviceName}, "", ch)
@ -84,6 +84,47 @@ func TestServerIntentionUpstreams(t *testing.T) {
require.Equal(t, "db", result.Services[0].Name) require.Equal(t, "db", result.Services[0].Name)
} }
// Variant of TestServerIntentionUpstreams where a default allow intention policy
// returns "db" service as an IntentionUpstream even if there are no explicit
// intentions for "db".
func TestServerIntentionUpstreams_DefaultIntentionPolicy(t *testing.T) {
const serviceName = "web"
var index uint64
getIndex := func() uint64 {
index++
return index
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
store := state.NewStateStore(nil)
disableLegacyIntentions(t, store)
require.NoError(t, store.EnsureRegistration(getIndex(), &structs.RegisterRequest{
Node: "node-1",
Service: &structs.NodeService{
Service: "db",
},
}))
// Ensures that "db" service will not be filtered due to ACLs
authz := policyAuthorizer(t, `service "db" { policy = "read" }`)
dataSource := ServerIntentionUpstreams(ServerDataSourceDeps{
ACLResolver: newStaticResolver(authz),
GetStore: func() Store { return store },
}, "allow")
ch := make(chan proxycfg.UpdateEvent)
require.NoError(t, dataSource.Notify(ctx, &structs.ServiceSpecificRequest{ServiceName: serviceName}, "", ch))
result := getEventResult[*structs.IndexedServiceList](t, ch)
require.Len(t, result.Services, 1)
require.Equal(t, "db", result.Services[0].Name)
}
func disableLegacyIntentions(t *testing.T, store *state.Store) { func disableLegacyIntentions(t *testing.T, store *state.Store) {
t.Helper() t.Helper()

View File

@ -30,6 +30,9 @@ const (
// fix up all the places where this was used with the proper namespace // fix up all the places where this was used with the proper namespace
// value. // value.
IntentionDefaultNamespace = "default" IntentionDefaultNamespace = "default"
IntentionDefaultPolicyAllow = "allow"
IntentionDefaultPolicyDeny = "deny"
) )
// Intention defines an intention for the Connect Service Graph. This defines // Intention defines an intention for the Connect Service Graph. This defines
@ -727,7 +730,7 @@ type IntentionQueryCheckResponse struct {
// - Whether the matching intention has L7 permissions attached // - Whether the matching intention has L7 permissions attached
// - Whether the intention is managed by an external source like k8s // - Whether the intention is managed by an external source like k8s
// - Whether there is an exact, or wildcard, intention referencing the two services // - Whether there is an exact, or wildcard, intention referencing the two services
// - Whether ACLs are in DefaultAllow mode // - Whether intentions are in DefaultAllow mode
type IntentionDecisionSummary struct { type IntentionDecisionSummary struct {
Allowed bool Allowed bool
HasPermissions bool HasPermissions bool