diff --git a/authorize/evaluator/evaluator.go b/authorize/evaluator/evaluator.go index 9cda1e241..0b1c313a3 100644 --- a/authorize/evaluator/evaluator.go +++ b/authorize/evaluator/evaluator.go @@ -38,6 +38,7 @@ type Request struct { // RequestHTTP is the HTTP field in the request. type RequestHTTP struct { Method string `json:"method"` + Hostname string `json:"hostname"` Path string `json:"path"` URL string `json:"url"` Headers map[string]string `json:"headers"` @@ -55,6 +56,7 @@ func NewRequestHTTP( ) RequestHTTP { return RequestHTTP{ Method: method, + Hostname: requestURL.Hostname(), Path: requestURL.Path, URL: requestURL.String(), Headers: headers, @@ -162,7 +164,7 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error) var headersOutput *HeadersResponse eg.Go(func() error { - headersReq := NewHeadersRequestFromPolicy(req.Policy) + headersReq := NewHeadersRequestFromPolicy(req.Policy, req.HTTP.Hostname) headersReq.Session = req.Session var err error headersOutput, err = e.headersEvaluators.Evaluate(ectx, headersReq) diff --git a/authorize/evaluator/headers_evaluator.go b/authorize/evaluator/headers_evaluator.go index 70d3d759f..4b16a8455 100644 --- a/authorize/evaluator/headers_evaluator.go +++ b/authorize/evaluator/headers_evaluator.go @@ -12,7 +12,6 @@ import ( "github.com/pomerium/pomerium/authorize/internal/store" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/telemetry/trace" - "github.com/pomerium/pomerium/internal/urlutil" configpb "github.com/pomerium/pomerium/pkg/grpc/config" ) @@ -29,14 +28,12 @@ type HeadersRequest struct { } // NewHeadersRequestFromPolicy creates a new HeadersRequest from a policy. -func NewHeadersRequestFromPolicy(policy *config.Policy) *HeadersRequest { +func NewHeadersRequestFromPolicy(policy *config.Policy, hostname string) *HeadersRequest { input := new(HeadersRequest) input.EnableGoogleCloudServerlessAuthentication = policy.EnableGoogleCloudServerlessAuthentication input.EnableRoutingKey = policy.EnvoyOpts.GetLbPolicy() == envoy_config_cluster_v3.Cluster_RING_HASH || policy.EnvoyOpts.GetLbPolicy() == envoy_config_cluster_v3.Cluster_MAGLEV - if u, err := urlutil.ParseAndValidateURL(policy.From); err == nil { - input.Issuer = u.Hostname() - } + input.Issuer = hostname input.KubernetesServiceAccountToken = policy.KubernetesServiceAccountToken for _, wu := range policy.To { input.ToAudience = "https://" + wu.URL.Hostname() diff --git a/authorize/evaluator/headers_evaluator_test.go b/authorize/evaluator/headers_evaluator_test.go index 0892569ac..fd02a517c 100644 --- a/authorize/evaluator/headers_evaluator_test.go +++ b/authorize/evaluator/headers_evaluator_test.go @@ -22,13 +22,13 @@ import ( func TestNewHeadersRequestFromPolicy(t *testing.T) { req := NewHeadersRequestFromPolicy(&config.Policy{ EnableGoogleCloudServerlessAuthentication: true, - From: "https://from.example.com", + From: "https://*.example.com", To: config.WeightedURLs{ { URL: *mustParseURL("http://to.example.com"), }, }, - }) + }, "from.example.com") assert.Equal(t, &HeadersRequest{ EnableGoogleCloudServerlessAuthentication: true, Issuer: "from.example.com", diff --git a/config/envoyconfig/listeners.go b/config/envoyconfig/listeners.go index b27b5e156..9776ff9e1 100644 --- a/config/envoyconfig/listeners.go +++ b/config/envoyconfig/listeners.go @@ -7,6 +7,7 @@ import ( "fmt" "net" "net/url" + "strings" "time" envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" @@ -570,7 +571,13 @@ func getAllRouteableHosts(options *config.Options, addr string) ([]string, error allHosts.Add(hosts...) } - return allHosts.ToSlice(), nil + var filtered []string + for _, host := range allHosts.ToSlice() { + if !strings.Contains(host, "*") { + filtered = append(filtered, host) + } + } + return filtered, nil } func urlsMatchHost(urls []*url.URL, host string) bool { diff --git a/config/envoyconfig/route_configurations.go b/config/envoyconfig/route_configurations.go index dae0d507c..8bc6275d1 100644 --- a/config/envoyconfig/route_configurations.go +++ b/config/envoyconfig/route_configurations.go @@ -78,7 +78,7 @@ func (b *Builder) buildMainRouteConfiguration( // if we're the proxy, add all the policy routes if config.IsProxy(cfg.Options.Services) { - rs, err := b.buildPolicyRoutes(cfg.Options, host, requireStrictTransportSecurity) + rs, err := b.buildRoutesForPoliciesWithHost(cfg, host) if err != nil { return nil, err } @@ -94,6 +94,14 @@ func (b *Builder) buildMainRouteConfiguration( if err != nil { return nil, err } + if config.IsProxy(cfg.Options.Services) { + rs, err := b.buildRoutesForPoliciesWithCatchAll(cfg) + if err != nil { + return nil, err + } + vh.Routes = append(vh.Routes, rs...) + } + virtualHosts = append(virtualHosts, vh) rc, err := b.buildRouteConfiguration("main", virtualHosts) diff --git a/config/envoyconfig/route_configurations_test.go b/config/envoyconfig/route_configurations_test.go new file mode 100644 index 000000000..e91f561b5 --- /dev/null +++ b/config/envoyconfig/route_configurations_test.go @@ -0,0 +1,141 @@ +package envoyconfig + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/config/envoyconfig/filemgr" + "github.com/pomerium/pomerium/internal/testutil" + "github.com/pomerium/pomerium/pkg/cryptutil" +) + +func TestBuilder_buildMainRouteConfiguration(t *testing.T) { + t.Parallel() + + ctx := context.Background() + cfg := &config.Config{Options: &config.Options{ + CookieName: "pomerium", + DefaultUpstreamTimeout: time.Second * 3, + SharedKey: cryptutil.NewBase64Key(), + Services: "proxy", + Policies: []config.Policy{ + { + From: "https://*.example.com", + }, + }, + }} + b := New("grpc", "http", "metrics", filemgr.NewManager(), nil) + routeConfiguration, err := b.buildMainRouteConfiguration(ctx, cfg) + assert.NoError(t, err) + testutil.AssertProtoJSONEqual(t, `{ + "name": "main", + "validateClusters": false, + "virtualHosts": [ + { + "name": "catch-all", + "domains": ["*"], + "routes": [ + `+protojson.Format(b.buildControlPlanePathRoute(cfg.Options, "/.pomerium/jwt", true, false))+`, + `+protojson.Format(b.buildControlPlanePathRoute(cfg.Options, "/.pomerium/webauthn", true, false))+`, + `+protojson.Format(b.buildControlPlanePathRoute(cfg.Options, "/ping", false, false))+`, + `+protojson.Format(b.buildControlPlanePathRoute(cfg.Options, "/healthz", false, false))+`, + `+protojson.Format(b.buildControlPlanePathRoute(cfg.Options, "/.pomerium", false, false))+`, + `+protojson.Format(b.buildControlPlanePrefixRoute(cfg.Options, "/.pomerium/", false, false))+`, + `+protojson.Format(b.buildControlPlanePathRoute(cfg.Options, "/.well-known/pomerium", false, false))+`, + `+protojson.Format(b.buildControlPlanePrefixRoute(cfg.Options, "/.well-known/pomerium/", false, false))+`, + `+protojson.Format(b.buildControlPlanePathRoute(cfg.Options, "/robots.txt", false, false))+`, + { + "name": "policy-0", + "match": { + "headers": [ + { "name": ":authority", "stringMatch": { "safeRegex": { "regex": "^(.*)\\.example\\.com$" } }} + ], + "prefix": "/" + }, + "metadata": { + "filterMetadata": { + "envoy.filters.http.lua": { + "remove_impersonate_headers": false, + "remove_pomerium_authorization": true, + "remove_pomerium_cookie": "pomerium", + "rewrite_response_headers": [] + } + } + }, + "requestHeadersToRemove": [ + "x-pomerium-jwt-assertion", + "x-pomerium-jwt-assertion-for", + "x-pomerium-reproxy-policy", + "x-pomerium-reproxy-policy-hmac" + ], + "responseHeadersToAdd": [ + { "appendAction": "OVERWRITE_IF_EXISTS_OR_ADD", "header": { "key": "X-Frame-Options", "value": "SAMEORIGIN" } }, + { "appendAction": "OVERWRITE_IF_EXISTS_OR_ADD", "header": { "key": "X-XSS-Protection", "value": "1; mode=block" } } + ], + "route": { + "autoHostRewrite": true, + "cluster": "route-0", + "hashPolicy": [ + { "header": { "headerName": "x-pomerium-routing-key" }, "terminal": true }, + { "connectionProperties": { "sourceIp": true }, "terminal": true } + ], + "timeout": "3s", + "upgradeConfigs": [ + { "enabled": false, "upgradeType": "websocket" }, + { "enabled": false, "upgradeType": "spdy/3.1" } + ] + } + }, + { + "name": "policy-0", + "match": { + "headers": [ + { "name": ":authority", "stringMatch": { "safeRegex": { "regex": "^(.*)\\.example\\.com:443$" } }} + ], + "prefix": "/" + }, + "metadata": { + "filterMetadata": { + "envoy.filters.http.lua": { + "remove_impersonate_headers": false, + "remove_pomerium_authorization": true, + "remove_pomerium_cookie": "pomerium", + "rewrite_response_headers": [] + } + } + }, + "requestHeadersToRemove": [ + "x-pomerium-jwt-assertion", + "x-pomerium-jwt-assertion-for", + "x-pomerium-reproxy-policy", + "x-pomerium-reproxy-policy-hmac" + ], + "responseHeadersToAdd": [ + { "appendAction": "OVERWRITE_IF_EXISTS_OR_ADD", "header": { "key": "X-Frame-Options", "value": "SAMEORIGIN" } }, + { "appendAction": "OVERWRITE_IF_EXISTS_OR_ADD", "header": { "key": "X-XSS-Protection", "value": "1; mode=block" } } + ], + "route": { + "autoHostRewrite": true, + "cluster": "route-0", + "hashPolicy": [ + { "header": { "headerName": "x-pomerium-routing-key" }, "terminal": true }, + { "connectionProperties": { "sourceIp": true }, "terminal": true } + ], + "timeout": "3s", + "upgradeConfigs": [ + { "enabled": false, "upgradeType": "websocket" }, + { "enabled": false, "upgradeType": "spdy/3.1" } + ] + } + } + ] + } + ] + + }`, routeConfiguration) +} diff --git a/config/envoyconfig/routes.go b/config/envoyconfig/routes.go index f6f719311..7257d5414 100644 --- a/config/envoyconfig/routes.go +++ b/config/envoyconfig/routes.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "sort" + "strings" envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" @@ -18,6 +19,7 @@ import ( "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/urlutil" + "github.com/pomerium/pomerium/pkg/cryptutil" ) const ( @@ -189,14 +191,12 @@ func getClusterStatsName(policy *config.Policy) string { return "" } -func (b *Builder) buildPolicyRoutes( - options *config.Options, +func (b *Builder) buildRoutesForPoliciesWithHost( + cfg *config.Config, host string, - requireStrictTransportSecurity bool, ) ([]*envoy_config_route_v3.Route, error) { var routes []*envoy_config_route_v3.Route - - for i, p := range options.GetAllPolicies() { + for i, p := range cfg.Options.GetAllPolicies() { policy := p fromURL, err := urlutil.ParseAndValidateURL(policy.From) if err != nil { @@ -207,83 +207,162 @@ func (b *Builder) buildPolicyRoutes( continue } - match := mkRouteMatch(&policy) - envoyRoute := &envoy_config_route_v3.Route{ - Name: fmt.Sprintf("policy-%d", i), - Match: match, - Metadata: &envoy_config_core_v3.Metadata{}, - RequestHeadersToAdd: toEnvoyHeaders(policy.SetRequestHeaders), - RequestHeadersToRemove: getRequestHeadersToRemove(options, &policy), - ResponseHeadersToAdd: toEnvoyHeaders(options.GetSetResponseHeadersForPolicy(&policy, requireStrictTransportSecurity)), - } - if policy.Redirect != nil { - action, err := b.buildPolicyRouteRedirectAction(policy.Redirect) - if err != nil { - return nil, err - } - envoyRoute.Action = &envoy_config_route_v3.Route_Redirect{Redirect: action} - } else { - action, err := b.buildPolicyRouteRouteAction(options, &policy) - if err != nil { - return nil, err - } - envoyRoute.Action = &envoy_config_route_v3.Route_Route{Route: action} - } - - luaMetadata := map[string]*structpb.Value{ - "rewrite_response_headers": getRewriteHeadersMetadata(policy.RewriteResponseHeaders), - } - - // disable authentication entirely when the proxy is fronting authenticate - isFrontingAuthenticate, err := isProxyFrontingAuthenticate(options, host) + policyRoutes, err := b.buildRoutesForPolicy(cfg, &policy, fmt.Sprintf("policy-%d", i)) if err != nil { return nil, err } - if isFrontingAuthenticate { - envoyRoute.TypedPerFilterConfig = map[string]*any.Any{ - "envoy.filters.http.ext_authz": disableExtAuthz, - } - } else { - luaMetadata["remove_pomerium_cookie"] = &structpb.Value{ - Kind: &structpb.Value_StringValue{ - StringValue: options.CookieName, - }, - } - luaMetadata["remove_pomerium_authorization"] = &structpb.Value{ - Kind: &structpb.Value_BoolValue{ - BoolValue: true, - }, - } - luaMetadata["remove_impersonate_headers"] = &structpb.Value{ - Kind: &structpb.Value_BoolValue{ - BoolValue: policy.IsForKubernetes(), - }, - } - } - if policy.IsForKubernetes() { - policyID, _ := policy.RouteID() - for _, hdr := range b.reproxy.GetPolicyIDHeaders(policyID) { - envoyRoute.RequestHeadersToAdd = append(envoyRoute.RequestHeadersToAdd, - &envoy_config_core_v3.HeaderValueOption{ - Header: &envoy_config_core_v3.HeaderValue{ - Key: hdr[0], - Value: hdr[1], - }, - AppendAction: envoy_config_core_v3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD, - }) - } - } - - envoyRoute.Metadata.FilterMetadata = map[string]*structpb.Struct{ - "envoy.filters.http.lua": {Fields: luaMetadata}, - } - - routes = append(routes, envoyRoute) + routes = append(routes, policyRoutes...) } return routes, nil } +func (b *Builder) buildRoutesForPoliciesWithCatchAll( + cfg *config.Config, +) ([]*envoy_config_route_v3.Route, error) { + var routes []*envoy_config_route_v3.Route + for i, p := range cfg.Options.GetAllPolicies() { + policy := p + fromURL, err := urlutil.ParseAndValidateURL(policy.From) + if err != nil { + return nil, err + } + + if !strings.Contains(fromURL.Host, "*") { + continue + } + + policyRoutes, err := b.buildRoutesForPolicy(cfg, &policy, fmt.Sprintf("policy-%d", i)) + if err != nil { + return nil, err + } + + routes = append(routes, policyRoutes...) + } + return routes, nil +} + +func (b *Builder) buildRoutesForPolicy( + cfg *config.Config, + policy *config.Policy, + name string, +) ([]*envoy_config_route_v3.Route, error) { + fromURL, err := urlutil.ParseAndValidateURL(policy.From) + if err != nil { + return nil, err + } + + var routes []*envoy_config_route_v3.Route + if strings.Contains(fromURL.Host, "*") { + // we have to match '*.example.com' and '*.example.com:443', so there are two routes + for _, host := range urlutil.GetDomainsForURL(fromURL) { + route, err := b.buildRouteForPolicyAndMatch(cfg, policy, name, mkRouteMatchForHost(policy, host)) + if err != nil { + return nil, err + } + routes = append(routes, route) + } + } else { + route, err := b.buildRouteForPolicyAndMatch(cfg, policy, name, mkRouteMatch(policy)) + if err != nil { + return nil, err + } + routes = append(routes, route) + } + return routes, nil +} + +func (b *Builder) buildRouteForPolicyAndMatch( + cfg *config.Config, + policy *config.Policy, + name string, + match *envoy_config_route_v3.RouteMatch, +) (*envoy_config_route_v3.Route, error) { + fromURL, err := urlutil.ParseAndValidateURL(policy.From) + if err != nil { + return nil, err + } + + certs, err := getAllCertificates(cfg) + if err != nil { + return nil, err + } + + requireStrictTransportSecurity := cryptutil.HasCertificateForServerName(certs, fromURL.Hostname()) + + route := &envoy_config_route_v3.Route{ + Name: name, + Match: match, + Metadata: &envoy_config_core_v3.Metadata{}, + RequestHeadersToAdd: toEnvoyHeaders(policy.SetRequestHeaders), + RequestHeadersToRemove: getRequestHeadersToRemove(cfg.Options, policy), + ResponseHeadersToAdd: toEnvoyHeaders(cfg.Options.GetSetResponseHeadersForPolicy(policy, requireStrictTransportSecurity)), + } + if policy.Redirect != nil { + action, err := b.buildPolicyRouteRedirectAction(policy.Redirect) + if err != nil { + return nil, err + } + route.Action = &envoy_config_route_v3.Route_Redirect{Redirect: action} + } else { + action, err := b.buildPolicyRouteRouteAction(cfg.Options, policy) + if err != nil { + return nil, err + } + route.Action = &envoy_config_route_v3.Route_Route{Route: action} + } + + luaMetadata := map[string]*structpb.Value{ + "rewrite_response_headers": getRewriteHeadersMetadata(policy.RewriteResponseHeaders), + } + + // disable authentication entirely when the proxy is fronting authenticate + isFrontingAuthenticate, err := isProxyFrontingAuthenticate(cfg.Options, fromURL.Hostname()) + if err != nil { + return nil, err + } + if isFrontingAuthenticate { + route.TypedPerFilterConfig = map[string]*any.Any{ + "envoy.filters.http.ext_authz": disableExtAuthz, + } + } else { + luaMetadata["remove_pomerium_cookie"] = &structpb.Value{ + Kind: &structpb.Value_StringValue{ + StringValue: cfg.Options.CookieName, + }, + } + luaMetadata["remove_pomerium_authorization"] = &structpb.Value{ + Kind: &structpb.Value_BoolValue{ + BoolValue: true, + }, + } + luaMetadata["remove_impersonate_headers"] = &structpb.Value{ + Kind: &structpb.Value_BoolValue{ + BoolValue: policy.IsForKubernetes(), + }, + } + } + + if policy.IsForKubernetes() { + policyID, _ := policy.RouteID() + for _, hdr := range b.reproxy.GetPolicyIDHeaders(policyID) { + route.RequestHeadersToAdd = append(route.RequestHeadersToAdd, + &envoy_config_core_v3.HeaderValueOption{ + Header: &envoy_config_core_v3.HeaderValue{ + Key: hdr[0], + Value: hdr[1], + }, + AppendAction: envoy_config_core_v3.HeaderValueOption_OVERWRITE_IF_EXISTS_OR_ADD, + }) + } + } + + route.Metadata.FilterMetadata = map[string]*structpb.Struct{ + "envoy.filters.http.lua": {Fields: luaMetadata}, + } + return route, nil +} + func (b *Builder) buildPolicyRouteRedirectAction(r *config.PolicyRedirect) (*envoy_config_route_v3.RedirectAction, error) { action := &envoy_config_route_v3.RedirectAction{} switch { @@ -420,9 +499,6 @@ func mkRouteMatch(policy *config.Policy) *envoy_config_route_v3.RouteMatch { case policy.Regex != "": match.PathSpecifier = &envoy_config_route_v3.RouteMatch_SafeRegex{ SafeRegex: &envoy_type_matcher_v3.RegexMatcher{ - EngineType: &envoy_type_matcher_v3.RegexMatcher_GoogleRe2{ - GoogleRe2: &envoy_type_matcher_v3.RegexMatcher_GoogleRE2{}, - }, Regex: policy.Regex, }, } @@ -436,6 +512,26 @@ func mkRouteMatch(policy *config.Policy) *envoy_config_route_v3.RouteMatch { return match } +func mkRouteMatchForHost( + policy *config.Policy, + host string, +) *envoy_config_route_v3.RouteMatch { + match := mkRouteMatch(policy) + match.Headers = append(match.Headers, &envoy_config_route_v3.HeaderMatcher{ + Name: ":authority", + HeaderMatchSpecifier: &envoy_config_route_v3.HeaderMatcher_StringMatch{ + StringMatch: &envoy_type_matcher_v3.StringMatcher{ + MatchPattern: &envoy_type_matcher_v3.StringMatcher_SafeRegex{ + SafeRegex: &envoy_type_matcher_v3.RegexMatcher{ + Regex: config.WildcardToRegex(host), + }, + }, + }, + }, + }) + return match +} + func getRequestHeadersToRemove(options *config.Options, policy *config.Policy) []string { requestHeadersToRemove := policy.RemoveRequestHeaders if !policy.PassIdentityHeaders { @@ -489,9 +585,6 @@ func getRewriteOptions(policy *config.Policy) (prefixRewrite string, regexRewrit } else if policy.RegexRewritePattern != "" { regexRewrite = &envoy_type_matcher_v3.RegexMatchAndSubstitute{ Pattern: &envoy_type_matcher_v3.RegexMatcher{ - EngineType: &envoy_type_matcher_v3.RegexMatcher_GoogleRe2{ - GoogleRe2: &envoy_type_matcher_v3.RegexMatcher_GoogleRE2{}, - }, Regex: policy.RegexRewritePattern, }, Substitution: policy.RegexRewriteSubstitution, @@ -517,9 +610,6 @@ func setHostRewriteOptions(policy *config.Policy, action *envoy_config_route_v3. action.HostRewriteSpecifier = &envoy_config_route_v3.RouteAction_HostRewritePathRegex{ HostRewritePathRegex: &envoy_type_matcher_v3.RegexMatchAndSubstitute{ Pattern: &envoy_type_matcher_v3.RegexMatcher{ - EngineType: &envoy_type_matcher_v3.RegexMatcher_GoogleRe2{ - GoogleRe2: &envoy_type_matcher_v3.RegexMatcher_GoogleRE2{}, - }, Regex: policy.HostPathRegexRewritePattern, }, Substitution: policy.HostPathRegexRewriteSubstitution, diff --git a/config/envoyconfig/routes_test.go b/config/envoyconfig/routes_test.go index 61080474f..36f8da500 100644 --- a/config/envoyconfig/routes_test.go +++ b/config/envoyconfig/routes_test.go @@ -17,6 +17,7 @@ import ( "github.com/pomerium/pomerium/config/envoyconfig/filemgr" "github.com/pomerium/pomerium/internal/testutil" "github.com/pomerium/pomerium/internal/urlutil" + "github.com/pomerium/pomerium/pkg/cryptutil" ) func policyNameFunc() func(*config.Policy) string { @@ -293,9 +294,10 @@ func TestTimeouts(t *testing.T) { for _, tc := range testCases { b := &Builder{filemgr: filemgr.NewManager()} - routes, err := b.buildPolicyRoutes(&config.Options{ + routes, err := b.buildRoutesForPoliciesWithHost(&config.Config{Options: &config.Options{ CookieName: "pomerium", DefaultUpstreamTimeout: time.Second * 3, + SharedKey: cryptutil.NewBase64Key(), Policies: []config.Policy{ { From: "https://example.com", @@ -305,7 +307,7 @@ func TestTimeouts(t *testing.T) { AllowWebsockets: tc.allowWebsockets, }, }, - }, "example.com", false) + }}, "example.com") if !assert.NoError(t, err, "%v", tc) || !assert.Len(t, routes, 1, tc) || !assert.NotNil(t, routes[0].GetRoute(), "%v", tc) { continue } @@ -347,9 +349,10 @@ func Test_buildPolicyRoutes(t *testing.T) { ten := time.Second * 10 b := &Builder{filemgr: filemgr.NewManager()} - routes, err := b.buildPolicyRoutes(&config.Options{ + routes, err := b.buildRoutesForPoliciesWithHost(&config.Config{Options: &config.Options{ CookieName: "pomerium", DefaultUpstreamTimeout: time.Second * 3, + SharedKey: cryptutil.NewBase64Key(), Policies: []config.Policy{ { From: "https://ignore.example.com", @@ -409,7 +412,7 @@ func Test_buildPolicyRoutes(t *testing.T) { UpstreamTimeout: &ten, }, }, - }, "example.com", false) + }}, "example.com") require.NoError(t, err) testutil.AssertProtoJSONEqual(t, ` @@ -603,7 +606,6 @@ func Test_buildPolicyRoutes(t *testing.T) { "name": "policy-4", "match": { "safeRegex": { - "googleRe2": {}, "regex": "^/[a]+$" } }, @@ -904,18 +906,19 @@ func Test_buildPolicyRoutes(t *testing.T) { `, routes) t.Run("fronting-authenticate", func(t *testing.T) { - routes, err := b.buildPolicyRoutes(&config.Options{ + routes, err := b.buildRoutesForPoliciesWithHost(&config.Config{Options: &config.Options{ AuthenticateURLString: "https://authenticate.example.com", Services: "proxy", CookieName: "pomerium", DefaultUpstreamTimeout: time.Second * 3, + SharedKey: cryptutil.NewBase64Key(), Policies: []config.Policy{ { From: "https://authenticate.example.com", PassIdentityHeaders: true, }, }, - }, "authenticate.example.com", false) + }}, "authenticate.example.com") require.NoError(t, err) testutil.AssertProtoJSONEqual(t, ` @@ -987,9 +990,10 @@ func Test_buildPolicyRoutes(t *testing.T) { }) t.Run("tcp", func(t *testing.T) { - routes, err := b.buildPolicyRoutes(&config.Options{ + routes, err := b.buildRoutesForPoliciesWithHost(&config.Config{Options: &config.Options{ CookieName: "pomerium", DefaultUpstreamTimeout: time.Second * 3, + SharedKey: cryptutil.NewBase64Key(), Policies: []config.Policy{ { From: "tcp+https://example.com:22", @@ -1001,7 +1005,7 @@ func Test_buildPolicyRoutes(t *testing.T) { UpstreamTimeout: &ten, }, }, - }, "example.com:22", false) + }}, "example.com:22") require.NoError(t, err) testutil.AssertProtoJSONEqual(t, ` @@ -1133,11 +1137,12 @@ func Test_buildPolicyRoutes(t *testing.T) { }) t.Run("remove-pomerium-headers", func(t *testing.T) { - routes, err := b.buildPolicyRoutes(&config.Options{ + routes, err := b.buildRoutesForPoliciesWithHost(&config.Config{Options: &config.Options{ AuthenticateURLString: "https://authenticate.example.com", Services: "proxy", CookieName: "pomerium", DefaultUpstreamTimeout: time.Second * 3, + SharedKey: cryptutil.NewBase64Key(), JWTClaimsHeaders: map[string]string{ "x-email": "email", }, @@ -1146,7 +1151,7 @@ func Test_buildPolicyRoutes(t *testing.T) { From: "https://from.example.com", }, }, - }, "from.example.com", false) + }}, "from.example.com") require.NoError(t, err) testutil.AssertProtoJSONEqual(t, ` @@ -1224,9 +1229,10 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { }(getClusterID) getClusterID = policyNameFunc() b := &Builder{filemgr: filemgr.NewManager()} - routes, err := b.buildPolicyRoutes(&config.Options{ + routes, err := b.buildRoutesForPoliciesWithHost(&config.Config{Options: &config.Options{ CookieName: "pomerium", DefaultUpstreamTimeout: time.Second * 3, + SharedKey: cryptutil.NewBase64Key(), Policies: []config.Policy{ { From: "https://example.com", @@ -1266,7 +1272,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { HostPathRegexRewriteSubstitution: "\\1", }, }, - }, "example.com", false) + }}, "example.com") require.NoError(t, err) testutil.AssertProtoJSONEqual(t, ` @@ -1410,7 +1416,6 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { "autoHostRewrite": true, "regexRewrite": { "pattern": { - "googleRe2": {}, "regex": "^/service/([^/]+)(/.*)$" }, "substitution": "\\2/instance/\\1" @@ -1595,7 +1600,6 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { "route": { "hostRewritePathRegex": { "pattern": { - "googleRe2": {}, "regex": "^/(.+)/.+$" }, "substitution": "\\1" diff --git a/config/from.go b/config/from.go new file mode 100644 index 000000000..359600f2e --- /dev/null +++ b/config/from.go @@ -0,0 +1,47 @@ +package config + +import ( + "net/url" + "regexp" + "strings" + + "github.com/pomerium/pomerium/internal/urlutil" +) + +// FromURLMatchesRequestURL returns true if the from URL matches the request URL. +func FromURLMatchesRequestURL(fromURL, requestURL *url.URL) bool { + for _, domain := range urlutil.GetDomainsForURL(fromURL) { + if domain == requestURL.Host { + return true + } + + if !strings.Contains(domain, "*") { + continue + } + + reStr := WildcardToRegex(domain) + re := regexp.MustCompile(reStr) + if re.MatchString(requestURL.Host) { + return true + } + } + return false +} + +// WildcardToRegex converts a wildcard string to a regular expression. +func WildcardToRegex(wildcard string) string { + var b strings.Builder + b.WriteByte('^') + for { + idx := strings.IndexByte(wildcard, '*') + if idx < 0 { + break + } + b.WriteString(regexp.QuoteMeta(wildcard[:idx])) + b.WriteString("(.*)") + wildcard = wildcard[idx+1:] + } + b.WriteString(regexp.QuoteMeta(wildcard)) + b.WriteByte('$') + return b.String() +} diff --git a/config/from_test.go b/config/from_test.go new file mode 100644 index 000000000..48a6661a4 --- /dev/null +++ b/config/from_test.go @@ -0,0 +1,38 @@ +package config + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/pomerium/pomerium/internal/urlutil" +) + +func TestFromURLMatchesRequestURL(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + pattern string + input string + matches bool + }{ + {"https://from.example.com", "https://from.example.com/some/path", true}, + {"https://from.example.com", "https://to.example.com/some/path", false}, + {"https://*.example.com", "https://from.example.com/some/path", true}, + {"https://*.example.com", "https://example.com/some/path", false}, + } { + fromURL := urlutil.MustParseAndValidateURL(tc.pattern) + requestURL := urlutil.MustParseAndValidateURL(tc.input) + assert.Equal(t, tc.matches, FromURLMatchesRequestURL(&fromURL, &requestURL), + "from-url: %s\nrequest-url: %s", tc.pattern, tc.input) + } +} + +func TestWildcardToRegex(t *testing.T) { + t.Parallel() + + re, err := regexp.Compile(WildcardToRegex("*.internal.*.example.com")) + assert.NoError(t, err) + assert.True(t, re.MatchString("a.internal.b.example.com")) +} diff --git a/config/policy.go b/config/policy.go index 3f8a8776c..9258cb523 100644 --- a/config/policy.go +++ b/config/policy.go @@ -595,12 +595,7 @@ func (p *Policy) Matches(requestURL url.URL) bool { return false } - // make sure one of the host domains matches the incoming url - found := false - for _, host := range urlutil.GetDomainsForURL(fromURL) { - found = found || host == requestURL.Host - } - if !found { + if !FromURLMatchesRequestURL(fromURL, &requestURL) { return false } diff --git a/internal/autocert/manager.go b/internal/autocert/manager.go index 3af8f8c2b..2ca39b96f 100644 --- a/internal/autocert/manager.go +++ b/internal/autocert/manager.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "sort" + "strings" "sync" "sync/atomic" "time" @@ -439,7 +440,7 @@ func sourceHostnames(cfg *config.Config) []string { dedupe := map[string]struct{}{} for _, p := range policies { - if u, _ := urlutil.ParseAndValidateURL(p.From); u != nil { + if u, _ := urlutil.ParseAndValidateURL(p.From); u != nil && !strings.Contains(u.Host, "*") { dedupe[u.Hostname()] = struct{}{} } }