diff --git a/controllers/nginx/configuration.md b/controllers/nginx/configuration.md index ce235ecf90..33f6abc1b2 100644 --- a/controllers/nginx/configuration.md +++ b/controllers/nginx/configuration.md @@ -336,7 +336,7 @@ The recommendation above prioritizes algorithms that provide perfect [forward se Please check the [Mozilla SSL Configuration Generator](https://mozilla.github.io/server-side-tls/ssl-config-generator/). -**ssl-dh-param:** sets the Base64 string that contains Diffie-Hellman key to help with "Perfect Forward Secrecy". +**ssl-dh-param:** Sets the name of the secret that contains Diffie-Hellman key to help with "Perfect Forward Secrecy". https://www.openssl.org/docs/manmaster/apps/dhparam.html https://wiki.mozilla.org/Security/Server_Side_TLS#DHE_handshake_and_dhparam http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_dhparam diff --git a/controllers/nginx/pkg/cmd/controller/nginx.go b/controllers/nginx/pkg/cmd/controller/nginx.go index 3f02756522..f8e92bac0d 100644 --- a/controllers/nginx/pkg/cmd/controller/nginx.go +++ b/controllers/nginx/pkg/cmd/controller/nginx.go @@ -25,6 +25,7 @@ import ( "net/http" "os" "os/exec" + "strings" "syscall" "time" @@ -38,6 +39,7 @@ import ( "k8s.io/ingress/controllers/nginx/pkg/version" "k8s.io/ingress/core/pkg/ingress" "k8s.io/ingress/core/pkg/ingress/defaults" + "k8s.io/ingress/core/pkg/net/ssl" ) const ( @@ -346,6 +348,32 @@ func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) ([]byte, er } } + sslDHParam := "" + if cfg.SSLDHParam != "" { + secretName := cfg.SSLDHParam + s, exists, err := n.storeLister.Secret.GetByKey(secretName) + if err != nil { + glog.Warningf("unexpected error reading secret %v: %v", secretName, err) + } + + if exists { + secret := s.(*api.Secret) + nsSecName := strings.Replace(secretName, "/", "-", -1) + + dh, ok := secret.Data["dhparam.pem"] + if ok { + pemFileName, err := ssl.AddOrUpdateDHParam(nsSecName, dh) + if err != nil { + glog.Warningf("unexpected error adding or updating dhparam %v file: %v", nsSecName, err) + } else { + sslDHParam = pemFileName + } + } + } + } + + cfg.SSLDHParam = sslDHParam + content, err := n.t.Write(config.TemplateConfig{ ProxySetHeaders: setHeaders, MaxOpenFiles: maxOpenFiles, diff --git a/controllers/nginx/pkg/config/config.go b/controllers/nginx/pkg/config/config.go index 558e94e8f3..9f3ebef69b 100644 --- a/controllers/nginx/pkg/config/config.go +++ b/controllers/nginx/pkg/config/config.go @@ -191,7 +191,7 @@ type Configuration struct { // http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ciphers SSLCiphers string `json:"ssl-ciphers,omitempty"` - // Base64 string that contains Diffie-Hellman key to help with "Perfect Forward Secrecy" + // The secret that contains Diffie-Hellman key to help with "Perfect Forward Secrecy" // https://www.openssl.org/docs/manmaster/apps/dhparam.html // https://wiki.mozilla.org/Security/Server_Side_TLS#DHE_handshake_and_dhparam // http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_dhparam diff --git a/core/pkg/net/ssl/ssl.go b/core/pkg/net/ssl/ssl.go index 93811ecf36..2088c2a61a 100644 --- a/core/pkg/net/ssl/ssl.go +++ b/core/pkg/net/ssl/ssl.go @@ -175,25 +175,52 @@ func AddCertAuth(name string, ca []byte) (*ingress.SSLCert, error) { }, nil } -// SearchDHParamFile iterates all the secrets mounted inside the /etc/nginx-ssl directory -// in order to find a file with the name dhparam.pem. If such file exists it will -// returns the path. If not it just returns an empty string -func SearchDHParamFile(baseDir string) string { - files, _ := ioutil.ReadDir(baseDir) - for _, file := range files { - if !file.IsDir() { - continue - } +// AddOrUpdateDHParam creates a dh parameters file with the specified name +func AddOrUpdateDHParam(name string, dh []byte) (string, error) { + pemName := fmt.Sprintf("%v.pem", name) + pemFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, pemName) - dhPath := fmt.Sprintf("%v/%v/dhparam.pem", baseDir, file.Name()) - if _, err := os.Stat(dhPath); err == nil { - glog.Infof("using file '%v' for parameter ssl_dhparam", dhPath) - return dhPath - } + tempPemFile, err := ioutil.TempFile(ingress.DefaultSSLDirectory, pemName) + + glog.V(3).Infof("Creating temp file %v for DH param: %v", tempPemFile.Name(), pemName) + if err != nil { + return "", fmt.Errorf("could not create temp pem file %v: %v", pemFileName, err) + } + + _, err = tempPemFile.Write(dh) + if err != nil { + return "", fmt.Errorf("could not write to pem file %v: %v", tempPemFile.Name(), err) + } + + err = tempPemFile.Close() + if err != nil { + return "", fmt.Errorf("could not close temp pem file %v: %v", tempPemFile.Name(), err) + } + + pemCerts, err := ioutil.ReadFile(tempPemFile.Name()) + if err != nil { + _ = os.Remove(tempPemFile.Name()) + return "", err + } + + pemBlock, _ := pem.Decode(pemCerts) + if pemBlock == nil { + _ = os.Remove(tempPemFile.Name()) + return "", fmt.Errorf("No valid PEM formatted block found") + } + + // If the file does not start with 'BEGIN DH PARAMETERS' it's invalid and must not be used. + if pemBlock.Type != "DH PARAMETERS" { + _ = os.Remove(tempPemFile.Name()) + return "", fmt.Errorf("Certificate %v contains invalid data", name) + } + + err = os.Rename(tempPemFile.Name(), pemFileName) + if err != nil { + return "", fmt.Errorf("could not move temp pem file %v to destination %v: %v", tempPemFile.Name(), pemFileName, err) } - glog.Warning("no file dhparam.pem found in secrets") - return "" + return pemFileName, nil } // PemSHA1 returns the SHA1 of a pem file. This is used to diff --git a/examples/customization/ssl-dh-param/nginx/README.md b/examples/customization/ssl-dh-param/nginx/README.md new file mode 100644 index 0000000000..54f3287fa1 --- /dev/null +++ b/examples/customization/ssl-dh-param/nginx/README.md @@ -0,0 +1,79 @@ +# Deploying the Nginx Ingress controller + +This example aims to demonstrate the deployment of an nginx ingress controller and +use a ConfigMap to configure custom Diffie-Hellman parameters file to help with +"Perfect Forward Secrecy". + +## Default Backend + +The default backend is a Service capable of handling all url paths and hosts the +nginx controller doesn't understand. This most basic implementation just returns +a 404 page: + +```console +$ kubectl apply -f default-backend.yaml +deployment "default-http-backend" created +service "default-http-backend" created + +$ kubectl -n kube-system get po +NAME READY STATUS RESTARTS AGE +default-http-backend-2657704409-qgwdd 1/1 Running 0 28s +``` + +## Custom configuration + +```console +$ cat nginx-load-balancer-conf.yaml +apiVersion: v1 +data: + ssl-dh-param: "kube-system/lb-dhparam" +kind: ConfigMap +metadata: + name: nginx-load-balancer-conf +``` + +```console +$ kubectl create -f nginx-load-balancer-conf.yaml +``` + +## Custom DH parameters secret + +```console +$> openssl dhparam 1024 2> /dev/null | base64 +LS0tLS1CRUdJTiBESCBQQVJBTUVURVJ... +``` + +```console +$ cat ssl-dh-param.yaml +apiVersion: v1 +data: + dhparam.pem: "LS0tLS1CRUdJTiBESCBQQVJBTUVURVJ..." +kind: Secret +type: Opaque +metadata: + name: lb-dhparam + namespace: kube-system +``` + +```console +$ kubectl create -f ssl-dh-param.yaml +``` + +## Controller + +You can deploy the controller as follows: + +```console +$ kubectl apply -f nginx-ingress-controller.yaml +deployment "nginx-ingress-controller" created + +$ kubectl -n kube-system get po +NAME READY STATUS RESTARTS AGE +default-http-backend-2657704409-qgwdd 1/1 Running 0 2m +nginx-ingress-controller-873061567-4n3k2 1/1 Running 0 42s +``` + +## Test + +Check the contents of the configmap is present in the nginx.conf file using: +`kubectl exec nginx-ingress-controller-873061567-4n3k2 -n kube-system cat /etc/nginx/nginx.conf` diff --git a/examples/customization/ssl-dh-param/nginx/default-backend.yaml b/examples/customization/ssl-dh-param/nginx/default-backend.yaml new file mode 100644 index 0000000000..3c40989a31 --- /dev/null +++ b/examples/customization/ssl-dh-param/nginx/default-backend.yaml @@ -0,0 +1,51 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: default-http-backend + labels: + k8s-app: default-http-backend + namespace: kube-system +spec: + replicas: 1 + template: + metadata: + labels: + k8s-app: default-http-backend + spec: + terminationGracePeriodSeconds: 60 + containers: + - name: default-http-backend + # Any image is permissable as long as: + # 1. It serves a 404 page at / + # 2. It serves 200 on a /healthz endpoint + image: gcr.io/google_containers/defaultbackend:1.0 + livenessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + initialDelaySeconds: 30 + timeoutSeconds: 5 + ports: + - containerPort: 8080 + resources: + limits: + cpu: 10m + memory: 20Mi + requests: + cpu: 10m + memory: 20Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: default-http-backend + namespace: kube-system + labels: + k8s-app: default-http-backend +spec: + ports: + - port: 80 + targetPort: 8080 + selector: + k8s-app: default-http-backend diff --git a/examples/customization/ssl-dh-param/nginx/nginx-ingress-controller.yaml b/examples/customization/ssl-dh-param/nginx/nginx-ingress-controller.yaml new file mode 100644 index 0000000000..5786f03d94 --- /dev/null +++ b/examples/customization/ssl-dh-param/nginx/nginx-ingress-controller.yaml @@ -0,0 +1,53 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: nginx-ingress-controller + labels: + k8s-app: nginx-ingress-controller + namespace: kube-system +spec: + replicas: 1 + template: + metadata: + labels: + k8s-app: nginx-ingress-controller + spec: + # hostNetwork makes it possible to use ipv6 and to preserve the source IP correctly regardless of docker configuration + # however, it is not a hard dependency of the nginx-ingress-controller itself and it may cause issues if port 10254 already is taken on the host + # that said, since hostPort is broken on CNI (https://github.com/kubernetes/kubernetes/issues/31307) we have to use hostNetwork where CNI is used + # like with kubeadm + # hostNetwork: true + terminationGracePeriodSeconds: 60 + containers: + - image: gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.3 + name: nginx-ingress-controller + readinessProbe: + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + livenessProbe: + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + timeoutSeconds: 1 + ports: + - containerPort: 80 + hostPort: 80 + - containerPort: 443 + hostPort: 443 + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + args: + - /nginx-ingress-controller + - --default-backend-service=$(POD_NAMESPACE)/default-http-backend + - --configmap=$(POD_NAMESPACE)/nginx-load-balancer-conf diff --git a/examples/customization/ssl-dh-param/nginx/nginx-load-balancer-conf.yaml b/examples/customization/ssl-dh-param/nginx/nginx-load-balancer-conf.yaml new file mode 100644 index 0000000000..6e8858c67d --- /dev/null +++ b/examples/customization/ssl-dh-param/nginx/nginx-load-balancer-conf.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + ssl-dh-param: "kube-system/lb-dhparam" +kind: ConfigMap +metadata: + name: nginx-load-balancer-conf + namespace: kube-system diff --git a/examples/customization/ssl-dh-param/nginx/ssl-dh-param.yaml b/examples/customization/ssl-dh-param/nginx/ssl-dh-param.yaml new file mode 100644 index 0000000000..14fdfb30ef --- /dev/null +++ b/examples/customization/ssl-dh-param/nginx/ssl-dh-param.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +data: + dhparam.pem: "...base64 encoded data..." +kind: Secret +type: Opaque +metadata: + name: lb-dhparam + namespace: kube-system