19896: Configurable minimum TLS version for LDAP connection.
authorTom Clegg <tom@curii.com>
Wed, 4 Jan 2023 23:35:03 +0000 (18:35 -0500)
committerTom Clegg <tom@curii.com>
Wed, 4 Jan 2023 23:35:03 +0000 (18:35 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

lib/config/config.default.yml
lib/config/export.go
lib/controller/localdb/login_ldap.go
sdk/go/arvados/config.go
sdk/go/arvados/config_test.go

index 2d9119adfc747c2a937a39405dac88638ce5b5bb..3ca137e9c4c6f0417a86d10dddeabf2296c39134 100644 (file)
@@ -803,6 +803,12 @@ Clusters:
         # Skip TLS certificate name verification.
         InsecureTLS: false
 
+        # Mininum TLS version to negotiate when connecting to server
+        # (ldaps://... or StartTLS). It may be necessary to set this
+        # to "1.1" for compatibility with older LDAP servers. If
+        # blank, use the recommended minimum version (1.2).
+        MinTLSVersion: ""
+
         # Strip the @domain part if a user supplies an email-style
         # username with this domain. If "*", strip any user-provided
         # domain. If "", never strip the domain part. Example:
index 069e300c5b4d0f6eb72175a6d311670ab5fa9fd4..dfd62ce04c6fd16e38add874cef81bbad162a7b0 100644 (file)
@@ -162,6 +162,7 @@ var whitelist = map[string]bool{
        "Login.LDAP.EmailAttribute":                           false,
        "Login.LDAP.Enable":                                   true,
        "Login.LDAP.InsecureTLS":                              false,
+       "Login.LDAP.MinTLSVersion":                            false,
        "Login.LDAP.SearchAttribute":                          false,
        "Login.LDAP.SearchBase":                               false,
        "Login.LDAP.SearchBindPassword":                       false,
index 3f13c7b27aafed09f1c6b201270e034ca9c011d9..f8fe9084d7d4701bda55d3f410ca8d1c49ee040d 100644 (file)
@@ -47,7 +47,25 @@ func (ctrl *ldapLoginController) UserAuthenticate(ctx context.Context, opts arva
        }
 
        log = log.WithField("URL", conf.URL.String())
-       l, err := ldap.DialURL(conf.URL.String())
+       var l *ldap.Conn
+       var err error
+       if conf.URL.Scheme == "ldaps" {
+               // ldap.DialURL does not currently allow us to control
+               // tls.Config, so we need to figure out the port
+               // ourselves and call DialTLS.
+               host, port, err := net.SplitHostPort(conf.URL.Host)
+               if err != nil {
+                       // Assume error means no port given
+                       host = conf.URL.Host
+                       port = ldap.DefaultLdapsPort
+               }
+               l, err = ldap.DialTLS("tcp", net.JoinHostPort(host, port), &tls.Config{
+                       ServerName: host,
+                       MinVersion: uint16(conf.MinTLSVersion),
+               })
+       } else {
+               l, err = ldap.DialURL(conf.URL.String())
+       }
        if err != nil {
                log.WithError(err).Error("ldap connection failed")
                return arvados.APIClientAuthorization{}, err
@@ -58,6 +76,7 @@ func (ctrl *ldapLoginController) UserAuthenticate(ctx context.Context, opts arva
                var tlsconfig tls.Config
                if conf.InsecureTLS {
                        tlsconfig.InsecureSkipVerify = true
+                       tlsconfig.MinVersion = uint16(conf.MinTLSVersion)
                } else {
                        if host, _, err := net.SplitHostPort(conf.URL.Host); err != nil {
                                // Assume SplitHostPort error means
index fbbcb78ec2991b00aca4f4b2f4e94fb7ac209760..51e74edb782a71850278eda6f8ee895f2b14c030 100644 (file)
@@ -5,6 +5,7 @@
 package arvados
 
 import (
+       "crypto/tls"
        "encoding/json"
        "errors"
        "fmt"
@@ -162,6 +163,7 @@ type Cluster struct {
                        URL                URL
                        StartTLS           bool
                        InsecureTLS        bool
+                       MinTLSVersion      TLSVersion
                        StripDomain        string
                        AppendDomain       string
                        SearchAttribute    string
@@ -404,6 +406,51 @@ func (su URL) String() string {
        return (*url.URL)(&su).String()
 }
 
+type TLSVersion uint16
+
+func (v TLSVersion) MarshalText() ([]byte, error) {
+       switch v {
+       case 0:
+               return []byte{}, nil
+       case tls.VersionTLS10:
+               return []byte("1.0"), nil
+       case tls.VersionTLS11:
+               return []byte("1.1"), nil
+       case tls.VersionTLS12:
+               return []byte("1.2"), nil
+       case tls.VersionTLS13:
+               return []byte("1.3"), nil
+       default:
+               return nil, fmt.Errorf("unsupported TLSVersion %x", v)
+       }
+}
+
+func (v *TLSVersion) UnmarshalJSON(text []byte) error {
+       if len(text) > 0 && text[0] == '"' {
+               var s string
+               err := json.Unmarshal(text, &s)
+               if err != nil {
+                       return err
+               }
+               text = []byte(s)
+       }
+       switch string(text) {
+       case "":
+               *v = 0
+       case "1.0":
+               *v = tls.VersionTLS10
+       case "1.1":
+               *v = tls.VersionTLS11
+       case "1.2":
+               *v = tls.VersionTLS12
+       case "1.3":
+               *v = tls.VersionTLS13
+       default:
+               return fmt.Errorf("unsupported TLSVersion %q", text)
+       }
+       return nil
+}
+
 type ServiceInstance struct {
        ListenURL  URL
        Rendezvous string `json:",omitempty"`
index 58f4b961bbcc7c9b8945ed2b56ff69b502e1f99c..3c65643bea576a983b0b104f5cccef09024d1fb1 100644 (file)
@@ -5,6 +5,7 @@
 package arvados
 
 import (
+       "crypto/tls"
        "encoding/json"
 
        "github.com/ghodss/yaml"
@@ -71,3 +72,19 @@ func (s *ConfigSuite) TestURLTrailingSlash(c *check.C) {
        json.Unmarshal([]byte(`{"https://foo.example/": true}`), &b)
        c.Check(a, check.DeepEquals, b)
 }
+
+func (s *ConfigSuite) TestTLSVersion(c *check.C) {
+       var v struct {
+               Version TLSVersion
+       }
+       err := json.Unmarshal([]byte(`{"Version": 1.0}`), &v)
+       c.Check(err, check.IsNil)
+       c.Check(v.Version, check.Equals, TLSVersion(tls.VersionTLS10))
+
+       err = json.Unmarshal([]byte(`{"Version": "1.3"}`), &v)
+       c.Check(err, check.IsNil)
+       c.Check(v.Version, check.Equals, TLSVersion(tls.VersionTLS13))
+
+       err = json.Unmarshal([]byte(`{"Version": "1.345"}`), &v)
+       c.Check(err, check.NotNil)
+}