Skip to content

Commit 69a005a

Browse files
authored
Merge pull request #75 from thetechnick/hsts
Added hsts settings, Refactored bool/integer parsing
2 parents 8e42b83 + faf07d5 commit 69a005a

File tree

7 files changed

+294
-38
lines changed

7 files changed

+294
-38
lines changed

nginx-controller/controller/controller.go

+36-13
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package controller
1919
import (
2020
"fmt"
2121
"reflect"
22-
"strconv"
2322
"strings"
2423
"time"
2524

@@ -325,22 +324,46 @@ func (lbc *LoadBalancerController) syncCfgm(key string) {
325324
if serverNamesHashMaxSize, exists := cfgm.Data["server-names-hash-max-size"]; exists {
326325
cfg.MainServerNamesHashMaxSize = serverNamesHashMaxSize
327326
}
328-
if HTTP2Str, exists := cfgm.Data["http2"]; exists {
329-
if HTTP2, err := strconv.ParseBool(HTTP2Str); err == nil {
330-
cfg.HTTP2 = HTTP2
331-
} else {
332-
glog.Errorf("In configmap %v/%v 'http2' contains invalid declaration: %v, ignoring", cfgm.Namespace, cfgm.Name, err)
333-
}
327+
HTTP2, err := nginx.GetMapKeyAsBool(cfgm.Data, "http2", cfgm)
328+
if err != nil && err != nginx.ErrorKeyNotFound {
329+
glog.Error(err)
330+
} else {
331+
cfg.HTTP2 = HTTP2
332+
}
333+
334+
// HSTS block
335+
HSTSErrors := false
336+
HSTS, err := nginx.GetMapKeyAsBool(cfgm.Data, "hsts", cfgm)
337+
if err != nil && err != nginx.ErrorKeyNotFound {
338+
glog.Error(err)
339+
HSTSErrors = true
340+
}
341+
HSTSMaxAge, err := nginx.GetMapKeyAsInt(cfgm.Data, "hsts-max-age", cfgm)
342+
if err != nil && err != nginx.ErrorKeyNotFound {
343+
glog.Error(err)
344+
HSTSErrors = true
345+
}
346+
HSTSIncludeSubdomains, err := nginx.GetMapKeyAsBool(cfgm.Data, "hsts-include-subdomains", cfgm)
347+
if err != nil && err != nginx.ErrorKeyNotFound {
348+
glog.Error(err)
349+
HSTSErrors = true
334350
}
351+
if HSTSErrors {
352+
glog.Warningf("Configmap %s/%s: There are configuration issues with hsts annotations, skipping options for all hsts settings", cfgm.GetNamespace(), cfgm.GetName())
353+
} else {
354+
cfg.HSTS = HSTS
355+
cfg.HSTSMaxAge = HSTSMaxAge
356+
cfg.HSTSIncludeSubdomains = HSTSIncludeSubdomains
357+
}
358+
335359
if logFormat, exists := cfgm.Data["log-format"]; exists {
336360
cfg.MainLogFormat = logFormat
337361
}
338-
if proxyBufferingStr, exists := cfgm.Data["proxy-buffering"]; exists {
339-
if ProxyBuffering, err := strconv.ParseBool(proxyBufferingStr); err == nil {
340-
cfg.ProxyBuffering = ProxyBuffering
341-
} else {
342-
glog.Errorf("In configmap %v/%v 'proxy-buffering' contains invalid declaration: %v, ignoring", cfgm.Namespace, cfgm.Name, err)
343-
}
362+
ProxyBuffering, err := nginx.GetMapKeyAsBool(cfgm.Data, "proxy-buffering", cfgm)
363+
if err != nil && err != nginx.ErrorKeyNotFound {
364+
glog.Error(err)
365+
} else {
366+
cfg.ProxyBuffering = ProxyBuffering
344367
}
345368
if proxyBuffers, exists := cfgm.Data["proxy-buffers"]; exists {
346369
cfg.ProxyBuffers = proxyBuffers

nginx-controller/nginx/config.go

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ type Config struct {
1313
ProxyBuffers string
1414
ProxyBufferSize string
1515
ProxyMaxTempFileSize string
16+
HSTS bool
17+
HSTSMaxAge int64
18+
HSTSIncludeSubdomains bool
1619
}
1720

1821
// NewDefaultConfig creates a Config with default values
@@ -23,5 +26,6 @@ func NewDefaultConfig() *Config {
2326
ClientMaxBodySize: "1m",
2427
MainServerNamesHashMaxSize: "512",
2528
ProxyBuffering: true,
29+
HSTSMaxAge: 2592000,
2630
}
2731
}

nginx-controller/nginx/configurator.go

+46-17
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package nginx
22

33
import (
44
"fmt"
5-
"strconv"
65
"strings"
76
"sync"
87

@@ -104,8 +103,11 @@ func (cnf *Configurator) generateNginxCfg(ingEx *IngressEx, pems map[string]stri
104103
}
105104

106105
server := Server{
107-
Name: serverName,
108-
HTTP2: ingCfg.HTTP2,
106+
Name: serverName,
107+
HTTP2: ingCfg.HTTP2,
108+
HSTS: ingCfg.HSTS,
109+
HSTSMaxAge: ingCfg.HSTSMaxAge,
110+
HSTSIncludeSubdomains: ingCfg.HSTSIncludeSubdomains,
109111
}
110112

111113
if pemFile, ok := pems[serverName]; ok {
@@ -145,8 +147,11 @@ func (cnf *Configurator) generateNginxCfg(ingEx *IngressEx, pems map[string]stri
145147

146148
if len(ingEx.Ingress.Spec.Rules) == 0 && ingEx.Ingress.Spec.Backend != nil {
147149
server := Server{
148-
Name: emptyHost,
149-
HTTP2: ingCfg.HTTP2,
150+
Name: emptyHost,
151+
HTTP2: ingCfg.HTTP2,
152+
HSTS: ingCfg.HSTS,
153+
HSTSMaxAge: ingCfg.HSTSMaxAge,
154+
HSTSIncludeSubdomains: ingCfg.HSTSIncludeSubdomains,
150155
}
151156

152157
if pemFile, ok := pems[emptyHost]; ok {
@@ -180,20 +185,44 @@ func (cnf *Configurator) createConfig(ingEx *IngressEx) Config {
180185
if clientMaxBodySize, exists := ingEx.Ingress.Annotations["nginx.org/client-max-body-size"]; exists {
181186
ingCfg.ClientMaxBodySize = clientMaxBodySize
182187
}
183-
if HTTP2Str, exists := ingEx.Ingress.Annotations["nginx.org/http2"]; exists {
184-
if HTTP2, err := strconv.ParseBool(HTTP2Str); err == nil {
185-
ingCfg.HTTP2 = HTTP2
186-
} else {
187-
glog.Errorf("In %v/%v nginx.org/http2 contains invalid declaration: %v, ignoring", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err)
188-
}
188+
HTTP2, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, "nginx.org/http2", ingEx.Ingress)
189+
if err != nil && err != ErrorKeyNotFound {
190+
glog.Error(err)
191+
} else {
192+
ingCfg.HTTP2 = HTTP2
189193
}
190-
if proxyBufferingStr, exists := ingEx.Ingress.Annotations["nginx.org/proxy-buffering"]; exists {
191-
if ProxyBuffering, err := strconv.ParseBool(proxyBufferingStr); err == nil {
192-
ingCfg.ProxyBuffering = ProxyBuffering
193-
} else {
194-
glog.Errorf("In %v/%v nginx.org/proxy-buffering contains invalid declaration: %v, ignoring", ingEx.Ingress.Namespace, ingEx.Ingress.Name, err)
195-
}
194+
ProxyBuffering, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, "nginx.org/proxy-buffering", ingEx.Ingress)
195+
if err != nil && err != ErrorKeyNotFound {
196+
glog.Error(err)
197+
} else {
198+
ingCfg.ProxyBuffering = ProxyBuffering
199+
}
200+
201+
// HSTS block
202+
HSTSErrors := false
203+
HSTS, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, "nginx.org/hsts", ingEx.Ingress)
204+
if err != nil && err != ErrorKeyNotFound {
205+
glog.Error(err)
206+
HSTSErrors = true
196207
}
208+
HSTSMaxAge, err := GetMapKeyAsInt(ingEx.Ingress.Annotations, "nginx.org/hsts-max-age", ingEx.Ingress)
209+
if err != nil && err != ErrorKeyNotFound {
210+
glog.Error(err)
211+
HSTSErrors = true
212+
}
213+
HSTSIncludeSubdomains, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, "nginx.org/hsts-include-subdomains", ingEx.Ingress)
214+
if err != nil && err != ErrorKeyNotFound {
215+
glog.Error(err)
216+
HSTSErrors = true
217+
}
218+
if HSTSErrors {
219+
glog.Warningf("Ingress %s/%s: There are configuration issues with hsts annotations, skipping annotions for all hsts settings", ingEx.Ingress.GetNamespace(), ingEx.Ingress.GetName())
220+
} else {
221+
ingCfg.HSTS = HSTS
222+
ingCfg.HSTSMaxAge = HSTSMaxAge
223+
ingCfg.HSTSIncludeSubdomains = HSTSIncludeSubdomains
224+
}
225+
197226
if proxyBuffers, exists := ingEx.Ingress.Annotations["nginx.org/proxy-buffers"]; exists {
198227
ingCfg.ProxyBuffers = proxyBuffers
199228
}

nginx-controller/nginx/convert.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package nginx
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strconv"
7+
8+
"k8s.io/kubernetes/pkg/api/meta"
9+
"k8s.io/kubernetes/pkg/runtime"
10+
)
11+
12+
var (
13+
// ErrorKeyNotFound is returned if the key was not found in the map
14+
ErrorKeyNotFound = errors.New("Key not found in map")
15+
)
16+
17+
// There seems to be no composite interface in the kubernetes api package,
18+
// so we have to declare our own.
19+
type apiObject interface {
20+
meta.Object
21+
runtime.Object
22+
}
23+
24+
// GetMapKeyAsBool searches the map for the given key and parses the key as bool
25+
func GetMapKeyAsBool(m map[string]string, key string, context apiObject) (bool, error) {
26+
if str, exists := m[key]; exists {
27+
b, err := strconv.ParseBool(str)
28+
if err != nil {
29+
return false, fmt.Errorf("%s %v/%v '%s' contains invalid bool: %v, ignoring", context.GetObjectKind().GroupVersionKind().Kind, context.GetNamespace(), context.GetName(), key, err)
30+
}
31+
return b, nil
32+
}
33+
return false, ErrorKeyNotFound
34+
}
35+
36+
// GetMapKeyAsInt tries to find and parse a key in a map as int64
37+
func GetMapKeyAsInt(m map[string]string, key string, context apiObject) (int64, error) {
38+
if str, exists := m[key]; exists {
39+
i, err := strconv.ParseInt(str, 10, 64)
40+
if err != nil {
41+
return 0, fmt.Errorf("%s %v/%v '%s' contains invalid integer: %v, ignoring", context.GetObjectKind().GroupVersionKind().Kind, context.GetNamespace(), context.GetName(), key, err)
42+
}
43+
return i, nil
44+
}
45+
return 0, ErrorKeyNotFound
46+
}
+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package nginx
2+
3+
import (
4+
"testing"
5+
6+
"k8s.io/kubernetes/pkg/api"
7+
"k8s.io/kubernetes/pkg/api/unversioned"
8+
"k8s.io/kubernetes/pkg/apis/extensions"
9+
)
10+
11+
var configMap = api.ConfigMap{
12+
ObjectMeta: api.ObjectMeta{
13+
Name: "test",
14+
Namespace: "default",
15+
},
16+
TypeMeta: unversioned.TypeMeta{
17+
Kind: "ConfigMap",
18+
APIVersion: "v1",
19+
},
20+
}
21+
var ingress = extensions.Ingress{
22+
ObjectMeta: api.ObjectMeta{
23+
Name: "test",
24+
Namespace: "kube-system",
25+
},
26+
TypeMeta: unversioned.TypeMeta{
27+
Kind: "Ingress",
28+
APIVersion: "extensions/v1beta1",
29+
},
30+
}
31+
32+
//
33+
// GetMapKeyAsBool
34+
//
35+
func TestGetMapKeyAsBool(t *testing.T) {
36+
configMap := configMap
37+
configMap.Data = map[string]string{
38+
"key": "True",
39+
}
40+
41+
b, err := GetMapKeyAsBool(configMap.Data, "key", &configMap)
42+
if err != nil {
43+
t.Errorf("Unexpected error: %v", err)
44+
}
45+
if b != true {
46+
t.Errorf("Result should be true")
47+
}
48+
}
49+
50+
func TestGetMapKeyAsBoolNotFound(t *testing.T) {
51+
configMap := configMap
52+
configMap.Data = map[string]string{}
53+
54+
_, err := GetMapKeyAsBool(configMap.Data, "key", &configMap)
55+
if err != ErrorKeyNotFound {
56+
t.Errorf("ErrorKeyNotFound was expected, got: %v", err)
57+
}
58+
}
59+
60+
func TestGetMapKeyAsBoolErrorMessage(t *testing.T) {
61+
cfgm := configMap
62+
cfgm.Data = map[string]string{
63+
"key": "string",
64+
}
65+
66+
// Test with configmap
67+
_, err := GetMapKeyAsBool(cfgm.Data, "key", &cfgm)
68+
if err == nil {
69+
t.Error("An error was expected")
70+
}
71+
expected := `ConfigMap default/test 'key' contains invalid bool: strconv.ParseBool: parsing "string": invalid syntax, ignoring`
72+
if err.Error() != expected {
73+
t.Errorf("The error message does not match expectations:\nGot: %v\nExpected: %v", err, expected)
74+
}
75+
76+
// Test with ingress object
77+
ingress := ingress
78+
ingress.Annotations = map[string]string{
79+
"key": "other_string",
80+
}
81+
_, err = GetMapKeyAsBool(ingress.Annotations, "key", &ingress)
82+
if err == nil {
83+
t.Error("An error was expected")
84+
}
85+
expected = `Ingress kube-system/test 'key' contains invalid bool: strconv.ParseBool: parsing "other_string": invalid syntax, ignoring`
86+
if err.Error() != expected {
87+
t.Errorf("The error message does not match expectations:\nGot: %v\nExpected: %v", err, expected)
88+
}
89+
}
90+
91+
//
92+
// GetMapKeyAsInt
93+
//
94+
func TestGetMapKeyAsInt(t *testing.T) {
95+
configMap := configMap
96+
configMap.Data = map[string]string{
97+
"key": "123456789",
98+
}
99+
100+
i, err := GetMapKeyAsInt(configMap.Data, "key", &configMap)
101+
if err != nil {
102+
t.Errorf("Unexpected error: %v", err)
103+
}
104+
var expected int64 = 123456789
105+
if i != expected {
106+
t.Errorf("Unexpected return value:\nGot: %v\nExpected: %v", i, expected)
107+
}
108+
}
109+
110+
func TestGetMapKeyAsIntNotFound(t *testing.T) {
111+
configMap := configMap
112+
configMap.Data = map[string]string{}
113+
114+
_, err := GetMapKeyAsInt(configMap.Data, "key", &configMap)
115+
if err != ErrorKeyNotFound {
116+
t.Errorf("ErrorKeyNotFound was expected, got: %v", err)
117+
}
118+
}
119+
120+
func TestGetMapKeyAsIntErrorMessage(t *testing.T) {
121+
cfgm := configMap
122+
cfgm.Data = map[string]string{
123+
"key": "string",
124+
}
125+
126+
// Test with configmap
127+
_, err := GetMapKeyAsInt(cfgm.Data, "key", &cfgm)
128+
if err == nil {
129+
t.Error("An error was expected")
130+
}
131+
expected := `ConfigMap default/test 'key' contains invalid integer: strconv.ParseInt: parsing "string": invalid syntax, ignoring`
132+
if err.Error() != expected {
133+
t.Errorf("The error message does not match expectations:\nGot: %v\nExpected: %v", err, expected)
134+
}
135+
136+
// Test with ingress object
137+
ingress := ingress
138+
ingress.Annotations = map[string]string{
139+
"key": "other_string",
140+
}
141+
_, err = GetMapKeyAsInt(ingress.Annotations, "key", &ingress)
142+
if err == nil {
143+
t.Error("An error was expected")
144+
}
145+
expected = `Ingress kube-system/test 'key' contains invalid integer: strconv.ParseInt: parsing "other_string": invalid syntax, ignoring`
146+
if err.Error() != expected {
147+
t.Errorf("The error message does not match expectations:\nGot: %v\nExpected: %v", err, expected)
148+
}
149+
}

nginx-controller/nginx/ingress.tmpl

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ server {
2121
if ($scheme = http) {
2222
return 301 https://$host$request_uri;
2323
}
24-
{{end}}
24+
{{- if $server.HSTS}}
25+
add_header Strict-Transport-Security "max-age={{$server.HSTSMaxAge}}; {{if $server.HSTSIncludeSubdomains}}includeSubDomains; {{end}}preload" always;{{end}}
26+
{{- end}}
2527

2628
{{range $location := $server.Locations}}
2729
location {{$location.Path}} {

0 commit comments

Comments
 (0)