Skip to content

Commit

Permalink
internal: filter misdirected TLS requests
Browse files Browse the repository at this point in the history
TLS routes are specialized to a unique virtual hostname. However, if
wildcard certificates are being used, browsers will aggressively coalesce
and reuse server connections even when the full origin hostname doesn't
match. This results on 404 responses because each TLS virtual host only
has routes for one host.

We can avoid this behaviour bleeding out to users by generating a 421
Misdirected Request response if the request authority doesn't match
the FQDN of the virtual host. In this case, the browser is supposed
to understand that the request wasn't processed and re-send it on a
new connection.

This fixes projectcontour#1493.

Signed-off-by: James Peach <jpeach@vmware.com>
  • Loading branch information
jpeach committed May 20, 2020
1 parent bec00c2 commit 2efdcc0
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 92 deletions.
204 changes: 204 additions & 0 deletions _integration/testsuite/httpproxy/009-https-misdirected-request.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Copyright 2020 VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import data.contour.resources

# Ensure that cert-manager is installed.
# Version check the certificates resource.

Group := "cert-manager.io"
Version := "v1alpha2"

have_certmanager_version {
v := resources.versions["certificates"]
v[_].Group == Group
v[_].Version == Version
}

skip[msg] {
not resources.is_supported("certificates")
msg := "cert-manager is not installed"
}

skip[msg] {
not have_certmanager_version

avail := resources.versions["certificates"]

msg := concat("\n", [
sprintf("cert-manager version %s/%s is not installed", [Group, Version]),
"available versions:",
yaml.marshal(avail)
])
}

---

# Create a self-signed issuer to give us secrets.

apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
name: selfsigned
spec:
selfSigned: {}

---

apiVersion: apps/v1
kind: Deployment
metadata:
name: ingress-conformance-echo
$apply:
fixture:
as: echo

---

apiVersion: v1
kind: Service
metadata:
name: ingress-conformance-echo
$apply:
fixture:
as: echo

---

apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: echo-cert
spec:
dnsNames:
- echo.projectcontour.io
secretName: echo
issuerRef:
name: selfsigned

---

apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: echo
spec:
virtualhost:
fqdn: echo.projectcontour.io
tls:
secretName: echo
routes:
- services:
- name: echo
port: 80

---

import data.contour.resources

Name := "echo"

fatal_proxy_is_not_present[msg] {
not resources.is_present("httpproxies", Name)
msg := sprintf("HTTPProxy for %q is not present", [ Name ])
}

---

import data.contour.resources

Name := "echo"

fatal_proxy_is_not_valid[msg] {
status := resources.status("httpproxies", Name)

object.get(status, "currentStatus", "") != "valid"

msg := sprintf("HTTPProxy %q is not valid\n%s", [
Name, yaml.marshal(status)
])
}

---

import data.contour.http.client
import data.contour.http.request
import data.contour.http.response

Response := client.Get({
"url": sprintf("https://%s/misdirected/%d", [
client.target_addr, time.now_ns()
]),
"headers": {
"Host": "echo.projectcontour.io",
"User-Agent": client.ua("misdirected-request"),
},
"tls_insecure_skip_verify": true,
})

error_non_200_response [msg] {
not Response
msg := "no response"
}

error_non_200_response [msg] {
status := object.get(Response, "status_code", 000)
status != 200
msg := sprintf("got status %d, wanted %d", [status, 200])
}

error_wrong_routing [msg] {
not response.has_testid(Response)
msg := "response has missing body or test ID"
}

error_wrong_routing[msg] {
wanted := "echo"
testid := response.testid(Response)
testid != wanted
msg := sprintf("got test ID %q, wanted %q", [testid, wanted])
}

---

import data.contour.http.client
import data.contour.http.request
import data.contour.http.response

# Send a request with a Host header that doesn't match the SNI name that
# we have for the proxy document. We expect the mismatch will generate a
# 421 respnse, not 404.

Response := client.Get({
"url": sprintf("https://%s/misdirected/%d", [
client.target_addr, time.now_ns()
]),
"headers": {
"Host": "echo-two.projectcontour.io",
"User-Agent": client.ua("misdirected-request"),
},
"tls_server_name": "echo.projectcontour.io",
"tls_insecure_skip_verify": true,
})

error_non_421_response [msg] {
not Response
msg := "no response"
}

error_non_421_response [msg] {
status := object.get(Response, "status_code", 000)
status != 421
msg := sprintf("got status %d, wanted %d", [status, 421])
}
11 changes: 11 additions & 0 deletions _integration/testsuite/policies/contour-resources.rego
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,14 @@ get(resource, name) = obj {
} else = obj {
obj := {}
}

# status returns the status field of the named resource. If the resource
# is not present, and empty object is returned. Implemented in terms of
# 'get', so namespace syntax works for the object name.
#
# Examples:
# resources.status("httpproxies", "foo")
status(resource, name) = s {
r := get(resource, name)
s := object.get(r, "status", {})
}
3 changes: 3 additions & 0 deletions internal/contour/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ func visitListeners(root dag.Vertex, lvc *ListenerVisitorConfig) map[string]*v2.
if lv.http {
// Add a listener if there are vhosts bound to http.
cm := envoy.HTTPConnectionManagerBuilder().
DefaultFilters().
RouteConfigName(ENVOY_HTTP_LISTENER).
MetricsPrefix(ENVOY_HTTP_LISTENER).
AccessLoggers(lvc.newInsecureAccessLog()).
Expand Down Expand Up @@ -366,6 +367,8 @@ func (v *listenerVisitor) visit(vertex dag.Vertex) {
// coded into monitoring dashboards.
filters = envoy.Filters(
envoy.HTTPConnectionManagerBuilder().
AddFilter(envoy.FilterMisdirectedRequests(vh.VirtualHost.Name)).
DefaultFilters().
RouteConfigName(path.Join("https", vh.VirtualHost.Name)).
MetricsPrefix(ENVOY_HTTPS_LISTENER).
AccessLoggers(v.ListenerVisitorConfig.newSecureAccessLog()).
Expand Down
4 changes: 4 additions & 0 deletions internal/contour/listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ func TestListenerCacheQuery(t *testing.T) {
func TestListenerVisit(t *testing.T) {
httpsFilterFor := func(vhost string) *envoy_api_v2_listener.Filter {
return envoy.HTTPConnectionManagerBuilder().
AddFilter(envoy.FilterMisdirectedRequests(vhost)).
DefaultFilters().
MetricsPrefix(ENVOY_HTTPS_LISTENER).
RouteConfigName(path.Join("https", vhost)).
AccessLoggers(envoy.FileAccessLogEnvoy(DEFAULT_HTTP_ACCESS_LOG)).
Expand Down Expand Up @@ -790,6 +792,8 @@ func TestListenerVisit(t *testing.T) {
},
TransportSocket: transportSocket("secret", envoy_api_v2_auth.TlsParameters_TLSv1_1, "h2", "http/1.1"),
Filters: envoy.Filters(envoy.HTTPConnectionManagerBuilder().
AddFilter(envoy.FilterMisdirectedRequests("whatever.example.com")).
DefaultFilters().
MetricsPrefix(ENVOY_HTTPS_LISTENER).
RouteConfigName(path.Join("https", "whatever.example.com")).
AccessLoggers(envoy.FileAccessLogEnvoy("/tmp/https_access.log")).
Expand Down
Loading

0 comments on commit 2efdcc0

Please sign in to comment.