From 8f4efb4e3d1111a193054631ebe2a3f26edcfad7 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Fri, 8 Jul 2016 17:20:14 -0400 Subject: [PATCH 1/2] Add HTTPS default backend --- controllers/nginx/README.md | 8 ++++++++ controllers/nginx/main.go | 3 +++ 2 files changed, 11 insertions(+) diff --git a/controllers/nginx/README.md b/controllers/nginx/README.md index 3e3f29fd1..f2729a52b 100644 --- a/controllers/nginx/README.md +++ b/controllers/nginx/README.md @@ -9,6 +9,7 @@ This is a nginx Ingress controller that uses [ConfigMap](https://github.com/kube * [Deployment](#deployment) * [HTTP](#http) * [HTTPS](#https) + * [Default SSL Certificate](#default-ssl-certificate) * [HTTPS enforcement](#server-side-https-enforcement) * [HSTS](#http-strict-transport-security) * [TCP Services](#exposing-tcp-services) @@ -133,6 +134,13 @@ Please follow [test.sh](https://github.com/bprashanth/Ingress/blob/master/exampl Check the [example](examples/tls/README.md) +### Default SSL Certificate + +NGINX provides the option [default_server](http://nginx.org/en/docs/http/server_names.html) to allow a catch-all server in case of request with a not configured server name. This configuration works without issues for HTTP traffic. +In case of HTTPS NGINX requires a certificate. For this reason the Ingress controller provides the flag `--default-ssl-certificate`. The secret behind this flag contains the default certificate to be used in the mentioned case. +If this flag is not provided NGINX will reject the request with the HTTP code 444. + + ### Server-side HTTPS enforcement By default the controller redirects (301) to HTTPS if TLS is enabled for that ingress . If you want to disable that behaviour globally, you can use `ssl-redirect: "false"` in the NGINX config map. diff --git a/controllers/nginx/main.go b/controllers/nginx/main.go index 9bd40076f..a30d0117a 100644 --- a/controllers/nginx/main.go +++ b/controllers/nginx/main.go @@ -85,6 +85,9 @@ var ( This can be used as a guide to create a custom configuration.`) profiling = flags.Bool("profiling", true, `Enable profiling via web interface host:port/debug/pprof/`) + + defSSLCertificate = flags.String("default-ssl-certificate", "", `Name of the secret that contains a SSL + certificate to be used as default for a HTTPS catch-all server`) ) func main() { From d3d6c879d5945a739e6dbd8d786fa3dd15237705 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Mon, 18 Jul 2016 17:08:00 -0400 Subject: [PATCH 2/2] Refactor nginx certificate creation. --- controllers/nginx/README.md | 68 +++++++++++++++++ controllers/nginx/controller.go | 125 ++++++++++++++++---------------- controllers/nginx/main.go | 4 +- controllers/nginx/nginx.tmpl | 10 +++ 4 files changed, 145 insertions(+), 62 deletions(-) diff --git a/controllers/nginx/README.md b/controllers/nginx/README.md index f2729a52b..d3a4ea977 100644 --- a/controllers/nginx/README.md +++ b/controllers/nginx/README.md @@ -140,6 +140,74 @@ NGINX provides the option [default_server](http://nginx.org/en/docs/http/server_ In case of HTTPS NGINX requires a certificate. For this reason the Ingress controller provides the flag `--default-ssl-certificate`. The secret behind this flag contains the default certificate to be used in the mentioned case. If this flag is not provided NGINX will reject the request with the HTTP code 444. +Running without the flag `--default-ssl-certificate`: + +``` +$ curl -v https://10.2.78.7:443 +* Rebuilt URL to: https://10.2.78.7:443/ +* Trying 10.2.78.7... +* Connected to 10.2.78.7 (10.2.78.7) port 443 (#0) +* ALPN, offering http/1.1 +* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH +* successfully set certificate verify locations: +* CAfile: /etc/ssl/certs/ca-certificates.crt + CApath: /etc/ssl/certs +* TLSv1.2 (OUT), TLS header, Certificate Status (22): +* TLSv1.2 (OUT), TLS handshake, Client hello (1): +* Unknown SSL protocol error in connection to 10.2.78.7:443 +* Closing connection 0 +curl: (35) Unknown SSL protocol error in connection to 10.2.78.7:443 +``` + +Specifyng `--default-ssl-certificate=default/foo-tls`: + +``` +core@localhost ~ $ curl -v https://10.2.78.7:443 -k +* Rebuilt URL to: https://10.2.78.7:443/ +* Trying 10.2.78.7... +* Connected to 10.2.78.7 (10.2.78.7) port 443 (#0) +* ALPN, offering http/1.1 +* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH +* successfully set certificate verify locations: +* CAfile: /etc/ssl/certs/ca-certificates.crt + CApath: /etc/ssl/certs +* TLSv1.2 (OUT), TLS header, Certificate Status (22): +* TLSv1.2 (OUT), TLS handshake, Client hello (1): +* TLSv1.2 (IN), TLS handshake, Server hello (2): +* TLSv1.2 (IN), TLS handshake, Certificate (11): +* TLSv1.2 (IN), TLS handshake, Server key exchange (12): +* TLSv1.2 (IN), TLS handshake, Server finished (14): +* TLSv1.2 (OUT), TLS handshake, Client key exchange (16): +* TLSv1.2 (OUT), TLS change cipher, Client hello (1): +* TLSv1.2 (OUT), TLS handshake, Finished (20): +* TLSv1.2 (IN), TLS change cipher, Client hello (1): +* TLSv1.2 (IN), TLS handshake, Finished (20): +* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 +* ALPN, server accepted to use http/1.1 +* Server certificate: +* subject: CN=foo.bar.com +* start date: Apr 13 00:50:56 2016 GMT +* expire date: Apr 13 00:50:56 2017 GMT +* issuer: CN=foo.bar.com +* SSL certificate verify result: self signed certificate (18), continuing anyway. +> GET / HTTP/1.1 +> Host: 10.2.78.7 +> User-Agent: curl/7.47.1 +> Accept: */* +> +< HTTP/1.1 404 Not Found +< Server: nginx/1.11.1 +< Date: Mon, 18 Jul 2016 21:02:59 GMT +< Content-Type: text/html +< Transfer-Encoding: chunked +< Connection: keep-alive +< Strict-Transport-Security: max-age=15724800; includeSubDomains; preload +< +The page you're looking for could not be found. + +* Connection #0 to host 10.2.78.7 left intact +``` + ### Server-side HTTPS enforcement diff --git a/controllers/nginx/controller.go b/controllers/nginx/controller.go index bfe7e06a0..5b27d6670 100644 --- a/controllers/nginx/controller.go +++ b/controllers/nginx/controller.go @@ -88,23 +88,24 @@ func (npm namedPortMapping) getPortMappings() map[string]string { // loadBalancerController watches the kubernetes api and adds/removes services // from the loadbalancer type loadBalancerController struct { - client *client.Client - ingController *framework.Controller - endpController *framework.Controller - svcController *framework.Controller - secrController *framework.Controller - mapController *framework.Controller - ingLister StoreToIngressLister - svcLister cache.StoreToServiceLister - endpLister cache.StoreToEndpointsLister - secrLister StoreToSecretsLister - mapLister StoreToConfigmapLister - nginx *nginx.Manager - podInfo *podInfo - defaultSvc string - nxgConfigMap string - tcpConfigMap string - udpConfigMap string + client *client.Client + ingController *framework.Controller + endpController *framework.Controller + svcController *framework.Controller + secrController *framework.Controller + mapController *framework.Controller + ingLister StoreToIngressLister + svcLister cache.StoreToServiceLister + endpLister cache.StoreToEndpointsLister + secrLister StoreToSecretsLister + mapLister StoreToConfigmapLister + nginx *nginx.Manager + podInfo *podInfo + defaultSvc string + nxgConfigMap string + tcpConfigMap string + udpConfigMap string + defSSLCertificate string recorder record.EventRecorder @@ -123,22 +124,24 @@ type loadBalancerController struct { } // newLoadBalancerController creates a controller for nginx loadbalancer -func newLoadBalancerController(kubeClient *client.Client, resyncPeriod time.Duration, defaultSvc, - namespace, nxgConfigMapName, tcpConfigMapName, udpConfigMapName string, runtimeInfo *podInfo) (*loadBalancerController, error) { +func newLoadBalancerController(kubeClient *client.Client, resyncPeriod time.Duration, + defaultSvc, namespace, nxgConfigMapName, tcpConfigMapName, udpConfigMapName, + defSSLCertificate string, runtimeInfo *podInfo) (*loadBalancerController, error) { eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartLogging(glog.Infof) eventBroadcaster.StartRecordingToSink(kubeClient.Events(namespace)) lbc := loadBalancerController{ - client: kubeClient, - stopCh: make(chan struct{}), - podInfo: runtimeInfo, - nginx: nginx.NewManager(kubeClient), - nxgConfigMap: nxgConfigMapName, - tcpConfigMap: tcpConfigMapName, - udpConfigMap: udpConfigMapName, - defaultSvc: defaultSvc, + client: kubeClient, + stopCh: make(chan struct{}), + podInfo: runtimeInfo, + nginx: nginx.NewManager(kubeClient), + nxgConfigMap: nxgConfigMapName, + tcpConfigMap: tcpConfigMapName, + udpConfigMap: udpConfigMapName, + defSSLCertificate: defSSLCertificate, + defaultSvc: defaultSvc, recorder: eventBroadcaster.NewRecorder(api.EventSource{ Component: "nginx-ingress-controller", }), @@ -855,6 +858,14 @@ func (lbc *loadBalancerController) createServers(data []interface{}) map[string] servers := make(map[string]*nginx.Server) pems := lbc.getPemsFromIngress(data) + if lbc.defSSLCertificate != "" { + ngxCert, err := lbc.getPemCertificate(lbc.defSSLCertificate) + if err == nil { + pems["_"] = ngxCert + } else { + glog.Warningf("%v", err) + } + } for _, ingIf := range data { ing := ingIf.(*extensions.Ingress) @@ -896,41 +907,10 @@ func (lbc *loadBalancerController) getPemsFromIngress(data []interface{}) map[st for _, tls := range ing.Spec.TLS { secretName := tls.SecretName secretKey := fmt.Sprintf("%s/%s", ing.Namespace, secretName) - secretInterface, exists, err := lbc.secrLister.Store.GetByKey(secretKey) + + ngxCert, err := lbc.getPemCertificate(secretKey) if err != nil { - glog.Warningf("Error retriveing secret %v for ing %v: %v", secretName, ing.Name, err) - continue - } - if !exists { - glog.Warningf("Secret %v is not existing", secretKey) - continue - } - secret := secretInterface.(*api.Secret) - cert, ok := secret.Data[api.TLSCertKey] - if !ok { - glog.Warningf("Secret %v has no private key", secretName) - continue - } - key, ok := secret.Data[api.TLSPrivateKeyKey] - if !ok { - glog.Warningf("Secret %v has no cert", secretName) - continue - } - - ngxCert, err := lbc.nginx.AddOrUpdateCertAndKey(fmt.Sprintf("%v-%v", ing.Namespace, secretName), string(cert), string(key)) - if err != nil { - glog.Errorf("No valid SSL certificate found in secret %v: %v", secretName, err) - continue - } - - if len(tls.Hosts) == 0 { - if _, ok := pems["_"]; ok { - glog.Warningf("It is not possible to use %v secret for default SSL certificate because there is one already defined", secretName) - continue - } - - pems["_"] = ngxCert - glog.Infof("Using the secret %v as source for the default SSL certificate", secretName) + glog.Warningf("%v", err) continue } @@ -947,6 +927,29 @@ func (lbc *loadBalancerController) getPemsFromIngress(data []interface{}) map[st return pems } +func (lbc *loadBalancerController) getPemCertificate(secretName string) (nginx.SSLCert, error) { + secretInterface, exists, err := lbc.secrLister.Store.GetByKey(secretName) + if err != nil { + return nginx.SSLCert{}, fmt.Errorf("Error retriveing secret %v: %v", secretName, err) + } + if !exists { + return nginx.SSLCert{}, fmt.Errorf("Secret %v does not exists", secretName) + } + + secret := secretInterface.(*api.Secret) + cert, ok := secret.Data[api.TLSCertKey] + if !ok { + return nginx.SSLCert{}, fmt.Errorf("Secret %v has no private key", secretName) + } + key, ok := secret.Data[api.TLSPrivateKeyKey] + if !ok { + return nginx.SSLCert{}, fmt.Errorf("Secret %v has no cert", secretName) + } + + nsSecName := strings.Replace(secretName, "/", "-", -1) + return lbc.nginx.AddOrUpdateCertAndKey(nsSecName, string(cert), string(key)) +} + // check if secret is referenced in this controller's config func (lbc *loadBalancerController) secrReferenced(namespace string, name string) bool { for _, ingIf := range lbc.ingLister.Store.List() { diff --git a/controllers/nginx/main.go b/controllers/nginx/main.go index a30d0117a..47aad386f 100644 --- a/controllers/nginx/main.go +++ b/controllers/nginx/main.go @@ -140,7 +140,9 @@ func main() { } } - lbc, err := newLoadBalancerController(kubeClient, *resyncPeriod, *defaultSvc, *watchNamespace, *nxgConfigMap, *tcpConfigMapName, *udpConfigMapName, runtimePodInfo) + lbc, err := newLoadBalancerController(kubeClient, *resyncPeriod, + *defaultSvc, *watchNamespace, *nxgConfigMap, *tcpConfigMapName, + *udpConfigMapName, *defSSLCertificate, runtimePodInfo) if err != nil { glog.Fatalf("%v", err) } diff --git a/controllers/nginx/nginx.tmpl b/controllers/nginx/nginx.tmpl index 1f178a636..f5b94a7d6 100644 --- a/controllers/nginx/nginx.tmpl +++ b/controllers/nginx/nginx.tmpl @@ -180,6 +180,16 @@ http { {{ end }} {{ range $server := .servers }} + {{/* Check for default SSL backend */}} + {{ if and (eq $server.Name "_") (not $server.SSL) -}} + server { + server_name {{ $server.Name }}; + listen 443; + # return protocol error. + return 444; + } + {{ end }} + server { server_name {{ $server.Name }}; listen 80{{ if $cfg.useProxyProtocol }} proxy_protocol{{ end }};