authenticate: protect /.pomerium/admin endpoint (#1500)

* authenticate: protect /.pomerium/admin endpoint

* add integration test
This commit is contained in:
Caleb Doxsey 2020-10-08 15:44:12 -06:00 committed by GitHub
parent dc1c83c4de
commit 27d0cf180a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 131 additions and 78 deletions

View file

@ -8,6 +8,8 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/pomerium/pomerium/integration/internal/flows"
) )
func TestDashboard(t *testing.T) { func TestDashboard(t *testing.T) {
@ -15,6 +17,28 @@ func TestDashboard(t *testing.T) {
ctx, clearTimeout := context.WithTimeout(ctx, time.Second*30) ctx, clearTimeout := context.WithTimeout(ctx, time.Second*30)
defer clearTimeout() defer clearTimeout()
t.Run("admin impersonate", func(t *testing.T) {
client := testcluster.NewHTTPClient()
res, err := flows.Authenticate(ctx, client, mustParseURL("https://httpdetails.localhost.pomerium.io/by-user"),
flows.WithEmail("bob@dogs.test"), flows.WithGroups("user"))
if !assert.NoError(t, err) {
return
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://httpdetails.localhost.pomerium.io/.pomerium/admin/impersonate", nil)
if err != nil {
t.Fatal(err)
}
res, err = client.Do(req)
if !assert.NoError(t, err, "unexpected http error") {
return
}
defer res.Body.Close()
assertDeniedAccess(t, res)
})
t.Run("user dashboard", func(t *testing.T) { t.Run("user dashboard", func(t *testing.T) {
client := testcluster.NewHTTPClient() client := testcluster.NewHTTPClient()

View file

@ -94,7 +94,7 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
"domains": ["example.com"], "domains": ["example.com"],
"routes": [ "routes": [
{ {
"name": "pomerium-protected-path-/.pomerium/jwt", "name": "pomerium-path-/.pomerium/jwt",
"match": { "match": {
"path": "/.pomerium/jwt" "path": "/.pomerium/jwt"
}, },
@ -132,6 +132,24 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
} }
} }
}, },
{
"name": "pomerium-path-/.pomerium/admin",
"match": {
"path": "/.pomerium/admin"
},
"route": {
"cluster": "pomerium-control-plane-http"
}
},
{
"name": "pomerium-prefix-/.pomerium/admin/",
"match": {
"prefix": "/.pomerium/admin/"
},
"route": {
"cluster": "pomerium-control-plane-http"
}
},
{ {
"name": "pomerium-path-/.pomerium", "name": "pomerium-path-/.pomerium",
"match": { "match": {
@ -214,7 +232,7 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
"domains": ["*"], "domains": ["*"],
"routes": [ "routes": [
{ {
"name": "pomerium-protected-path-/.pomerium/jwt", "name": "pomerium-path-/.pomerium/jwt",
"match": { "match": {
"path": "/.pomerium/jwt" "path": "/.pomerium/jwt"
}, },
@ -252,6 +270,24 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
} }
} }
}, },
{
"name": "pomerium-path-/.pomerium/admin",
"match": {
"path": "/.pomerium/admin"
},
"route": {
"cluster": "pomerium-control-plane-http"
}
},
{
"name": "pomerium-prefix-/.pomerium/admin/",
"match": {
"prefix": "/.pomerium/admin/"
},
"route": {
"cluster": "pomerium-control-plane-http"
}
},
{ {
"name": "pomerium-path-/.pomerium", "name": "pomerium-path-/.pomerium",
"match": { "match": {

View file

@ -49,22 +49,24 @@ func buildGRPCRoutes() []*envoy_config_route_v3.Route {
func buildPomeriumHTTPRoutes(options *config.Options, domain string) []*envoy_config_route_v3.Route { func buildPomeriumHTTPRoutes(options *config.Options, domain string) []*envoy_config_route_v3.Route {
routes := []*envoy_config_route_v3.Route{ routes := []*envoy_config_route_v3.Route{
// enable ext_authz // enable ext_authz
buildControlPlaneProtectedPathRoute("/.pomerium/jwt"), buildControlPlanePathRoute("/.pomerium/jwt", true),
// disable ext_authz and passthrough to proxy handlers // disable ext_authz and passthrough to proxy handlers
buildControlPlanePathRoute("/ping"), buildControlPlanePathRoute("/ping", false),
buildControlPlanePathRoute("/healthz"), buildControlPlanePathRoute("/healthz", false),
buildControlPlanePathRoute("/.pomerium"), buildControlPlanePathRoute("/.pomerium/admin", true),
buildControlPlanePrefixRoute("/.pomerium/"), buildControlPlanePrefixRoute("/.pomerium/admin/", true),
buildControlPlanePathRoute("/.well-known/pomerium"), buildControlPlanePathRoute("/.pomerium", false),
buildControlPlanePrefixRoute("/.well-known/pomerium/"), 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 // per #837, only add robots.txt if there are no unauthenticated routes
if !hasPublicPolicyMatchingURL(options, mustParseURL("https://"+domain+"/robots.txt")) { if !hasPublicPolicyMatchingURL(options, mustParseURL("https://"+domain+"/robots.txt")) {
routes = append(routes, buildControlPlanePathRoute("/robots.txt")) routes = append(routes, buildControlPlanePathRoute("/robots.txt", false))
} }
// if we're handling authentication, add the oauth2 callback url // if we're handling authentication, add the oauth2 callback url
if config.IsAuthenticate(options.Services) && hostMatchesDomain(options.GetAuthenticateURL(), domain) { if config.IsAuthenticate(options.Services) && hostMatchesDomain(options.GetAuthenticateURL(), domain) {
routes = append(routes, buildControlPlanePathRoute(options.AuthenticateCallbackPath)) routes = append(routes, buildControlPlanePathRoute(options.AuthenticateCallbackPath, false))
} }
// if we're the proxy and this is the forward-auth url // if we're the proxy and this is the forward-auth url
if config.IsProxy(options.Services) && options.ForwardAuthURL != nil && hostMatchesDomain(options.GetForwardAuthURL(), domain) { if config.IsProxy(options.Services) && options.ForwardAuthURL != nil && hostMatchesDomain(options.GetForwardAuthURL(), domain) {
@ -97,22 +99,6 @@ func buildControlPlaneProtectedPrefixRoute(prefix string) *envoy_config_route_v3
} }
} }
func buildControlPlaneProtectedPathRoute(path string) *envoy_config_route_v3.Route {
return &envoy_config_route_v3.Route{
Name: "pomerium-protected-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,
},
},
},
}
}
func buildControlPlanePathAndQueryRoute(path string, queryparams []string) *envoy_config_route_v3.Route { func buildControlPlanePathAndQueryRoute(path string, queryparams []string) *envoy_config_route_v3.Route {
var queryParameterMatchers []*envoy_config_route_v3.QueryParameterMatcher var queryParameterMatchers []*envoy_config_route_v3.QueryParameterMatcher
for _, q := range queryparams { for _, q := range queryparams {
@ -142,8 +128,8 @@ func buildControlPlanePathAndQueryRoute(path string, queryparams []string) *envo
} }
} }
func buildControlPlanePathRoute(path string) *envoy_config_route_v3.Route { func buildControlPlanePathRoute(path string, protected bool) *envoy_config_route_v3.Route {
return &envoy_config_route_v3.Route{ r := &envoy_config_route_v3.Route{
Name: "pomerium-path-" + path, Name: "pomerium-path-" + path,
Match: &envoy_config_route_v3.RouteMatch{ Match: &envoy_config_route_v3.RouteMatch{
PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{Path: path}, PathSpecifier: &envoy_config_route_v3.RouteMatch_Path{Path: path},
@ -155,14 +141,17 @@ func buildControlPlanePathRoute(path string) *envoy_config_route_v3.Route {
}, },
}, },
}, },
TypedPerFilterConfig: map[string]*any.Any{
"envoy.filters.http.ext_authz": disableExtAuthz,
},
} }
if !protected {
r.TypedPerFilterConfig = map[string]*any.Any{
"envoy.filters.http.ext_authz": disableExtAuthz,
}
}
return r
} }
func buildControlPlanePrefixRoute(prefix string) *envoy_config_route_v3.Route { func buildControlPlanePrefixRoute(prefix string, protected bool) *envoy_config_route_v3.Route {
return &envoy_config_route_v3.Route{ r := &envoy_config_route_v3.Route{
Name: "pomerium-prefix-" + prefix, Name: "pomerium-prefix-" + prefix,
Match: &envoy_config_route_v3.RouteMatch{ Match: &envoy_config_route_v3.RouteMatch{
PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: prefix}, PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: prefix},
@ -174,10 +163,13 @@ func buildControlPlanePrefixRoute(prefix string) *envoy_config_route_v3.Route {
}, },
}, },
}, },
TypedPerFilterConfig: map[string]*any.Any{
"envoy.filters.http.ext_authz": disableExtAuthz,
},
} }
if !protected {
r.TypedPerFilterConfig = map[string]*any.Any{
"envoy.filters.http.ext_authz": disableExtAuthz,
}
}
return r
} }
var getPolicyName = func(policy *config.Policy) string { var getPolicyName = func(policy *config.Policy) string {

View file

@ -42,33 +42,28 @@ func Test_buildGRPCRoutes(t *testing.T) {
} }
func Test_buildPomeriumHTTPRoutes(t *testing.T) { func Test_buildPomeriumHTTPRoutes(t *testing.T) {
routeString := func(typ, name string) string { routeString := func(typ, name string, protected bool) string {
return `{ str := `{
"name": "pomerium-` + typ + `-` + name + `", "name": "pomerium-` + typ + `-` + name + `",
"match": { "match": {
"` + typ + `": "` + name + `" "` + typ + `": "` + name + `"
}, },
"route": { "route": {
"cluster": "pomerium-control-plane-http" "cluster": "pomerium-control-plane-http"
}, }
`
if !protected {
str += `,
"typedPerFilterConfig": { "typedPerFilterConfig": {
"envoy.filters.http.ext_authz": { "envoy.filters.http.ext_authz": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute", "@type": "type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute",
"disabled": true "disabled": true
} }
} }
}` `
} }
protectedRouteString := func(typ, name string) string { str += "}"
return `{ return str
"name": "pomerium-protected-` + typ + `-` + name + `",
"match": {
"` + typ + `": "` + name + `"
},
"route": {
"cluster": "pomerium-control-plane-http"
}
}`
} }
t.Run("authenticate", func(t *testing.T) { t.Run("authenticate", func(t *testing.T) {
options := &config.Options{ options := &config.Options{
@ -80,15 +75,17 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
routes := buildPomeriumHTTPRoutes(options, "authenticate.example.com") routes := buildPomeriumHTTPRoutes(options, "authenticate.example.com")
testutil.AssertProtoJSONEqual(t, `[ testutil.AssertProtoJSONEqual(t, `[
`+protectedRouteString("path", "/.pomerium/jwt")+`, `+routeString("path", "/.pomerium/jwt", true)+`,
`+routeString("path", "/ping")+`, `+routeString("path", "/ping", false)+`,
`+routeString("path", "/healthz")+`, `+routeString("path", "/healthz", false)+`,
`+routeString("path", "/.pomerium")+`, `+routeString("path", "/.pomerium/admin", true)+`,
`+routeString("prefix", "/.pomerium/")+`, `+routeString("prefix", "/.pomerium/admin/", true)+`,
`+routeString("path", "/.well-known/pomerium")+`, `+routeString("path", "/.pomerium", false)+`,
`+routeString("prefix", "/.well-known/pomerium/")+`, `+routeString("prefix", "/.pomerium/", false)+`,
`+routeString("path", "/robots.txt")+`, `+routeString("path", "/.well-known/pomerium", false)+`,
`+routeString("path", "/oauth2/callback")+` `+routeString("prefix", "/.well-known/pomerium/", false)+`,
`+routeString("path", "/robots.txt", false)+`,
`+routeString("path", "/oauth2/callback", false)+`
]`, routes) ]`, routes)
}) })
@ -107,14 +104,16 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
routes := buildPomeriumHTTPRoutes(options, "from.example.com") routes := buildPomeriumHTTPRoutes(options, "from.example.com")
testutil.AssertProtoJSONEqual(t, `[ testutil.AssertProtoJSONEqual(t, `[
`+protectedRouteString("path", "/.pomerium/jwt")+`, `+routeString("path", "/.pomerium/jwt", true)+`,
`+routeString("path", "/ping")+`, `+routeString("path", "/ping", false)+`,
`+routeString("path", "/healthz")+`, `+routeString("path", "/healthz", false)+`,
`+routeString("path", "/.pomerium")+`, `+routeString("path", "/.pomerium/admin", true)+`,
`+routeString("prefix", "/.pomerium/")+`, `+routeString("prefix", "/.pomerium/admin/", true)+`,
`+routeString("path", "/.well-known/pomerium")+`, `+routeString("path", "/.pomerium", false)+`,
`+routeString("prefix", "/.well-known/pomerium/")+`, `+routeString("prefix", "/.pomerium/", false)+`,
`+routeString("path", "/robots.txt")+` `+routeString("path", "/.well-known/pomerium", false)+`,
`+routeString("prefix", "/.well-known/pomerium/", false)+`,
`+routeString("path", "/robots.txt", false)+`
]`, routes) ]`, routes)
}) })
@ -134,19 +133,21 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
routes := buildPomeriumHTTPRoutes(options, "from.example.com") routes := buildPomeriumHTTPRoutes(options, "from.example.com")
testutil.AssertProtoJSONEqual(t, `[ testutil.AssertProtoJSONEqual(t, `[
`+protectedRouteString("path", "/.pomerium/jwt")+`, `+routeString("path", "/.pomerium/jwt", true)+`,
`+routeString("path", "/ping")+`, `+routeString("path", "/ping", false)+`,
`+routeString("path", "/healthz")+`, `+routeString("path", "/healthz", false)+`,
`+routeString("path", "/.pomerium")+`, `+routeString("path", "/.pomerium/admin", true)+`,
`+routeString("prefix", "/.pomerium/")+`, `+routeString("prefix", "/.pomerium/admin/", true)+`,
`+routeString("path", "/.well-known/pomerium")+`, `+routeString("path", "/.pomerium", false)+`,
`+routeString("prefix", "/.well-known/pomerium/")+` `+routeString("prefix", "/.pomerium/", false)+`,
`+routeString("path", "/.well-known/pomerium", false)+`,
`+routeString("prefix", "/.well-known/pomerium/", false)+`
]`, routes) ]`, routes)
}) })
} }
func Test_buildControlPlanePathRoute(t *testing.T) { func Test_buildControlPlanePathRoute(t *testing.T) {
route := buildControlPlanePathRoute("/hello/world") route := buildControlPlanePathRoute("/hello/world", false)
testutil.AssertProtoJSONEqual(t, ` testutil.AssertProtoJSONEqual(t, `
{ {
"name": "pomerium-path-/hello/world", "name": "pomerium-path-/hello/world",
@ -167,7 +168,7 @@ func Test_buildControlPlanePathRoute(t *testing.T) {
} }
func Test_buildControlPlanePrefixRoute(t *testing.T) { func Test_buildControlPlanePrefixRoute(t *testing.T) {
route := buildControlPlanePrefixRoute("/hello/world/") route := buildControlPlanePrefixRoute("/hello/world/", false)
testutil.AssertProtoJSONEqual(t, ` testutil.AssertProtoJSONEqual(t, `
{ {
"name": "pomerium-prefix-/hello/world/", "name": "pomerium-prefix-/hello/world/",