SNI support #5097

Merged
merged 1 commit into from Oct 15, 2015

Projects

None yet

3 participants

@liggitt
Member
liggitt commented Oct 13, 2015

Enable SNI certificate selection for the master, asset server, and node
Allow setting a certificate to use for a particular hostname or wildcard.
A default cert (servingInfo.{certFile,keyFile}) is required in order to specify per host certs.

servingInfo:
  certFile: master.server.crt
  keyFile: master.server.key
  namedCertificates:
  - certFile: custom.crt
    keyFile: custom.key
    names:
    - "customhost.com"
    - "api.customhost.com"
    - "console.customhost.com"
  - certFile: wildcard.crt
    keyFile: wildcard.key
    names:
    - "*.wildcardhost.com"
  ...

TODO:

Follow-ups:

  • improve config file file reference extraction (maybe using reflection?) #5125
@liggitt
Member
liggitt commented Oct 13, 2015
@liggitt liggitt changed the title from WIP - SNI support to SNI support Oct 13, 2015
@liggitt
Member
liggitt commented Oct 13, 2015

[test]

@liggitt
Member
liggitt commented Oct 14, 2015

@deads2k PTAL

@deads2k deads2k commented on the diff Oct 14, 2015
pkg/cmd/server/api/helpers.go
@@ -120,6 +121,10 @@ func GetMasterFileReferences(config *MasterConfig) []*string {
refs = append(refs, &config.ServingInfo.ServerCert.CertFile)
refs = append(refs, &config.ServingInfo.ServerCert.KeyFile)
refs = append(refs, &config.ServingInfo.ClientCA)
+ for i := range config.ServingInfo.NamedCertificates {
@deads2k
deads2k Oct 14, 2015 Contributor

This is causing is problems. Embedding in ServingInfo makes sense, but now you're trying to chase all ServingInfo fields in the config and you've missed some. I agree that it would be crazy to attach this to say EtcdConfig, but it could be there anyway. Maybe a reflective recurse would be better?

@liggitt
liggitt Oct 14, 2015 Member

I only added references for the ones we support (I explicitly disallow it in etcdConfig via validation, since etcd doesn't support SNI)... add to HTTPServingInfo instead? felt weird there

@deads2k
deads2k Oct 14, 2015 Contributor

I only added references for the ones we support (I explicitly disallow it in etcdConfig via validation, since etcd doesn't support SNI)... add to HTTPServingInfo instead? felt weird there

I'm just looking at this function and decision about file ref or not is based mostly on type. I'd feel better about finding all references to types and adding them as file refs.

@deads2k deads2k commented on the diff Oct 14, 2015
pkg/cmd/server/api/helpers.go
@@ -317,6 +331,62 @@ func GetAPIClientCertCAPool(options MasterConfig) (*x509.CertPool, error) {
return cmdutil.CertPoolFromFile(options.ServingInfo.ClientCA)
}
+// GetNamedCertificateMap returns a map of strings to *tls.Certificate, suitable for use in tls.Config#NamedCertificates
+// Returns an error if any of the certs cannot be loaded
+// Returns nil if len(namedCertificates) == 0
+func GetNamedCertificateMap(namedCertificates []NamedCertificate) (map[string]*tls.Certificate, error) {
+ if len(namedCertificates) == 0 {
+ return nil, nil
+ }
+ namedCerts := map[string]*tls.Certificate{}
+ for _, namedCertificate := range namedCertificates {
+ cert, err := tls.LoadX509KeyPair(namedCertificate.CertFile, namedCertificate.KeyFile)
@deads2k
deads2k Oct 14, 2015 Contributor

Will go validate that the cert is legal to serve for what you're asking? If not, you've already loaded it can you inspect for its validity to serve the requested names here?

@liggitt
liggitt Oct 14, 2015 Member

no, it'll let you use a cert to serve anything you want (it'll still secure the connection, the client just won't be able to validate the hostname). I can add validation and warn if the cert is invalid or doesn't contain a DNS name matching the configured names

@deads2k
deads2k Oct 14, 2015 Contributor

no, it'll let you use a cert to serve anything you want (it'll still secure the connection, the client just won't be able to validate the hostname). I can add validation and warn if the cert is invalid or doesn't contain a DNS name matching the configured names

Yeah, I see no valid reason for choosing a different cert that won't validate. I would do it here, not in the validate methods, since we haven't been loading certs there.

@liggitt
liggitt Oct 14, 2015 Member

I do... they might want to use a cert signed by their corporate CA, so signer validation would pass, even if hostname validation did not

@deads2k
deads2k Oct 14, 2015 Contributor

I do... they might want to use a cert signed by their corporate CA, so signer validation would pass, even if hostname validation did not

"Security is hard and users don't know any better"

@deads2k deads2k and 1 other commented on an outdated diff Oct 14, 2015
pkg/cmd/server/api/helpers.go
+ return nil, nil
+ }
+
+ name := clientHello.ServerName
+ name = strings.ToLower(name)
+ name = strings.TrimRight(name, ".")
+
+ if cert, ok := certs[name]; ok {
+ return cert, nil
+ }
+
+ // try replacing labels in the name with wildcards until we get a match.
+ labels := strings.Split(name, ".")
+ for i := range labels {
+ labels[i] = "*"
+ candidate := strings.Join(labels, ".")
@deads2k
deads2k Oct 14, 2015 Contributor

This would result in looking for "foo.bar.example.com" with "*.*.example.com", but don't we want "*.example.com"? I didn't think I had to specify multiple wildcards counting off the subdomains.

@liggitt
liggitt Oct 14, 2015 Member

that's an extract of the golang implementation for matching against NameToCertificate (so that we can use this in the node), so I'm inclined to leave it as-is.

@deads2k deads2k commented on an outdated diff Oct 14, 2015
pkg/cmd/server/api/v1/types.go
@@ -319,6 +319,13 @@ type ServingInfo struct {
CertInfo `json:",inline"`
// ClientCA is the certificate bundle for all the signers that you'll recognize for incoming client certificates
ClientCA string `json:"clientCA"`
+
+ NamedCertificates []NamedCertificate `json:"namedCertificates"`
@deads2k
deads2k Oct 14, 2015 Contributor

godoc. We'll want it if we ever encode with comments.

@deads2k deads2k commented on an outdated diff Oct 14, 2015
pkg/cmd/server/api/validation/validation.go
+ for i, namedCertificate := range info.NamedCertificates {
+ fieldName := fmt.Sprintf("namedCertificates[%d]", i)
+
+ if len(namedCertificate.CertFile) == 0 {
+ allErrs = append(allErrs, fielderrors.NewFieldRequired(fieldName+".certInfo"))
+ } else {
+ allErrs = append(allErrs, ValidateCertInfo(namedCertificate.CertInfo, false).Prefix(fieldName)...)
+ }
+
+ if len(namedCertificate.Names) == 0 {
+ allErrs = append(allErrs, fielderrors.NewFieldRequired(fieldName+".names"))
+ }
+ for j, name := range namedCertificate.Names {
+ nameFieldName := fieldName + fmt.Sprintf(".names[%d]", j)
+ if len(name) == 0 {
+ allErrs = append(allErrs, fielderrors.NewFieldInvalid(nameFieldName, name, "cannot be empty"))
@deads2k
deads2k Oct 14, 2015 Contributor

this looks more like NewFieldRequired to me.

@deads2k deads2k commented on an outdated diff Oct 14, 2015
pkg/cmd/server/api/validation/validation.go
+ allErrs = append(allErrs, ValidateCertInfo(namedCertificate.CertInfo, false).Prefix(fieldName)...)
+ }
+
+ if len(namedCertificate.Names) == 0 {
+ allErrs = append(allErrs, fielderrors.NewFieldRequired(fieldName+".names"))
+ }
+ for j, name := range namedCertificate.Names {
+ nameFieldName := fieldName + fmt.Sprintf(".names[%d]", j)
+ if len(name) == 0 {
+ allErrs = append(allErrs, fielderrors.NewFieldInvalid(nameFieldName, name, "cannot be empty"))
+ } else if takenNames.Has(name) {
+ allErrs = append(allErrs, fielderrors.NewFieldInvalid(nameFieldName, name, "this name is already used in another named certificate"))
+ } else {
+ takenNames.Insert(name)
+ }
+ // validate names as domain names or *.... domain names?
@deads2k
deads2k Oct 14, 2015 Contributor

Yes.

@deads2k deads2k and 1 other commented on an outdated diff Oct 14, 2015
pkg/cmd/server/kubernetes/node_config.go
@@ -198,6 +202,9 @@ func BuildKubernetesNodeConfig(options configapi.NodeConfig) (*NodeConfig, error
// Verification is done by the authn layer
ClientAuth: tls.RequestClientCert,
ClientCAs: clientCAs,
+ // Set SNI certificate func
+ // Use instead of NameToCertificate, since the server ignores that map when started with http.Server#ListenAndServeTLS
@deads2k
deads2k Oct 14, 2015 Contributor

This sounds hard won. Documented in some way or does it look like a bug?

@liggitt
liggitt Oct 14, 2015 Member

looks like a bug

@deads2k deads2k and 1 other commented on an outdated diff Oct 14, 2015
pkg/cmd/server/origin/asset.go
server.TLSConfig = &tls.Config{
// Change default from SSLv3 to TLSv1.0 (because of POODLE vulnerability)
MinVersion: tls.VersionTLS10,
+ // Set additional SNI certificates, if provided
+ NameToCertificate: extraCerts,
@deads2k
deads2k Oct 14, 2015 Contributor

Wait, didn't the comment from node_config.go just tell me that this wouldn't work?

@liggitt
liggitt Oct 14, 2015 Member

we're not using Server#ListenAndServeTLS

@liggitt
Member
liggitt commented Oct 14, 2015

comments addressed

@deads2k deads2k commented on an outdated diff Oct 14, 2015
pkg/cmd/util/net.go
+ }
+}
+
+// HostnameMatchSpecCandidates returns a list of match specs that would match the provided hostname
+// Returns nil if len(hostname) == 0
+func HostnameMatchSpecCandidates(hostname string) []string {
+ if len(hostname) == 0 {
+ return nil
+ }
+
+ // Exact match has priority
+ candidates := []string{hostname}
+
+ // replace successive labels in the name with wildcards
+ // require an exact match on number of path segments, because certificates cannot wildcard multiple levels of subdomains
+ // this means that a.b.example.com matches *.*.example.com, but not *.example.com
@deads2k
deads2k Oct 14, 2015 Contributor

Give an example where this isn't dumb. The *.foo.example.com and *.bar.example.com wasn't great, but its something.

@deads2k deads2k commented on an outdated diff Oct 14, 2015
pkg/cmd/server/api/v1/types.go
@@ -319,6 +319,13 @@ type ServingInfo struct {
CertInfo `json:",inline"`
// ClientCA is the certificate bundle for all the signers that you'll recognize for incoming client certificates
ClientCA string `json:"clientCA"`
+ // NamedCertificates is a list of certificates to use to secure requests to specific hostnames
+ NamedCertificates []NamedCertificate `json:"namedCertificates"`
+}
+
+type NamedCertificate struct {
+ Names []string `json:"names"`
@deads2k
deads2k Oct 14, 2015 Contributor

Sorry, meant godoc on all fields. These too.

@deads2k
Contributor
deads2k commented Oct 14, 2015

minor comments. lgtm otherwise.

@liggitt
Member
liggitt commented Oct 14, 2015

[merge]

@openshift-bot
Member

continuous-integration/openshift-jenkins/merge SUCCESS (https://ci.openshift.redhat.com/jenkins/job/merge_pull_requests_origin/3627/) (Image: devenv-fedora_2467)

@liggitt liggitt Add SNI support
81b520f
@openshift-bot
Member

continuous-integration/openshift-jenkins/test SUCCESS (https://ci.openshift.redhat.com/jenkins/job/test_pull_requests_origin/5796/)

@openshift-bot
Member

Evaluated for origin test up to 81b520f

@openshift-bot
Member

Evaluated for origin merge up to 81b520f

@openshift-bot openshift-bot merged commit 47d1103 into openshift:master Oct 15, 2015

1 of 3 checks passed

continuous-integration/openshift-jenkins/merge Testing
Details
continuous-integration/travis-ci/pr The Travis CI build is in progress
Details
continuous-integration/openshift-jenkins/test Tested
Details
@liggitt liggitt deleted the liggitt:multi-cert branch Oct 15, 2015
@liggitt liggitt referenced this pull request in openshift/openshift-docs Nov 4, 2015
Closed

Document SNI certificates #1143

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment