From b52a65cc02ce7b0813d0e47c5d1fbc198cc0dfa3 Mon Sep 17 00:00:00 2001 From: Anish Ramasekar Date: Tue, 30 Jan 2018 23:26:35 -0600 Subject: [PATCH 1/2] Add support for adding ssl_ciphers --- internal/ingress/annotations/annotations.go | 3 + .../ingress/annotations/sslcipher/main.go | 39 ++++++++++++ .../annotations/sslcipher/main_test.go | 63 +++++++++++++++++++ internal/ingress/controller/controller.go | 6 ++ internal/ingress/types.go | 3 +- internal/ingress/types_equals.go | 3 + rootfs/etc/nginx/template/nginx.tmpl | 4 ++ 7 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 internal/ingress/annotations/sslcipher/main.go create mode 100644 internal/ingress/annotations/sslcipher/main_test.go diff --git a/internal/ingress/annotations/annotations.go b/internal/ingress/annotations/annotations.go index 2d56d4a878..d5321641d6 100644 --- a/internal/ingress/annotations/annotations.go +++ b/internal/ingress/annotations/annotations.go @@ -19,6 +19,7 @@ package annotations import ( "github.com/golang/glog" "github.com/imdario/mergo" + "k8s.io/ingress-nginx/internal/ingress/annotations/sslcipher" extensions "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -85,6 +86,7 @@ type Ingress struct { VtsFilterKey string Whitelist ipwhitelist.SourceRange XForwardedPrefix bool + SSLCiphers string } // Extractor defines the annotation parsers to be used in the extraction of annotations @@ -121,6 +123,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor { "VtsFilterKey": vtsfilterkey.NewParser(cfg), "Whitelist": ipwhitelist.NewParser(cfg), "XForwardedPrefix": xforwardedprefix.NewParser(cfg), + "SSLCiphers": sslcipher.NewParser(cfg), }, } } diff --git a/internal/ingress/annotations/sslcipher/main.go b/internal/ingress/annotations/sslcipher/main.go new file mode 100644 index 0000000000..e8321848ee --- /dev/null +++ b/internal/ingress/annotations/sslcipher/main.go @@ -0,0 +1,39 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sslcipher + +import ( + extensions "k8s.io/api/extensions/v1beta1" + + "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/resolver" +) + +type sslCipher struct { + r resolver.Resolver +} + +// NewParser creates a new sslCipher annotation parser +func NewParser(r resolver.Resolver) parser.IngressAnnotation { + return sslCipher{r} +} + +// Parse parses the annotations contained in the ingress rule +// used to add ssl-ciphers to the server name +func (sc sslCipher) Parse(ing *extensions.Ingress) (interface{}, error) { + return parser.GetStringAnnotation("ssl-ciphers", ing) +} diff --git a/internal/ingress/annotations/sslcipher/main_test.go b/internal/ingress/annotations/sslcipher/main_test.go new file mode 100644 index 0000000000..2a81738e84 --- /dev/null +++ b/internal/ingress/annotations/sslcipher/main_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sslcipher + +import ( + "testing" + + api "k8s.io/api/core/v1" + extensions "k8s.io/api/extensions/v1beta1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/resolver" +) + +func TestParse(t *testing.T) { + annotation := parser.GetAnnotationWithPrefix("ssl-ciphers") + ap := NewParser(&resolver.Mock{}) + if ap == nil { + t.Fatalf("expected a parser.IngressAnnotation but returned nil") + } + + testCases := []struct { + annotations map[string]string + expected string + }{ + {map[string]string{annotation: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, + {map[string]string{annotation: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"}, + "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"}, + {map[string]string{annotation: ""}, ""}, + {map[string]string{}, ""}, + {nil, ""}, + } + + ing := &extensions.Ingress{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Spec: extensions.IngressSpec{}, + } + + for _, testCase := range testCases { + ing.SetAnnotations(testCase.annotations) + result, _ := ap.Parse(ing) + if result != testCase.expected { + t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations) + } + } +} diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index abc8079201..a7647cf7ba 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -913,6 +913,7 @@ func (n *NGINXController) createServers(data []*extensions.Ingress, }, }, SSLPassthrough: anns.SSLPassthrough, + SSLCiphers: anns.SSLCiphers, } } } @@ -954,6 +955,11 @@ func (n *NGINXController) createServers(data []*extensions.Ingress, servers[host].ServerSnippet = anns.ServerSnippet } + // only add ssl ciphers if the server does not have one previously configured + if servers[host].SSLCiphers == "" && anns.SSLCiphers != "" { + servers[host].SSLCiphers = anns.SSLCiphers + } + // only add a certificate if the server does not have one previously configured if servers[host].SSLCertificate != "" { continue diff --git a/internal/ingress/types.go b/internal/ingress/types.go index eec95179f0..67c2a6cdb7 100644 --- a/internal/ingress/types.go +++ b/internal/ingress/types.go @@ -156,10 +156,11 @@ type Server struct { // CertificateAuth indicates the this server requires mutual authentication // +optional CertificateAuth authtls.Config `json:"certificateAuth"` - // ServerSnippet returns the snippet of server // +optional ServerSnippet string `json:"serverSnippet"` + // SSLCiphers returns list of ciphers to be enabled + SSLCiphers string `json:"sslCiphers,omitempty"` } // Location describes an URI inside a server. diff --git a/internal/ingress/types_equals.go b/internal/ingress/types_equals.go index 081542fe35..c4ad554ea9 100644 --- a/internal/ingress/types_equals.go +++ b/internal/ingress/types_equals.go @@ -281,6 +281,9 @@ func (s1 *Server) Equal(s2 *Server) bool { if len(s1.Locations) != len(s2.Locations) { return false } + if s1.SSLCiphers != s2.SSLCiphers { + return false + } // Location are sorted for idx, s1l := range s1.Locations { diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index e4c9e6c574..7e63253c12 100644 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -634,6 +634,10 @@ stream { {{ end }} {{ end }} + {{ if not (empty $server.SSLCiphers) }} + ssl_ciphers {{ $server.SSLCiphers }}; + {{ end }} + {{ if not (empty $server.ServerSnippet) }} {{ $server.ServerSnippet }} {{ end }} From 38cad21448f258a5db7c9c82398d6553b41ff282 Mon Sep 17 00:00:00 2001 From: Anish Ramasekar Date: Tue, 30 Jan 2018 23:31:33 -0600 Subject: [PATCH 2/2] Add documentation --- docs/user-guide/annotations.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/user-guide/annotations.md b/docs/user-guide/annotations.md index f6b686f1d8..3dcbbc1713 100644 --- a/docs/user-guide/annotations.md +++ b/docs/user-guide/annotations.md @@ -59,6 +59,7 @@ The following annotations are supported: |[nginx.ingress.kubernetes.io/upstream-vhost](#custom-nginx-upstream-vhost)|string| |[nginx.ingress.kubernetes.io/whitelist-source-range](#whitelist-source-range)|CIDR| |[nginx.ingress.kubernetes.io/proxy-buffering](#proxy-buffering)|string| +|[nginx.ingress.kubernetes.io/ssl-ciphers](#ssl-ciphers)|string| **Note:** all the values must be a string. In case of booleans or number it must be quoted. @@ -419,3 +420,13 @@ To use custom values in an Ingress rule define these annotation: ```yaml nginx.ingress.kubernetes.io/proxy-buffering: "on" ``` + +### SSL ciphers + +Specifies the [enabled ciphers](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ciphers). + +Using this annotation will set the `ssl_ciphers` directive at the server level. This configuration is active for all the paths in the host. + +```yaml +nginx.ingress.kubernetes.io/ssl-ciphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP" +```