From 092164a9ec43db96eeda81e26b8d955f5307ce52 Mon Sep 17 00:00:00 2001 From: Raul Marrero Date: Thu, 21 Feb 2019 12:33:16 +0000 Subject: [PATCH] Add Wildcard certificate to ingress resources * Closes #426 --- cmd/nginx-ingress/main.go | 49 +++++++++--- deployments/helm-chart/Chart.yaml | 2 +- deployments/helm-chart/README.md | 3 + .../templates/controller-daemonset.yaml | 5 ++ .../templates/controller-deployment.yaml | 5 ++ .../templates/controller-wildcard-secret.yaml | 15 ++++ deployments/helm-chart/values.yaml | 6 +- docs/cli-arguments.md | 4 + internal/controller/controller.go | 80 ++++++++++++------- internal/controller/controller_test.go | 8 +- internal/nginx/configurator.go | 52 +++++++----- internal/nginx/configurator_test.go | 26 +++++- 12 files changed, 182 insertions(+), 73 deletions(-) create mode 100644 deployments/helm-chart/templates/controller-wildcard-secret.yaml diff --git a/cmd/nginx-ingress/main.go b/cmd/nginx-ingress/main.go index 332bc26aab..ab4239d32c 100644 --- a/cmd/nginx-ingress/main.go +++ b/cmd/nginx-ingress/main.go @@ -93,6 +93,11 @@ The external address of the service is used when reporting the status of Ingress nginxDebug = flag.Bool("nginx-debug", false, "Enable debugging for NGINX. Uses the nginx-debug binary. Requires 'error-log-level: debug' in the ConfigMap.") + + wildcardTLSSecret = flag.String("wildcard-tls-secret", "", + `A Secret with a TLS certificate and key for TLS termination of every Ingress host for which TLS termination is enabled but the Secret is not specified. + Format: /. If the argument is not set, for such Ingress hosts NGINX will break any attempt to establish a TLS connection. + If the argument is set, but the Ingress controller is not able to fetch the Secret from Kubernetes API, the Ingress controller will fail to start.`) ) func main() { @@ -168,17 +173,9 @@ func main() { ngxc := nginx.NewNginxController("/etc/nginx/", nginxBinaryPath, local) if *defaultServerSecret != "" { - ns, name, err := utils.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 = nginx.ValidateTLSSecret(secret) + secret, err := getAndValidateSecret(kubeClient, *defaultServerSecret) if err != nil { - glog.Fatalf("%v is invalid: %v", *defaultServerSecret, err) + glog.Fatalf("Error trying to get the default server TLS secret %v: %v", *defaultServerSecret, err) } bytes := nginx.GenerateCertAndKeyFileContent(secret) @@ -190,6 +187,16 @@ func main() { } } + if *wildcardTLSSecret != "" { + secret, err := getAndValidateSecret(kubeClient, *wildcardTLSSecret) + if err != nil { + glog.Fatalf("Error trying to get the wildcard TLS secret %v: %v", *wildcardTLSSecret, err) + } + + bytes := nginx.GenerateCertAndKeyFileContent(secret) + ngxc.AddOrUpdateSecretFile(nginx.WildcardSecretName, bytes, nginx.TLSSecretFileMode) + } + cfg := nginx.NewDefaultConfig() if *nginxConfigMaps != "" { ns, name, err := utils.ParseNamespaceName(*nginxConfigMaps) @@ -242,8 +249,8 @@ func main() { glog.Fatalf("Failed to create NginxAPIController: %v", err) } } - - cnf := nginx.NewConfigurator(ngxc, cfg, nginxAPI, templateExecutor) + isWildcardEnabled := *wildcardTLSSecret != "" + cnf := nginx.NewConfigurator(ngxc, cfg, nginxAPI, templateExecutor, isWildcardEnabled) controllerNamespace := os.Getenv("POD_NAMESPACE") lbcInput := controller.NewLoadBalancerControllerInput{ @@ -259,6 +266,7 @@ func main() { ControllerNamespace: controllerNamespace, ReportIngressStatus: *reportIngressStatus, IsLeaderElectionEnabled: *leaderElectionEnabled, + WildcardTLSSecret: *wildcardTLSSecret, } lbc := controller.NewLoadBalancerController(lbcInput) @@ -381,3 +389,20 @@ func validateCIDRorIP(cidr string) error { } return nil } + +// getAndValidateSecret gets and validates a secret. +func getAndValidateSecret(kubeClient *kubernetes.Clientset, secretNsName string) (secret *api_v1.Secret, err error) { + ns, name, err := utils.ParseNamespaceName(secretNsName) + if err != nil { + return nil, fmt.Errorf("could not parse the %v argument: %v", secretNsName, err) + } + secret, err = kubeClient.CoreV1().Secrets(ns).Get(name, meta_v1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("could not get %v: %v", secretNsName, err) + } + err = nginx.ValidateTLSSecret(secret) + if err != nil { + return nil, fmt.Errorf("%v is invalid: %v", secretNsName, err) + } + return secret, nil +} diff --git a/deployments/helm-chart/Chart.yaml b/deployments/helm-chart/Chart.yaml index b10c022a9f..cdbd927ece 100644 --- a/deployments/helm-chart/Chart.yaml +++ b/deployments/helm-chart/Chart.yaml @@ -1,5 +1,5 @@ name: nginx-ingress -version: 0.3.2 +version: 0.3.3 appVersion: edge description: NGINX Ingress Controller sources: diff --git a/deployments/helm-chart/README.md b/deployments/helm-chart/README.md index 1b7554d366..c47ea7d1ed 100644 --- a/deployments/helm-chart/README.md +++ b/deployments/helm-chart/README.md @@ -69,6 +69,9 @@ Parameter | Description | Default `controller.defaultTLS.cert` | The base64-encoded TLS certificate for the default HTTPS server. If not specified, a pre-generated self-signed certificate is used. **Note:** It is recommended that you specify your own certificate. | A pre-generated self-signed certificate. `controller.defaultTLS.key` | The base64-encoded TLS key for the default HTTPS server. **Note:** If not specified, a pre-generated key is used. It is recommended that you specify your own key. | A pre-generated key. `controller.defaultTLS.secret` | The secret with a TLS certificate and key for the default HTTPS server. The value must follow the following format: `/`. Used as an alternative to specifiying a certifcate and key using `controller.defaultTLS.cert` and `controller.defaultTLS.key` parameters. | None +`controller.wildcardTLS.cert` | The base64-encoded TLS certificate for every Ingress host that has TLS enabled but no secret specified. If the parameter is not set, for such Ingress hosts NGINX will break any attempt to establish a TLS connection. | None +`controller.wildcardTLS.key` | The base64-encoded TLS key for every Ingress host that has TLS enabled but no secret specified. If the parameter is not set, for such Ingress hosts NGINX will break any attempt to establish a TLS connection. | None +`controller.wildcardTLS.secret` | The secret with a TLS certificate and key for every Ingress host that has TLS enabled but no secret specified. The value must follow the following format: `/`. Used as an alternative to specifying a certificate and key using `controller.wildcardTLS.cert` and `controller.wildcardTLS.key` parameters. | None `controller.nodeSelector` | The node selector for pod assignment for the Ingress controller pods. | { } `controller.terminationGracePeriodSeconds` | The termination grace period of the Ingress controller pod. | 30 `controller.tolerations` | The tolerations of the Ingress controller pods. | [] diff --git a/deployments/helm-chart/templates/controller-daemonset.yaml b/deployments/helm-chart/templates/controller-daemonset.yaml index 7fe9f2c5c9..1e0507b1fd 100644 --- a/deployments/helm-chart/templates/controller-daemonset.yaml +++ b/deployments/helm-chart/templates/controller-daemonset.yaml @@ -82,6 +82,11 @@ spec: - -external-service={{ .Values.controller.reportIngressStatus.externalService }} - -enable-leader-election={{ .Values.controller.reportIngressStatus.enableLeaderElection }} {{- end }} +{{- if .Values.controller.wildcardTLS.secret }} + - -wildcard-tls-secret={{ .Values.controller.wildcardTLS.secret }} +{{- else if and .Values.controller.wildcardTLS.cert .Values.controller.wildcardTLS.key }} + - -wildcard-tls-secret=$(POD_NAMESPACE)/wildcard-tls-secret +{{- end }} {{- if and .Values.prometheus.create .Values.controller.nginxStatus.enable }} - image: "{{ .Values.prometheus.image.repository }}:{{ .Values.prometheus.image.tag }}" name: nginx-prometheus-exporter diff --git a/deployments/helm-chart/templates/controller-deployment.yaml b/deployments/helm-chart/templates/controller-deployment.yaml index 00932d441f..02ecbd8d0e 100644 --- a/deployments/helm-chart/templates/controller-deployment.yaml +++ b/deployments/helm-chart/templates/controller-deployment.yaml @@ -80,6 +80,11 @@ spec: - -external-service={{ .Values.controller.reportIngressStatus.externalService }} - -enable-leader-election={{ .Values.controller.reportIngressStatus.enableLeaderElection }} {{- end }} +{{- if .Values.controller.wildcardTLS.secret }} + - -wildcard-tls-secret={{ .Values.controller.wildcardTLS.secret }} +{{- else if and .Values.controller.wildcardTLS.cert .Values.controller.wildcardTLS.key }} + - -wildcard-tls-secret=$(POD_NAMESPACE)/wildcard-tls-secret +{{- end }} {{- if and .Values.prometheus.create .Values.controller.nginxStatus.enable }} - image: "{{ .Values.prometheus.image.repository }}:{{ .Values.prometheus.image.tag }}" name: nginx-prometheus-exporter diff --git a/deployments/helm-chart/templates/controller-wildcard-secret.yaml b/deployments/helm-chart/templates/controller-wildcard-secret.yaml new file mode 100644 index 0000000000..f738da5b33 --- /dev/null +++ b/deployments/helm-chart/templates/controller-wildcard-secret.yaml @@ -0,0 +1,15 @@ +{{ if and (not .Values.controller.wildcardTLS.secret) (and .Values.controller.wildcardTLS.cert .Values.controller.wildcardTLS.key) }} +apiVersion: v1 +kind: Secret +metadata: + name: wildcard-tls-secret + labels: + app: {{ .Values.controller.name | trunc 63 }} + chart: {{ .Chart.Name }}-{{ .Chart.Version }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} +type: Opaque +data: + tls.crt: {{ .Values.controller.wildcardTLS.cert }} + tls.key: {{ .Values.controller.wildcardTLS.key }} +{{- end }} \ No newline at end of file diff --git a/deployments/helm-chart/values.yaml b/deployments/helm-chart/values.yaml index cf3c49739e..374d29332e 100644 --- a/deployments/helm-chart/values.yaml +++ b/deployments/helm-chart/values.yaml @@ -10,11 +10,15 @@ controller: pullPolicy: IfNotPresent config: entries: {} - # It is recommended to use your own TLS certificate and key + # It is recommended to use your own TLS certificates and keys defaultTLS: cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN2akNDQWFZQ0NRREFPRjl0THNhWFhEQU5CZ2txaGtpRzl3MEJBUXNGQURBaE1SOHdIUVlEVlFRRERCWk8KUjBsT1dFbHVaM0psYzNORGIyNTBjbTlzYkdWeU1CNFhEVEU0TURreE1qRTRNRE16TlZvWERUSXpNRGt4TVRFNApNRE16TlZvd0lURWZNQjBHQTFVRUF3d1dUa2RKVGxoSmJtZHlaWE56UTI5dWRISnZiR3hsY2pDQ0FTSXdEUVlKCktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUwvN2hIUEtFWGRMdjNyaUM3QlBrMTNpWkt5eTlyQ08KR2xZUXYyK2EzUDF0azIrS3YwVGF5aGRCbDRrcnNUcTZzZm8vWUk1Y2Vhbkw4WGM3U1pyQkVRYm9EN2REbWs1Qgo4eDZLS2xHWU5IWlg0Rm5UZ0VPaStlM2ptTFFxRlBSY1kzVnNPazFFeUZBL0JnWlJVbkNHZUtGeERSN0tQdGhyCmtqSXVuektURXUyaDU4Tlp0S21ScUJHdDEwcTNRYzhZT3ExM2FnbmovUWRjc0ZYYTJnMjB1K1lYZDdoZ3krZksKWk4vVUkxQUQ0YzZyM1lma1ZWUmVHd1lxQVp1WXN2V0RKbW1GNWRwdEMzN011cDBPRUxVTExSakZJOTZXNXIwSAo1TmdPc25NWFJNV1hYVlpiNWRxT3R0SmRtS3FhZ25TZ1JQQVpQN2MwQjFQU2FqYzZjNGZRVXpNQ0F3RUFBVEFOCkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWpLb2tRdGRPcEsrTzhibWVPc3lySmdJSXJycVFVY2ZOUitjb0hZVUoKdGhrYnhITFMzR3VBTWI5dm15VExPY2xxeC9aYzJPblEwMEJCLzlTb0swcitFZ1U2UlVrRWtWcitTTFA3NTdUWgozZWI4dmdPdEduMS9ienM3bzNBaS9kclkrcUI5Q2k1S3lPc3FHTG1US2xFaUtOYkcyR1ZyTWxjS0ZYQU80YTY3Cklnc1hzYktNbTQwV1U3cG9mcGltU1ZmaXFSdkV5YmN3N0NYODF6cFErUyt1eHRYK2VBZ3V0NHh3VlI5d2IyVXYKelhuZk9HbWhWNThDd1dIQnNKa0kxNXhaa2VUWXdSN0diaEFMSkZUUkk3dkhvQXprTWIzbjAxQjQyWjNrN3RXNQpJUDFmTlpIOFUvOWxiUHNoT21FRFZkdjF5ZytVRVJxbStGSis2R0oxeFJGcGZnPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBdi91RWM4b1JkMHUvZXVJTHNFK1RYZUprckxMMnNJNGFWaEMvYjVyYy9XMlRiNHEvClJOcktGMEdYaVN1eE9ycXgrajlnamx4NXFjdnhkenRKbXNFUkJ1Z1B0ME9hVGtIekhvb3FVWmcwZGxmZ1dkT0EKUTZMNTdlT1l0Q29VOUZ4amRXdzZUVVRJVUQ4R0JsRlNjSVo0b1hFTkhzbysyR3VTTWk2Zk1wTVM3YUhudzFtMApxWkdvRWEzWFNyZEJ6eGc2clhkcUNlUDlCMXl3VmRyYURiUzc1aGQzdUdETDU4cGszOVFqVUFQaHpxdmRoK1JWClZGNGJCaW9CbTVpeTlZTW1hWVhsMm0wTGZzeTZuUTRRdFFzdEdNVWozcGJtdlFmazJBNnljeGRFeFpkZFZsdmwKMm82MjBsMllxcHFDZEtCRThCay90elFIVTlKcU56cHpoOUJUTXdJREFRQUJBb0lCQVFDZklHbXowOHhRVmorNwpLZnZJUXQwQ0YzR2MxNld6eDhVNml4MHg4Mm15d1kxUUNlL3BzWE9LZlRxT1h1SENyUlp5TnUvZ2IvUUQ4bUFOCmxOMjRZTWl0TWRJODg5TEZoTkp3QU5OODJDeTczckM5bzVvUDlkazAvYzRIbjAzSkVYNzZ5QjgzQm9rR1FvYksKMjhMNk0rdHUzUmFqNjd6Vmc2d2szaEhrU0pXSzBwV1YrSjdrUkRWYmhDYUZhNk5nMUZNRWxhTlozVDhhUUtyQgpDUDNDeEFTdjYxWTk5TEI4KzNXWVFIK3NYaTVGM01pYVNBZ1BkQUk3WEh1dXFET1lvMU5PL0JoSGt1aVg2QnRtCnorNTZud2pZMy8yUytSRmNBc3JMTnIwMDJZZi9oY0IraVlDNzVWYmcydVd6WTY3TWdOTGQ5VW9RU3BDRkYrVm4KM0cyUnhybnhBb0dCQU40U3M0ZVlPU2huMVpQQjdhTUZsY0k2RHR2S2ErTGZTTXFyY2pOZjJlSEpZNnhubmxKdgpGenpGL2RiVWVTbWxSekR0WkdlcXZXaHFISy9iTjIyeWJhOU1WMDlRQ0JFTk5jNmtWajJTVHpUWkJVbEx4QzYrCk93Z0wyZHhKendWelU0VC84ajdHalRUN05BZVpFS2FvRHFyRG5BYWkyaW5oZU1JVWZHRXFGKzJyQW9HQkFOMVAKK0tZL0lsS3RWRzRKSklQNzBjUis3RmpyeXJpY05iWCtQVzUvOXFHaWxnY2grZ3l4b25BWlBpd2NpeDN3QVpGdwpaZC96ZFB2aTBkWEppc1BSZjRMazg5b2pCUmpiRmRmc2l5UmJYbyt3TFU4NUhRU2NGMnN5aUFPaTVBRHdVU0FkCm45YWFweUNweEFkREtERHdObit3ZFhtaTZ0OHRpSFRkK3RoVDhkaVpBb0dCQUt6Wis1bG9OOTBtYlF4VVh5YUwKMjFSUm9tMGJjcndsTmVCaWNFSmlzaEhYa2xpSVVxZ3hSZklNM2hhUVRUcklKZENFaHFsV01aV0xPb2I2NTNyZgo3aFlMSXM1ZUtka3o0aFRVdnpldm9TMHVXcm9CV2xOVHlGanIrSWhKZnZUc0hpOGdsU3FkbXgySkJhZUFVWUNXCndNdlQ4NmNLclNyNkQrZG8wS05FZzFsL0FvR0FlMkFVdHVFbFNqLzBmRzgrV3hHc1RFV1JqclRNUzRSUjhRWXQKeXdjdFA4aDZxTGxKUTRCWGxQU05rMXZLTmtOUkxIb2pZT2pCQTViYjhibXNVU1BlV09NNENoaFJ4QnlHbmR2eAphYkJDRkFwY0IvbEg4d1R0alVZYlN5T294ZGt5OEp0ek90ajJhS0FiZHd6NlArWDZDODhjZmxYVFo5MWpYL3RMCjF3TmRKS2tDZ1lCbyt0UzB5TzJ2SWFmK2UwSkN5TGhzVDQ5cTN3Zis2QWVqWGx2WDJ1VnRYejN5QTZnbXo5aCsKcDNlK2JMRUxwb3B0WFhNdUFRR0xhUkcrYlNNcjR5dERYbE5ZSndUeThXczNKY3dlSTdqZVp2b0ZpbmNvVlVIMwphdmxoTUVCRGYxSjltSDB5cDBwWUNaS2ROdHNvZEZtQktzVEtQMjJhTmtsVVhCS3gyZzR6cFE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= # secret: / + wildcardTLS: + cert: "" + key: "" + # secret: / nodeSelector: {} terminationGracePeriodSeconds: 30 tolerations: [] diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md index 320108e597..c37aa0de50 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -9,6 +9,10 @@ Usage of ./nginx-ingress: If not set, certificate and key in the file "/etc/nginx/secrets/default" are used. If a secret is set, but the Ingress controller is not able to fetch it from Kubernetes API or a secret is not set and the file "/etc/nginx/secrets/default" does not exist, the Ingress controller will fail to start + -wildcard-tls-secret string + A Secret with a TLS certificate and key for TLS termination of every Ingress host for which TLS termination is enabled but the Secret is not specified. + Format: /. If the argument is not set, for such Ingress hosts NGINX will break any attempt to establish a TLS connection. + If the argument is set, but the Ingress controller is not able to fetch the Secret from Kubernetes API, the Ingress controller will fail to start. -enable-leader-election Enable Leader election to avoid multiple replicas of the controller reporting the status of Ingress resources -- only one replica will report status. See -report-ingress-status flag. -external-service string diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 0b6344f178..e73acec8c5 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -79,6 +79,7 @@ type LoadBalancerController struct { resync time.Duration namespace string controllerNamespace string + wildcardTLSSecret string } var keyFunc = cache.DeletionHandlingMetaNamespaceKeyFunc @@ -97,6 +98,7 @@ type NewLoadBalancerControllerInput struct { ControllerNamespace string ReportIngressStatus bool IsLeaderElectionEnabled bool + WildcardTLSSecret string } // NewLoadBalancerController creates a controller @@ -113,6 +115,7 @@ func NewLoadBalancerController(input NewLoadBalancerControllerInput) *LoadBalanc resync: input.ResyncPeriod, namespace: input.Namespace, controllerNamespace: input.ControllerNamespace, + wildcardTLSSecret: input.WildcardTLSSecret, } eventBroadcaster := record.NewBroadcaster() @@ -647,10 +650,9 @@ func (lbc *LoadBalancerController) syncSecret(task queue.Task) { if !secrExists { glog.V(2).Infof("Deleting Secret: %v\n", key) - lbc.handleSecretDeletion(key, ings) - - if key == lbc.defaultServerSecret { - glog.Warningf("The default server Secret %v was removed. Retaining the Secret.", key) + lbc.handleRegularSecretDeletion(key, ings) + if lbc.isSpecialSecret(key) { + glog.Warningf("A special TLS Secret %v was removed. Retaining the Secret.", key) } return } @@ -659,9 +661,9 @@ func (lbc *LoadBalancerController) syncSecret(task queue.Task) { secret := obj.(*api_v1.Secret) - if key == lbc.defaultServerSecret { - lbc.handleDefaultSecretUpdate(secret) - // we don't return here in case the default secret is also used in Ingress resources + if lbc.isSpecialSecret(key) { + lbc.handleSpecialSecretUpdate(secret) + // we don't return here in case the special secret is also used in Ingress resources } if len(ings) > 0 { @@ -669,7 +671,11 @@ func (lbc *LoadBalancerController) syncSecret(task queue.Task) { } } -func (lbc *LoadBalancerController) handleSecretDeletion(key string, ings []extensions.Ingress) { +func (lbc *LoadBalancerController) isSpecialSecret(secretName string) bool { + return secretName == lbc.defaultServerSecret || secretName == lbc.wildcardTLSSecret +} + +func (lbc *LoadBalancerController) handleRegularSecretDeletion(key string, ings []extensions.Ingress) { eventType := api_v1.EventTypeWarning title := "Missing Secret" message := fmt.Sprintf("Secret %v was removed", key) @@ -702,7 +708,7 @@ func (lbc *LoadBalancerController) handleSecretUpdate(secret *api_v1.Secret, ing glog.Errorf("Couldn't validate secret %v: %v", secretNsName, err) glog.Errorf("Removing invalid secret %v", secretNsName) - lbc.handleSecretDeletion(secretNsName, ings) + lbc.handleRegularSecretDeletion(secretNsName, ings) lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "Rejected", "%v was rejected: %v", secretNsName, err) return @@ -726,24 +732,31 @@ func (lbc *LoadBalancerController) handleSecretUpdate(secret *api_v1.Secret, ing lbc.emitEventForIngresses(eventType, title, message, ings) } -func (lbc *LoadBalancerController) handleDefaultSecretUpdate(secret *api_v1.Secret) { +func (lbc *LoadBalancerController) handleSpecialSecretUpdate(secret *api_v1.Secret) { + var specialSecretsToUpdate []string secretNsName := secret.Namespace + "/" + secret.Name - err := nginx.ValidateTLSSecret(secret) if err != nil { - glog.Errorf("Couldn't validate the default server Secret %v: %v", secretNsName, err) - lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "Rejected", "the default server Secret %v was rejected, using the previous version: %v", secretNsName, err) + glog.Errorf("Couldn't validate the special Secret %v: %v", secretNsName, err) + lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "Rejected", "the special Secret %v was rejected, using the previous version: %v", secretNsName, err) return } - err = lbc.configurator.AddOrUpdateDefaultServerTLSSecret(secret) + if secretNsName == lbc.defaultServerSecret { + specialSecretsToUpdate = append(specialSecretsToUpdate, nginx.DefaultServerSecretName) + } + if secretNsName == lbc.wildcardTLSSecret { + specialSecretsToUpdate = append(specialSecretsToUpdate, nginx.WildcardSecretName) + } + + err = lbc.configurator.AddOrUpdateSpecialSecrets(secret, specialSecretsToUpdate) if err != nil { - glog.Errorf("Error when updating the default server Secret %v: %v", secretNsName, err) - lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "UpdatedWithError", "the default server Secret %v was updated, but not applied: %v", secretNsName, err) + glog.Errorf("Error when updating the special Secret %v: %v", secretNsName, err) + lbc.recorder.Eventf(secret, api_v1.EventTypeWarning, "UpdatedWithError", "the special Secret %v was updated, but not applied: %v", secretNsName, err) return } - lbc.recorder.Eventf(secret, api_v1.EventTypeNormal, "Updated", "the default server Secret %v was updated", secretNsName) + lbc.recorder.Eventf(secret, api_v1.EventTypeNormal, "Updated", "the special Secret %v was updated", secretNsName) } func (lbc *LoadBalancerController) emitEventForIngresses(eventType string, title string, message string, ings []extensions.Ingress) { @@ -906,6 +919,23 @@ func (lbc *LoadBalancerController) getIngressForEndpoints(obj interface{}) []ext return ings } +func (lbc *LoadBalancerController) getAndValidateSecret(secretKey string) (*api_v1.Secret, error) { + secretObject, secretExists, err := lbc.secretLister.GetByKey(secretKey) + if err != nil { + return nil, fmt.Errorf("error retrieving secret %v", secretKey) + } + if !secretExists { + return nil, fmt.Errorf("secret %v not found", secretKey) + } + secret := secretObject.(*api_v1.Secret) + + err = nginx.ValidateTLSSecret(secret) + if err != nil { + return nil, fmt.Errorf("error validating secret %v", secretKey) + } + return secret, nil +} + func (lbc *LoadBalancerController) createIngress(ing *extensions.Ingress) (*nginx.IngressEx, error) { ingEx := &nginx.IngressEx{ Ingress: ing, @@ -915,21 +945,9 @@ func (lbc *LoadBalancerController) createIngress(ing *extensions.Ingress) (*ngin for _, tls := range ing.Spec.TLS { secretName := tls.SecretName secretKey := ing.Namespace + "/" + secretName - - secretObject, secretExists, err := lbc.secretLister.GetByKey(secretKey) - if err != nil { - glog.Warningf("Error retrieving secret %v for Ingress %v: %v", secretName, ing.Name, err) - continue - } - if !secretExists { - glog.Warningf("secret %v not found for Ingress %v", secretKey, ing.Name) - continue - } - secret := secretObject.(*api_v1.Secret) - - err = nginx.ValidateTLSSecret(secret) + secret, err := lbc.getAndValidateSecret(secretKey) if err != nil { - glog.Warningf("Error validating secret %v for Ingress %v: %v", secretName, ing.Name, err) + glog.Warningf("Error trying to get the secret %v for Ingress %v: %v", secretName, ing.Name, err) continue } ingEx.TLSSecrets[secretName] = secret diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index f4cc6ff440..f992910a9a 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -549,7 +549,7 @@ func getMergableDefaults() (cafeMaster, coffeeMinion, teaMinion extensions.Ingre cafeMasterIngEx, _ := lbc.createIngress(&cafeMaster) ingExMap["default-cafe-master"] = cafeMasterIngEx - cnf := nginx.NewConfigurator(&nginx.Controller{}, &nginx.Config{}, &plus.NginxAPIController{}, &nginx.TemplateExecutor{}) + cnf := nginx.NewConfigurator(&nginx.Controller{}, &nginx.Config{}, &plus.NginxAPIController{}, &nginx.TemplateExecutor{}, false) // edit private field ingresses to use in testing pointerVal := reflect.ValueOf(cnf) @@ -782,7 +782,7 @@ func TestFindProbeForPods(t *testing.T) { func TestGetServicePortForIngressPort(t *testing.T) { fakeClient := fake.NewSimpleClientset() - cnf := nginx.NewConfigurator(&nginx.Controller{}, &nginx.Config{}, &plus.NginxAPIController{}, &nginx.TemplateExecutor{}) + cnf := nginx.NewConfigurator(&nginx.Controller{}, &nginx.Config{}, &plus.NginxAPIController{}, &nginx.TemplateExecutor{}, false) lbc := LoadBalancerController{ client: fakeClient, ingressClass: "nginx", @@ -936,7 +936,7 @@ func TestFindIngressesForSecret(t *testing.T) { t.Fatalf("NGINX API Controller could not start: %v", err) } - cnf := nginx.NewConfigurator(ngxc, &nginx.Config{}, apiCtrl, templateExecutor) + cnf := nginx.NewConfigurator(ngxc, &nginx.Config{}, apiCtrl, templateExecutor, false) lbc := LoadBalancerController{ client: fakeClient, ingressClass: "nginx", @@ -1117,7 +1117,7 @@ func TestFindIngressesForSecretWithMinions(t *testing.T) { t.Fatalf("NGINX API Controller could not start: %v", err) } - cnf := nginx.NewConfigurator(ngxc, &nginx.Config{}, apiCtrl, templateExecutor) + cnf := nginx.NewConfigurator(ngxc, &nginx.Config{}, apiCtrl, templateExecutor, false) lbc := LoadBalancerController{ client: fakeClient, ingressClass: "nginx", diff --git a/internal/nginx/configurator.go b/internal/nginx/configurator.go index 68ebf9dd22..dba4f9d187 100644 --- a/internal/nginx/configurator.go +++ b/internal/nginx/configurator.go @@ -18,10 +18,14 @@ import ( const emptyHost = "" const pemFileNameForMissingTLSSecret = "/etc/nginx/secrets/default" +const pemFileNameForWildcardTLSSecret = "/etc/nginx/secrets/wildcard" // DefaultServerSecretName is the filename of the Secret with a TLS cert and a key for the default server const DefaultServerSecretName = "default" +// WildcardSecretName is the filename of the Secret with a TLS cert and a key for the ingress resources with TLS termination enabled but not secret defined +const WildcardSecretName = "wildcard" + // JWTKeyKey is the key of the data field of a Secret where the JWK must be stored. const JWTKeyKey = "jwk" @@ -30,25 +34,26 @@ const JWTKeyAnnotation = "nginx.com/jwt-key" // Configurator transforms an Ingress resource into NGINX Configuration type Configurator struct { - nginx *Controller - config *Config - nginxAPI *plus.NginxAPIController - templateExecutor *TemplateExecutor - ingresses map[string]*IngressEx - minions map[string]map[string]bool + nginx *Controller + config *Config + nginxAPI *plus.NginxAPIController + templateExecutor *TemplateExecutor + ingresses map[string]*IngressEx + minions map[string]map[string]bool + isWildcardEnabled bool } // NewConfigurator creates a new Configurator -func NewConfigurator(nginx *Controller, config *Config, nginxAPI *plus.NginxAPIController, templateExecutor *TemplateExecutor) *Configurator { +func NewConfigurator(nginx *Controller, config *Config, nginxAPI *plus.NginxAPIController, templateExecutor *TemplateExecutor, isWildcardEnabled bool) *Configurator { cnf := Configurator{ - nginx: nginx, - config: config, - nginxAPI: nginxAPI, - ingresses: make(map[string]*IngressEx), - templateExecutor: templateExecutor, - minions: make(map[string]map[string]bool), + nginx: nginx, + config: config, + nginxAPI: nginxAPI, + ingresses: make(map[string]*IngressEx), + templateExecutor: templateExecutor, + minions: make(map[string]map[string]bool), + isWildcardEnabled: isWildcardEnabled, } - return &cnf } @@ -194,7 +199,9 @@ func (cnf *Configurator) updateTLSSecrets(ingEx *IngressEx) map[string]string { secretName := tls.SecretName pemFileName := pemFileNameForMissingTLSSecret - if secret, exists := ingEx.TLSSecrets[secretName]; exists { + if secretName == "" && cnf.isWildcardEnabled { + pemFileName = pemFileNameForWildcardTLSSecret + } else if secret, exists := ingEx.TLSSecrets[secretName]; exists { pemFileName = cnf.addOrUpdateSecret(secret) } @@ -254,7 +261,7 @@ func (cnf *Configurator) generateNginxCfg(ingEx *IngressEx, pems map[string]stri serverName := rule.Host - statuzZone := rule.Host + statusZone := rule.Host server := Server{ Name: serverName, @@ -267,7 +274,7 @@ func (cnf *Configurator) generateNginxCfg(ingEx *IngressEx, pems map[string]stri HSTSMaxAge: ingCfg.HSTSMaxAge, HSTSIncludeSubdomains: ingCfg.HSTSIncludeSubdomains, HSTSBehindProxy: ingCfg.HSTSBehindProxy, - StatusZone: statuzZone, + StatusZone: statusZone, RealIPHeader: ingCfg.RealIPHeader, SetRealIPFrom: ingCfg.SetRealIPFrom, RealIPRecursive: ingCfg.RealIPRecursive, @@ -959,13 +966,14 @@ func (cnf *Configurator) addOrUpdateSecret(secret *api_v1.Secret) string { return cnf.nginx.AddOrUpdateSecretFile(name, data, mode) } -// AddOrUpdateDefaultServerTLSSecret creates or updates a file with a TLS cert and a key from the secret for the default server. -func (cnf *Configurator) AddOrUpdateDefaultServerTLSSecret(secret *api_v1.Secret) error { +// AddOrUpdateSpecialSecrets creates or updates a file with a TLS cert and a key from a Special Secret (eg. DefaultServerSecret, WildcardTLSSecret) +func (cnf *Configurator) AddOrUpdateSpecialSecrets(secret *api_v1.Secret, secretNames []string) error { data := GenerateCertAndKeyFileContent(secret) - cnf.nginx.AddOrUpdateSecretFile(DefaultServerSecretName, data, TLSSecretFileMode) - + for _, secretName := range secretNames { + cnf.nginx.AddOrUpdateSecretFile(secretName, data, TLSSecretFileMode) + } if err := cnf.nginx.Reload(); err != nil { - return fmt.Errorf("Error when reloading NGINX when updating the default server Secret: %v", err) + return fmt.Errorf("Error when reloading NGINX when updating the special Secrets: %v", err) } return nil } diff --git a/internal/nginx/configurator_test.go b/internal/nginx/configurator_test.go index 39388d3254..d86666a4d3 100644 --- a/internal/nginx/configurator_test.go +++ b/internal/nginx/configurator_test.go @@ -533,7 +533,7 @@ func createTestConfigurator() (*Configurator, error) { if err != nil { return nil, err } - return NewConfigurator(ngxc, NewDefaultConfig(), apiCtrl, templateExecutor), nil + return NewConfigurator(ngxc, NewDefaultConfig(), apiCtrl, templateExecutor, false), nil } func createTestConfiguratorInvalidIngressTemplate() (*Configurator, error) { @@ -547,7 +547,7 @@ func createTestConfiguratorInvalidIngressTemplate() (*Configurator, error) { } ngxc := NewNginxController("/etc/nginx", "nginx", true) apiCtrl, _ := plus.NewNginxAPIController(&http.Client{}, "", true) - return NewConfigurator(ngxc, NewDefaultConfig(), apiCtrl, templateExecutor), nil + return NewConfigurator(ngxc, NewDefaultConfig(), apiCtrl, templateExecutor, false), nil } func TestGenerateNginxCfg(t *testing.T) { @@ -723,6 +723,28 @@ func TestGenerateNginxCfgWithMissingTLSSecret(t *testing.T) { } } +func TestGenerateNginxCfgWithWildcardTLSSecret(t *testing.T) { + cafeIngressEx := createCafeIngressEx() + cnf, err := createTestConfigurator() + if err != nil { + t.Errorf("Failed to create a test configurator: %v", err) + } + + pems := map[string]string{ + "cafe.example.com": pemFileNameForWildcardTLSSecret, + } + + result := cnf.generateNginxCfg(&cafeIngressEx, pems, false) + + resultServer := result.Servers[0] + if !reflect.DeepEqual(resultServer.SSLCertificate, pemFileNameForWildcardTLSSecret) { + t.Errorf("generateNginxCfg returned SSLCertificate %v, but expected %v", resultServer.SSLCertificate, pemFileNameForWildcardTLSSecret) + } + if !reflect.DeepEqual(resultServer.SSLCertificateKey, pemFileNameForWildcardTLSSecret) { + t.Errorf("generateNginxCfg returned SSLCertificateKey %v, but expected %v", resultServer.SSLCertificateKey, pemFileNameForWildcardTLSSecret) + } +} + func TestAddOrUpdateIngress(t *testing.T) { cnf, err := createTestConfigurator() if err != nil {