Skip to content

Commit

Permalink
implement fallback certificate so that clients who do not offer SNI w…
Browse files Browse the repository at this point in the history
…ill still match a filterchain in envoy and will present a default/fallback certificate to the client

Signed-off-by: Steve Sloka <[email protected]>
  • Loading branch information
stevesloka committed May 15, 2020
1 parent 0fc1475 commit af6ee6e
Show file tree
Hide file tree
Showing 25 changed files with 2,092 additions and 42 deletions.
4 changes: 4 additions & 0 deletions apis/projectcontour/v1/httpproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ type TLS struct {
// 3. Specifies how the client certificate will be validated.
// +optional
ClientValidation *DownstreamValidation `json:"clientValidation,omitempty"`

// EnableFallbackCertificate defines if the vhost should allow a default certificate to
// be applied which handles all requests which don't match the SNI defined in this vhost.
EnableFallbackCertificate bool `json:"enableFallbackCertificate,omitempty"`
}

// Route contains the set of routes for a virtual host.
Expand Down
36 changes: 33 additions & 3 deletions cmd/contour/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,23 @@ func doServe(log logrus.FieldLogger, ctx *serveContext) error {
// Create a set of SharedInformerFactories for each root-ingressroute namespace (if defined)
namespacedInformerFactories := map[string]coreinformers.SharedInformerFactory{}

for _, namespace := range ctx.ingressRouteRootNamespaces() {
if _, ok := namespacedInformerFactories[namespace]; !ok {
namespacedInformerFactories[namespace] = clients.NewInformerFactoryForNamespace(namespace)
// Validate fallback certificate parameters
fallbackCert, err := ctx.fallbackCertificate()
if err != nil {
log.WithField("context", "fallback-certificate").Fatalf("invalid fallback certificate configuration: %q", err)
}

if rootNamespaces := ctx.ingressRouteRootNamespaces(); len(rootNamespaces) > 0 {
// Add the FallbackCertificateNamespace to the root-namespaces if not already
if !contains(rootNamespaces, ctx.TLSConfig.FallbackCertificate.Namespace) && fallbackCert != nil {
rootNamespaces = append(rootNamespaces, ctx.FallbackCertificate.Namespace)
log.WithField("context", "fallback-certificate").Infof("fallback certificate namespace %q not defined in 'root-namespaces', adding namespace to watch", ctx.FallbackCertificate.Namespace)
}

for _, namespace := range rootNamespaces {
if _, ok := namespacedInformerFactories[namespace]; !ok {
namespacedInformerFactories[namespace] = clients.NewInformerFactoryForNamespace(namespace)
}
}
}

Expand Down Expand Up @@ -194,6 +208,13 @@ func doServe(log logrus.FieldLogger, ctx *serveContext) error {
FieldLogger: log.WithField("context", "contourEventHandler"),
}

// Set the fallbackcertificate if configured
if fallbackCert != nil {
log.WithField("context", "fallback-certificate").Infof("enabled fallback certificate with secret: %q", fallbackCert)

eventHandler.FallbackCertificate = fallbackCert
}

// wrap eventHandler in a converter for objects from the dynamic client.
// and an EventRecorder which tracks API server events.
dynamicHandler := &k8s.DynamicClientHandler{
Expand Down Expand Up @@ -378,6 +399,15 @@ func doServe(log logrus.FieldLogger, ctx *serveContext) error {
return g.Run()
}

func contains(namespaces []string, ns string) bool {
for _, namespace := range namespaces {
if ns == namespace {
return true
}
}
return false
}

type informer interface {
Start(stopCh <-chan struct{})
}
Expand Down
35 changes: 35 additions & 0 deletions cmd/contour/servecontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"strings"
"time"

"github.com/projectcontour/contour/internal/k8s"

"github.com/projectcontour/contour/internal/contour"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
Expand Down Expand Up @@ -172,6 +174,38 @@ func newServeContext() *serveContext {
// TLSConfig holds configuration file TLS configuration details.
type TLSConfig struct {
MinimumProtocolVersion string `yaml:"minimum-protocol-version"`

// FallbackCertificate defines the namespace/name of the Kubernetes secret to
// use as fallback when a non-SNI request is received.
FallbackCertificate FallbackCertificate `yaml:"fallback-certificate,omitempty"`
}

// FallbackCertificate defines the namespace/name of the Kubernetes secret to
// use as fallback when a non-SNI request is received.
type FallbackCertificate struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
}

func (ctx *serveContext) fallbackCertificate() (*k8s.FullName, error) {
if len(strings.TrimSpace(ctx.TLSConfig.FallbackCertificate.Name)) == 0 && len(strings.TrimSpace(ctx.TLSConfig.FallbackCertificate.Namespace)) == 0 {
return nil, nil
}

// Validate namespace is defined
if len(strings.TrimSpace(ctx.TLSConfig.FallbackCertificate.Namespace)) == 0 {
return nil, errors.New("namespace must be defined")
}

// Validate name is defined
if len(strings.TrimSpace(ctx.TLSConfig.FallbackCertificate.Name)) == 0 {
return nil, errors.New("name must be defined")
}

return &k8s.FullName{
Name: ctx.TLSConfig.FallbackCertificate.Name,
Namespace: ctx.TLSConfig.FallbackCertificate.Namespace,
}, nil
}

// LeaderElectionConfig holds the config bits for leader election inside the
Expand Down Expand Up @@ -272,6 +306,7 @@ func (ctx *serveContext) verifyTLSFlags() error {
if !(ctx.caFile != "" && ctx.contourCert != "" && ctx.contourKey != "") {
return errors.New("you must supply all three TLS parameters - --contour-cafile, --contour-cert-file, --contour-key-file, or none of them")
}

return nil
}

Expand Down
68 changes: 68 additions & 0 deletions cmd/contour/servecontext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"testing"
"time"

"github.com/projectcontour/contour/internal/k8s"

"github.com/google/go-cmp/cmp"
"github.com/projectcontour/contour/internal/assert"
"google.golang.org/grpc"
Expand Down Expand Up @@ -200,6 +202,72 @@ leaderelection:
}
}

func TestFallbackCertificateParams(t *testing.T) {
tests := map[string]struct {
ctx serveContext
want *k8s.FullName
expecterror bool
}{
"fallback cert params passed correctly": {
ctx: serveContext{
TLSConfig: TLSConfig{
FallbackCertificate: FallbackCertificate{
Name: "fallbacksecret",
Namespace: "root-namespace",
},
},
},
want: &k8s.FullName{
Name: "fallbacksecret",
Namespace: "root-namespace",
},
expecterror: false,
},
"missing namespace": {
ctx: serveContext{
TLSConfig: TLSConfig{
FallbackCertificate: FallbackCertificate{
Name: "fallbacksecret",
},
},
},
want: nil,
expecterror: true,
},
"missing name": {
ctx: serveContext{
TLSConfig: TLSConfig{
FallbackCertificate: FallbackCertificate{
Namespace: "root-namespace",
},
},
},
want: nil,
expecterror: true,
},
"fallback cert not defined": {
ctx: serveContext{},
want: nil,
expecterror: false,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got, err := tc.ctx.fallbackCertificate()

if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatal(diff)
}

goterror := err != nil
if goterror != tc.expecterror {
t.Errorf("Expected Fallback Certificate error: %s", err)
}
})
}
}

// Testdata for this test case can be re-generated by running:
// make gencerts
// cp certs/*.pem cmd/contour/testdata/X/
Expand Down
10 changes: 8 additions & 2 deletions examples/contour/01-contour-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ data:
# disable ingressroute permitInsecure field
disablePermitInsecure: false
tls:
# minimum TLS version that Contour will negotiate
# minimum-protocol-version: "1.1"
# minimum TLS version that Contour will negotiate
# minimum-protocol-version: "1.1"
# Defines the Kubernetes name/namespace matching a secret to use
# as the fallback certificate when requests which don't match the
# SNI defined for a vhost.
fallback-certificate:
# name: fallback-secret-name
# namespace: projectcontour
# The following config shows the defaults for the leader election.
# leaderelection:
# configmap-name: leader-elect
Expand Down
5 changes: 5 additions & 0 deletions examples/contour/01-crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,11 @@ spec:
required:
- caSecret
type: object
enableFallbackCertificate:
description: EnableFallbackCertificate defines if the vhost
should allow a default certificate to be applied which handles
all requests which don't match the SNI defined in this vhost.
type: boolean
minimumProtocolVersion:
description: Minimum TLS version this vhost should negotiate
type: string
Expand Down
15 changes: 13 additions & 2 deletions examples/render/contour.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,14 @@ data:
# disable ingressroute permitInsecure field
disablePermitInsecure: false
tls:
# minimum TLS version that Contour will negotiate
# minimum-protocol-version: "1.1"
# minimum TLS version that Contour will negotiate
# minimum-protocol-version: "1.1"
# Defines the Kubernetes name/namespace matching a secret to use
# as the fallback certificate when requests which don't match the
# SNI defined for a vhost.
fallback-certificate:
# name: fallback-secret-name
# namespace: projectcontour
# The following config shows the defaults for the leader election.
# leaderelection:
# configmap-name: leader-elect
Expand Down Expand Up @@ -1227,6 +1233,11 @@ spec:
required:
- caSecret
type: object
enableFallbackCertificate:
description: EnableFallbackCertificate defines if the vhost
should allow a default certificate to be applied which handles
all requests which don't match the SNI defined in this vhost.
type: boolean
minimumProtocolVersion:
description: Minimum TLS version this vhost should negotiate
type: string
Expand Down
30 changes: 28 additions & 2 deletions internal/contour/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (

const (
ENVOY_HTTP_LISTENER = "ingress_http"
ENVOY_FALLBACK_ROUTECONFIG = "ingress_fallbackcert"
ENVOY_HTTPS_LISTENER = "ingress_https"
DEFAULT_HTTP_ACCESS_LOG = "/dev/stdout"
DEFAULT_HTTP_LISTENER_ADDRESS = "0.0.0.0"
Expand Down Expand Up @@ -294,8 +295,8 @@ func visitListeners(root dag.Vertex, lvc *ListenerVisitorConfig) map[string]*v2.

lv.visit(root)

// Add a listener if there are vhosts bound to http.
if lv.http {
// Add a listener if there are vhosts bound to http.
cm := envoy.HTTPConnectionManagerBuilder().
RouteConfigName(ENVOY_HTTP_LISTENER).
MetricsPrefix(ENVOY_HTTP_LISTENER).
Expand All @@ -310,7 +311,6 @@ func visitListeners(root dag.Vertex, lvc *ListenerVisitorConfig) map[string]*v2.
proxyProtocol(lvc.UseProxyProto),
cm,
)

}

// Remove the https listener if there are no vhosts bound to it.
Expand Down Expand Up @@ -403,6 +403,32 @@ func (v *listenerVisitor) visit(vertex dag.Vertex) {
v.listeners[ENVOY_HTTPS_LISTENER].FilterChains = append(v.listeners[ENVOY_HTTPS_LISTENER].FilterChains,
envoy.FilterChainTLS(vh.VirtualHost.Name, downstreamTLS, filters))

// If this VirtualHost has enabled the fallback certificate then set a default FilterChain which will allow
// routes with this vhost to accept non SNI TLS requests
if vh.FallbackCertificate != nil && !envoy.ContainsFallbackFilterChain(v.listeners[ENVOY_HTTPS_LISTENER].FilterChains) {

// Construct the downstreamTLSContext passing the configured fallbackCertificate. The TLS minProtocolVersion will use
// the value defined in the Contour Configuration file if defined.
downstreamTLS = envoy.DownstreamTLSContext(
vh.FallbackCertificate,
v.ListenerVisitorConfig.minProtoVersion(),
vh.DownstreamValidation,
alpnProtos...)

// Default filter chain
filters = envoy.Filters(
envoy.HTTPConnectionManagerBuilder().
RouteConfigName(ENVOY_FALLBACK_ROUTECONFIG).
MetricsPrefix(ENVOY_HTTPS_LISTENER).
AccessLoggers(v.ListenerVisitorConfig.newSecureAccessLog()).
RequestTimeout(v.ListenerVisitorConfig.requestTimeout()).
Get(),
)

v.listeners[ENVOY_HTTPS_LISTENER].FilterChains = append(v.listeners[ENVOY_HTTPS_LISTENER].FilterChains,
envoy.FilterChainTLSFallback(downstreamTLS, filters))
}

default:
// recurse
vertex.Visit(v.visit)
Expand Down
Loading

0 comments on commit af6ee6e

Please sign in to comment.