From 138f73566961e4ab58394e672b160a60ca473025 Mon Sep 17 00:00:00 2001 From: Eduard Serra Date: Fri, 7 May 2021 00:58:16 -0700 Subject: [PATCH] feat(externAuthz): bring external authorization to 0.8 This commit bring configurable inbound external authorization to OSM v0.8. Signed-off-by: Eduard Serra --- charts/osm/README.md | 1 + charts/osm/templates/osm-configmap.yaml | 9 ++ charts/osm/values.yaml | 10 ++ docs/content/docs/external_auth_opa.md | 119 ++++++++++++++++++ .../manifests/opa/deploy-opa-envoy.yaml | 99 +++++++++++++++ pkg/configurator/client.go | 42 +++++++ pkg/configurator/client_test.go | 34 ++--- pkg/configurator/methods.go | 21 ++++ pkg/configurator/mock_client_generated.go | 14 +++ pkg/configurator/types.go | 13 ++ pkg/envoy/ads/response_test.go | 3 + pkg/envoy/lds/auth.go | 49 ++++++++ pkg/envoy/lds/connection_manager.go | 27 +++- pkg/envoy/lds/ingress.go | 2 +- pkg/envoy/lds/ingress_test.go | 4 + pkg/envoy/lds/inmesh.go | 4 +- pkg/envoy/lds/inmesh_test.go | 9 ++ pkg/envoy/lds/listener_test.go | 65 ++++++++-- pkg/envoy/lds/response_test.go | 3 + 19 files changed, 496 insertions(+), 32 deletions(-) create mode 100644 docs/content/docs/external_auth_opa.md create mode 100644 docs/example/manifests/opa/deploy-opa-envoy.yaml create mode 100644 pkg/envoy/lds/auth.go diff --git a/charts/osm/README.md b/charts/osm/README.md index 1528003ddd..5b46d2f008 100644 --- a/charts/osm/README.md +++ b/charts/osm/README.md @@ -93,6 +93,7 @@ The following table lists the configurable parameters of the osm chart and their | OpenServiceMesh.image.registry | string | `"openservicemesh"` | `osm-controller` image registry | | OpenServiceMesh.image.tag | string | `"v0.8.3"` | `osm-controller` image tag | | OpenServiceMesh.imagePullSecrets | list | `[]` | `osm-controller` image pull secret | +| OpenServiceMesh.inbound_extauthz | object | `{"address":"","enable":false,"failureModeAllow":false,"port":0,"statPrefix":"","timeout":"1s"}` | Inbound external authorization, allows configuring a remote service to provide external authorization upon request | | OpenServiceMesh.injector | object | `{"podLabels":{},"replicaCount":1,"resource":{"limits":{"cpu":"0.5","memory":"64M"},"requests":{"cpu":"0.3","memory":"64M"}}}` | Sidecar injector configuration | | OpenServiceMesh.meshName | string | `"osm"` | Name for the new control plane instance | | OpenServiceMesh.osmNamespace | string | `""` | Optional parameter. If not specified, the release namespace is used to deploy the osm components. | diff --git a/charts/osm/templates/osm-configmap.yaml b/charts/osm/templates/osm-configmap.yaml index 1df6b2ecd9..b61b76782c 100644 --- a/charts/osm/templates/osm-configmap.yaml +++ b/charts/osm/templates/osm-configmap.yaml @@ -13,6 +13,15 @@ data: enable_debug_server: {{ .Values.OpenServiceMesh.enableDebugServer | quote }} prometheus_scraping: {{ .Values.OpenServiceMesh.enablePrometheusScraping | quote }} + inbound_extauthz_enable: {{ .Values.OpenServiceMesh.inbound_extauthz.enable | quote }} +{{- if .Values.OpenServiceMesh.inbound_extauthz.enable }} + inbound_extauthz_address: {{ .Values.OpenServiceMesh.inbound_extauthz.address | quote }} + inbound_extauthz_port: {{ .Values.OpenServiceMesh.inbound_extauthz.port | quote }} + inbound_extauthz_statprefix: {{ .Values.OpenServiceMesh.inbound_extauthz.statPrefix | quote }} + inbound_extauthz_timeout: {{ .Values.OpenServiceMesh.inbound_extauthz.timeout | quote }} + inbound_extauthz_failuremodeallow: {{ .Values.OpenServiceMesh.inbound_extauthz.failureModeAllow | quote }} +{{- end }} + tracing_enable: {{ .Values.OpenServiceMesh.tracing.enable | quote }} {{- if .Values.OpenServiceMesh.tracing.enable }} tracing_address: {{ include "osm.tracingAddress" . | quote }} diff --git a/charts/osm/values.yaml b/charts/osm/values.yaml index f88f760a66..3f913a5dc3 100644 --- a/charts/osm/values.yaml +++ b/charts/osm/values.yaml @@ -131,6 +131,16 @@ OpenServiceMesh: # -- Destination's API or collector endpoint where the spans will be sent to endpoint: "/api/v2/spans" + # -- Inbound external authorization, allows configuring a remote service to provide + # external authorization upon request + inbound_extauthz: + enable: false + address: "" + port: 0 + statPrefix: "" + timeout: 1s + failureModeAllow: false + # -- Optional parameter to specify a global list of IP ranges to exclude from outbound traffic interception by the sidecar proxy. # If specified, must be a list of IP ranges of the form a.b.c.d/x. outboundIPRangeExclusionList: [] diff --git a/docs/content/docs/external_auth_opa.md b/docs/content/docs/external_auth_opa.md new file mode 100644 index 0000000000..488b920294 --- /dev/null +++ b/docs/content/docs/external_auth_opa.md @@ -0,0 +1,119 @@ +# OPA-plugin-enabled OSM POC + +## Overview and limitations +- Allows configuring an envoy's [External Authorization extension](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter) through OSM's configmap. +- Authorization filter is currently applied in inmesh `inbound` and `ingress` connections. +- This demo DOES NOT inject the OPA plugin sidecar on every pod, though this seems to be the intended model for OPA to run with for obvious latency reasons. + +If using `values.yaml`: + ``` + # External authz + inbound_extauthz: + enable: true + address: opa.opa.svc.cluster.local + port: 9191 + statPrefix: authz_opa + timeout: 1s + failureModeAllow: false +``` +Example OSM's configmap deployed by this POC: +``` + inbound_extauthz_enable: true + inbound_extauthz_address: opa.opa.svc.cluster.local + inbound_extauthz_port: 9191 + inbound_extauthz_statprefix: authz_opa + inbound_extauthz_timeout: 1s + inbound_extauthz_failuremodeallow: false +``` + +- Uses (as `opa-envoy-plugin`) a configmap to set OPA's policy. [This is not intended for production](https://github.com/open-policy-agent/opa-envoy-plugin#example-bundle-configuration). +- `opa-envoy-plugin` does not seem to react to changes on the configmap. To update policies, currently the OPA container needs to be restarted. + +## Demo Walkthrough + +- Deploy an `opa-envoy-plugin`, use curated yaml in this folder. + - NOTE: This POC is using a single plugin to handle all pod connections, and through the network. This is not intended for production. +``` +kubectl create namespace opa +kubectl apply -f docs/example/manifests/opa/deploy-opa-envoy.yaml +``` +`deploy-opa-envoy.yaml` will deploy OPA's envoy plugin and run it as a service, allowing external authorization calls from envoys through the network. + +- Deploy OSM's Demo, follow `demo/run-osm-demo.sh` +``` +demo/run-osm-demo.sh # wait for all services to come up +``` +- This branch has an External Authorization server expected by default at `opa.opa.svc.cluster.local:9191`. Timeout for authorization RTT is `1s` by default, and will not allow traffic in case of failure. + +Traffic should fail right out of the bat: +``` +kubectl logs -n bookbuyer bookbuyer +``` +``` +... +--- bookbuyer:[ 8 ] ----------------------------------------- + +Fetching http://bookstore.bookstore:14001/books-bought +Request Headers: map[Client-App:[bookbuyer] User-Agent:[Go-http-client/1.1]] +Identity: n/a +Booksbought: n/a +Server: envoy +Date: Tue, 04 May 2021 01:20:39 GMT +Status: 403 Forbidden +ERROR: response code for "http://bookstore.bookstore:14001/books-bought" is 403; expected 200 +... +``` + +You shoud also be able to see the logs in `opa-envoy-plugin` for rejected authorization calls: +``` +kubectl logs -n opa +``` +``` +{"decision_id":"1df154b5-658a-47bf-ac18-be52998605da","input":{"attributes":{"destination":{"address":{"socketAddress":{"address":"10.0.16.44","portValue":14001}}},"metadataContext":{},"request":{"http":{"headers":{":authority":"bookstore.bookstore:14001",":method":"GET",":path":"/books-bought","accept-encoding":"gzip","client-app":"bookbuyer","user-agent":"Go-http-client/1.1","x-forwarded-proto":"http","x-request-id":"69b80716-6af4-4986-bf8c-8f209d96f131"},"host":"bookstore.bookstore:14001","id":"6079090369556950701","method":"GET","path":"/books-bought","protocol":"HTTP/1.1"},"time":"2021-05-04T01:21:18.195876Z"},"source":{"address":{"socketAddress":{"address":"10.244.2.10","portValue":53488}}}},"parsed_body":null,"parsed_path":["books-bought"],"parsed_query":{},"truncated_body":false,"version":{"encoding":"protojson","ext_authz":"v3"}},"labels":{"id":"6e56bc11-a212-4c3e-be4d-b33186fd581d","version":"0.28.0-envoy"},"level":"info","metrics":{"timer_rego_query_eval_ns":105799,"timer_server_handler_ns":425097},"msg":"Decision Log","path":"envoy/authz/allow","requested_by":"","result":false,"time":"2021-05-04T01:21:18Z","timestamp":"2021-05-04T01:21:18.1971808Z","type":"openpolicyagent.org/decision_logs"} +``` + +- Now edit OPA's policy: +``` +kubectl edit configmap opa-policy -n opa +``` +change specifically the default-all from: +``` +default allow = false +``` +to +``` +default allow = true +``` + +- Finally, restart `opa-envoy-plugin`: +``` +kubectl rollout restart deployment opa -n opa +``` + +- Observe that bookbuyer calls are now being allowed: +``` +--- bookbuyer:[ 2663 ] ----------------------------------------- + +Fetching http://bookstore.bookstore:14001/books-bought +Request Headers: map[Client-App:[bookbuyer] User-Agent:[Go-http-client/1.1]] +Identity: bookstore-v1 +Booksbought: 1087 +Server: envoy +Date: Tue, 04 May 2021 02:00:46 GMT +Status: 200 OK +MAESTRO! THIS TEST SUCCEEDED! + +Fetching http://bookstore.bookstore:14001/buy-a-book/new +Request Headers: map[] +Identity: bookstore-v1 +Booksbought: 1088 +Server: envoy +Date: Tue, 04 May 2021 02:00:47 GMT +Status: 200 OK +ESC[90m2:00AMESC[0m ESC[32mINFESC[0m BooksCountV1=21490056 ESC[36mcomponent=ESC[0mdemo ESC[36mfile=ESC[0mbooks.go:167 +MAESTRO! THIS TEST SUCCEEDED! +``` + +``` +{"decision_id":"3f29d449-7f71-4721-b93c-ad7d375e0f80","input":{"attributes":{"destination":{"address":{"socketAddress":{"address":"10.0.16.44","portValue":14001}}},"metadataContext":{},"request":{"http":{"headers":{":authority":"bookstore.bookstore:14001",":method":"GET",":path":"/buy-a-book/new","accept-encoding":"gzip","user-agent":"Go-http-client/1.1","x-forwarded-proto":"http","x-request-id":"97bd9339-448f-4710-bba4-bda3b5103aa0"},"host":"bookstore.bookstore:14001","id":"14741973070759351541","method":"GET","path":"/buy-a-book/new","protocol":"HTTP/1.1"},"time":"2021-05-04T02:01:35.813125Z"},"source":{"address":{"socketAddress":{"address":"10.244.2.10","portValue":48860}}}},"parsed_body":null,"parsed_path":["buy-a-book","new"],"parsed_query":{},"truncated_body":false,"version":{"encoding":"protojson","ext_authz":"v3"}},"labels":{"id":"e8f143eb-9edf-425d-9210-340001993841","version":"0.28.0-envoy"},"level":"info","metrics":{"timer_rego_query_eval_ns":50500,"timer_server_handler_ns":370797},"msg":"Decision Log","path":"envoy/authz/allow","requested_by":"","result":true,"time":"2021-05-04T02:01:35Z","timestamp":"2021-05-04T02:01:35.816768454Z","type":"openpolicyagent.org/decision_logs"} +``` diff --git a/docs/example/manifests/opa/deploy-opa-envoy.yaml b/docs/example/manifests/opa/deploy-opa-envoy.yaml new file mode 100644 index 0000000000..a67cc4ef42 --- /dev/null +++ b/docs/example/manifests/opa/deploy-opa-envoy.yaml @@ -0,0 +1,99 @@ +#################################################### +# App Deployment with OPA-Envoy and Envoy sidecars. +#################################################### +apiVersion: apps/v1 +kind: Deployment +metadata: + name: opa + namespace: opa + labels: + app: opa +spec: + replicas: 1 + selector: + matchLabels: + app: opa + template: + metadata: + labels: + app: opa + spec: + containers: + - name: opa-envoy + image: openpolicyagent/opa:0.28.0-envoy + securityContext: + runAsUser: 1111 + volumeMounts: + - readOnly: true + mountPath: /policy + name: opa-policy + - readOnly: true + mountPath: /config + name: opa-envoy-config + args: + - "run" + - "--server" + - "--config-file=/config/config.yaml" + - "--addr=0.0.0.0:8181" + - "--diagnostic-addr=0.0.0.0:8282" + - "--ignore=.*" + - "/policy/policy.rego" + volumes: + - name: proxy-config + configMap: + name: proxy-config + - name: opa-policy + configMap: + name: opa-policy + - name: opa-envoy-config + configMap: + name: opa-envoy-config +--- +############################################################ +# Example configuration to bootstrap OPA-Envoy sidecars. +############################################################ +apiVersion: v1 +kind: ConfigMap +metadata: + name: opa-envoy-config + namespace: opa +data: + config.yaml: | + plugins: + envoy_ext_authz_grpc: + addr: :9191 + path: envoy/authz/allow + decision_logs: + console: true +--- +apiVersion: v1 +kind: Service +metadata: + name: opa + namespace: opa + labels: + app: opa +spec: + ports: + - port: 9191 + protocol: TCP + targetPort: 9191 + selector: + app: opa +--- +############################################################ +# Example policy to enforce into OPA-Envoy sidecars. +############################################################ +apiVersion: v1 +kind: ConfigMap +metadata: + name: opa-policy + namespace: opa +data: + policy.rego: | + package envoy.authz + + import input.attributes.request.http as http_request + + default allow = false +--- diff --git a/pkg/configurator/client.go b/pkg/configurator/client.go index 1b2056989d..e3e5ddd42a 100644 --- a/pkg/configurator/client.go +++ b/pkg/configurator/client.go @@ -58,6 +58,24 @@ const ( // configResyncInterval is the key name used to configure the resync interval for regular proxy broadcast updates configResyncInterval = "config_resync_interval" + + // inboundExtauthzEnable is the key used to enable the inbound external authorization filter + inboundExtauthzEnable = "inbound_extauthz_enable" + + // inboundExtauthzAddress is the key used to specify external authorization address + inboundExtauthzAddress = "inbound_extauthz_address" + + // inboundExtauthzPort is the key used to specify external authorization port + inboundExtauthzPort = "inbound_extauthz_port" + + // inboundExtauthzStatPrefix is the key used to specify external authorization stats prefix + inboundExtauthzStatPrefix = "inbound_extauthz_statprefix" + + // inboundExtauthzTimeout is the key used to specify external authorization connection timeout + inboundExtauthzTimeout = "inbound_extauthz_timeout" + + // inboundExtauthzFailureModeAllow is the key used to specify external authorization failure mode + inboundExtauthzFailureModeAllow = "inbound_extauthz_failuremodeallow" ) // NewConfigurator implements configurator.Configurator and creates the Kubernetes client to manage namespaces. @@ -163,6 +181,13 @@ func (c *Client) configMapListener() { triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.TracingAddress != newConfigMap.TracingAddress) triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.TracingEndpoint != newConfigMap.TracingEndpoint) triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.TracingPort != newConfigMap.TracingPort) + triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.PrometheusScraping != newConfigMap.PrometheusScraping) + triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.InboundExternAuthzAddress != newConfigMap.InboundExternAuthzAddress) + triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.InboundExternAuthzStatPrefix != newConfigMap.InboundExternAuthzStatPrefix) + triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.InboundExternAuthzTimeout != newConfigMap.InboundExternAuthzTimeout) + triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.InboundExternAuthzEnable != newConfigMap.InboundExternAuthzEnable) + triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.InboundExternAuthzFailureModeAllow != newConfigMap.InboundExternAuthzFailureModeAllow) + triggerGlobalBroadcast = triggerGlobalBroadcast || (prevConfigMap.InboundExternAuthzPort != newConfigMap.InboundExternAuthzPort) if triggerGlobalBroadcast { log.Debug().Msgf("[%s] OSM ConfigMap update triggered global proxy broadcast", @@ -229,6 +254,14 @@ type osmConfig struct { // ConfigResyncInterval is a flag to configure resync interval for regular proxy broadcast updates ConfigResyncInterval string `yaml:"config_resync_interval"` + + // Inbound external authorization flags, as specified by keys and explained above + InboundExternAuthzEnable bool `yaml:"inbound_extauthz_enable"` + InboundExternAuthzAddress string `yaml:"inbound_extauthz_address"` + InboundExternAuthzPort int `yaml:"inbound_extauthz_port"` + InboundExternAuthzStatPrefix string `yaml:"inbound_extauthz_statprefix"` + InboundExternAuthzTimeout string `yaml:"inbound_extauthz_timeout"` + InboundExternAuthzFailureModeAllow bool `yaml:"inbound_extauthz_failuremodeallow"` } func (c *Client) run(stop <-chan struct{}) { @@ -278,6 +311,7 @@ func parseOSMConfigMap(configMap *v1.ConfigMap) *osmConfig { osmConfigMap.PrometheusScraping, _ = GetBoolValueForKey(configMap, prometheusScrapingKey) osmConfigMap.UseHTTPSIngress, _ = GetBoolValueForKey(configMap, useHTTPSIngressKey) osmConfigMap.TracingEnable, _ = GetBoolValueForKey(configMap, tracingEnableKey) + osmConfigMap.InboundExternAuthzEnable, _ = GetBoolValueForKey(configMap, inboundExtauthzEnable) osmConfigMap.EnvoyLogLevel, _ = GetStringValueForKey(configMap, envoyLogLevel) osmConfigMap.ServiceCertValidityDuration, _ = GetStringValueForKey(configMap, serviceCertValidityDurationKey) osmConfigMap.OutboundIPRangeExclusionList, _ = GetStringValueForKey(configMap, outboundIPRangeExclusionListKey) @@ -290,6 +324,14 @@ func parseOSMConfigMap(configMap *v1.ConfigMap) *osmConfig { osmConfigMap.TracingEndpoint, _ = GetStringValueForKey(configMap, tracingEndpointKey) } + if osmConfigMap.InboundExternAuthzEnable { + osmConfigMap.InboundExternAuthzAddress, _ = GetStringValueForKey(configMap, inboundExtauthzAddress) + osmConfigMap.InboundExternAuthzPort, _ = GetIntValueForKey(configMap, inboundExtauthzPort) + osmConfigMap.InboundExternAuthzStatPrefix, _ = GetStringValueForKey(configMap, inboundExtauthzStatPrefix) + osmConfigMap.InboundExternAuthzTimeout, _ = GetStringValueForKey(configMap, inboundExtauthzTimeout) + osmConfigMap.InboundExternAuthzFailureModeAllow, _ = GetBoolValueForKey(configMap, inboundExtauthzFailureModeAllow) + } + return &osmConfigMap } diff --git a/pkg/configurator/client_test.go b/pkg/configurator/client_test.go index 97ffbae106..8b4c86463d 100644 --- a/pkg/configurator/client_test.go +++ b/pkg/configurator/client_test.go @@ -51,20 +51,26 @@ var _ = Describe("Test OSM ConfigMap parsing", func() { It("Tag matches const key for all fields of OSM ConfigMap struct", func() { fieldNameTag := map[string]string{ - "PermissiveTrafficPolicyMode": PermissiveTrafficPolicyModeKey, - "Egress": egressKey, - "EnableDebugServer": enableDebugServer, - "PrometheusScraping": prometheusScrapingKey, - "TracingEnable": tracingEnableKey, - "TracingAddress": tracingAddressKey, - "TracingPort": tracingPortKey, - "TracingEndpoint": tracingEndpointKey, - "UseHTTPSIngress": useHTTPSIngressKey, - "EnvoyLogLevel": envoyLogLevel, - "ServiceCertValidityDuration": serviceCertValidityDurationKey, - "OutboundIPRangeExclusionList": outboundIPRangeExclusionListKey, - "EnablePrivilegedInitContainer": enablePrivilegedInitContainer, - "ConfigResyncInterval": configResyncInterval, + "PermissiveTrafficPolicyMode": PermissiveTrafficPolicyModeKey, + "Egress": egressKey, + "EnableDebugServer": enableDebugServer, + "PrometheusScraping": prometheusScrapingKey, + "TracingEnable": tracingEnableKey, + "TracingAddress": tracingAddressKey, + "TracingPort": tracingPortKey, + "TracingEndpoint": tracingEndpointKey, + "UseHTTPSIngress": useHTTPSIngressKey, + "EnvoyLogLevel": envoyLogLevel, + "ServiceCertValidityDuration": serviceCertValidityDurationKey, + "OutboundIPRangeExclusionList": outboundIPRangeExclusionListKey, + "EnablePrivilegedInitContainer": enablePrivilegedInitContainer, + "ConfigResyncInterval": configResyncInterval, + "InboundExternAuthzEnable": inboundExtauthzEnable, + "InboundExternAuthzAddress": inboundExtauthzAddress, + "InboundExternAuthzPort": inboundExtauthzPort, + "InboundExternAuthzStatPrefix": inboundExtauthzStatPrefix, + "InboundExternAuthzTimeout": inboundExtauthzTimeout, + "InboundExternAuthzFailureModeAllow": inboundExtauthzFailureModeAllow, } t := reflect.TypeOf(osmConfig{}) diff --git a/pkg/configurator/methods.go b/pkg/configurator/methods.go index dc82dad7c2..afa7c6645f 100644 --- a/pkg/configurator/methods.go +++ b/pkg/configurator/methods.go @@ -147,3 +147,24 @@ func (c *Client) GetConfigResyncInterval() time.Duration { } return duration } + +// GetInboundExternalAuthConfig returns the External Authentication configuration for incoming traffic, if any +func (c *Client) GetInboundExternalAuthConfig() ExternAuthConfig { + extAuthRet := ExternAuthConfig{} + cfgMap := c.getConfigMap() + + extAuthRet.Enable = cfgMap.InboundExternAuthzEnable + extAuthRet.Address = cfgMap.InboundExternAuthzAddress + extAuthRet.Port = uint16(cfgMap.InboundExternAuthzPort) + extAuthRet.StatPrefix = cfgMap.InboundExternAuthzStatPrefix + extAuthRet.FailureModeAllow = cfgMap.InboundExternAuthzFailureModeAllow + + duration, err := time.ParseDuration(cfgMap.InboundExternAuthzTimeout) + if err != nil { + log.Debug().Err(err).Msgf("ExternAuthzTimeout: Not a valid duration %s. defaulting to 1s.", duration) + duration = 1 * time.Second + } + extAuthRet.AuthzTimeout = duration + + return extAuthRet +} diff --git a/pkg/configurator/mock_client_generated.go b/pkg/configurator/mock_client_generated.go index d01cbe4278..00665c50a5 100644 --- a/pkg/configurator/mock_client_generated.go +++ b/pkg/configurator/mock_client_generated.go @@ -77,6 +77,20 @@ func (mr *MockConfiguratorMockRecorder) GetEnvoyLogLevel() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvoyLogLevel", reflect.TypeOf((*MockConfigurator)(nil).GetEnvoyLogLevel)) } +// GetInboundExternalAuthConfig mocks base method +func (m *MockConfigurator) GetInboundExternalAuthConfig() ExternAuthConfig { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInboundExternalAuthConfig") + ret0, _ := ret[0].(ExternAuthConfig) + return ret0 +} + +// GetInboundExternalAuthConfig indicates an expected call of GetInboundExternalAuthConfig +func (mr *MockConfiguratorMockRecorder) GetInboundExternalAuthConfig() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInboundExternalAuthConfig", reflect.TypeOf((*MockConfigurator)(nil).GetInboundExternalAuthConfig)) +} + // GetOSMNamespace mocks base method func (m *MockConfigurator) GetOSMNamespace() string { m.ctrl.T.Helper() diff --git a/pkg/configurator/types.go b/pkg/configurator/types.go index 4d4e083251..8f48dde69f 100644 --- a/pkg/configurator/types.go +++ b/pkg/configurator/types.go @@ -72,4 +72,17 @@ type Configurator interface { // GetConfigResyncInterval returns the duration for resync interval. // If error or non-parsable value, returns 0 duration GetConfigResyncInterval() time.Duration + + // GetInboundExternalAuthConfig returns the External Authentication configuration for incoming traffic, if any + GetInboundExternalAuthConfig() ExternAuthConfig +} + +// ExternAuthConfig implements a generic subset of External Authz to configure external authorization in envoy's format +type ExternAuthConfig struct { + Enable bool + Address string + Port uint16 + StatPrefix string + AuthzTimeout time.Duration + FailureModeAllow bool } diff --git a/pkg/envoy/ads/response_test.go b/pkg/envoy/ads/response_test.go index 90660e8271..0da1e51af2 100644 --- a/pkg/envoy/ads/response_test.go +++ b/pkg/envoy/ads/response_test.go @@ -214,6 +214,9 @@ var _ = Describe("Test ADS response functions", func() { mockConfigurator.EXPECT().IsPermissiveTrafficPolicyMode().Return(false).AnyTimes() mockConfigurator.EXPECT().GetServiceCertValidityPeriod().Return(certDuration).AnyTimes() mockConfigurator.EXPECT().IsDebugServerEnabled().Return(true).AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).AnyTimes() It("returns Aggregated Discovery Service response", func() { s := NewADSServer(mc, true, tests.Namespace, mockConfigurator, mockCertManager) diff --git a/pkg/envoy/lds/auth.go b/pkg/envoy/lds/auth.go new file mode 100644 index 0000000000..745822377a --- /dev/null +++ b/pkg/envoy/lds/auth.go @@ -0,0 +1,49 @@ +package lds + +import ( + "fmt" + + envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + xds_ext_authz "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ext_authz/v3" + xds_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + "github.com/envoyproxy/go-control-plane/pkg/wellknown" + "github.com/golang/protobuf/ptypes" + + "github.com/openservicemesh/osm/pkg/configurator" +) + +func getExtAuthzHTTPFilter(extAuthConfig configurator.ExternAuthConfig) *xds_hcm.HttpFilter { + extAuth := &xds_ext_authz.ExtAuthz{ + Services: &xds_ext_authz.ExtAuthz_GrpcService{ + GrpcService: &envoy_config_core_v3.GrpcService{ + TargetSpecifier: &envoy_config_core_v3.GrpcService_GoogleGrpc_{ + GoogleGrpc: &envoy_config_core_v3.GrpcService_GoogleGrpc{ + TargetUri: fmt.Sprintf("%s:%d", + extAuthConfig.Address, + extAuthConfig.Port), + StatPrefix: extAuthConfig.StatPrefix, + }, + }, + Timeout: ptypes.DurationProto(extAuthConfig.AuthzTimeout), + }, + }, + TransportApiVersion: envoy_config_core_v3.ApiVersion_V3, + WithRequestBody: &xds_ext_authz.BufferSettings{ + MaxRequestBytes: 8192, + AllowPartialMessage: true, + }, + FailureModeAllow: extAuthConfig.FailureModeAllow, + } + + extAuthMarshalled, err := ptypes.MarshalAny(extAuth) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal External Authorization config") + } + + return &xds_hcm.HttpFilter{ + Name: wellknown.HTTPExternalAuthorization, + ConfigType: &xds_hcm.HttpFilter_TypedConfig{ + TypedConfig: extAuthMarshalled, + }, + } +} diff --git a/pkg/envoy/lds/connection_manager.go b/pkg/envoy/lds/connection_manager.go index 95b8205996..0a3e9106b4 100644 --- a/pkg/envoy/lds/connection_manager.go +++ b/pkg/envoy/lds/connection_manager.go @@ -15,11 +15,20 @@ import ( "github.com/openservicemesh/osm/pkg/featureflags" ) +// connectionDirection defines, for filter terms, the direction of the traffic from an application +// perspective, in which the connection manager filters will be applied +type connectionDirection string + const ( statPrefix = "http" + + // incoming defines in-mesh inbound and ingress traffic driections + incoming = "incoming" + // outgoing defines in-mesh outbound and egress traffic directions + outgoing = "outgoing" ) -func getHTTPConnectionManager(routeName string, cfg configurator.Configurator, headers map[string]string) *xds_hcm.HttpConnectionManager { +func getHTTPConnectionManager(routeName string, cfg configurator.Configurator, headers map[string]string, direction connectionDirection) *xds_hcm.HttpConnectionManager { connManager := &xds_hcm.HttpConnectionManager{ StatPrefix: statPrefix, CodecType: xds_hcm.HttpConnectionManager_AUTO, @@ -28,12 +37,7 @@ func getHTTPConnectionManager(routeName string, cfg configurator.Configurator, h // HTTP RBAC filter Name: wellknown.HTTPRoleBasedAccessControl, }, - { - // HTTP Router filter - Name: wellknown.Router, - }, }, - RouteSpecifier: &xds_hcm.HttpConnectionManager_Rds{ Rds: &xds_hcm.Rds{ ConfigSource: envoy.GetADSConfigSource(), @@ -43,6 +47,17 @@ func getHTTPConnectionManager(routeName string, cfg configurator.Configurator, h AccessLog: envoy.GetAccessLog(), } + // TODO Outgoing External Auth + incomingExtAuthCfg := cfg.GetInboundExternalAuthConfig() + if direction == incoming && incomingExtAuthCfg.Enable { + connManager.HttpFilters = append(connManager.HttpFilters, getExtAuthzHTTPFilter(incomingExtAuthCfg)) + } + + connManager.HttpFilters = append(connManager.HttpFilters, &xds_hcm.HttpFilter{ + // HTTP Router filter + Name: wellknown.Router, + }) + if cfg.IsTracingEnabled() { connManager.GenerateRequestId = &wrappers.BoolValue{ Value: true, diff --git a/pkg/envoy/lds/ingress.go b/pkg/envoy/lds/ingress.go index 3536b1cacf..0d688351fb 100644 --- a/pkg/envoy/lds/ingress.go +++ b/pkg/envoy/lds/ingress.go @@ -38,7 +38,7 @@ func newIngressHTTPFilterChain(cfg configurator.Configurator, svc service.MeshSe return nil } - inboundConnManager := getHTTPConnectionManager(route.InboundRouteConfigName, cfg, nil) + inboundConnManager := getHTTPConnectionManager(route.InboundRouteConfigName, cfg, nil, incoming) marshalledInboundConnManager, err := ptypes.MarshalAny(inboundConnManager) if err != nil { log.Error().Err(err).Msgf("Error marshalling inbound HttpConnectionManager object for proxy %s", svc) diff --git a/pkg/envoy/lds/ingress_test.go b/pkg/envoy/lds/ingress_test.go index 98baf082cb..814cd84243 100644 --- a/pkg/envoy/lds/ingress_test.go +++ b/pkg/envoy/lds/ingress_test.go @@ -103,6 +103,10 @@ func TestGetIngressFilterChains(t *testing.T) { mockConfigurator.EXPECT().UseHTTPSIngress().Return(tc.httpsIngress).AnyTimes() // Mock calls used to build the HTTP connection manager mockConfigurator.EXPECT().IsTracingEnabled().Return(false).AnyTimes() + // Expect no External Auth config + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).AnyTimes() filterChains := lb.getIngressFilterChains(proxyService) diff --git a/pkg/envoy/lds/inmesh.go b/pkg/envoy/lds/inmesh.go index 44d942e6c5..1d18042278 100644 --- a/pkg/envoy/lds/inmesh.go +++ b/pkg/envoy/lds/inmesh.go @@ -84,7 +84,7 @@ func (lb *listenerBuilder) getInboundHTTPFilters(proxyService service.MeshServic } // Apply the HTTP Connection Manager Filter - inboundConnManager := getHTTPConnectionManager(route.InboundRouteConfigName, lb.cfg, lb.statsHeaders) + inboundConnManager := getHTTPConnectionManager(route.InboundRouteConfigName, lb.cfg, lb.statsHeaders, incoming) marshalledInboundConnManager, err := ptypes.MarshalAny(inboundConnManager) if err != nil { log.Error().Err(err).Msgf("Error marshalling inbound HttpConnectionManager for proxy service %s", proxyService) @@ -234,7 +234,7 @@ func (lb *listenerBuilder) getOutboundHTTPFilter() (*xds_listener.Filter, error) var err error marshalledFilter, err = ptypes.MarshalAny( - getHTTPConnectionManager(route.OutboundRouteConfigName, lb.cfg, lb.statsHeaders)) + getHTTPConnectionManager(route.OutboundRouteConfigName, lb.cfg, lb.statsHeaders, outgoing)) if err != nil { log.Error().Err(err).Msgf("Error marshalling HTTP connection manager object") return nil, err diff --git a/pkg/envoy/lds/inmesh_test.go b/pkg/envoy/lds/inmesh_test.go index 18f948b29a..454529be80 100644 --- a/pkg/envoy/lds/inmesh_test.go +++ b/pkg/envoy/lds/inmesh_test.go @@ -35,6 +35,9 @@ func TestGetOutboundHTTPFilterChainForService(t *testing.T) { // Mock calls used to build the HTTP connection manager mockConfigurator.EXPECT().IsTracingEnabled().Return(false).AnyTimes() mockConfigurator.EXPECT().GetTracingEndpoint().Return("test-api").AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).AnyTimes() lb := &listenerBuilder{ meshCatalog: mockCatalog, @@ -113,6 +116,9 @@ func TestGetInboundMeshHTTPFilterChain(t *testing.T) { // Mock calls used to build the HTTP connection manager mockConfigurator.EXPECT().IsTracingEnabled().Return(false).AnyTimes() mockConfigurator.EXPECT().GetTracingEndpoint().Return("test-api").AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).AnyTimes() lb := &listenerBuilder{ meshCatalog: mockCatalog, @@ -203,6 +209,9 @@ func TestGetInboundMeshTCPFilterChain(t *testing.T) { // Mock calls used to build the HTTP connection manager mockConfigurator.EXPECT().IsTracingEnabled().Return(false).AnyTimes() mockConfigurator.EXPECT().GetTracingEndpoint().Return("test-api").AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).AnyTimes() lb := &listenerBuilder{ meshCatalog: mockCatalog, diff --git a/pkg/envoy/lds/listener_test.go b/pkg/envoy/lds/listener_test.go index bf90cb16ab..3a9f9b0d86 100644 --- a/pkg/envoy/lds/listener_test.go +++ b/pkg/envoy/lds/listener_test.go @@ -2,6 +2,7 @@ package lds import ( "testing" + "time" xds_core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" xds_hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" @@ -34,6 +35,9 @@ func TestGetFilterForService(t *testing.T) { mockConfigurator.EXPECT().IsPermissiveTrafficPolicyMode().Return(false) mockConfigurator.EXPECT().IsTracingEnabled().Return(true) mockConfigurator.EXPECT().GetTracingEndpoint().Return("test-endpoint") + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).AnyTimes() // Check we get HTTP connection manager filter without Permissive mode filter, err := lb.getOutboundHTTPFilter() @@ -91,17 +95,22 @@ var _ = Describe("Test getHTTPConnectionManager", func() { mockConfigurator *configurator.MockConfigurator ) - mockCtrl = gomock.NewController(GinkgoT()) - mockConfigurator = configurator.NewMockConfigurator(mockCtrl) - Context("Test creation of HTTP connection manager", func() { + BeforeEach(func() { + mockCtrl = gomock.NewController(GinkgoT()) + mockConfigurator = configurator.NewMockConfigurator(mockCtrl) + }) + It("Returns proper Zipkin config given when tracing is enabled", func() { mockConfigurator.EXPECT().GetTracingHost().Return(constants.DefaultTracingHost).Times(1) mockConfigurator.EXPECT().GetTracingPort().Return(constants.DefaultTracingPort).Times(1) mockConfigurator.EXPECT().GetTracingEndpoint().Return(constants.DefaultTracingEndpoint).Times(1) mockConfigurator.EXPECT().IsTracingEnabled().Return(true).Times(1) + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).Times(1) - connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, nil) + connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, nil, incoming) Expect(connManager.Tracing.Verbose).To(Equal(true)) Expect(connManager.Tracing.Provider.Name).To(Equal("envoy.tracers.zipkin")) @@ -109,8 +118,11 @@ var _ = Describe("Test getHTTPConnectionManager", func() { It("Returns proper Zipkin config given when tracing is disabled", func() { mockConfigurator.EXPECT().IsTracingEnabled().Return(false).Times(1) + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).Times(1) - connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, nil) + connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, nil, incoming) var nilHcmTrace *xds_hcm.HttpConnectionManager_Tracing = nil Expect(connManager.Tracing).To(Equal(nilHcmTrace)) @@ -118,13 +130,17 @@ var _ = Describe("Test getHTTPConnectionManager", func() { It("Returns no stats config when WASM is disabled", func() { mockConfigurator.EXPECT().IsTracingEnabled().AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).Times(1) + oldWASMflag := featureflags.Features.WASMStats featureflags.Features.WASMStats = false oldStatsWASMBytes := statsWASMBytes statsWASMBytes = testWASM - connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, map[string]string{"k1": "v1"}) + connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, map[string]string{"k1": "v1"}, incoming) Expect(connManager.HttpFilters).To(HaveLen(2)) Expect(connManager.HttpFilters[0].GetName()).To(Equal(wellknown.HTTPRoleBasedAccessControl)) @@ -138,13 +154,17 @@ var _ = Describe("Test getHTTPConnectionManager", func() { It("Returns no stats config when WASM is disabled and no WASM is defined", func() { mockConfigurator.EXPECT().IsTracingEnabled().AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).Times(1) + oldWASMflag := featureflags.Features.WASMStats featureflags.Features.WASMStats = true oldStatsWASMBytes := statsWASMBytes statsWASMBytes = "" - connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, map[string]string{"k1": "v1"}) + connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, map[string]string{"k1": "v1"}, incoming) Expect(connManager.HttpFilters).To(HaveLen(2)) Expect(connManager.HttpFilters[0].GetName()).To(Equal(wellknown.HTTPRoleBasedAccessControl)) @@ -158,13 +178,17 @@ var _ = Describe("Test getHTTPConnectionManager", func() { It("Returns no Lua headers filter config when there are no headers to add", func() { mockConfigurator.EXPECT().IsTracingEnabled().AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).Times(1) + oldWASMflag := featureflags.Features.WASMStats featureflags.Features.WASMStats = true oldStatsWASMBytes := statsWASMBytes statsWASMBytes = testWASM - connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, nil) + connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, nil, incoming) Expect(connManager.HttpFilters).To(HaveLen(3)) Expect(connManager.HttpFilters[0].GetName()).To(Equal("envoy.filters.http.wasm")) @@ -179,13 +203,17 @@ var _ = Describe("Test getHTTPConnectionManager", func() { It("Returns proper stats config when WASM is enabled", func() { mockConfigurator.EXPECT().IsTracingEnabled().AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).AnyTimes() + oldWASMflag := featureflags.Features.WASMStats featureflags.Features.WASMStats = true oldStatsWASMBytes := statsWASMBytes statsWASMBytes = testWASM - connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, map[string]string{"k1": "v1"}) + connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, map[string]string{"k1": "v1"}, incoming) Expect(connManager.GetHttpFilters()).To(HaveLen(4)) Expect(connManager.GetHttpFilters()[0].GetName()).To(Equal(wellknown.Lua)) @@ -199,5 +227,24 @@ var _ = Describe("Test getHTTPConnectionManager", func() { statsWASMBytes = oldStatsWASMBytes featureflags.Features.WASMStats = oldWASMflag }) + + It("Returns inbound external authorization enabled connection manager when configuration requires", func() { + mockConfigurator.EXPECT().IsTracingEnabled().AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: true, + Address: "test.xyz", + Port: 123, + StatPrefix: "pref", + AuthzTimeout: 3 * time.Second, + FailureModeAllow: false, + }).Times(1) + + connManager := getHTTPConnectionManager(route.InboundRouteConfigName, mockConfigurator, nil, incoming) + + Expect(connManager.GetHttpFilters()).To(HaveLen(3)) + Expect(connManager.GetHttpFilters()[0].GetName()).To(Equal(wellknown.HTTPRoleBasedAccessControl)) + Expect(connManager.GetHttpFilters()[1].GetName()).To(Equal(wellknown.HTTPExternalAuthorization)) + Expect(connManager.GetHttpFilters()[2].GetName()).To(Equal(wellknown.Router)) + }) }) }) diff --git a/pkg/envoy/lds/response_test.go b/pkg/envoy/lds/response_test.go index bb0fe59277..81236c45be 100644 --- a/pkg/envoy/lds/response_test.go +++ b/pkg/envoy/lds/response_test.go @@ -68,6 +68,9 @@ func TestListenerConfiguration(t *testing.T) { mockConfigurator.EXPECT().IsPrometheusScrapingEnabled().Return(true).AnyTimes() mockConfigurator.EXPECT().IsTracingEnabled().Return(false).AnyTimes() mockConfigurator.EXPECT().IsEgressEnabled().Return(true).AnyTimes() + mockConfigurator.EXPECT().GetInboundExternalAuthConfig().Return(configurator.ExternAuthConfig{ + Enable: false, + }).AnyTimes() actual, err := NewResponse(meshCatalog, proxy, nil, mockConfigurator, nil) assert.Empty(err)