diff --git a/.gitignore b/.gitignore index 5a5ef25c61..21b7c1cfd2 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ nginx-plus-ingress # Visual Studio Code settings .vscode +# Default certificate and key +default.pem + diff --git a/nginx-controller/Dockerfile b/nginx-controller/Dockerfile index c9424655ab..811eef67fc 100644 --- a/nginx-controller/Dockerfile +++ b/nginx-controller/Dockerfile @@ -9,4 +9,7 @@ COPY nginx-ingress nginx/templates/nginx.ingress.tmpl nginx/templates/nginx.tmpl RUN rm /etc/nginx/conf.d/* +RUN mkdir -p /etc/nginx/ssl +ADD default.pem /etc/nginx/ssl + ENTRYPOINT ["/nginx-ingress"] diff --git a/nginx-controller/DockerfileForAlpine b/nginx-controller/DockerfileForAlpine index 039274375f..a4bd87a282 100644 --- a/nginx-controller/DockerfileForAlpine +++ b/nginx-controller/DockerfileForAlpine @@ -9,4 +9,7 @@ COPY nginx-ingress nginx/templates/nginx.ingress.tmpl nginx/templates/nginx.tmpl RUN rm /etc/nginx/conf.d/* +RUN mkdir -p /etc/nginx/ssl +ADD default.pem /etc/nginx/ssl + ENTRYPOINT ["/nginx-ingress"] diff --git a/nginx-controller/DockerfileForPlus b/nginx-controller/DockerfileForPlus index 34c2e3920b..952ab8b211 100644 --- a/nginx-controller/DockerfileForPlus +++ b/nginx-controller/DockerfileForPlus @@ -34,4 +34,7 @@ RUN mkdir -p /var/lib/nginx/state && chown -R nginx:nginx /var/lib/nginx COPY nginx-ingress nginx/templates/nginx-plus.ingress.tmpl nginx/templates/nginx-plus.tmpl / RUN rm /etc/nginx/conf.d/* +RUN mkdir -p /etc/nginx/ssl +ADD default.pem /etc/nginx/ssl + ENTRYPOINT ["/nginx-ingress"] diff --git a/nginx-controller/Makefile b/nginx-controller/Makefile index 6fee60c071..6d4f94329d 100644 --- a/nginx-controller/Makefile +++ b/nginx-controller/Makefile @@ -25,7 +25,10 @@ else go test ./... endif -container: test nginx-ingress +certificate-and-key: + ./generate_default_cert_and_key.sh + +container: test nginx-ingress certificate-and-key docker build -f $(DOCKERFILE) -t $(PREFIX):$(TAG) . push: container diff --git a/nginx-controller/controller/controller.go b/nginx-controller/controller/controller.go index c83247765b..48980991ea 100644 --- a/nginx-controller/controller/controller.go +++ b/nginx-controller/controller/controller.go @@ -64,17 +64,19 @@ type LoadBalancerController struct { watchNginxConfigMaps bool nginxPlus bool recorder record.EventRecorder + defaultServerSecret string } var keyFunc = cache.DeletionHandlingMetaNamespaceKeyFunc // NewLoadBalancerController creates a controller -func NewLoadBalancerController(kubeClient kubernetes.Interface, resyncPeriod time.Duration, namespace string, cnf *nginx.Configurator, nginxConfigMaps string, nginxPlus bool) (*LoadBalancerController, error) { +func NewLoadBalancerController(kubeClient kubernetes.Interface, resyncPeriod time.Duration, namespace string, cnf *nginx.Configurator, nginxConfigMaps string, defaultServerSecret string, nginxPlus bool) (*LoadBalancerController, error) { lbc := LoadBalancerController{ - client: kubeClient, - stopCh: make(chan struct{}), - cnf: cnf, - nginxPlus: nginxPlus, + client: kubeClient, + stopCh: make(chan struct{}), + cnf: cnf, + defaultServerSecret: defaultServerSecret, + nginxPlus: nginxPlus, } eventBroadcaster := record.NewBroadcaster() @@ -203,6 +205,14 @@ func NewLoadBalancerController(kubeClient kubernetes.Interface, resyncPeriod tim &api_v1.Endpoints{}, resyncPeriod, endpHandlers) secrHandlers := cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + secr := obj.(*api_v1.Secret) + nsname := secr.Namespace + "/" + secr.Name + if nsname == lbc.defaultServerSecret { + glog.V(3).Infof("Adding default server Secret: %v", secr.Name) + lbc.syncQueue.enqueue(obj) + } + }, DeleteFunc: func(obj interface{}) { remSecr, isSecr := obj.(*api_v1.Secret) if !isSecr { @@ -244,7 +254,7 @@ func NewLoadBalancerController(kubeClient kubernetes.Interface, resyncPeriod tim &api_v1.Secret{}, resyncPeriod, secrHandlers) if nginxConfigMaps != "" { - nginxConfigMapsNS, nginxConfigMapsName, err := parseNamespaceName(nginxConfigMaps) + nginxConfigMapsNS, nginxConfigMapsName, err := ParseNamespaceName(nginxConfigMaps) if err != nil { glog.Warning(err) } else { @@ -644,7 +654,7 @@ func (lbc *LoadBalancerController) syncSecret(task Task) { return } - _, name, err := parseNamespaceName(key) + _, name, err := ParseNamespaceName(key) if err != nil { glog.Warningf("Secret key %v is invalid: %v", key, err) return @@ -669,11 +679,32 @@ func (lbc *LoadBalancerController) syncSecret(task Task) { lbc.syncQueue.enqueue(&ing) lbc.recorder.Eventf(&ing, api_v1.EventTypeWarning, "Rejected", "%v/%v was rejected due to deleted Secret %v: %v", ing.Namespace, ing.Name, key) } + + if key == lbc.defaultServerSecret { + glog.Warningf("The default server Secret %v was removed. Retaining the Secret.") + } } else { glog.V(2).Infof("Updating Secret: %v\n", key) secret := obj.(*api_v1.Secret) + if key == lbc.defaultServerSecret { + err := ValidateTLSSecret(secret) + if err != nil { + glog.Errorf("Couldn't validate the default server Secret %v: %v", key, err) + lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "Rejected", "the default server Secret %v was rejected, using the previous version: %v", key, err) + } else { + err := lbc.cnf.AddOrUpdateDefaultServerTLSSecret(secret) + if err != nil { + glog.Errorf("Error when updating the default server Secret %v: %v", key, err) + lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "UpdatedWithError", "the default server Secret %v was updated, but not applied: %v", key, err) + + } else { + lbc.recorder.Eventf(secret, api_v1.EventTypeNormal, "Updated", "the default server Secret %v was updated", key) + } + } + } + if len(ings) > 0 { err := ValidateTLSSecret(secret) if err != nil { @@ -919,10 +950,10 @@ func (lbc *LoadBalancerController) getServiceForIngressBackend(backend *extensio return nil, fmt.Errorf("service %s doesn't exists", svcKey) } -func parseNamespaceName(value string) (ns string, name string, err error) { +func ParseNamespaceName(value string) (ns string, name string, err error) { res := strings.Split(value, "/") if len(res) != 2 { - return "", "", fmt.Errorf("%v must follow the format /", value) + return "", "", fmt.Errorf("%q must follow the format /", value) } return res[0], res[1], nil } diff --git a/nginx-controller/generate_default_cert_and_key.sh b/nginx-controller/generate_default_cert_and_key.sh new file mode 100755 index 0000000000..1581f5d6a3 --- /dev/null +++ b/nginx-controller/generate_default_cert_and_key.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout default.key -out default.crt -subj "/CN=NGINXIngressController" +cat default.key default.crt > default.pem +rm default.key default.crt \ No newline at end of file diff --git a/nginx-controller/main.go b/nginx-controller/main.go index 2e0390b946..6ea87bbbee 100644 --- a/nginx-controller/main.go +++ b/nginx-controller/main.go @@ -12,6 +12,7 @@ import ( "github.com/nginxinc/kubernetes-ingress/nginx-controller/controller" "github.com/nginxinc/kubernetes-ingress/nginx-controller/nginx" "github.com/nginxinc/kubernetes-ingress/nginx-controller/nginx/plus" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/pkg/api" "k8s.io/client-go/rest" @@ -43,6 +44,11 @@ var ( nginxPlus = flag.Bool("nginx-plus", false, `Enables support for NGINX Plus.`) + + defaultServerSecret = flag.String("default-server-tls-secret", "", + `Specifies a secret with a TLS certificate and key for SSL termination of + the default server. The value must follow the following format: /. + If not specified, the key and the cert from /etc/nginx/default.pem is used.`) ) func main() { @@ -84,6 +90,25 @@ func main() { nginxIngressTemplatePath = "nginx-plus.ingress.tmpl" } ngxc, _ := nginx.NewNginxController("/etc/nginx/", local, *healthStatus, nginxConfTemplatePath, nginxIngressTemplatePath) + + if *defaultServerSecret != "" { + ns, name, err := controller.ParseNamespaceName(*defaultServerSecret) + if err != nil { + glog.Fatalf("Error parsing the default-server-tls-secret argument: %v", err) + } + secret, err := kubeClient.CoreV1().Secrets(ns).Get(name, meta_v1.GetOptions{}) + if err != nil { + glog.Fatalf("Error when getting %v: %v", *defaultServerSecret, err) + } + err = controller.ValidateTLSSecret(secret) + if err != nil { + glog.Fatalf("%v is invalid: %v", *defaultServerSecret, err) + } + + bytes := nginx.GenerateCertAndKeyFileContent(secret) + ngxc.AddOrUpdatePemFile(nginx.DefaultServerPemName, bytes) + } + nginxDone := make(chan error, 1) ngxc.Start(nginxDone) @@ -98,7 +123,7 @@ func main() { } cnf := nginx.NewConfigurator(ngxc, nginxConfig, nginxAPI) - lbc, _ := controller.NewLoadBalancerController(kubeClient, 30*time.Second, *watchNamespace, cnf, *nginxConfigMaps, *nginxPlus) + lbc, _ := controller.NewLoadBalancerController(kubeClient, 30*time.Second, *watchNamespace, cnf, *nginxConfigMaps, *defaultServerSecret, *nginxPlus) go handleTermination(lbc, ngxc, nginxDone) lbc.Run() diff --git a/nginx-controller/nginx/configurator.go b/nginx-controller/nginx/configurator.go index 81ac34548d..370431c81a 100644 --- a/nginx-controller/nginx/configurator.go +++ b/nginx-controller/nginx/configurator.go @@ -13,6 +13,7 @@ import ( ) const emptyHost = "" +const DefaultServerPemName = "default" // Configurator transforms an Ingress resource into NGINX Configuration type Configurator struct { @@ -445,11 +446,22 @@ func (cnf *Configurator) AddOrUpdateTLSSecret(secret *api_v1.Secret, reload bool func (cnf *Configurator) addOrUpdateTLSSecret(secret *api_v1.Secret) string { name := objectMetaToFileName(&secret.ObjectMeta) - data := generateCertAndKeyFileContent(secret) + data := GenerateCertAndKeyFileContent(secret) return cnf.nginx.AddOrUpdatePemFile(name, data) } -func generateCertAndKeyFileContent(secret *api_v1.Secret) []byte { +func (cnf *Configurator) AddOrUpdateDefaultServerTLSSecret(secret *api_v1.Secret) error { + data := GenerateCertAndKeyFileContent(secret) + cnf.nginx.AddOrUpdatePemFile(DefaultServerPemName, data) + + if err := cnf.nginx.Reload(); err != nil { + return fmt.Errorf("Error when reloading NGINX when updating the default server Secret: %v", err) + } + return nil +} + +// GenerateCertAndKeyFileContent generates a pem file content from the secret +func GenerateCertAndKeyFileContent(secret *api_v1.Secret) []byte { var res bytes.Buffer res.Write(secret.Data[api_v1.TLSCertKey]) @@ -539,6 +551,9 @@ func (cnf *Configurator) UpdateConfig(config *Config, ingExes []*IngressEx) erro SSLCiphers: config.MainServerSSLCiphers, SSLDHParam: config.MainServerSSLDHParam, SSLPreferServerCiphers: config.MainServerSSLPreferServerCiphers, + HTTP2: config.HTTP2, + ServerTokens: config.ServerTokens, + ProxyProtocol: config.ProxyProtocol, } cnf.nginx.UpdateMainConfigFile(mainCfg) diff --git a/nginx-controller/nginx/nginx.go b/nginx-controller/nginx/nginx.go index 5ad0424331..d78a067837 100644 --- a/nginx-controller/nginx/nginx.go +++ b/nginx-controller/nginx/nginx.go @@ -98,6 +98,9 @@ type NginxMainConfig struct { SSLPreferServerCiphers bool SSLCiphers string SSLDHParam string + HTTP2 bool + ServerTokens string + ProxyProtocol bool } // NewUpstreamWithDefaultServer creates an upstream with the default server. @@ -121,11 +124,10 @@ func NewNginxController(nginxConfPath string, local bool, healthStatus bool, ngi nginxIngressTempatePath: nginxIngressTemplatePath, } - if !local { - createDir(ngxc.nginxCertsPath) + cfg := &NginxMainConfig{ + ServerNamesHashMaxSize: NewDefaultConfig().MainServerNamesHashMaxSize, + ServerTokens: NewDefaultConfig().ServerTokens, } - - cfg := &NginxMainConfig{ServerNamesHashMaxSize: NewDefaultConfig().MainServerNamesHashMaxSize} ngxc.UpdateMainConfigFile(cfg) return &ngxc, nil diff --git a/nginx-controller/nginx/templates/nginx-plus.tmpl b/nginx-controller/nginx/templates/nginx-plus.tmpl index 987f45706b..072ac53224 100644 --- a/nginx-controller/nginx/templates/nginx-plus.tmpl +++ b/nginx-controller/nginx/templates/nginx-plus.tmpl @@ -51,18 +51,30 @@ http { {{if .SSLPreferServerCiphers}}ssl_prefer_server_ciphers on;{{end}} {{if .SSLDHParam}}ssl_dhparam {{.SSLDHParam}};{{end}} - {{if .HealthStatus}} + server { - listen 80 default_server; + listen 80 default_server{{if .ProxyProtocol}} proxy_protocol{{end}}; + listen 443 ssl default_server{{if .HTTP2}} http2{{end}}{{if .ProxyProtocol}} proxy_protocol{{end}}; + + ssl_certificate /etc/nginx/ssl/default.pem; + ssl_certificate_key /etc/nginx/ssl/default.pem; + server_name _; + server_tokens "{{.ServerTokens}}"; + access_log off; + {{if .HealthStatus}} location /nginx-health { - access_log off; default_type text/plain; return 200 "healthy\n"; } + {{end}} + + location / { + return 404; + } } - {{end}} + # status and on-the-fly reconfiguration APIs server { diff --git a/nginx-controller/nginx/templates/nginx.tmpl b/nginx-controller/nginx/templates/nginx.tmpl index 30486ae757..bd174a6b60 100644 --- a/nginx-controller/nginx/templates/nginx.tmpl +++ b/nginx-controller/nginx/templates/nginx.tmpl @@ -50,18 +50,28 @@ http { {{if .SSLPreferServerCiphers}}ssl_prefer_server_ciphers on;{{end}} {{if .SSLDHParam}}ssl_dhparam {{.SSLDHParam}};{{end}} - {{if .HealthStatus}} server { - listen 80 default_server; + listen 80 default_server{{if .ProxyProtocol}} proxy_protocol{{end}}; + listen 443 ssl default_server{{if .HTTP2}} http2{{end}}{{if .ProxyProtocol}} proxy_protocol{{end}}; + + ssl_certificate /etc/nginx/ssl/default.pem; + ssl_certificate_key /etc/nginx/ssl/default.pem; + server_name _; + server_tokens "{{.ServerTokens}}"; + access_log off; + {{if .HealthStatus}} location /nginx-health { - access_log off; default_type text/plain; return 200 "healthy\n"; } + {{end}} + + location / { + return 404; + } } - {{end}} include /etc/nginx/conf.d/*.conf; }