open-consul/agent/configentry/resolve.go
Paul Glass d8d89d4b59
Permissive mTLS (#17035)
This implements permissive mTLS , which allows toggling services into "permissive" mTLS mode.
Permissive mTLS mode allows incoming "non Consul-mTLS" traffic to be forward unmodified to the application.

* Update service-defaults and proxy-defaults config entries with a MutualTLSMode field
* Update the mesh config entry with an AllowEnablingPermissiveMutualTLS field and implement the necessary validation. AllowEnablingPermissiveMutualTLS must be true to allow changing to MutualTLSMode=permissive, but this does not require that all proxy-defaults and service-defaults are currently in strict mode.
* Update xDS listener config to add a "permissive filter chain" when MutualTLSMode=permissive for a particular service. The permissive filter chain matches incoming traffic by the destination port. If the destination port matches the service port from the catalog, then no mTLS is required and the traffic sent is forwarded unmodified to the application.
2023-04-19 14:45:00 -05:00

279 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package configentry
import (
"fmt"
"github.com/hashicorp/go-hclog"
"github.com/imdario/mergo"
"github.com/mitchellh/copystructure"
"github.com/hashicorp/consul/agent/structs"
)
func ComputeResolvedServiceConfig(
args *structs.ServiceConfigRequest,
entries *ResolvedServiceConfigSet,
logger hclog.Logger,
) (*structs.ServiceConfigResponse, error) {
var thisReply structs.ServiceConfigResponse
thisReply.MeshGateway.Mode = structs.MeshGatewayModeDefault
// Store the upstream defaults under a wildcard key so that they can be applied to
// upstreams that are inferred from intentions and do not have explicit upstream configuration.
wildcard := structs.PeeredServiceName{
ServiceName: structs.NewServiceName(structs.WildcardSpecifier, args.WithWildcardNamespace()),
}
wildcardUpstreamDefaults := make(map[string]interface{})
// resolvedConfigs stores the opaque config map for each upstream and is keyed on the upstream's ID.
resolvedConfigs := make(map[structs.PeeredServiceName]map[string]interface{})
// TODO(freddy) Refactor this into smaller set of state store functions
// Pass the WatchSet to both the service and proxy config lookups. If either is updated during the
// blocking query, this function will be rerun and these state store lookups will both be current.
// We use the default enterprise meta to look up the global proxy defaults because they are not namespaced.
proxyConf := entries.GetProxyDefaults(args.PartitionOrDefault())
if proxyConf != nil {
// Apply the proxy defaults to the sidecar's proxy config
mapCopy, err := copystructure.Copy(proxyConf.Config)
if err != nil {
return nil, fmt.Errorf("failed to copy global proxy-defaults: %v", err)
}
thisReply.ProxyConfig = mapCopy.(map[string]interface{})
thisReply.Mode = proxyConf.Mode
thisReply.TransparentProxy = proxyConf.TransparentProxy
thisReply.MutualTLSMode = proxyConf.MutualTLSMode
thisReply.MeshGateway = proxyConf.MeshGateway
thisReply.Expose = proxyConf.Expose
thisReply.EnvoyExtensions = proxyConf.EnvoyExtensions
thisReply.AccessLogs = proxyConf.AccessLogs
// Only MeshGateway and Protocol should affect upstreams.
// MeshGateway is strange. It's marshaled into UpstreamConfigs via the arbitrary map, but it
// uses concrete fields everywhere else. We always take the explicit definition here for
// wildcard upstreams and discard the user setting it via arbitrary map in proxy-defaults.
if mgw, ok := thisReply.ProxyConfig["mesh_gateway"]; ok {
wildcardUpstreamDefaults["mesh_gateway"] = mgw
}
if !proxyConf.MeshGateway.IsZero() {
wildcardUpstreamDefaults["mesh_gateway"] = proxyConf.MeshGateway
}
if protocol, ok := thisReply.ProxyConfig["protocol"]; ok {
wildcardUpstreamDefaults["protocol"] = protocol
}
}
serviceConf := entries.GetServiceDefaults(
structs.NewServiceID(args.Name, &args.EnterpriseMeta),
)
if serviceConf != nil {
if serviceConf.Expose.Checks {
thisReply.Expose.Checks = true
}
if len(serviceConf.Expose.Paths) >= 1 {
thisReply.Expose.Paths = serviceConf.Expose.Paths
}
if serviceConf.MeshGateway.Mode != structs.MeshGatewayModeDefault {
thisReply.MeshGateway.Mode = serviceConf.MeshGateway.Mode
wildcardUpstreamDefaults["mesh_gateway"] = serviceConf.MeshGateway
}
if serviceConf.TransparentProxy.OutboundListenerPort != 0 {
thisReply.TransparentProxy.OutboundListenerPort = serviceConf.TransparentProxy.OutboundListenerPort
}
if serviceConf.TransparentProxy.DialedDirectly {
thisReply.TransparentProxy.DialedDirectly = serviceConf.TransparentProxy.DialedDirectly
}
if serviceConf.Mode != structs.ProxyModeDefault {
thisReply.Mode = serviceConf.Mode
}
if serviceConf.Destination != nil {
thisReply.Destination = *serviceConf.Destination
}
// Populate values for the proxy config map
proxyConf := thisReply.ProxyConfig
if proxyConf == nil {
proxyConf = make(map[string]interface{})
}
if serviceConf.Protocol != "" {
proxyConf["protocol"] = serviceConf.Protocol
}
if serviceConf.BalanceInboundConnections != "" {
proxyConf["balance_inbound_connections"] = serviceConf.BalanceInboundConnections
}
if serviceConf.MaxInboundConnections > 0 {
proxyConf["max_inbound_connections"] = serviceConf.MaxInboundConnections
}
if serviceConf.LocalConnectTimeoutMs > 0 {
proxyConf["local_connect_timeout_ms"] = serviceConf.LocalConnectTimeoutMs
}
if serviceConf.LocalRequestTimeoutMs > 0 {
proxyConf["local_request_timeout_ms"] = serviceConf.LocalRequestTimeoutMs
}
// Add the proxy conf to the response if any fields were populated
if len(proxyConf) > 0 {
thisReply.ProxyConfig = proxyConf
}
if serviceConf.MutualTLSMode != structs.MutualTLSModeDefault {
thisReply.MutualTLSMode = serviceConf.MutualTLSMode
}
thisReply.Meta = serviceConf.Meta
// Service defaults' envoy extensions are appended to the proxy defaults extensions so that proxy defaults
// extensions are applied first.
thisReply.EnvoyExtensions = append(thisReply.EnvoyExtensions, serviceConf.EnvoyExtensions...)
}
// First collect all upstreams into a set of seen upstreams.
// Upstreams can come from:
// - Explicitly from proxy registrations, and therefore as an argument to this RPC endpoint
// - Implicitly from centralized upstream config in service-defaults
seenUpstreams := map[structs.PeeredServiceName]struct{}{}
var (
noUpstreamArgs = len(args.UpstreamServiceNames) == 0
// Check the args and the resolved value. If it was exclusively set via a config entry, then args.Mode
// will never be transparent because the service config request does not use the resolved value.
tproxy = args.Mode == structs.ProxyModeTransparent || thisReply.Mode == structs.ProxyModeTransparent
)
// The upstreams passed as arguments to this endpoint are the upstreams explicitly defined in a proxy registration.
// If no upstreams were passed, then we should only return the resolved config if the proxy is in transparent mode.
// Otherwise we would return a resolved upstream config to a proxy with no configured upstreams.
if noUpstreamArgs && !tproxy {
return &thisReply, nil
}
// First store all upstreams that were provided in the request
for _, psn := range args.UpstreamServiceNames {
if _, ok := seenUpstreams[psn]; !ok {
seenUpstreams[psn] = struct{}{}
}
}
// Then store upstreams inferred from service-defaults and mapify the overrides.
var (
upstreamDefaults *structs.UpstreamConfig
upstreamOverrides = make(map[structs.PeeredServiceName]*structs.UpstreamConfig)
)
if serviceConf != nil && serviceConf.UpstreamConfig != nil {
for i, override := range serviceConf.UpstreamConfig.Overrides {
if override.Name == "" {
logger.Warn(
"Skipping UpstreamConfig.Overrides entry without a required name field",
"entryIndex", i,
"kind", serviceConf.GetKind(),
"name", serviceConf.GetName(),
"namespace", serviceConf.GetEnterpriseMeta().NamespaceOrEmpty(),
)
continue // skip this impossible condition
}
psn := override.PeeredServiceName()
seenUpstreams[psn] = struct{}{}
upstreamOverrides[psn] = override
}
if serviceConf.UpstreamConfig.Defaults != nil {
upstreamDefaults = serviceConf.UpstreamConfig.Defaults
if upstreamDefaults.MeshGateway.Mode == structs.MeshGatewayModeDefault {
upstreamDefaults.MeshGateway.Mode = thisReply.MeshGateway.Mode
}
upstreamDefaults.MergeInto(wildcardUpstreamDefaults)
// Always add the wildcard upstream if a service-defaults default-upstream was configured.
resolvedConfigs[wildcard] = wildcardUpstreamDefaults
}
}
if !args.MeshGateway.IsZero() {
wildcardUpstreamDefaults["mesh_gateway"] = args.MeshGateway
}
// Add the wildcard upstream if any fields were populated (it may have been already
// added if a service-defaults exists). We likely could always add it without issues,
// but this has been existing behavior, and many unit tests would break.
if len(wildcardUpstreamDefaults) > 0 {
resolvedConfigs[wildcard] = wildcardUpstreamDefaults
}
for upstream := range seenUpstreams {
resolvedCfg := make(map[string]interface{})
// The protocol of an upstream is resolved in this order:
// 1. Default protocol from proxy-defaults (how all services should be addressed)
// 2. Protocol for upstream service defined in its service-defaults (how the upstream wants to be addressed)
// 3. Protocol defined for the upstream in the service-defaults.(upstream_config.defaults|upstream_config.overrides) of the downstream
// (how the downstream wants to address it)
if err := mergo.MergeWithOverwrite(&resolvedCfg, wildcardUpstreamDefaults); err != nil {
return nil, fmt.Errorf("failed to merge wildcard defaults into upstream: %v", err)
}
upstreamSvcDefaults := entries.GetServiceDefaults(upstream.ServiceName.ToServiceID())
if upstreamSvcDefaults != nil {
if upstreamSvcDefaults.Protocol != "" {
resolvedCfg["protocol"] = upstreamSvcDefaults.Protocol
}
}
// When dialing an upstream, the goal is to flatten the mesh gateway mode in this order
// (larger number wins):
// 1. Value from the proxy-defaults
// 2. Value from top-level of service-defaults (ServiceDefaults.MeshGateway)
// 3. Value from centralized upstream defaults (ServiceDefaults.UpstreamConfig.Defaults)
// 4. Value from local proxy registration (NodeService.Proxy.MeshGateway)
// 5. Value from centralized upstream override (ServiceDefaults.UpstreamConfig.Overrides)
// 6. Value from local upstream definition (NodeService.Proxy.Upstreams[].MeshGateway)
//
// The MeshGateway value from upstream definitions in the proxy registration override
// the one from UpstreamConfig.Defaults and UpstreamConfig.Overrides because they are
// specific to the proxy instance.
//
// Step 6 is handled by the dialer's ServiceManager in MergeServiceConfig.
// Start with the merged value from proxyConf and serviceConf. (steps 1-2)
if !thisReply.MeshGateway.IsZero() {
resolvedCfg["mesh_gateway"] = thisReply.MeshGateway
}
// Merge in the upstream defaults (step 3).
if upstreamDefaults != nil {
upstreamDefaults.MergeInto(resolvedCfg)
}
// Merge in the top-level mode from the proxy instance (step 4).
if !args.MeshGateway.IsZero() {
// This means each upstream inherits the value from the `NodeService.Proxy.MeshGateway` field.
resolvedCfg["mesh_gateway"] = args.MeshGateway
}
// Merge in Overrides for the upstream (step 5).
if upstreamOverrides[upstream] != nil {
upstreamOverrides[upstream].MergeInto(resolvedCfg)
}
if len(resolvedCfg) > 0 {
resolvedConfigs[upstream] = resolvedCfg
}
}
// don't allocate the slices just to not fill them
if len(resolvedConfigs) == 0 {
return &thisReply, nil
}
thisReply.UpstreamConfigs = make(structs.OpaqueUpstreamConfigs, 0, len(resolvedConfigs))
for us, conf := range resolvedConfigs {
thisReply.UpstreamConfigs = append(thisReply.UpstreamConfigs,
structs.OpaqueUpstreamConfig{Upstream: us, Config: conf})
}
return &thisReply, nil
}