Browse code

Add SNI support

Jordan Liggitt authored on 2015/10/13 13:12:50
Showing 21 changed files
... ...
@@ -94,12 +94,13 @@ func (o *ValidateMasterConfigOptions) Run() (bool, error) {
94 94
 }
95 95
 
96 96
 const (
97
-	minColumnWidth          = 4
98
-	tabWidth                = 4
99
-	padding                 = 2
100
-	padchar                 = byte(' ')
101
-	flags                   = 0
102
-	validationErrorHeadings = "ERROR\tFIELD\tVALUE\tDETAILS\n"
97
+	minColumnWidth            = 4
98
+	tabWidth                  = 4
99
+	padding                   = 2
100
+	padchar                   = byte(' ')
101
+	flags                     = 0
102
+	validationErrorHeadings   = "ERROR\tFIELD\tVALUE\tDETAILS\n"
103
+	validationWarningHeadings = "WARNING\tFIELD\tVALUE\tDETAILS\n"
103 104
 )
104 105
 
105 106
 // prettyPrintValidationResults prints the contents of the ValidationResults into the buffer of a tabwriter.Writer.
... ...
@@ -107,14 +108,14 @@ const (
107 107
 func prettyPrintValidationResults(results validation.ValidationResults, writer *tabwriter.Writer) error {
108 108
 	if len(results.Errors) > 0 {
109 109
 		fmt.Fprintf(writer, "VALIDATION ERRORS:\t\t\t\n")
110
-		err := prettyPrintValidationErrorList(results.Errors, writer)
110
+		err := prettyPrintValidationErrorList(validationErrorHeadings, results.Errors, writer)
111 111
 		if err != nil {
112 112
 			return err
113 113
 		}
114 114
 	}
115 115
 	if len(results.Warnings) > 0 {
116 116
 		fmt.Fprintf(writer, "VALIDATION WARNINGS:\t\t\t\n")
117
-		err := prettyPrintValidationErrorList(results.Errors, writer)
117
+		err := prettyPrintValidationErrorList(validationWarningHeadings, results.Warnings, writer)
118 118
 		if err != nil {
119 119
 			return err
120 120
 		}
... ...
@@ -124,9 +125,9 @@ func prettyPrintValidationResults(results validation.ValidationResults, writer *
124 124
 
125 125
 // prettyPrintValidationErrorList prints the contents of the ValidationErrorList into the buffer of a tabwriter.Writer.
126 126
 // The writer must be Flush()ed after calling this to write the buffered data.
127
-func prettyPrintValidationErrorList(validationErrors fielderrors.ValidationErrorList, writer *tabwriter.Writer) error {
127
+func prettyPrintValidationErrorList(headings string, validationErrors fielderrors.ValidationErrorList, writer *tabwriter.Writer) error {
128 128
 	if len(validationErrors) > 0 {
129
-		fmt.Fprintf(writer, validationErrorHeadings)
129
+		fmt.Fprintf(writer, headings)
130 130
 		for _, err := range validationErrors {
131 131
 			switch validationError := err.(type) {
132 132
 			case (*fielderrors.ValidationError):
... ...
@@ -84,10 +84,10 @@ func (o *ValidateNodeConfigOptions) Run() (ok bool, err error) {
84 84
 
85 85
 	results := validation.ValidateNodeConfig(nodeConfig)
86 86
 	writer := tabwriter.NewWriter(o.Out, minColumnWidth, tabWidth, padding, padchar, flags)
87
-	err = prettyPrintValidationErrorList(results, writer)
87
+	err = prettyPrintValidationResults(results, writer)
88 88
 	if err != nil {
89
-		return len(results) == 0, fmt.Errorf("could not print results: %v", err)
89
+		return len(results.Errors) == 0, fmt.Errorf("could not print results: %v", err)
90 90
 	}
91 91
 	writer.Flush()
92
-	return len(results) == 0, nil
92
+	return len(results.Errors) == 0, nil
93 93
 }
... ...
@@ -1,6 +1,7 @@
1 1
 package api
2 2
 
3 3
 import (
4
+	"crypto/tls"
4 5
 	"crypto/x509"
5 6
 	"fmt"
6 7
 	"net"
... ...
@@ -120,6 +121,10 @@ func GetMasterFileReferences(config *MasterConfig) []*string {
120 120
 	refs = append(refs, &config.ServingInfo.ServerCert.CertFile)
121 121
 	refs = append(refs, &config.ServingInfo.ServerCert.KeyFile)
122 122
 	refs = append(refs, &config.ServingInfo.ClientCA)
123
+	for i := range config.ServingInfo.NamedCertificates {
124
+		refs = append(refs, &config.ServingInfo.NamedCertificates[i].CertFile)
125
+		refs = append(refs, &config.ServingInfo.NamedCertificates[i].KeyFile)
126
+	}
123 127
 
124 128
 	refs = append(refs, &config.EtcdClientInfo.ClientCert.CertFile)
125 129
 	refs = append(refs, &config.EtcdClientInfo.ClientCert.KeyFile)
... ...
@@ -133,10 +138,18 @@ func GetMasterFileReferences(config *MasterConfig) []*string {
133 133
 		refs = append(refs, &config.EtcdConfig.ServingInfo.ServerCert.CertFile)
134 134
 		refs = append(refs, &config.EtcdConfig.ServingInfo.ServerCert.KeyFile)
135 135
 		refs = append(refs, &config.EtcdConfig.ServingInfo.ClientCA)
136
+		for i := range config.EtcdConfig.ServingInfo.NamedCertificates {
137
+			refs = append(refs, &config.EtcdConfig.ServingInfo.NamedCertificates[i].CertFile)
138
+			refs = append(refs, &config.EtcdConfig.ServingInfo.NamedCertificates[i].KeyFile)
139
+		}
136 140
 
137 141
 		refs = append(refs, &config.EtcdConfig.PeerServingInfo.ServerCert.CertFile)
138 142
 		refs = append(refs, &config.EtcdConfig.PeerServingInfo.ServerCert.KeyFile)
139 143
 		refs = append(refs, &config.EtcdConfig.PeerServingInfo.ClientCA)
144
+		for i := range config.EtcdConfig.PeerServingInfo.NamedCertificates {
145
+			refs = append(refs, &config.EtcdConfig.PeerServingInfo.NamedCertificates[i].CertFile)
146
+			refs = append(refs, &config.EtcdConfig.PeerServingInfo.NamedCertificates[i].KeyFile)
147
+		}
140 148
 
141 149
 		refs = append(refs, &config.EtcdConfig.StorageDir)
142 150
 	}
... ...
@@ -182,6 +195,11 @@ func GetMasterFileReferences(config *MasterConfig) []*string {
182 182
 		refs = append(refs, &config.AssetConfig.ServingInfo.ServerCert.CertFile)
183 183
 		refs = append(refs, &config.AssetConfig.ServingInfo.ServerCert.KeyFile)
184 184
 		refs = append(refs, &config.AssetConfig.ServingInfo.ClientCA)
185
+		for i := range config.AssetConfig.ServingInfo.NamedCertificates {
186
+			refs = append(refs, &config.AssetConfig.ServingInfo.NamedCertificates[i].CertFile)
187
+			refs = append(refs, &config.AssetConfig.ServingInfo.NamedCertificates[i].KeyFile)
188
+		}
189
+
185 190
 		for i := range config.AssetConfig.ExtensionScripts {
186 191
 			refs = append(refs, &config.AssetConfig.ExtensionScripts[i])
187 192
 		}
... ...
@@ -228,6 +246,10 @@ func GetNodeFileReferences(config *NodeConfig) []*string {
228 228
 	refs = append(refs, &config.ServingInfo.ServerCert.CertFile)
229 229
 	refs = append(refs, &config.ServingInfo.ServerCert.KeyFile)
230 230
 	refs = append(refs, &config.ServingInfo.ClientCA)
231
+	for i := range config.ServingInfo.NamedCertificates {
232
+		refs = append(refs, &config.ServingInfo.NamedCertificates[i].CertFile)
233
+		refs = append(refs, &config.ServingInfo.NamedCertificates[i].KeyFile)
234
+	}
231 235
 
232 236
 	refs = append(refs, &config.MasterKubeConfig)
233 237
 
... ...
@@ -317,6 +339,26 @@ func GetAPIClientCertCAPool(options MasterConfig) (*x509.CertPool, error) {
317 317
 	return cmdutil.CertPoolFromFile(options.ServingInfo.ClientCA)
318 318
 }
319 319
 
320
+// GetNamedCertificateMap returns a map of strings to *tls.Certificate, suitable for use in tls.Config#NamedCertificates
321
+// Returns an error if any of the certs cannot be loaded, or do not match the configured name
322
+// Returns nil if len(namedCertificates) == 0
323
+func GetNamedCertificateMap(namedCertificates []NamedCertificate) (map[string]*tls.Certificate, error) {
324
+	if len(namedCertificates) == 0 {
325
+		return nil, nil
326
+	}
327
+	namedCerts := map[string]*tls.Certificate{}
328
+	for _, namedCertificate := range namedCertificates {
329
+		cert, err := tls.LoadX509KeyPair(namedCertificate.CertFile, namedCertificate.KeyFile)
330
+		if err != nil {
331
+			return nil, err
332
+		}
333
+		for _, name := range namedCertificate.Names {
334
+			namedCerts[name] = &cert
335
+		}
336
+	}
337
+	return namedCerts, nil
338
+}
339
+
320 340
 // GetClientCertCAPool returns a cert pool containing all client CAs that could be presented (union of API and OAuth)
321 341
 func GetClientCertCAPool(options MasterConfig) (*x509.CertPool, error) {
322 342
 	roots := x509.NewCertPool()
... ...
@@ -339,6 +339,17 @@ type ServingInfo struct {
339 339
 	ServerCert CertInfo
340 340
 	// ClientCA is the certificate bundle for all the signers that you'll recognize for incoming client certificates
341 341
 	ClientCA string
342
+	// NamedCertificates is a list of certificates to use to secure requests to specific hostnames
343
+	NamedCertificates []NamedCertificate
344
+}
345
+
346
+// NamedCertificate specifies a certificate/key, and the names it should be served for
347
+type NamedCertificate struct {
348
+	// Names is a list of DNS names this certificate should be used to secure
349
+	// A name can be a normal DNS name, or can contain leading wildcard segments.
350
+	Names []string
351
+	// CertInfo is the TLS cert info for serving secure traffic
352
+	CertInfo
342 353
 }
343 354
 
344 355
 type HTTPServingInfo struct {
... ...
@@ -141,17 +141,17 @@ func init() {
141 141
 			return s.DefaultConvert(in, out, conversion.IgnoreMissingFields)
142 142
 		},
143 143
 		func(in *ServingInfo, out *newer.ServingInfo, s conversion.Scope) error {
144
-			out.BindAddress = in.BindAddress
145
-			out.BindNetwork = in.BindNetwork
146
-			out.ClientCA = in.ClientCA
144
+			if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil {
145
+				return err
146
+			}
147 147
 			out.ServerCert.CertFile = in.CertFile
148 148
 			out.ServerCert.KeyFile = in.KeyFile
149 149
 			return nil
150 150
 		},
151 151
 		func(in *newer.ServingInfo, out *ServingInfo, s conversion.Scope) error {
152
-			out.BindAddress = in.BindAddress
153
-			out.BindNetwork = in.BindNetwork
154
-			out.ClientCA = in.ClientCA
152
+			if err := s.DefaultConvert(in, out, conversion.IgnoreMissingFields); err != nil {
153
+				return err
154
+			}
155 155
 			out.CertFile = in.ServerCert.CertFile
156 156
 			out.KeyFile = in.ServerCert.KeyFile
157 157
 			return nil
... ...
@@ -319,6 +319,17 @@ type ServingInfo struct {
319 319
 	CertInfo `json:",inline"`
320 320
 	// ClientCA is the certificate bundle for all the signers that you'll recognize for incoming client certificates
321 321
 	ClientCA string `json:"clientCA"`
322
+	// NamedCertificates is a list of certificates to use to secure requests to specific hostnames
323
+	NamedCertificates []NamedCertificate `json:"namedCertificates"`
324
+}
325
+
326
+// NamedCertificate specifies a certificate/key, and the names it should be served for
327
+type NamedCertificate struct {
328
+	// Names is a list of DNS names this certificate should be used to secure
329
+	// A name can be a normal DNS name, or can contain leading wildcard segments.
330
+	Names []string `json:"names"`
331
+	// CertInfo is the TLS cert info for serving secure traffic
332
+	CertInfo `json:",inline"`
322 333
 }
323 334
 
324 335
 type HTTPServingInfo struct {
... ...
@@ -46,6 +46,7 @@ servingInfo:
46 46
   certFile: ""
47 47
   clientCA: ""
48 48
   keyFile: ""
49
+  namedCertificates: null
49 50
 volumeDirectory: ""
50 51
 `
51 52
 
... ...
@@ -76,6 +77,7 @@ assetConfig:
76 76
     clientCA: ""
77 77
     keyFile: ""
78 78
     maxRequestsInFlight: 0
79
+    namedCertificates: null
79 80
     requestTimeoutSeconds: 0
80 81
 controllerLeaseTTL: 0
81 82
 controllers: ""
... ...
@@ -98,12 +100,14 @@ etcdConfig:
98 98
     certFile: ""
99 99
     clientCA: ""
100 100
     keyFile: ""
101
+    namedCertificates: null
101 102
   servingInfo:
102 103
     bindAddress: ""
103 104
     bindNetwork: ""
104 105
     certFile: ""
105 106
     clientCA: ""
106 107
     keyFile: ""
108
+    namedCertificates: null
107 109
   storageDirectory: ""
108 110
 etcdStorageConfig:
109 111
   kubernetesStoragePrefix: ""
... ...
@@ -276,6 +280,10 @@ servingInfo:
276 276
   clientCA: ""
277 277
   keyFile: ""
278 278
   maxRequestsInFlight: 0
279
+  namedCertificates:
280
+  - certFile: ""
281
+    keyFile: ""
282
+    names: null
279 283
   requestTimeoutSeconds: 0
280 284
 `
281 285
 )
... ...
@@ -295,6 +303,11 @@ func TestNodeConfig(t *testing.T) {
295 295
 
296 296
 func TestMasterConfig(t *testing.T) {
297 297
 	config := &internal.MasterConfig{
298
+		ServingInfo: internal.HTTPServingInfo{
299
+			ServingInfo: internal.ServingInfo{
300
+				NamedCertificates: []internal.NamedCertificate{{}},
301
+			},
302
+		},
298 303
 		KubernetesMasterConfig: &internal.KubernetesMasterConfig{},
299 304
 		EtcdConfig:             &internal.EtcdConfig{},
300 305
 		OAuthConfig: &internal.OAuthConfig{
... ...
@@ -9,7 +9,7 @@ func ValidateAllInOneConfig(master *api.MasterConfig, node *api.NodeConfig) Vali
9 9
 
10 10
 	validationResults.Append(ValidateMasterConfig(master).Prefix("masterConfig"))
11 11
 
12
-	validationResults.AddErrors(ValidateNodeConfig(node).Prefix("nodeConfig")...)
12
+	validationResults.Append(ValidateNodeConfig(node).Prefix("nodeConfig"))
13 13
 
14 14
 	// Validation between the configs
15 15
 
... ...
@@ -56,24 +56,31 @@ func ValidateEtcdConnectionInfo(config api.EtcdConnectionInfo, server *api.EtcdC
56 56
 	return allErrs
57 57
 }
58 58
 
59
-func ValidateEtcdConfig(config *api.EtcdConfig) fielderrors.ValidationErrorList {
60
-	allErrs := fielderrors.ValidationErrorList{}
59
+func ValidateEtcdConfig(config *api.EtcdConfig) ValidationResults {
60
+	validationResults := ValidationResults{}
61 61
 
62
-	allErrs = append(allErrs, ValidateServingInfo(config.ServingInfo).Prefix("servingInfo")...)
62
+	validationResults.Append(ValidateServingInfo(config.ServingInfo).Prefix("servingInfo"))
63 63
 	if config.ServingInfo.BindNetwork == "tcp6" {
64
-		allErrs = append(allErrs, fielderrors.NewFieldInvalid("servingInfo.bindNetwork", config.ServingInfo.BindNetwork, "tcp6 is not a valid bindNetwork for etcd, must be tcp or tcp4"))
64
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("servingInfo.bindNetwork", config.ServingInfo.BindNetwork, "tcp6 is not a valid bindNetwork for etcd, must be tcp or tcp4"))
65
+	}
66
+	if len(config.ServingInfo.NamedCertificates) > 0 {
67
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("servingInfo.namedCertificates", "<not shown>", "namedCertificates are not supported for etcd"))
65 68
 	}
66
-	allErrs = append(allErrs, ValidateServingInfo(config.PeerServingInfo).Prefix("peerServingInfo")...)
69
+
70
+	validationResults.Append(ValidateServingInfo(config.PeerServingInfo).Prefix("peerServingInfo"))
67 71
 	if config.ServingInfo.BindNetwork == "tcp6" {
68
-		allErrs = append(allErrs, fielderrors.NewFieldInvalid("peerServingInfo.bindNetwork", config.ServingInfo.BindNetwork, "tcp6 is not a valid bindNetwork for etcd peers, must be tcp or tcp4"))
72
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("peerServingInfo.bindNetwork", config.ServingInfo.BindNetwork, "tcp6 is not a valid bindNetwork for etcd peers, must be tcp or tcp4"))
73
+	}
74
+	if len(config.ServingInfo.NamedCertificates) > 0 {
75
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("peerServingInfo.namedCertificates", "<not shown>", "namedCertificates are not supported for etcd"))
69 76
 	}
70 77
 
71
-	allErrs = append(allErrs, ValidateHostPort(config.Address, "address")...)
72
-	allErrs = append(allErrs, ValidateHostPort(config.PeerAddress, "peerAddress")...)
78
+	validationResults.AddErrors(ValidateHostPort(config.Address, "address")...)
79
+	validationResults.AddErrors(ValidateHostPort(config.PeerAddress, "peerAddress")...)
73 80
 
74 81
 	if len(config.StorageDir) == 0 {
75
-		allErrs = append(allErrs, fielderrors.NewFieldRequired("storageDirectory"))
82
+		validationResults.AddErrors(fielderrors.NewFieldRequired("storageDirectory"))
76 83
 	}
77 84
 
78
-	return allErrs
85
+	return validationResults
79 86
 }
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"fmt"
5 5
 	"net"
6 6
 	"net/url"
7
+	"reflect"
7 8
 	"regexp"
8 9
 	"strings"
9 10
 	"time"
... ...
@@ -57,8 +58,6 @@ func (r ValidationResults) Prefix(prefix string) ValidationResults {
57 57
 func ValidateMasterConfig(config *api.MasterConfig) ValidationResults {
58 58
 	validationResults := ValidationResults{}
59 59
 
60
-	validationResults.AddErrors(ValidateHTTPServingInfo(config.ServingInfo).Prefix("servingInfo")...)
61
-
62 60
 	if _, urlErrs := ValidateURL(config.MasterPublicURL, "masterPublicURL"); len(urlErrs) > 0 {
63 61
 		validationResults.AddErrors(urlErrs...)
64 62
 	}
... ...
@@ -73,13 +72,16 @@ func ValidateMasterConfig(config *api.MasterConfig) ValidationResults {
73 73
 	validationResults.AddErrors(ValidateDisabledFeatures(config.DisabledFeatures, "disabledFeatures")...)
74 74
 
75 75
 	if config.AssetConfig != nil {
76
-		validationResults.AddErrors(ValidateAssetConfig(config.AssetConfig).Prefix("assetConfig")...)
76
+		validationResults.Append(ValidateAssetConfig(config.AssetConfig).Prefix("assetConfig"))
77 77
 		colocated := config.AssetConfig.ServingInfo.BindAddress == config.ServingInfo.BindAddress
78 78
 		if colocated {
79 79
 			publicURL, _ := url.Parse(config.AssetConfig.PublicURL)
80 80
 			if publicURL.Path == "/" {
81 81
 				validationResults.AddErrors(fielderrors.NewFieldInvalid("assetConfig.publicURL", config.AssetConfig.PublicURL, "path can not be / when colocated with master API"))
82 82
 			}
83
+			if !reflect.DeepEqual(config.AssetConfig.ServingInfo, config.ServingInfo) {
84
+				validationResults.AddWarnings(fielderrors.NewFieldInvalid("assetConfig.servingInfo", "<not displayed>", "assetConfig.servingInfo is ignored when colocated with master API"))
85
+			}
83 86
 		}
84 87
 
85 88
 		if config.OAuthConfig != nil {
... ...
@@ -106,9 +108,9 @@ func ValidateMasterConfig(config *api.MasterConfig) ValidationResults {
106 106
 
107 107
 	if config.EtcdConfig != nil {
108 108
 		etcdConfigErrs := ValidateEtcdConfig(config.EtcdConfig).Prefix("etcdConfig")
109
-		validationResults.AddErrors(etcdConfigErrs...)
109
+		validationResults.Append(etcdConfigErrs)
110 110
 
111
-		if len(etcdConfigErrs) == 0 {
111
+		if len(etcdConfigErrs.Errors) == 0 {
112 112
 			// Validate the etcdClientInfo with the internal etcdConfig
113 113
 			validationResults.AddErrors(ValidateEtcdConnectionInfo(config.EtcdClientInfo, config.EtcdConfig).Prefix("etcdClientInfo")...)
114 114
 		} else {
... ...
@@ -157,7 +159,7 @@ func ValidateMasterConfig(config *api.MasterConfig) ValidationResults {
157 157
 
158 158
 	validationResults.Append(ValidateServiceAccountConfig(config.ServiceAccountConfig, builtInKubernetes).Prefix("serviceAccountConfig"))
159 159
 
160
-	validationResults.AddErrors(ValidateHTTPServingInfo(config.ServingInfo).Prefix("servingInfo")...)
160
+	validationResults.Append(ValidateHTTPServingInfo(config.ServingInfo).Prefix("servingInfo"))
161 161
 
162 162
 	validationResults.Append(ValidateProjectConfig(config.ProjectConfig).Prefix("projectConfig"))
163 163
 
... ...
@@ -261,65 +263,65 @@ func ValidateServiceAccountConfig(config api.ServiceAccountConfig, builtInKubern
261 261
 	return validationResults
262 262
 }
263 263
 
264
-func ValidateAssetConfig(config *api.AssetConfig) fielderrors.ValidationErrorList {
265
-	allErrs := fielderrors.ValidationErrorList{}
264
+func ValidateAssetConfig(config *api.AssetConfig) ValidationResults {
265
+	validationResults := ValidationResults{}
266 266
 
267
-	allErrs = append(allErrs, ValidateHTTPServingInfo(config.ServingInfo).Prefix("servingInfo")...)
267
+	validationResults.Append(ValidateHTTPServingInfo(config.ServingInfo).Prefix("servingInfo"))
268 268
 
269 269
 	if len(config.LogoutURL) > 0 {
270 270
 		_, urlErrs := ValidateURL(config.LogoutURL, "logoutURL")
271 271
 		if len(urlErrs) > 0 {
272
-			allErrs = append(allErrs, urlErrs...)
272
+			validationResults.AddErrors(urlErrs...)
273 273
 		}
274 274
 	}
275 275
 
276 276
 	urlObj, urlErrs := ValidateURL(config.PublicURL, "publicURL")
277 277
 	if len(urlErrs) > 0 {
278
-		allErrs = append(allErrs, urlErrs...)
278
+		validationResults.AddErrors(urlErrs...)
279 279
 	}
280 280
 	if urlObj != nil {
281 281
 		if !strings.HasSuffix(urlObj.Path, "/") {
282
-			allErrs = append(allErrs, fielderrors.NewFieldInvalid("publicURL", config.PublicURL, "must have a trailing slash in path"))
282
+			validationResults.AddErrors(fielderrors.NewFieldInvalid("publicURL", config.PublicURL, "must have a trailing slash in path"))
283 283
 		}
284 284
 	}
285 285
 
286 286
 	if _, urlErrs := ValidateURL(config.MasterPublicURL, "masterPublicURL"); len(urlErrs) > 0 {
287
-		allErrs = append(allErrs, urlErrs...)
287
+		validationResults.AddErrors(urlErrs...)
288 288
 	}
289 289
 
290 290
 	if len(config.LoggingPublicURL) > 0 {
291 291
 		if _, loggingURLErrs := ValidateSecureURL(config.LoggingPublicURL, "loggingPublicURL"); len(loggingURLErrs) > 0 {
292
-			allErrs = append(allErrs, loggingURLErrs...)
292
+			validationResults.AddErrors(loggingURLErrs...)
293 293
 		}
294 294
 	}
295 295
 
296 296
 	if len(config.MetricsPublicURL) > 0 {
297 297
 		if _, metricsURLErrs := ValidateSecureURL(config.MetricsPublicURL, "metricsPublicURL"); len(metricsURLErrs) > 0 {
298
-			allErrs = append(allErrs, metricsURLErrs...)
298
+			validationResults.AddErrors(metricsURLErrs...)
299 299
 		}
300 300
 	}
301 301
 
302 302
 	for i, scriptFile := range config.ExtensionScripts {
303
-		allErrs = append(allErrs, ValidateFile(scriptFile, fmt.Sprintf("extensionScripts[%d]", i))...)
303
+		validationResults.AddErrors(ValidateFile(scriptFile, fmt.Sprintf("extensionScripts[%d]", i))...)
304 304
 	}
305 305
 
306 306
 	for i, stylesheetFile := range config.ExtensionStylesheets {
307
-		allErrs = append(allErrs, ValidateFile(stylesheetFile, fmt.Sprintf("extensionStylesheets[%d]", i))...)
307
+		validationResults.AddErrors(ValidateFile(stylesheetFile, fmt.Sprintf("extensionStylesheets[%d]", i))...)
308 308
 	}
309 309
 
310 310
 	nameTaken := map[string]bool{}
311 311
 	for i, extConfig := range config.Extensions {
312 312
 		extConfigErrors := ValidateAssetExtensionsConfig(extConfig).Prefix(fmt.Sprintf("extensions[%d]", i))
313
-		allErrs = append(allErrs, extConfigErrors...)
313
+		validationResults.AddErrors(extConfigErrors...)
314 314
 		if nameTaken[extConfig.Name] {
315 315
 			dupError := fielderrors.NewFieldInvalid(fmt.Sprintf("extensions[%d].name", i), extConfig.Name, "duplicate extension name")
316
-			allErrs = append(allErrs, dupError)
316
+			validationResults.AddErrors(dupError)
317 317
 		} else {
318 318
 			nameTaken[extConfig.Name] = true
319 319
 		}
320 320
 	}
321 321
 
322
-	return allErrs
322
+	return validationResults
323 323
 }
324 324
 
325 325
 var extNameExp = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
... ...
@@ -11,45 +11,45 @@ import (
11 11
 	"github.com/openshift/origin/pkg/cmd/server/api"
12 12
 )
13 13
 
14
-func ValidateNodeConfig(config *api.NodeConfig) fielderrors.ValidationErrorList {
15
-	allErrs := fielderrors.ValidationErrorList{}
14
+func ValidateNodeConfig(config *api.NodeConfig) ValidationResults {
15
+	validationResults := ValidationResults{}
16 16
 
17 17
 	if len(config.NodeName) == 0 {
18
-		allErrs = append(allErrs, fielderrors.NewFieldRequired("nodeName"))
18
+		validationResults.AddErrors(fielderrors.NewFieldRequired("nodeName"))
19 19
 	}
20 20
 	if len(config.NodeIP) > 0 {
21
-		allErrs = append(allErrs, ValidateSpecifiedIP(config.NodeIP, "nodeIP")...)
21
+		validationResults.AddErrors(ValidateSpecifiedIP(config.NodeIP, "nodeIP")...)
22 22
 	}
23 23
 
24
-	allErrs = append(allErrs, ValidateServingInfo(config.ServingInfo).Prefix("servingInfo")...)
24
+	validationResults.Append(ValidateServingInfo(config.ServingInfo).Prefix("servingInfo"))
25 25
 	if config.ServingInfo.BindNetwork == "tcp6" {
26
-		allErrs = append(allErrs, fielderrors.NewFieldInvalid("servingInfo.bindNetwork", config.ServingInfo.BindNetwork, "tcp6 is not a valid bindNetwork for nodes, must be tcp or tcp4"))
26
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("servingInfo.bindNetwork", config.ServingInfo.BindNetwork, "tcp6 is not a valid bindNetwork for nodes, must be tcp or tcp4"))
27 27
 	}
28
-	allErrs = append(allErrs, ValidateKubeConfig(config.MasterKubeConfig, "masterKubeConfig")...)
28
+	validationResults.AddErrors(ValidateKubeConfig(config.MasterKubeConfig, "masterKubeConfig")...)
29 29
 
30 30
 	if len(config.DNSIP) > 0 {
31
-		allErrs = append(allErrs, ValidateSpecifiedIP(config.DNSIP, "dnsIP")...)
31
+		validationResults.AddErrors(ValidateSpecifiedIP(config.DNSIP, "dnsIP")...)
32 32
 	}
33 33
 
34
-	allErrs = append(allErrs, ValidateImageConfig(config.ImageConfig).Prefix("imageConfig")...)
34
+	validationResults.AddErrors(ValidateImageConfig(config.ImageConfig).Prefix("imageConfig")...)
35 35
 
36 36
 	if config.PodManifestConfig != nil {
37
-		allErrs = append(allErrs, ValidatePodManifestConfig(config.PodManifestConfig).Prefix("podManifestConfig")...)
37
+		validationResults.AddErrors(ValidatePodManifestConfig(config.PodManifestConfig).Prefix("podManifestConfig")...)
38 38
 	}
39 39
 
40
-	allErrs = append(allErrs, ValidateNetworkConfig(config.NetworkConfig).Prefix("networkConfig")...)
40
+	validationResults.AddErrors(ValidateNetworkConfig(config.NetworkConfig).Prefix("networkConfig")...)
41 41
 
42
-	allErrs = append(allErrs, ValidateDockerConfig(config.DockerConfig).Prefix("dockerConfig")...)
42
+	validationResults.AddErrors(ValidateDockerConfig(config.DockerConfig).Prefix("dockerConfig")...)
43 43
 
44
-	allErrs = append(allErrs, ValidateNodeAuthConfig(config.AuthConfig).Prefix("authConfig")...)
44
+	validationResults.AddErrors(ValidateNodeAuthConfig(config.AuthConfig).Prefix("authConfig")...)
45 45
 
46
-	allErrs = append(allErrs, ValidateKubeletExtendedArguments(config.KubeletArguments).Prefix("kubeletArguments")...)
46
+	validationResults.AddErrors(ValidateKubeletExtendedArguments(config.KubeletArguments).Prefix("kubeletArguments")...)
47 47
 
48 48
 	if _, err := time.ParseDuration(config.IPTablesSyncPeriod); err != nil {
49
-		allErrs = append(allErrs, fielderrors.NewFieldInvalid("iptablesSyncPeriod", config.IPTablesSyncPeriod, fmt.Sprintf("unable to parse iptablesSyncPeriod: %v. Examples with correct format: '5s', '1m', '2h22m'", err)))
49
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("iptablesSyncPeriod", config.IPTablesSyncPeriod, fmt.Sprintf("unable to parse iptablesSyncPeriod: %v. Examples with correct format: '5s', '1m', '2h22m'", err)))
50 50
 	}
51 51
 
52
-	return allErrs
52
+	return validationResults
53 53
 }
54 54
 
55 55
 func ValidateNodeAuthConfig(config api.NodeAuthConfig) fielderrors.ValidationErrorList {
... ...
@@ -1,6 +1,8 @@
1 1
 package validation
2 2
 
3 3
 import (
4
+	"crypto/tls"
5
+	"crypto/x509"
4 6
 	"fmt"
5 7
 	"net"
6 8
 	"net/url"
... ...
@@ -10,9 +12,12 @@ import (
10 10
 	"github.com/spf13/pflag"
11 11
 
12 12
 	kvalidation "k8s.io/kubernetes/pkg/api/validation"
13
+	kutil "k8s.io/kubernetes/pkg/util"
13 14
 	"k8s.io/kubernetes/pkg/util/fielderrors"
15
+	"k8s.io/kubernetes/pkg/util/sets"
14 16
 
15 17
 	"github.com/openshift/origin/pkg/cmd/server/api"
18
+	cmdutil "github.com/openshift/origin/pkg/cmd/util"
16 19
 	cmdflags "github.com/openshift/origin/pkg/cmd/util/flags"
17 20
 )
18 21
 
... ...
@@ -48,46 +53,124 @@ func ValidateCertInfo(certInfo api.CertInfo, required bool) fielderrors.Validati
48 48
 		allErrs = append(allErrs, ValidateFile(certInfo.KeyFile, "keyFile")...)
49 49
 	}
50 50
 
51
+	// validate certfile/keyfile load/parse?
52
+
51 53
 	return allErrs
52 54
 }
53 55
 
54
-func ValidateServingInfo(info api.ServingInfo) fielderrors.ValidationErrorList {
55
-	allErrs := fielderrors.ValidationErrorList{}
56
+func ValidateServingInfo(info api.ServingInfo) ValidationResults {
57
+	validationResults := ValidationResults{}
58
+
59
+	validationResults.AddErrors(ValidateHostPort(info.BindAddress, "bindAddress")...)
60
+	validationResults.AddErrors(ValidateCertInfo(info.ServerCert, false)...)
61
+
62
+	if len(info.NamedCertificates) > 0 && len(info.ServerCert.CertFile) == 0 {
63
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("namedCertificates", "", "a default certificate and key is required in certFile/keyFile in order to use namedCertificates"))
64
+	}
56 65
 
57
-	allErrs = append(allErrs, ValidateHostPort(info.BindAddress, "bindAddress")...)
58
-	allErrs = append(allErrs, ValidateCertInfo(info.ServerCert, false)...)
66
+	validationResults.Append(ValidateNamedCertificates("namedCertificates", info.NamedCertificates))
59 67
 
60 68
 	switch info.BindNetwork {
61 69
 	case "tcp", "tcp4", "tcp6":
62 70
 	default:
63
-		allErrs = append(allErrs, fielderrors.NewFieldInvalid("bindNetwork", info.BindNetwork, "must be 'tcp', 'tcp4', or 'tcp6'"))
71
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("bindNetwork", info.BindNetwork, "must be 'tcp', 'tcp4', or 'tcp6'"))
64 72
 	}
65 73
 
66 74
 	if len(info.ServerCert.CertFile) > 0 {
67 75
 		if len(info.ClientCA) > 0 {
68
-			allErrs = append(allErrs, ValidateFile(info.ClientCA, "clientCA")...)
76
+			validationResults.AddErrors(ValidateFile(info.ClientCA, "clientCA")...)
69 77
 		}
70 78
 	} else {
71 79
 		if len(info.ClientCA) > 0 {
72
-			allErrs = append(allErrs, fielderrors.NewFieldInvalid("clientCA", info.ClientCA, "cannot specify a clientCA without a certFile"))
80
+			validationResults.AddErrors(fielderrors.NewFieldInvalid("clientCA", info.ClientCA, "cannot specify a clientCA without a certFile"))
73 81
 		}
74 82
 	}
75 83
 
76
-	return allErrs
84
+	return validationResults
85
+}
86
+
87
+func ValidateNamedCertificates(fieldName string, namedCertificates []api.NamedCertificate) ValidationResults {
88
+	validationResults := ValidationResults{}
89
+
90
+	takenNames := sets.NewString()
91
+	for i, namedCertificate := range namedCertificates {
92
+		fieldName := fmt.Sprintf("%s[%d]", fieldName, i)
93
+
94
+		certDNSNames := []string{}
95
+		if len(namedCertificate.CertFile) == 0 {
96
+			validationResults.AddErrors(fielderrors.NewFieldRequired(fieldName + ".certInfo"))
97
+		} else if certInfoErrors := ValidateCertInfo(namedCertificate.CertInfo, false); len(certInfoErrors) > 0 {
98
+			validationResults.AddErrors(certInfoErrors.Prefix(fieldName)...)
99
+		} else if cert, err := tls.LoadX509KeyPair(namedCertificate.CertFile, namedCertificate.KeyFile); err != nil {
100
+			validationResults.AddErrors(fielderrors.NewFieldInvalid(fieldName+".certInfo", namedCertificate.CertInfo, fmt.Sprintf("error loading certificate/key: %v", err)))
101
+		} else {
102
+			leaf, _ := x509.ParseCertificate(cert.Certificate[0])
103
+			certDNSNames = append(certDNSNames, leaf.Subject.CommonName)
104
+			certDNSNames = append(certDNSNames, leaf.DNSNames...)
105
+		}
106
+
107
+		if len(namedCertificate.Names) == 0 {
108
+			validationResults.AddErrors(fielderrors.NewFieldRequired(fieldName + ".names"))
109
+		}
110
+		for j, name := range namedCertificate.Names {
111
+			nameFieldName := fieldName + fmt.Sprintf(".names[%d]", j)
112
+			if len(name) == 0 {
113
+				validationResults.AddErrors(fielderrors.NewFieldRequired(nameFieldName))
114
+				continue
115
+			}
116
+
117
+			if takenNames.Has(name) {
118
+				validationResults.AddErrors(fielderrors.NewFieldInvalid(nameFieldName, name, "this name is already used in another named certificate"))
119
+				continue
120
+			}
121
+
122
+			// validate names as domain names or *.*.foo.com domain names
123
+			validDNSName := true
124
+			for _, s := range strings.Split(name, ".") {
125
+				if s != "*" && !kutil.IsDNS1123Label(s) {
126
+					validDNSName = false
127
+				}
128
+			}
129
+			if !validDNSName {
130
+				validationResults.AddErrors(fielderrors.NewFieldInvalid(nameFieldName, name, "must be a valid DNS name"))
131
+				continue
132
+			}
133
+
134
+			takenNames.Insert(name)
135
+
136
+			// validate certificate has common name or subject alt names that match
137
+			if len(certDNSNames) > 0 {
138
+				foundMatch := false
139
+				for _, dnsName := range certDNSNames {
140
+					if cmdutil.HostnameMatches(dnsName, name) {
141
+						foundMatch = true
142
+						break
143
+					}
144
+				}
145
+				if !foundMatch {
146
+					validationResults.AddWarnings(fielderrors.NewFieldInvalid(nameFieldName, name, "the specified certificate does not have a CommonName or DNS subjectAltName that matches this name"))
147
+				}
148
+			}
149
+		}
150
+	}
151
+
152
+	return validationResults
77 153
 }
78 154
 
79
-func ValidateHTTPServingInfo(info api.HTTPServingInfo) fielderrors.ValidationErrorList {
80
-	allErrs := ValidateServingInfo(info.ServingInfo)
155
+func ValidateHTTPServingInfo(info api.HTTPServingInfo) ValidationResults {
156
+	validationResults := ValidationResults{}
157
+
158
+	validationResults.Append(ValidateServingInfo(info.ServingInfo))
81 159
 
82 160
 	if info.MaxRequestsInFlight < 0 {
83
-		allErrs = append(allErrs, fielderrors.NewFieldInvalid("maxRequestsInFlight", info.MaxRequestsInFlight, "must be zero (no limit) or greater"))
161
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("maxRequestsInFlight", info.MaxRequestsInFlight, "must be zero (no limit) or greater"))
84 162
 	}
85 163
 
86 164
 	if info.RequestTimeoutSeconds < -1 {
87
-		allErrs = append(allErrs, fielderrors.NewFieldInvalid("requestTimeoutSeconds", info.RequestTimeoutSeconds, "must be -1 (no timeout), 0 (default timeout), or greater"))
165
+		validationResults.AddErrors(fielderrors.NewFieldInvalid("requestTimeoutSeconds", info.RequestTimeoutSeconds, "must be -1 (no timeout), 0 (default timeout), or greater"))
88 166
 	}
89 167
 
90
-	return allErrs
168
+	return validationResults
91 169
 }
92 170
 
93 171
 func ValidateDisabledFeatures(disabledFeatures []string, field string) fielderrors.ValidationErrorList {
94 172
new file mode 100644
... ...
@@ -0,0 +1,208 @@
0
+package validation
1
+
2
+import (
3
+	"io/ioutil"
4
+	"os"
5
+	"strings"
6
+	"testing"
7
+
8
+	"github.com/openshift/origin/pkg/cmd/server/api"
9
+)
10
+
11
+func TestValidateServingInfo(t *testing.T) {
12
+	certFile, err := ioutil.TempFile("", "cert.crt")
13
+	if err != nil {
14
+		t.Fatalf("unexpected error: %v", err)
15
+	}
16
+	defer os.Remove(certFile.Name())
17
+	certFileName := certFile.Name()
18
+	ioutil.WriteFile(certFile.Name(), localhostCert, os.FileMode(0755))
19
+
20
+	keyFile, err := ioutil.TempFile("", "cert.key")
21
+	if err != nil {
22
+		t.Fatalf("unexpected error: %v", err)
23
+	}
24
+	defer os.Remove(keyFile.Name())
25
+	keyFileName := keyFile.Name()
26
+	ioutil.WriteFile(keyFile.Name(), localhostKey, os.FileMode(0755))
27
+
28
+	testcases := map[string]struct {
29
+		ServingInfo      api.ServingInfo
30
+		ExpectedErrors   []string
31
+		ExpectedWarnings []string
32
+	}{
33
+		"basic": {
34
+			ServingInfo: api.ServingInfo{
35
+				BindAddress: "0.0.0.0:1234",
36
+				BindNetwork: "tcp",
37
+			},
38
+		},
39
+		"missing key": {
40
+			ServingInfo: api.ServingInfo{
41
+				BindAddress: "0.0.0.0:1234",
42
+				BindNetwork: "tcp",
43
+				ServerCert: api.CertInfo{
44
+					CertFile: certFileName,
45
+				},
46
+			},
47
+			ExpectedErrors: []string{"keyFile: required"},
48
+		},
49
+
50
+		"namedCertificates valid": {
51
+			ServingInfo: api.ServingInfo{
52
+				BindAddress: "0.0.0.0:1234",
53
+				BindNetwork: "tcp",
54
+				ServerCert:  api.CertInfo{CertFile: certFileName, KeyFile: keyFileName},
55
+				NamedCertificates: []api.NamedCertificate{
56
+					{Names: []string{"example.com"}, CertInfo: api.CertInfo{CertFile: certFileName, KeyFile: keyFileName}},
57
+				},
58
+			},
59
+		},
60
+
61
+		"namedCertificates without default cert": {
62
+			ServingInfo: api.ServingInfo{
63
+				BindAddress: "0.0.0.0:1234",
64
+				BindNetwork: "tcp",
65
+				//ServerCert:  api.CertInfo{CertFile: certFileName, KeyFile: keyFileName},
66
+				NamedCertificates: []api.NamedCertificate{
67
+					{Names: []string{"example.com"}, CertInfo: api.CertInfo{CertFile: certFileName, KeyFile: keyFileName}},
68
+				},
69
+			},
70
+			ExpectedErrors: []string{"namedCertificates: invalid"},
71
+		},
72
+
73
+		"namedCertificates with missing names": {
74
+			ServingInfo: api.ServingInfo{
75
+				BindAddress: "0.0.0.0:1234",
76
+				BindNetwork: "tcp",
77
+				ServerCert:  api.CertInfo{CertFile: certFileName, KeyFile: keyFileName},
78
+				NamedCertificates: []api.NamedCertificate{
79
+					{Names: []string{ /*"example.com"*/ }, CertInfo: api.CertInfo{CertFile: certFileName, KeyFile: keyFileName}},
80
+				},
81
+			},
82
+			ExpectedErrors: []string{"namedCertificates[0].names: required"},
83
+		},
84
+		"namedCertificates with missing key": {
85
+			ServingInfo: api.ServingInfo{
86
+				BindAddress: "0.0.0.0:1234",
87
+				BindNetwork: "tcp",
88
+				ServerCert:  api.CertInfo{CertFile: certFileName, KeyFile: keyFileName},
89
+				NamedCertificates: []api.NamedCertificate{
90
+					{Names: []string{"example.com"}, CertInfo: api.CertInfo{CertFile: certFileName /*, KeyFile: keyFileName*/}},
91
+				},
92
+			},
93
+			ExpectedErrors: []string{"namedCertificates[0].keyFile: required"},
94
+		},
95
+		"namedCertificates with duplicate names": {
96
+			ServingInfo: api.ServingInfo{
97
+				BindAddress: "0.0.0.0:1234",
98
+				BindNetwork: "tcp",
99
+				ServerCert:  api.CertInfo{CertFile: certFileName, KeyFile: keyFileName},
100
+				NamedCertificates: []api.NamedCertificate{
101
+					{Names: []string{"example.com"}, CertInfo: api.CertInfo{CertFile: certFileName, KeyFile: keyFileName}},
102
+					{Names: []string{"example.com"}, CertInfo: api.CertInfo{CertFile: certFileName, KeyFile: keyFileName}},
103
+				},
104
+			},
105
+			ExpectedErrors: []string{"namedCertificates[1].names[0]: invalid"},
106
+		},
107
+		"namedCertificates with empty name": {
108
+			ServingInfo: api.ServingInfo{
109
+				BindAddress: "0.0.0.0:1234",
110
+				BindNetwork: "tcp",
111
+				ServerCert:  api.CertInfo{CertFile: certFileName, KeyFile: keyFileName},
112
+				NamedCertificates: []api.NamedCertificate{
113
+					{Names: []string{""}, CertInfo: api.CertInfo{CertFile: certFileName, KeyFile: keyFileName}},
114
+				},
115
+			},
116
+			ExpectedErrors: []string{"namedCertificates[0].names[0]: required"},
117
+		},
118
+
119
+		"namedCertificates with unmatched DNS name": {
120
+			ServingInfo: api.ServingInfo{
121
+				BindAddress: "0.0.0.0:1234",
122
+				BindNetwork: "tcp",
123
+				ServerCert:  api.CertInfo{CertFile: certFileName, KeyFile: keyFileName},
124
+				NamedCertificates: []api.NamedCertificate{
125
+					{Names: []string{"badexample.com"}, CertInfo: api.CertInfo{CertFile: certFileName, KeyFile: keyFileName}},
126
+				},
127
+			},
128
+			ExpectedWarnings: []string{"namedCertificates[0].names[0]: invalid"},
129
+		},
130
+		"namedCertificates with non-DNS names": {
131
+			ServingInfo: api.ServingInfo{
132
+				BindAddress: "0.0.0.0:1234",
133
+				BindNetwork: "tcp",
134
+				ServerCert:  api.CertInfo{CertFile: certFileName, KeyFile: keyFileName},
135
+				NamedCertificates: []api.NamedCertificate{
136
+					{Names: []string{"foo bar.com"}, CertInfo: api.CertInfo{CertFile: certFileName, KeyFile: keyFileName}},
137
+				},
138
+			},
139
+			ExpectedErrors: []string{
140
+				"namedCertificates[0].names[0]: invalid value 'foo bar.com', Details: must be a valid DNS name",
141
+			},
142
+		},
143
+	}
144
+
145
+	for k, tc := range testcases {
146
+		result := ValidateServingInfo(tc.ServingInfo)
147
+
148
+		if len(tc.ExpectedErrors) != len(result.Errors) {
149
+			t.Errorf("%s: Expected %d errors, got %d", k, len(tc.ExpectedErrors), len(result.Errors))
150
+			for _, e := range tc.ExpectedErrors {
151
+				t.Logf("\tExpected error: %s", e)
152
+			}
153
+			for _, r := range result.Errors {
154
+				t.Logf("\tActual error: %s", r.Error())
155
+			}
156
+			continue
157
+		}
158
+		for i, r := range result.Errors {
159
+			if !strings.Contains(r.Error(), tc.ExpectedErrors[i]) {
160
+				t.Errorf("%s: Expected error containing %s, got %s", k, tc.ExpectedErrors[i], r.Error())
161
+			}
162
+		}
163
+
164
+		if len(tc.ExpectedWarnings) != len(result.Warnings) {
165
+			t.Errorf("%s: Expected %d warning, got %d", k, len(tc.ExpectedWarnings), len(result.Warnings))
166
+			for _, e := range tc.ExpectedErrors {
167
+				t.Logf("\tExpected warning: %s", e)
168
+			}
169
+			for _, r := range result.Warnings {
170
+				t.Logf("\tActual warning: %s", r.Error())
171
+			}
172
+			continue
173
+		}
174
+		for i, r := range result.Warnings {
175
+			if !strings.Contains(r.Error(), tc.ExpectedWarnings[i]) {
176
+				t.Errorf("%s: Expected warning containing %s, got %s", k, tc.ExpectedWarnings[i], r.Error())
177
+			}
178
+		}
179
+	}
180
+}
181
+
182
+// localhostCert is a PEM-encoded TLS cert with SAN IPs
183
+// "127.0.0.1" and "[::1]", expiring at the last second of 2049 (the end
184
+// of ASN.1 time).
185
+// generated from src/crypto/tls:
186
+// go run generate_cert.go  --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
187
+var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
188
+MIIBdzCCASOgAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD
189
+bzAeFw03MDAxMDEwMDAwMDBaFw00OTEyMzEyMzU5NTlaMBIxEDAOBgNVBAoTB0Fj
190
+bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAN55NcYKZeInyTuhcCwFMhDHCmwa
191
+IUSdtXdcbItRB/yfXGBhiex00IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEA
192
+AaNoMGYwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud
193
+EwEB/wQFMAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAA
194
+AAAAAAAAAAAAAAEwCwYJKoZIhvcNAQEFA0EAAoQn/ytgqpiLcZu9XKbCJsJcvkgk
195
+Se6AbGXgSlq+ZCEVo0qIwSgeBqmsJxUu7NCSOwVJLYNEBO2DtIxoYVk+MA==
196
+-----END CERTIFICATE-----`)
197
+
198
+// localhostKey is the private key for localhostCert.
199
+var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
200
+MIIBPAIBAAJBAN55NcYKZeInyTuhcCwFMhDHCmwaIUSdtXdcbItRB/yfXGBhiex0
201
+0IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEAAQJBAQdUx66rfh8sYsgfdcvV
202
+NoafYpnEcB5s4m/vSVe6SU7dCK6eYec9f9wpT353ljhDUHq3EbmE4foNzJngh35d
203
+AekCIQDhRQG5Li0Wj8TM4obOnnXUXf1jRv0UkzE9AHWLG5q3AwIhAPzSjpYUDjVW
204
+MCUXgckTpKCuGwbJk7424Nb8bLzf3kllAiA5mUBgjfr/WtFSJdWcPQ4Zt9KTMNKD
205
+EUO0ukpTwEIl6wIhAMbGqZK3zAAFdq8DD2jPx+UJXnh0rnOkZBzDtJ6/iN69AiEA
206
+1Aq8MJgTaYsDQWyU/hDq5YkDJc9e9DSCvUIzqxQWMQE=
207
+-----END RSA PRIVATE KEY-----`)
... ...
@@ -190,6 +190,10 @@ func BuildKubernetesNodeConfig(options configapi.NodeConfig) (*NodeConfig, error
190 190
 
191 191
 	// TODO: could be cleaner
192 192
 	if configapi.UseTLS(options.ServingInfo) {
193
+		extraCerts, err := configapi.GetNamedCertificateMap(options.ServingInfo.NamedCertificates)
194
+		if err != nil {
195
+			return nil, err
196
+		}
193 197
 		cfg.TLSOptions = &kubelet.TLSOptions{
194 198
 			Config: &tls.Config{
195 199
 				// Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)
... ...
@@ -198,6 +202,10 @@ func BuildKubernetesNodeConfig(options configapi.NodeConfig) (*NodeConfig, error
198 198
 				// Verification is done by the authn layer
199 199
 				ClientAuth: tls.RequestClientCert,
200 200
 				ClientCAs:  clientCAs,
201
+				// Set SNI certificate func
202
+				// Do not use NameToCertificate, since that requires certificates be included in the server's tlsConfig.Certificates list,
203
+				// which we do not control when running with http.Server#ListenAndServeTLS
204
+				GetCertificate: cmdutil.GetCertificateFunc(extraCerts),
201 205
 			},
202 206
 			CertFile: options.ServingInfo.ServerCert.CertFile,
203 207
 			KeyFile:  options.ServingInfo.ServerCert.KeyFile,
... ...
@@ -81,9 +81,15 @@ func (c *AssetConfig) Run() {
81 81
 
82 82
 	go util.Forever(func() {
83 83
 		if isTLS {
84
+			extraCerts, err := configapi.GetNamedCertificateMap(c.Options.ServingInfo.NamedCertificates)
85
+			if err != nil {
86
+				glog.Fatal(err)
87
+			}
84 88
 			server.TLSConfig = &tls.Config{
85 89
 				// Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)
86 90
 				MinVersion: tls.VersionTLS10,
91
+				// Set SNI certificate func
92
+				GetCertificate: cmdutil.GetCertificateFunc(extraCerts),
87 93
 			}
88 94
 			glog.Infof("Web console listening at https://%s", c.Options.ServingInfo.BindAddress)
89 95
 			glog.Fatal(cmdutil.ListenAndServeTLS(server, c.Options.ServingInfo.BindNetwork, c.Options.ServingInfo.ServerCert.CertFile, c.Options.ServingInfo.ServerCert.KeyFile))
... ...
@@ -230,6 +230,10 @@ func (c *MasterConfig) serve(handler http.Handler, extra []string) {
230 230
 			glog.Infof(s, c.Options.ServingInfo.BindAddress)
231 231
 		}
232 232
 		if c.TLS {
233
+			extraCerts, err := configapi.GetNamedCertificateMap(c.Options.ServingInfo.NamedCertificates)
234
+			if err != nil {
235
+				glog.Fatal(err)
236
+			}
233 237
 			server.TLSConfig = &tls.Config{
234 238
 				// Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)
235 239
 				MinVersion: tls.VersionTLS10,
... ...
@@ -237,6 +241,8 @@ func (c *MasterConfig) serve(handler http.Handler, extra []string) {
237 237
 				// This allows certificates to be validated by authenticators, while still allowing other auth types
238 238
 				ClientAuth: tls.RequestClientCert,
239 239
 				ClientCAs:  c.ClientCAs,
240
+				// Set SNI certificate func
241
+				GetCertificate: cmdutil.GetCertificateFunc(extraCerts),
240 242
 			}
241 243
 			glog.Fatal(cmdutil.ListenAndServeTLS(server, c.Options.ServingInfo.BindNetwork, c.Options.ServingInfo.ServerCert.CertFile, c.Options.ServingInfo.ServerCert.KeyFile))
242 244
 		} else {
... ...
@@ -163,9 +163,14 @@ func (o NodeOptions) RunNode() error {
163 163
 		return err
164 164
 	}
165 165
 
166
-	errs := validation.ValidateNodeConfig(nodeConfig)
167
-	if len(errs) != 0 {
168
-		return kerrors.NewInvalid("NodeConfig", o.ConfigFile, errs)
166
+	validationResults := validation.ValidateNodeConfig(nodeConfig)
167
+	if len(validationResults.Warnings) != 0 {
168
+		for _, warning := range validationResults.Warnings {
169
+			glog.Warningf("%v", warning)
170
+		}
171
+	}
172
+	if len(validationResults.Errors) != 0 {
173
+		return kerrors.NewInvalid("NodeConfig", o.ConfigFile, validationResults.Errors)
169 174
 	}
170 175
 
171 176
 	_, kubeClientConfig, err := configapi.GetKubeClient(nodeConfig.MasterKubeConfig)
... ...
@@ -6,8 +6,11 @@ import (
6 6
 	"fmt"
7 7
 	"net"
8 8
 	"net/http"
9
+	"strings"
9 10
 	"time"
10 11
 
12
+	"k8s.io/kubernetes/pkg/util/sets"
13
+
11 14
 	"github.com/golang/glog"
12 15
 )
13 16
 
... ...
@@ -141,3 +144,56 @@ func TransportFor(ca string, certFile string, keyFile string) (http.RoundTripper
141 141
 
142 142
 	return &transport, nil
143 143
 }
144
+
145
+// GetCertificateFunc returns a function that can be used in tls.Config#GetCertificate
146
+// Returns nil if len(certs) == 0
147
+func GetCertificateFunc(certs map[string]*tls.Certificate) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
148
+	if len(certs) == 0 {
149
+		return nil
150
+	}
151
+	// Replica of tls.Config#getCertificate logic
152
+	return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
153
+		if clientHello == nil {
154
+			return nil, nil
155
+		}
156
+
157
+		name := clientHello.ServerName
158
+		name = strings.ToLower(name)
159
+		name = strings.TrimRight(name, ".")
160
+		for _, candidate := range HostnameMatchSpecCandidates(name) {
161
+			if cert, ok := certs[candidate]; ok {
162
+				return cert, nil
163
+			}
164
+		}
165
+		return nil, nil
166
+	}
167
+}
168
+
169
+// HostnameMatchSpecCandidates returns a list of match specs that would match the provided hostname
170
+// Returns nil if len(hostname) == 0
171
+func HostnameMatchSpecCandidates(hostname string) []string {
172
+	if len(hostname) == 0 {
173
+		return nil
174
+	}
175
+
176
+	// Exact match has priority
177
+	candidates := []string{hostname}
178
+
179
+	// Replace successive labels in the name with wildcards, to require an exact match on number of
180
+	// path segments, because certificates cannot wildcard multiple levels of subdomains
181
+	//
182
+	// This is primarily to be consistent with tls.Config#getCertificate implementation
183
+	//
184
+	// It using a cert signed for *.foo.example.com and *.bar.example.com by specifying the name *.*.example.com
185
+	labels := strings.Split(hostname, ".")
186
+	for i := range labels {
187
+		labels[i] = "*"
188
+		candidates = append(candidates, strings.Join(labels, "."))
189
+	}
190
+	return candidates
191
+}
192
+
193
+// HostnameMatches returns true if the given hostname is matched by the given matchSpec
194
+func HostnameMatches(hostname string, matchSpec string) bool {
195
+	return sets.NewString(HostnameMatchSpecCandidates(hostname)...).Has(matchSpec)
196
+}
144 197
new file mode 100644
... ...
@@ -0,0 +1,75 @@
0
+package util
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+)
6
+
7
+func TestHostnameMatchSpecCandidates(t *testing.T) {
8
+	testcases := []struct {
9
+		Hostname      string
10
+		ExpectedSpecs []string
11
+	}{
12
+		{
13
+			Hostname:      "",
14
+			ExpectedSpecs: nil,
15
+		},
16
+		{
17
+			Hostname:      "a",
18
+			ExpectedSpecs: []string{"a", "*"},
19
+		},
20
+		{
21
+			Hostname:      "foo.bar",
22
+			ExpectedSpecs: []string{"foo.bar", "*.bar", "*.*"},
23
+		},
24
+	}
25
+
26
+	for _, tc := range testcases {
27
+		specs := HostnameMatchSpecCandidates(tc.Hostname)
28
+		if !reflect.DeepEqual(specs, tc.ExpectedSpecs) {
29
+			t.Errorf("%s: Expected %#v, got %#v", tc.Hostname, tc.ExpectedSpecs, specs)
30
+		}
31
+	}
32
+}
33
+
34
+func TestHostnameMatches(t *testing.T) {
35
+	testcases := []struct {
36
+		Hostname      string
37
+		Spec          string
38
+		ExpectedMatch bool
39
+	}{
40
+		// Empty hostname matches nothing
41
+		{Hostname: "", Spec: "", ExpectedMatch: false},
42
+
43
+		// Empty spec matches nothing
44
+		{Hostname: "a", Spec: "", ExpectedMatch: false},
45
+
46
+		// Exact match
47
+		{Hostname: "a", Spec: "a", ExpectedMatch: true},
48
+		// Single segment wildcard match
49
+		{Hostname: "a", Spec: "*", ExpectedMatch: true},
50
+
51
+		// Mismatched segment count should not match
52
+		{Hostname: "a", Spec: "*.a", ExpectedMatch: false},
53
+		{Hostname: "a", Spec: "*.*", ExpectedMatch: false},
54
+
55
+		// Exact match, multi-segment
56
+		{Hostname: "a.b", Spec: "a.b", ExpectedMatch: true},
57
+		// Wildcard subdomain match
58
+		{Hostname: "a.b", Spec: "*.b", ExpectedMatch: true},
59
+		// Multi-level wildcard match
60
+		{Hostname: "a.b", Spec: "*.*", ExpectedMatch: true},
61
+
62
+		// Only subdomain wildcards are allowed
63
+		{Hostname: "a.b", Spec: "a.*", ExpectedMatch: false},
64
+		// Mismatched segment count should not match
65
+		{Hostname: "a.b", Spec: "*.a.b", ExpectedMatch: false},
66
+	}
67
+
68
+	for i, tc := range testcases {
69
+		matches := HostnameMatches(tc.Hostname, tc.Spec)
70
+		if matches != tc.ExpectedMatch {
71
+			t.Errorf("%d: Expected match=%v, got %v (hostname=%s, specs=%v)", i, tc.ExpectedMatch, matches, tc.Hostname, tc.Spec)
72
+		}
73
+	}
74
+}
... ...
@@ -41,8 +41,12 @@ func (d NodeConfigCheck) Check() types.DiagnosticResult {
41 41
 
42 42
 	r.Info("DH1003", fmt.Sprintf("Found a node config file: %[1]s", d.NodeConfigFile))
43 43
 
44
-	for _, err := range configvalidation.ValidateNodeConfig(nodeConfig) {
44
+	results := configvalidation.ValidateNodeConfig(nodeConfig)
45
+	for _, err := range results.Errors {
45 46
 		r.Error("DH1004", err, fmt.Sprintf("Validation of node config file '%s' failed:\n(%T) %[2]v", d.NodeConfigFile, err))
46 47
 	}
48
+	for _, err := range results.Warnings {
49
+		r.Warn("DH1005", err, fmt.Sprintf("Validation of node config file '%s' warning:\n(%T) %[2]v", d.NodeConfigFile, err))
50
+	}
47 51
 	return r
48 52
 }
49 53
new file mode 100644
... ...
@@ -0,0 +1,306 @@
0
+// +build integration,etcd
1
+
2
+package integration
3
+
4
+import (
5
+	"crypto/tls"
6
+	"io/ioutil"
7
+	"net"
8
+	"net/http"
9
+	"net/url"
10
+	"os"
11
+	"testing"
12
+
13
+	configapi "github.com/openshift/origin/pkg/cmd/server/api"
14
+	"github.com/openshift/origin/pkg/cmd/util"
15
+	testserver "github.com/openshift/origin/test/util/server"
16
+)
17
+
18
+const (
19
+	// oadm ca create-signer-cert --cert=sni-ca.crt --key=sni-ca.key --name=sni-signer --serial=sni-serial.txt
20
+	sniCACert = "sni-ca.crt"
21
+
22
+	// oadm ca create-server-cert --cert=sni.crt --key=sni.key --hostnames=127.0.0.1,customhost.com,*.wildcardhost.com --signer-cert=sni-ca.crt --signer-key=sni-ca.key --signer-serial=sni-serial.txt
23
+	sniServerCert = "sni-server.crt"
24
+	sniServerKey  = "sni-server.key"
25
+)
26
+
27
+func TestSNI(t *testing.T) {
28
+	// Create tempfiles with certs and keys we're going to use
29
+	certNames := map[string]string{}
30
+	for certName, certContents := range sniCerts {
31
+		f, err := ioutil.TempFile("", certName)
32
+		if err != nil {
33
+			t.Fatalf("unexpected error: %v", err)
34
+		}
35
+		defer os.Remove(f.Name())
36
+		if err := ioutil.WriteFile(f.Name(), certContents, os.FileMode(0600)); err != nil {
37
+			t.Fatalf("unexpected error: %v", err)
38
+		}
39
+		certNames[certName] = f.Name()
40
+	}
41
+
42
+	// Build master config
43
+	masterOptions, err := testserver.DefaultMasterOptions()
44
+	if err != nil {
45
+		t.Fatalf("unexpected error: %v", err)
46
+	}
47
+
48
+	// Set custom cert
49
+	masterOptions.ServingInfo.NamedCertificates = []configapi.NamedCertificate{
50
+		{
51
+			Names: []string{"customhost.com"},
52
+			CertInfo: configapi.CertInfo{
53
+				CertFile: certNames[sniServerCert],
54
+				KeyFile:  certNames[sniServerKey],
55
+			},
56
+		},
57
+		{
58
+			Names: []string{"*.wildcardhost.com"},
59
+			CertInfo: configapi.CertInfo{
60
+				CertFile: certNames[sniServerCert],
61
+				KeyFile:  certNames[sniServerKey],
62
+			},
63
+		},
64
+	}
65
+
66
+	// Start server
67
+	_, err = testserver.StartConfiguredMaster(masterOptions)
68
+	if err != nil {
69
+		t.Fatalf("unexpected error: %v", err)
70
+	}
71
+
72
+	// Build transports
73
+	sniRoots, err := util.CertPoolFromFile(certNames[sniCACert])
74
+	if err != nil {
75
+		t.Fatalf("unexpected error: %v", err)
76
+	}
77
+	sniConfig := &tls.Config{RootCAs: sniRoots}
78
+
79
+	generatedRoots, err := util.CertPoolFromFile(masterOptions.ServiceAccountConfig.MasterCA)
80
+	if err != nil {
81
+		t.Fatalf("unexpected error: %v", err)
82
+	}
83
+	generatedConfig := &tls.Config{RootCAs: generatedRoots}
84
+
85
+	insecureConfig := &tls.Config{InsecureSkipVerify: true}
86
+
87
+	tests := map[string]struct {
88
+		Hostname   string
89
+		TLSConfig  *tls.Config
90
+		ExpectedOK bool
91
+	}{
92
+		"sni client -> generated ip": {
93
+			Hostname:  "127.0.0.1",
94
+			TLSConfig: sniConfig,
95
+		},
96
+		"sni client -> generated hostname": {
97
+			Hostname:  "openshift",
98
+			TLSConfig: sniConfig,
99
+		},
100
+		"sni client -> sni host": {
101
+			Hostname:   "customhost.com",
102
+			TLSConfig:  sniConfig,
103
+			ExpectedOK: true,
104
+		},
105
+		"sni client -> sni wildcard host": {
106
+			Hostname:   "www.wildcardhost.com",
107
+			TLSConfig:  sniConfig,
108
+			ExpectedOK: true,
109
+		},
110
+		"sni client -> invalid ip": {
111
+			Hostname:  "10.10.10.10",
112
+			TLSConfig: sniConfig,
113
+		},
114
+		"sni client -> invalid host": {
115
+			Hostname:  "invalidhost.com",
116
+			TLSConfig: sniConfig,
117
+		},
118
+
119
+		"generated client -> generated ip": {
120
+			Hostname:   "127.0.0.1",
121
+			TLSConfig:  generatedConfig,
122
+			ExpectedOK: true,
123
+		},
124
+		"generated client -> generated hostname": {
125
+			Hostname:   "openshift",
126
+			TLSConfig:  generatedConfig,
127
+			ExpectedOK: true,
128
+		},
129
+		"generated client -> sni host": {
130
+			Hostname:  "customhost.com",
131
+			TLSConfig: generatedConfig,
132
+		},
133
+		"generated client -> sni wildcard host": {
134
+			Hostname:  "www.wildcardhost.com",
135
+			TLSConfig: generatedConfig,
136
+		},
137
+		"generated client -> invalid ip": {
138
+			Hostname:  "10.10.10.10",
139
+			TLSConfig: generatedConfig,
140
+		},
141
+		"generated client -> invalid host": {
142
+			Hostname:  "invalidhost.com",
143
+			TLSConfig: generatedConfig,
144
+		},
145
+
146
+		"insecure client -> generated ip": {
147
+			Hostname:   "127.0.0.1",
148
+			TLSConfig:  insecureConfig,
149
+			ExpectedOK: true,
150
+		},
151
+		"insecure client -> generated hostname": {
152
+			Hostname:   "openshift",
153
+			TLSConfig:  insecureConfig,
154
+			ExpectedOK: true,
155
+		},
156
+		"insecure client -> sni host": {
157
+			Hostname:   "customhost.com",
158
+			TLSConfig:  insecureConfig,
159
+			ExpectedOK: true,
160
+		},
161
+		"insecure client -> sni wildcard host": {
162
+			Hostname:   "www.wildcardhost.com",
163
+			TLSConfig:  insecureConfig,
164
+			ExpectedOK: true,
165
+		},
166
+		"insecure client -> invalid ip": {
167
+			Hostname:   "10.10.10.10",
168
+			TLSConfig:  insecureConfig,
169
+			ExpectedOK: true,
170
+		},
171
+		"insecure client -> invalid host": {
172
+			Hostname:   "invalidhost.com",
173
+			TLSConfig:  insecureConfig,
174
+			ExpectedOK: true,
175
+		},
176
+	}
177
+
178
+	masterPublicURL, err := url.Parse(masterOptions.MasterPublicURL)
179
+	if err != nil {
180
+		t.Fatalf("unexpected error: %v", err)
181
+	}
182
+
183
+	for k, tc := range tests {
184
+		u := *masterPublicURL
185
+		if err != nil {
186
+			t.Errorf("%s: unexpected error: %v", k, err)
187
+			continue
188
+		}
189
+		u.Path = "/healthz"
190
+
191
+		if _, port, err := net.SplitHostPort(u.Host); err == nil {
192
+			u.Host = net.JoinHostPort(tc.Hostname, port)
193
+		} else {
194
+			u.Host = tc.Hostname
195
+		}
196
+
197
+		req, err := http.NewRequest("GET", u.String(), nil)
198
+		if err != nil {
199
+			t.Errorf("%s: unexpected error: %v", k, err)
200
+			continue
201
+		}
202
+
203
+		transport := &http.Transport{
204
+			// Custom Dial func to always dial the real master, no matter what host is asked for
205
+			Dial: func(network, addr string) (net.Conn, error) {
206
+				// t.Logf("%s: Dialing for %s", k, addr)
207
+				return net.Dial(network, masterPublicURL.Host)
208
+			},
209
+			TLSClientConfig: tc.TLSConfig,
210
+		}
211
+		resp, err := transport.RoundTrip(req)
212
+		if tc.ExpectedOK && err != nil {
213
+			t.Errorf("%s: unexpected error: %v", k, err)
214
+			continue
215
+		}
216
+		if !tc.ExpectedOK && err == nil {
217
+			t.Errorf("%s: expected error, got none", k)
218
+			continue
219
+		}
220
+		if err == nil {
221
+			data, err := ioutil.ReadAll(resp.Body)
222
+			if err != nil {
223
+				t.Errorf("%s: unexpected error: %v", k, err)
224
+				continue
225
+			}
226
+			if string(data) != "ok" {
227
+				t.Errorf("%s: expected %q, got %q", k, "ok", string(data))
228
+				continue
229
+			}
230
+		}
231
+	}
232
+}
233
+
234
+var (
235
+	sniCerts = map[string][]byte{
236
+		sniCACert: []byte(`-----BEGIN CERTIFICATE-----
237
+MIICxjCCAbCgAwIBAgIBATALBgkqhkiG9w0BAQswFTETMBEGA1UEAxMKc25pLXNp
238
+Z25lcjAgFw0xNTEwMTMwNTEyMzFaGA8yMDY1MDkzMDA1MTIzMlowFTETMBEGA1UE
239
+AxMKc25pLXNpZ25lcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOb
240
+OG1vBlq8UdJZJbumpiSEZbC7Jkyh4IceLmRvojGlMPgyoT67PedZAOD4EAbMVh+h
241
+tAYZgcTC8ASCA1UelXFPng5OwurQv1uBdJTLiTBzPusDenWaCZc/zf2EJEM03WZ9
242
+k4VgYecYkOmSZqvDj5Dl4lvEsVUL9RBaNMzgbzXsZE1+brlUKR+UmF2yX56vBj2R
243
+WiQHOIQgjYLaAciHJsZVkqznoN155vPggX791l62fC5Ungil6TQTPdcizBR+deLN
244
+S+HFxH0+YjEPiTb8PdoSUH+W3Y6d2zSzebm1GZUOKtgOQTXhmIuB3GGwOSDLC9Rq
245
+6a1z3HTZNBMQ7Y8NOJsCAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgCkMA8GA1UdEwEB
246
+/wQFMAMBAf8wCwYJKoZIhvcNAQELA4IBAQCQ2laesgvXmT6EKXvnASbKPt35Lr26
247
+Jp0mayAGhJgf17WzQnmN0IFyZyu0H81TdIydximxKX6KWMrTk4z/7CbUa07AwVCy
248
+zBecwL0ajIgGakKqiiH3EJKrlg4jNKNOKboMuouNoROrwc5UkWfjSATFkjTTShDO
249
+Qd8JrAEBtEBaXr0Xueb5rdlrR/j7UMEpjUT7bGUxnhgF/h1TJ6cIiRpVKpA8NxyL
250
+ZBkouK3hPEeu92K7U/NBBE//YRQz6EghixQSv/ZEGmlsU8z6g8ay+d9iZa5DhYsh
251
+/IYxG0ykvGUH9d1AWplmHAqPwrcWSym49cZEmiHx/tO/wRp6+51lyfcn
252
+-----END CERTIFICATE-----
253
+`),
254
+
255
+		sniServerCert: []byte(`-----BEGIN CERTIFICATE-----
256
+MIIDIDCCAgqgAwIBAgIBAzALBgkqhkiG9w0BAQswFTETMBEGA1UEAxMKc25pLXNp
257
+Z25lcjAgFw0xNTEwMTMwNTMzMTVaGA8yMDY1MDkzMDA1MzMxNlowHTEbMBkGA1UE
258
+AxMSKi53aWxkY2FyZGhvc3QuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
259
+CgKCAQEA2hh1F9DXwkCDLi20i4ehoL40DhHFPsVfHa4/nIvmcOgJE6qZCh2oH+H8
260
+vDLQg4wNNpa47le5AuYqupXcXeTtrGq/AwXSnrC3LVJYqleAnVgLoL5+Y2NEj8Hx
261
+fzdWGhkCMDtA1QdZeq8HpCv3ZziRUxiZ/ddI4rZjFsCDoZUAhGGDzHCqkKbsuBI8
262
+bNkz9V0FfXn8OprfRCUPtMJrHusNsWCHrQ4ceRYOzsa9y4IVQnmvcpMlh7qu7kiO
263
+AbbFm41J8W/BEMxIBwJOITB2qAgkOKxF48IpsDeCWbOJ2qedisR5PNl/te4Qv/Kt
264
+D6FTqpIHv/cSkn9fz2ji6Jy574oR7wIDAQABo3UwczAOBgNVHQ8BAf8EBAMCAKAw
265
+EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADA+BgNVHREENzA1ghIq
266
+LndpbGRjYXJkaG9zdC5jb22CDmN1c3RvbWhvc3QuY29tggkxMjcuMC4wLjGHBH8A
267
+AAEwCwYJKoZIhvcNAQELA4IBAQB5Q7Pbx9jBP566XgjQGnpoNK5jJf3CqkjBKSdG
268
+OdoumjDEx2Ast3h1edXBVnU0DPxbfxMo3lBIAiJ+sNWGErYlIDVdpglFVyYIn+V6
269
+71gUDaA+1rXBS3f0QEQ9pOh3b4qSWbmYr9mbYRQus1cFYq+KTsLmzuNGvRwSvvE7
270
+5nSTgUozXTF4fSyWGGTcy13ZFg6mLlMoivjVswUJsi2nLf/yejnwdJGs4ZR06qCx
271
+dYB2LaUCbI6AQlaQMVrxsVXTfUkstKF5cuPKq/MenBcH88j3uqj8BF6YjR1wRfHc
272
+p0NuWIRsuXBHEr6o3iQ3KlmQLWfLeS0K+FkN60mwDTteIzuR
273
+-----END CERTIFICATE-----
274
+`),
275
+
276
+		sniServerKey: []byte(`-----BEGIN RSA PRIVATE KEY-----
277
+MIIEpQIBAAKCAQEA2hh1F9DXwkCDLi20i4ehoL40DhHFPsVfHa4/nIvmcOgJE6qZ
278
+Ch2oH+H8vDLQg4wNNpa47le5AuYqupXcXeTtrGq/AwXSnrC3LVJYqleAnVgLoL5+
279
+Y2NEj8HxfzdWGhkCMDtA1QdZeq8HpCv3ZziRUxiZ/ddI4rZjFsCDoZUAhGGDzHCq
280
+kKbsuBI8bNkz9V0FfXn8OprfRCUPtMJrHusNsWCHrQ4ceRYOzsa9y4IVQnmvcpMl
281
+h7qu7kiOAbbFm41J8W/BEMxIBwJOITB2qAgkOKxF48IpsDeCWbOJ2qedisR5PNl/
282
+te4Qv/KtD6FTqpIHv/cSkn9fz2ji6Jy574oR7wIDAQABAoIBAQDTq2UJpkGhYGdw
283
+zB8sRIjTr4ZqGUkscPatoc5PK2COOEWG9s3tiXcA6p4WMeM5qRWx43q8qBsB+02B
284
+Ja1o26To7/lO/7m5Fp3RuNghCyfije9LJVcZMuD5/StbYuOIFLmRAhEcMDPh5Dow
285
+VhOZ9MbmtTvPp8AveQCWtmWKz0hfMVLGUP91yqbXD/7h7/VpN+kKEOwoOUFqBshZ
286
+ziJs261VnI1UYeDg6jokZTDD9t1vS0EbPoe8NozPt8zF4bihXpLX7MCjJO32ISQl
287
+4cjH+94Wl1/X2MerG0RA8BTMlMSACiQdyeE7C4d/4tfRFp2EHqSBgwsTMLAJKy1P
288
+1NjejLmxAoGBAOxx1i22W/J1tqNvO7we3G8nuojRG5f69u1YKrW/yhk89a3hmyQ6
289
+MB15xz55CyTEl+FqPQReMJlmBsbKqHl8viOWhRc12t4i/JsYgroCcbIHME86FXuH
290
+boNJo0DB0Q1xoCQEuNiygyv3qLq5tKPBk0lAn2Sxhyt398MDukOcZ8ujAoGBAOwi
291
+HuM/d6A78F6l1NHJdOoTFvLXCbM3mWSdfoU/UxQmEk9Wr3kfudOu2ZAsWfiznjw4
292
+jgN/JS/WTY+NvVEXMIASz3QoNmLFSN0c5DCBSBVW/1B9rFP9GLCDrZMspluYg/gR
293
+8MLxC6AAVBcbNZj7Z16mmAyUs3LCJmP9P/7GcgVFAoGANesrvVbllt/zG0gFZjvf
294
+ZtW3evW8hibr4moFq1amHqVBHTriZxuB12bq4bs2qFbQj83rRjC4gnK6vuB+FN42
295
+eeUcSpO0ao2t7yxiu0pNZRywjpCfT4Et2XCUcvL/2kH8E9qj0H683OzoJFSu9dzx
296
+2nWLI6o8OdRswqL5+esT3GMCgYEApYnmDXnY60QZ5sBqygdpJw/q7qNB8Znwt1CR
297
++efC3kUyYNxsd4V+SKAzdZciG/AP5jfflyPzde3Oweyj481V+vM07EGknumfgyNV
298
+9YssdYlfw5XW0aqFPHmTnbGXjm8FVUt+dat2ctzIFsrEcFMOzJQN1AQLKVBiiYZo
299
+7rtAA+ECgYEAoR685udqJrCnvhL911cT+7/DrUyNLFmYvoIMlfG9DRXmKTIlyFH9
300
+A+TGxO02VaWYxm/zFTNIkXsEpNrxW4CVZtWM6biktT20S0p11IA1x8SCGqOimlF8
301
+Yg4OZRDUUYRAl0MUaslTyIxBfEVal4XgvBwhjXk0BMP6OJNDHWT3mUY=
302
+-----END RSA PRIVATE KEY-----
303
+`),
304
+	}
305
+)