diff --git a/controllers/nginx/README.md b/controllers/nginx/README.md index 6fc5b0342..3e9c07825 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) * [Kube-Lego](#automated-certificate-management-with-kube-lego) @@ -135,6 +136,81 @@ 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. + +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 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/controller.go b/controllers/nginx/controller.go index 337bc4748..57482ae0d 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", }), @@ -867,6 +870,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) @@ -908,41 +919,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 } @@ -959,6 +939,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 9bd40076f..47aad386f 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() { @@ -137,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 e72eb4f6f..dae4cbd11 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 }};