Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal: implement fallback certificate #2477

Merged
merged 1 commit into from
May 15, 2020
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
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 {
stevesloka marked this conversation as resolved.
Show resolved Hide resolved
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")
stevesloka marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
stevesloka marked this conversation as resolved.
Show resolved Hide resolved
# 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,
jpeach marked this conversation as resolved.
Show resolved Hide resolved
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