Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ check-integration:
integration: check-integration
./_integration/testsuite/make-kind-cluster.sh
./_integration/testsuite/install-contour-working.sh
./_integration/testsuite/run-test-case.sh ./_integration/testsuite/httpproxy/*.yaml ./_integration/testsuite/gatewayapi/*.yaml
./_integration/testsuite/run-test-case.sh ./_integration/testsuite/ingress/*.yaml ./_integration/testsuite/httpproxy/*.yaml ./_integration/testsuite/gatewayapi/*.yaml
./_integration/testsuite/cleanup.sh

check-ingress-conformance: ## Run Ingress controller conformance
Expand Down
165 changes: 165 additions & 0 deletions _integration/testsuite/ingress/001-ingress-tls-wildcard-host.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Copyright Project Contour Authors
#
# 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 := "v1"

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/v1
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/v1
kind: Certificate
metadata:
name: wildcard-cert
spec:
dnsNames:
- "*.projectcontour.io"
secretName: wildcard
issuerRef:
name: selfsigned

---

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wildcard-ingress
spec:
tls:
- hosts:
- "*.projectcontour.io"
secretName: wildcard
rules:
- host: "*.projectcontour.io"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: echo
port:
number: 80

---

import data.contour.http.client
import data.contour.http.client.url
import data.contour.http.expect

import data.builtin.result

# Hostname, SNI, expected status code.
cases := [
[ "random1.projectcontour.io", "", 200 ],
[ "random2.projectcontour.io", "random2.projectcontour.io", 200 ],
[ "a.random3.projectcontour.io", "a.random3.projectcontour.io", 404 ],
[ "random4.projectcontour.io", "other-random4.projectcontour.io", 421 ],
[ "random5.projectcontour.io", "a.random5.projectcontour.io", 421 ],
[ "random6.projectcontour.io:9999", "random6.projectcontour.io", 200 ],
]

request_for_host[host] = request {
c := cases[_]
host := c[0]
sni := c[1]
request := {
"method": "GET",
"url": url.https("/echo"),
"headers": {
"Host": host,
"User-Agent": client.ua("ingress-tls-wildcard"),
},
"tls_server_name": sni,
"tls_insecure_skip_verify": true,
}
}

response_for_host[host] = resp {
c := cases[_]
host := c[0]
request := request_for_host[host]
resp := http.send(request)
}

# Ensure that we get a response for each test case.
error_missing_responses {
count(cases) != count(response_for_host)
}

check_for_status_code [msg] {
c := cases[_]
host := c[0]
status := c[2]
resp := response_for_host[host]
msg := expect.response_status_is(resp, status)
}
57 changes: 30 additions & 27 deletions internal/dag/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2620,16 +2620,7 @@ func TestDAGInsert(t *testing.T) {
},
Spec: networking_v1.IngressSpec{
Rules: []networking_v1.IngressRule{{
// no hostname
IngressRuleValue: networking_v1.IngressRuleValue{
HTTP: &networking_v1.HTTPIngressRuleValue{
Paths: []networking_v1.HTTPIngressPath{{
Backend: *backendv1("kuard", intstr.FromString("http")),
}},
},
},
}, {
Host: "*",
// No hostname.
IngressRuleValue: networking_v1.IngressRuleValue{
HTTP: &networking_v1.HTTPIngressRuleValue{
Paths: []networking_v1.HTTPIngressPath{{
Expand All @@ -2638,11 +2629,13 @@ func TestDAGInsert(t *testing.T) {
},
},
}, {
// Allow wildcard as first label.
// K8s will only allow hostnames with wildcards of this form.
Host: "*.example.com",
IngressRuleValue: networking_v1.IngressRuleValue{
HTTP: &networking_v1.HTTPIngressRuleValue{
Paths: []networking_v1.HTTPIngressPath{{
Backend: *backendv1("kuarder", intstr.FromInt(8080)),
Backend: *backendv1("kuard", intstr.FromString("http")),
}},
},
},
Expand Down Expand Up @@ -3339,7 +3332,7 @@ func TestDAGInsert(t *testing.T) {
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{{
// no hostname
// No hostname.
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{{
Expand All @@ -3350,7 +3343,9 @@ func TestDAGInsert(t *testing.T) {
},
},
}, {
Host: "*",
// Allow wildcard as first label.
// K8s will only allow hostnames with wildcards of this form.
Host: "*.example.com",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{{
Expand All @@ -3361,18 +3356,6 @@ func TestDAGInsert(t *testing.T) {
}},
},
},
}, {
Host: "*.example.com",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{{
Backend: v1beta1.IngressBackend{
ServiceName: "kuarder",
ServicePort: intstr.FromInt(8080),
},
}},
},
},
}},
},
}
Expand Down Expand Up @@ -7343,7 +7326,6 @@ func TestDAGInsert(t *testing.T) {
},
),
},
// issue 1234
"insert ingress with wildcard hostnames": {
objs: []interface{}{
s1,
Expand All @@ -7354,6 +7336,17 @@ func TestDAGInsert(t *testing.T) {
Port: 80,
VirtualHosts: virtualhosts(
virtualhost("*", prefixroute("/", service(s1))),
virtualhost("*.example.com", &Route{
PathMatchCondition: &PrefixMatchCondition{Prefix: "/"},
HeaderMatchConditions: []HeaderMatchCondition{
{
Name: ":authority",
MatchType: HeaderMatchTypeRegex,
Value: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?\\.example\\.com",
},
},
Clusters: clusters(service(s1)),
}),
),
},
),
Expand Down Expand Up @@ -7592,7 +7585,6 @@ func TestDAGInsert(t *testing.T) {
},
),
},
// issue 1234
"ingressv1: insert ingress with wildcard hostnames": {
objs: []interface{}{
s1,
Expand All @@ -7603,6 +7595,17 @@ func TestDAGInsert(t *testing.T) {
Port: 80,
VirtualHosts: virtualhosts(
virtualhost("*", prefixroute("/", service(s1))),
virtualhost("*.example.com", &Route{
PathMatchCondition: &PrefixMatchCondition{Prefix: "/"},
HeaderMatchConditions: []HeaderMatchCondition{
{
Name: ":authority",
MatchType: HeaderMatchTypeRegex,
Value: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?\\.example\\.com",
},
},
Clusters: clusters(service(s1)),
}),
),
},
),
Expand Down
4 changes: 4 additions & 0 deletions internal/dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ const (

// HeaderMatchTypePresent matches a header if it is present in a request.
HeaderMatchTypePresent = "present"

// HeaderMatchTypeRegex matches a header if it matches the provided regular
// expression.
HeaderMatchTypeRegex = "regex"
)

// HeaderMatchCondition matches request headers by MatchType
Expand Down
33 changes: 26 additions & 7 deletions internal/dag/ingress_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package dag

import (
"regexp"
"strings"

"k8s.io/apimachinery/pkg/util/intstr"
Expand Down Expand Up @@ -109,12 +110,9 @@ func (p *IngressProcessor) computeIngresses() {

func (p *IngressProcessor) computeIngressRule(ing *networking_v1.Ingress, rule networking_v1.IngressRule) {
host := rule.Host
if strings.Contains(host, "*") {
// reject hosts with wildcard characters.
return
}

// If host name is blank, rewrite to Envoy's * default host.
if host == "" {
// if host name is blank, rewrite to Envoy's * default host.
host = "*"
}

Expand Down Expand Up @@ -156,7 +154,7 @@ func (p *IngressProcessor) computeIngressRule(ing *networking_v1.Ingress, rule n
continue
}

r, err := route(ing, path, pathType, s, clientCertSecret, p.FieldLogger)
r, err := route(ing, rule.Host, path, pathType, s, clientCertSecret, p.FieldLogger)
if err != nil {
p.WithError(err).
WithField("name", ing.GetName()).
Expand All @@ -181,8 +179,12 @@ func (p *IngressProcessor) computeIngressRule(ing *networking_v1.Ingress, rule n
}
}

const singleDNSLabelWildcardRegex = "^[a-z0-9]([-a-z0-9]*[a-z0-9])?"

var _ = regexp.MustCompile(singleDNSLabelWildcardRegex)

// route builds a dag.Route for the supplied Ingress.
func route(ingress *networking_v1.Ingress, path string, pathType networking_v1.PathType, service *Service, clientCertSecret *Secret, log logrus.FieldLogger) (*Route, error) {
func route(ingress *networking_v1.Ingress, host string, path string, pathType networking_v1.PathType, service *Service, clientCertSecret *Secret, log logrus.FieldLogger) (*Route, error) {
log = log.WithFields(logrus.Fields{
"name": ingress.Name,
"namespace": ingress.Namespace,
Expand Down Expand Up @@ -228,6 +230,23 @@ func route(ingress *networking_v1.Ingress, path string, pathType networking_v1.P
}
}

// If we have a wildcard match, add a header match regex rule to match the
// hostname so we can be sure to only match one DNS label. This is required
// as Envoy's virtualhost hostname wildcard matching can match multiple
// labels. This match ignores a port in the hostname in case it is present.
if strings.HasPrefix(host, "*.") {
r.HeaderMatchConditions = []HeaderMatchCondition{
{
// Internally Envoy uses the HTTP/2 ":authority" header in
// place of the HTTP/1 "host" header.
// See: https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#config-route-v3-headermatcher
Name: ":authority",
MatchType: HeaderMatchTypeRegex,
Value: singleDNSLabelWildcardRegex + regexp.QuoteMeta(host[1:]),
},
}
}

return r, nil
}

Expand Down
Loading