From d742edb82f23b8fc706fb21c49321813b5b267f2 Mon Sep 17 00:00:00 2001 From: Edward Viaene Date: Fri, 9 Apr 2021 16:34:20 -0400 Subject: [PATCH] mTLS IP filter --- README.md | 19 +++++++++++ pkg/api/mtls.go | 1 + pkg/envoy/jwtprovider.go | 6 ++-- pkg/envoy/listener.go | 10 ++++-- pkg/envoy/listener_test.go | 4 +-- pkg/envoy/listener_utils.go | 11 +++++- pkg/envoy/mtls.go | 67 +++++++++++++++++++++++++++++++++++-- pkg/envoy/types.go | 1 + pkg/envoy/xds.go | 1 + 9 files changed, 109 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 7a338a5..f96aebd 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,25 @@ spec: This will run the ACME validation on both hostnames (mocky-1.in4it.io and mocky-2.in4it.io). If successful, it'll create an https listener that redirects to www.mocky.io, a mocking service. +## mTLS +mTLS listeners can be added on different ports than the default listener. You just need to provide server key/crt and CA cert. +``` +api: proxy.in4it.io/v1 +kind: mTLS +metadata: + name: test-rule +spec: + privateKey: | + replaceme + certificate: | + replaceme + caCertificate: | + replaceme + port: 10002 + AllowedSubjectAltNames: ["client1.example.com"] # optional ALT Name subject restriction + AllowedIPRanges: ["1.2.3.4/16"] # optional IP restriction +``` + ## Run on AWS with terraform There is a terraform module available in this repository. It'll configure an S3 bucket, a Network Loadbalancer, and 3 fargate containers. The container setup consist of 2 envoy proxies (one for http and one for https), and the roxprox server. To start using it, add the following code to your terraform project: diff --git a/pkg/api/mtls.go b/pkg/api/mtls.go index b043113..3533869 100644 --- a/pkg/api/mtls.go +++ b/pkg/api/mtls.go @@ -13,4 +13,5 @@ type MTLSSpec struct { CACertificate string `json:"caCertificate" yaml:"caCertificate"` Port int64 `json:"port" yaml:"port"` AllowedSubjectAltNames []string `json:"allowedSubjectAltNames" yaml:"allowedSubjectAltNames"` + AllowedIPRanges []string `json:"allowedIPRanges" yaml:"allowedIPRanges"` } diff --git a/pkg/envoy/jwtprovider.go b/pkg/envoy/jwtprovider.go index cbc4765..9b39236 100644 --- a/pkg/envoy/jwtprovider.go +++ b/pkg/envoy/jwtprovider.go @@ -206,7 +206,7 @@ func (j *JwtProvider) updateListenerWithJwtProvider(cache *WorkQueueCache, param if err != nil { panic(err) } - ll.FilterChains[0].Filters[0].ConfigType = &api.Filter_TypedConfig{ + ll.FilterChains[0].Filters[getFilterIndexByName(ll.FilterChains[0].Filters, Envoy_HTTP_Filter)].ConfigType = &api.Filter_TypedConfig{ TypedConfig: pbst, } } @@ -306,7 +306,7 @@ func (j *JwtProvider) UpdateJwtRule(cache *WorkQueueCache, params ListenerParams } // modify filter - ll.FilterChains[filterId].Filters[0].ConfigType = &api.Filter_TypedConfig{ + ll.FilterChains[filterId].Filters[getFilterIndexByName(ll.FilterChains[0].Filters, Envoy_HTTP_Filter)].ConfigType = &api.Filter_TypedConfig{ TypedConfig: pbst, } @@ -383,7 +383,7 @@ func (j *JwtProvider) DeleteJwtRule(cache *WorkQueueCache, params ListenerParams filterId = 0 } - ll.FilterChains[filterId].Filters[0].ConfigType = &api.Filter_TypedConfig{ + ll.FilterChains[filterId].Filters[getFilterIndexByName(ll.FilterChains[0].Filters, Envoy_HTTP_Filter)].ConfigType = &api.Filter_TypedConfig{ TypedConfig: pbst, } diff --git a/pkg/envoy/listener.go b/pkg/envoy/listener.go index 9cb6aa3..e2d5a7c 100644 --- a/pkg/envoy/listener.go +++ b/pkg/envoy/listener.go @@ -24,6 +24,7 @@ import ( const Error_NoFilterChainFound = "NoFilterChainFound" const Error_NoFilterFound = "NoFilterFound" +const Envoy_HTTP_Filter = "envoy.filters.network.http_connection_manager" type listenerDefaultsMapping struct { rateLimit bool @@ -168,7 +169,7 @@ func (l *Listener) updateListenerWithChallenge(cache *WorkQueueCache, challenge if err != nil { panic(err) } - ll.FilterChains[0].Filters[0].ConfigType = &api.Filter_TypedConfig{ + ll.FilterChains[0].Filters[getFilterIndexByName(ll.FilterChains[0].Filters, Envoy_HTTP_Filter)].ConfigType = &api.Filter_TypedConfig{ TypedConfig: pbst, } } @@ -466,7 +467,7 @@ func (l *Listener) updateListener(cache *WorkQueueCache, params ListenerParams, } // modify filter - ll.FilterChains[filterId].Filters[0].ConfigType = &api.Filter_TypedConfig{ + ll.FilterChains[filterId].Filters[getFilterIndexByName(ll.FilterChains[0].Filters, Envoy_HTTP_Filter)].ConfigType = &api.Filter_TypedConfig{ TypedConfig: pbst, } @@ -696,7 +697,7 @@ func (l *Listener) DeleteRoute(cache *WorkQueueCache, params ListenerParams, par filterId = 0 } - ll.FilterChains[filterId].Filters[0].ConfigType = &api.Filter_TypedConfig{ + ll.FilterChains[filterId].Filters[getFilterIndexByName(ll.FilterChains[0].Filters, Envoy_HTTP_Filter)].ConfigType = &api.Filter_TypedConfig{ TypedConfig: pbst, } @@ -891,6 +892,9 @@ func (l *Listener) HasMTLSDefault(listenerName, attr string) bool { if attr == "envoy.filters.http.router" { return true // always allow the router filter } + if attr == "envoy.filters.network.rbac" { + return true // always allow the rbac filter + } if val, ok := l.mTLSListenerDefaultsMapping[listenerName]; ok { switch attr { case "envoy.filters.http.ratelimit": diff --git a/pkg/envoy/listener_test.go b/pkg/envoy/listener_test.go index a707a0e..0ffcb51 100644 --- a/pkg/envoy/listener_test.go +++ b/pkg/envoy/listener_test.go @@ -870,7 +870,7 @@ func validateJWTProvider(listeners []cacheTypes.Resource, auth Auth) error { if len(filterChain.Filters) == 0 { return fmt.Errorf("No filters found in listener %s", cachedListener.Name) } - manager, err := getManager((filterChain.Filters[0].ConfigType).(*api.Filter_TypedConfig)) + manager, err := getManager((filterChain.Filters[getFilterIndexByName(filterChain.Filters, Envoy_HTTP_Filter)].ConfigType).(*api.Filter_TypedConfig)) if err != nil { return fmt.Errorf("Could not extract manager from listener %s", cachedListener.Name) } @@ -1044,7 +1044,7 @@ func validateAuthz(listeners []cacheTypes.Resource, params ListenerParams) error if len(filterChain.Filters) == 0 { return fmt.Errorf("No filters found in listener %s", cachedListener.Name) } - manager, err := getManager((filterChain.Filters[0].ConfigType).(*api.Filter_TypedConfig)) + manager, err := getManager((filterChain.Filters[getFilterIndexByName(filterChain.Filters, Envoy_HTTP_Filter)].ConfigType).(*api.Filter_TypedConfig)) if err != nil { return fmt.Errorf("Could not extract manager from listener %s", cachedListener.Name) } diff --git a/pkg/envoy/listener_utils.go b/pkg/envoy/listener_utils.go index b17c4b0..e0b067e 100644 --- a/pkg/envoy/listener_utils.go +++ b/pkg/envoy/listener_utils.go @@ -27,7 +27,7 @@ func getListenerHTTPConnectionManager(ll *api.Listener) (*hcm.HttpConnectionMana if len(ll.FilterChains[0].Filters) == 0 { return manager, fmt.Errorf("No filters found in listener %s", ll.Name) } - manager, err = getManager((ll.FilterChains[0].Filters[0].ConfigType).(*api.Filter_TypedConfig)) + manager, err = getManager((ll.FilterChains[0].Filters[getFilterIndexByName(ll.FilterChains[0].Filters, Envoy_HTTP_Filter)].ConfigType).(*api.Filter_TypedConfig)) if err != nil { return manager, err } @@ -413,3 +413,12 @@ func isDefaultListener(listenerName string) bool { } return false } + +func getFilterIndexByName(filters []*api.Filter, name string) int { + for k, filter := range filters { + if filter.Name == name { + return k + } + } + return -1 +} diff --git a/pkg/envoy/mtls.go b/pkg/envoy/mtls.go index 7e322b1..5a18155 100644 --- a/pkg/envoy/mtls.go +++ b/pkg/envoy/mtls.go @@ -2,12 +2,18 @@ package envoy import ( "fmt" + "strconv" + "strings" core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" api "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + rbacConfig "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" + rbac "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/rbac/v3" tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/wrappers" + "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -42,12 +48,23 @@ func (l *MTLS) updateMTLSListener(cache *WorkQueueCache, params ListenerParams, } ll := cache.listeners[listenerIndex].(*api.Listener) // set proxy protocol filter + ll.ListenerFilters = []*api.ListenerFilter{} if mTLSParams.EnableProxyProtocol { - ll.ListenerFilters = []*api.ListenerFilter{ + ll.ListenerFilters = append(ll.ListenerFilters, &api.ListenerFilter{ + Name: "envoy.filters.listener.proxy_protocol", + }) + } + // set AllowedIPRanges + if len(mTLSParams.AllowedIPRanges) > 0 { + rbacFilter := []*api.Filter{ { - Name: "envoy.filters.listener.proxy_protocol", + Name: "envoy.filters.network.rbac", + ConfigType: &api.Filter_TypedConfig{ + TypedConfig: getRBACConfig(mTLSParams), + }, }, } + ll.FilterChains[0].Filters = append(rbacFilter, ll.FilterChains[0].Filters...) } matchSubjectAltNames := make([]*matcher.StringMatcher, len(mTLSParams.AllowedSubjectAltNames)) for k, name := range mTLSParams.AllowedSubjectAltNames { @@ -104,3 +121,49 @@ func (l *MTLS) updateMTLSListener(cache *WorkQueueCache, params ListenerParams, return nil } +func getRBACConfig(mTLSParams MTLSParams) *anypb.Any { + principals := []*rbacConfig.Principal{} + for _, ipRange := range mTLSParams.AllowedIPRanges { + ipRangeSplit := strings.Split(ipRange, "/") + prefixLen, err := strconv.ParseUint(ipRangeSplit[1], 10, 32) + if len(ipRangeSplit) != 2 || err != nil { + logger.Warningf("Invalid IP address range: %s in listener: l_mtls_%s", ipRange, mTLSParams.Name) + } else { + principals = append(principals, &rbacConfig.Principal{ + + Identifier: &rbacConfig.Principal_DirectRemoteIp{ + DirectRemoteIp: &core.CidrRange{ + AddressPrefix: ipRangeSplit[0], + PrefixLen: &wrappers.UInt32Value{ + Value: uint32(prefixLen), + }, + }, + }, + }) + } + } + r := &rbac.RBAC{ + StatPrefix: "rbac_" + mTLSParams.Name, + Rules: &rbacConfig.RBAC{ + Action: rbacConfig.RBAC_ALLOW, + Policies: map[string]*rbacConfig.Policy{ + "ip_filter": { + Principals: principals, + Permissions: []*rbacConfig.Permission{ + { + Rule: &rbacConfig.Permission_Any{ + Any: true, + }, + }, + }, + }, + }, + }, + } + pbst, err := ptypes.MarshalAny(r) + if err != nil { + panic(err) + } + + return pbst +} diff --git a/pkg/envoy/types.go b/pkg/envoy/types.go index 789b0d9..92a4a69 100644 --- a/pkg/envoy/types.go +++ b/pkg/envoy/types.go @@ -188,6 +188,7 @@ type MTLSParams struct { Certificate string Port int64 AllowedSubjectAltNames []string + AllowedIPRanges []string CACertificate string EnableProxyProtocol bool } diff --git a/pkg/envoy/xds.go b/pkg/envoy/xds.go index 28edc90..fef319a 100644 --- a/pkg/envoy/xds.go +++ b/pkg/envoy/xds.go @@ -363,6 +363,7 @@ func (x *XDS) importMTLS(mTLS pkgApi.MTLS) ([]WorkQueueItem, error) { Certificate: mTLS.Spec.Certificate, CACertificate: mTLS.Spec.CACertificate, AllowedSubjectAltNames: mTLS.Spec.AllowedSubjectAltNames, + AllowedIPRanges: mTLS.Spec.AllowedIPRanges, Port: mTLS.Spec.Port, EnableProxyProtocol: mTLS.Spec.EnableProxyProtocol, },