pomerium/internal/controlplane/xds_routes.go
Caleb Doxsey 27d0cf180a
authenticate: protect /.pomerium/admin endpoint (#1500)
* authenticate: protect /.pomerium/admin endpoint

* add integration test
2020-10-08 15:44:12 -06:00

391 lines
13 KiB
Go

package controlplane
import (
"fmt"
"net/url"
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"
envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/any"
"github.com/golang/protobuf/ptypes/wrappers"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/urlutil"
)
const (
httpCluster = "pomerium-control-plane-http"
)
func buildGRPCRoutes() []*envoy_config_route_v3.Route {
action := &envoy_config_route_v3.Route_Route{
Route: &envoy_config_route_v3.RouteAction{
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
Cluster: "pomerium-control-plane-grpc",
},
},
}
return []*envoy_config_route_v3.Route{{
Name: "pomerium-grpc",
Match: &envoy_config_route_v3.RouteMatch{
PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{
Prefix: "/",
},
Grpc: &envoy_config_route_v3.RouteMatch_GrpcRouteMatchOptions{},
},
Action: action,
TypedPerFilterConfig: map[string]*any.Any{
"envoy.filters.http.ext_authz": disableExtAuthz,
},
}}
}
func buildPomeriumHTTPRoutes(options *config.Options, domain string) []*envoy_config_route_v3.Route {
routes := []*envoy_config_route_v3.Route{
// enable ext_authz
buildControlPlanePathRoute("/.pomerium/jwt", true),
// disable ext_authz and passthrough to proxy handlers
buildControlPlanePathRoute("/ping", false),
buildControlPlanePathRoute("/healthz", false),
buildControlPlanePathRoute("/.pomerium/admin", true),
buildControlPlanePrefixRoute("/.pomerium/admin/", true),
buildControlPlanePathRoute("/.pomerium", false),
buildControlPlanePrefixRoute("/.pomerium/", false),
buildControlPlanePathRoute("/.well-known/pomerium", false),
buildControlPlanePrefixRoute("/.well-known/pomerium/", false),
}
// per #837, only add robots.txt if there are no unauthenticated routes
if !hasPublicPolicyMatchingURL(options, mustParseURL("https://"+domain+"/robots.txt")) {
routes = append(routes, buildControlPlanePathRoute("/robots.txt", false))
}
// if we're handling authentication, add the oauth2 callback url
if config.IsAuthenticate(options.Services) && hostMatchesDomain(options.GetAuthenticateURL(), domain) {
routes = append(routes, buildControlPlanePathRoute(options.AuthenticateCallbackPath, false))
}
// if we're the proxy and this is the forward-auth url
if config.IsProxy(options.Services) && options.ForwardAuthURL != nil && hostMatchesDomain(options.GetForwardAuthURL(), domain) {
routes = append(routes,
// disable ext_authz and pass request to proxy handlers that enable authN flow
buildControlPlanePathAndQueryRoute("/verify", []string{urlutil.QueryForwardAuthURI, urlutil.QuerySessionEncrypted, urlutil.QueryRedirectURI}),
buildControlPlanePathAndQueryRoute("/", []string{urlutil.QueryForwardAuthURI, urlutil.QuerySessionEncrypted, urlutil.QueryRedirectURI}),
buildControlPlanePathAndQueryRoute("/", []string{urlutil.QueryForwardAuthURI}),
// otherwise, enforce ext_authz; pass all other requests through to an upstream
// handler that will simply respond with http status 200 / OK indicating that
// the fronting forward-auth proxy can continue.
buildControlPlaneProtectedPrefixRoute("/"))
}
return routes
}
func buildControlPlaneProtectedPrefixRoute(prefix string) *envoy_config_route_v3.Route {
return &envoy_config_route_v3.Route{
Name: "pomerium-protected-prefix-" + prefix,
Match: &envoy_config_route_v3.RouteMatch{
PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: prefix},
},
Action: &envoy_config_route_v3.Route_Route{
Route: &envoy_config_route_v3.RouteAction{
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
Cluster: httpCluster,
},
},
},
}
}
func buildControlPlanePathAndQueryRoute(path string, queryparams []string) *envoy_config_route_v3.Route {
var queryParameterMatchers []*envoy_config_route_v3.QueryParameterMatcher
for _, q := range queryparams {
queryParameterMatchers = append(queryParameterMatchers,
&envoy_config_route_v3.QueryParameterMatcher{
Name: q,
QueryParameterMatchSpecifier: &envoy_config_route_v3.QueryParameterMatcher_PresentMatch{PresentMatch: true},
})
}
return &envoy_config_route_v3.Route{
Name: "pomerium-path-and-query" + path,
Match: &envoy_config_route_v3.RouteMatch{
PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{Path: path},
QueryParameters: queryParameterMatchers,
},
Action: &envoy_config_route_v3.Route_Route{
Route: &envoy_config_route_v3.RouteAction{
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
Cluster: httpCluster,
},
},
},
TypedPerFilterConfig: map[string]*any.Any{
"envoy.filters.http.ext_authz": disableExtAuthz,
},
}
}
func buildControlPlanePathRoute(path string, protected bool) *envoy_config_route_v3.Route {
r := &envoy_config_route_v3.Route{
Name: "pomerium-path-" + path,
Match: &envoy_config_route_v3.RouteMatch{
PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{Path: path},
},
Action: &envoy_config_route_v3.Route_Route{
Route: &envoy_config_route_v3.RouteAction{
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
Cluster: httpCluster,
},
},
},
}
if !protected {
r.TypedPerFilterConfig = map[string]*any.Any{
"envoy.filters.http.ext_authz": disableExtAuthz,
}
}
return r
}
func buildControlPlanePrefixRoute(prefix string, protected bool) *envoy_config_route_v3.Route {
r := &envoy_config_route_v3.Route{
Name: "pomerium-prefix-" + prefix,
Match: &envoy_config_route_v3.RouteMatch{
PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: prefix},
},
Action: &envoy_config_route_v3.Route_Route{
Route: &envoy_config_route_v3.RouteAction{
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
Cluster: httpCluster,
},
},
},
}
if !protected {
r.TypedPerFilterConfig = map[string]*any.Any{
"envoy.filters.http.ext_authz": disableExtAuthz,
}
}
return r
}
var getPolicyName = func(policy *config.Policy) string {
return fmt.Sprintf("policy-%x", policy.RouteID())
}
func buildPolicyRoutes(options *config.Options, domain string) []*envoy_config_route_v3.Route {
var routes []*envoy_config_route_v3.Route
responseHeadersToAdd := toEnvoyHeaders(options.Headers)
for i, policy := range options.Policies {
if !hostMatchesDomain(policy.Source.URL, domain) {
continue
}
match := mkRouteMatch(&policy)
clusterName := getPolicyName(&policy)
requestHeadersToAdd := toEnvoyHeaders(policy.SetRequestHeaders)
requestHeadersToRemove := getRequestHeadersToRemove(options, &policy)
routeTimeout := getRouteTimeout(options, &policy)
prefixRewrite, regexRewrite := getRewriteOptions(&policy)
routeAction := &envoy_config_route_v3.RouteAction{
ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
Cluster: clusterName,
},
UpgradeConfigs: []*envoy_config_route_v3.RouteAction_UpgradeConfig{
{
UpgradeType: "websocket",
Enabled: &wrappers.BoolValue{Value: policy.AllowWebsockets},
},
{
UpgradeType: "spdy/3.1",
Enabled: &wrappers.BoolValue{Value: policy.AllowSPDY},
},
},
HostRewriteSpecifier: &envoy_config_route_v3.RouteAction_AutoHostRewrite{
AutoHostRewrite: &wrappers.BoolValue{Value: !policy.PreserveHostHeader},
},
Timeout: routeTimeout,
PrefixRewrite: prefixRewrite,
RegexRewrite: regexRewrite,
}
setHostRewriteOptions(&policy, routeAction)
routes = append(routes, &envoy_config_route_v3.Route{
Name: fmt.Sprintf("policy-%d", i),
Match: match,
Metadata: &envoy_config_core_v3.Metadata{
FilterMetadata: map[string]*structpb.Struct{
"envoy.filters.http.lua": {
Fields: map[string]*structpb.Value{
"remove_pomerium_cookie": {
Kind: &structpb.Value_StringValue{
StringValue: options.CookieName,
},
},
"remove_pomerium_authorization": {
Kind: &structpb.Value_BoolValue{
BoolValue: true,
},
},
"remove_impersonate_headers": {
Kind: &structpb.Value_BoolValue{
BoolValue: policy.KubernetesServiceAccountTokenFile != "" || policy.KubernetesServiceAccountToken != "",
},
},
},
},
},
},
Action: &envoy_config_route_v3.Route_Route{Route: routeAction},
RequestHeadersToAdd: requestHeadersToAdd,
RequestHeadersToRemove: requestHeadersToRemove,
ResponseHeadersToAdd: responseHeadersToAdd,
})
}
return routes
}
func mkEnvoyHeader(k, v string) *envoy_config_core_v3.HeaderValueOption {
return &envoy_config_core_v3.HeaderValueOption{
Header: &envoy_config_core_v3.HeaderValue{
Key: k,
Value: v,
},
Append: &wrappers.BoolValue{Value: false},
}
}
func toEnvoyHeaders(headers map[string]string) []*envoy_config_core_v3.HeaderValueOption {
envoyHeaders := make([]*envoy_config_core_v3.HeaderValueOption, 0, len(headers))
for k, v := range headers {
envoyHeaders = append(envoyHeaders, mkEnvoyHeader(k, v))
}
return envoyHeaders
}
func mkRouteMatch(policy *config.Policy) *envoy_config_route_v3.RouteMatch {
match := &envoy_config_route_v3.RouteMatch{}
switch {
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,
},
}
case policy.Path != "":
match.PathSpecifier = &envoy_config_route_v3.RouteMatch_Path{Path: policy.Path}
case policy.Prefix != "":
match.PathSpecifier = &envoy_config_route_v3.RouteMatch_Prefix{Prefix: policy.Prefix}
default:
match.PathSpecifier = &envoy_config_route_v3.RouteMatch_Prefix{Prefix: "/"}
}
return match
}
func getRequestHeadersToRemove(options *config.Options, policy *config.Policy) []string {
requestHeadersToRemove := policy.RemoveRequestHeaders
if !policy.PassIdentityHeaders {
requestHeadersToRemove = append(requestHeadersToRemove, httputil.HeaderPomeriumJWTAssertion)
for _, claim := range options.JWTClaimsHeaders {
requestHeadersToRemove = append(requestHeadersToRemove, httputil.PomeriumJWTHeaderName(claim))
}
}
return requestHeadersToRemove
}
func getRouteTimeout(options *config.Options, policy *config.Policy) *durationpb.Duration {
var routeTimeout *durationpb.Duration
if policy.AllowWebsockets {
if policy.UpstreamTimeout != 0 {
routeTimeout = ptypes.DurationProto(policy.UpstreamTimeout)
} else {
// disable the default route timeout for websocket support
routeTimeout = ptypes.DurationProto(0)
}
} else {
if policy.UpstreamTimeout != 0 {
routeTimeout = ptypes.DurationProto(policy.UpstreamTimeout)
} else {
routeTimeout = ptypes.DurationProto(options.DefaultUpstreamTimeout)
}
}
return routeTimeout
}
func getRewriteOptions(policy *config.Policy) (prefixRewrite string, regexRewrite *envoy_type_matcher_v3.RegexMatchAndSubstitute) {
if policy.PrefixRewrite != "" {
prefixRewrite = policy.PrefixRewrite
} 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,
}
} else if policy.Destination != nil && policy.Destination.Path != "" {
prefixRewrite = policy.Destination.Path
}
return prefixRewrite, regexRewrite
}
func setHostRewriteOptions(policy *config.Policy, action *envoy_config_route_v3.RouteAction) {
switch {
case policy.HostRewrite != "":
action.HostRewriteSpecifier = &envoy_config_route_v3.RouteAction_HostRewriteLiteral{
HostRewriteLiteral: policy.HostRewrite,
}
case policy.HostRewriteHeader != "":
action.HostRewriteSpecifier = &envoy_config_route_v3.RouteAction_HostRewriteHeader{
HostRewriteHeader: policy.HostRewriteHeader,
}
case policy.HostPathRegexRewritePattern != "":
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,
},
}
case policy.PreserveHostHeader:
action.HostRewriteSpecifier = &envoy_config_route_v3.RouteAction_AutoHostRewrite{
AutoHostRewrite: wrapperspb.Bool(false),
}
default:
action.HostRewriteSpecifier = &envoy_config_route_v3.RouteAction_AutoHostRewrite{
AutoHostRewrite: wrapperspb.Bool(true),
}
}
}
func hasPublicPolicyMatchingURL(options *config.Options, requestURL *url.URL) bool {
for _, policy := range options.Policies {
if policy.AllowPublicUnauthenticatedAccess && policy.Matches(requestURL) {
return true
}
}
return false
}
func mustParseURL(str string) *url.URL {
u, err := url.Parse(str)
if err != nil {
panic(err)
}
return u
}