Browse code

Refactor TLS code with a new `tlsconfig` package

This patch creates a new `tlsconfig` package to handle creation of
secure-enough TLS configurations for clients and servers.

The package was created by refactoring TLS code in the client and the
daemon. After this patch, it is expected that all code creating TLS
configurations use this `tlsconfig` package for greater security,
consistency and readability.

On the server side, this fixes a bug where --tlsverify was not taken
into account. Now, if specified, it will require the client to
authenticate.

Signed-off-by: Tibor Vass <tibor@docker.com>

Tibor Vass authored on 2015/05/08 01:49:07
Showing 6 changed files
... ...
@@ -1,6 +1,7 @@
1 1
 package server
2 2
 
3 3
 import (
4
+	"crypto/tls"
4 5
 	"encoding/base64"
5 6
 	"encoding/json"
6 7
 	"fmt"
... ...
@@ -44,11 +45,7 @@ type ServerConfig struct {
44 44
 	CorsHeaders string
45 45
 	Version     string
46 46
 	SocketGroup string
47
-	Tls         bool
48
-	TlsVerify   bool
49
-	TlsCa       string
50
-	TlsCert     string
51
-	TlsKey      string
47
+	TLSConfig   *tls.Config
52 48
 }
53 49
 
54 50
 type Server struct {
... ...
@@ -1429,22 +1426,15 @@ func (s *Server) ping(version version.Version, w http.ResponseWriter, r *http.Re
1429 1429
 }
1430 1430
 
1431 1431
 func (s *Server) initTcpSocket(addr string) (l net.Listener, err error) {
1432
-	if !s.cfg.TlsVerify {
1432
+	if s.cfg.TLSConfig == nil || s.cfg.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert {
1433 1433
 		logrus.Warn("/!\\ DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
1434 1434
 	}
1435
-
1436
-	var c *sockets.TlsConfig
1437
-	if s.cfg.Tls || s.cfg.TlsVerify {
1438
-		c = sockets.NewTlsConfig(s.cfg.TlsCert, s.cfg.TlsKey, s.cfg.TlsCa, s.cfg.TlsVerify)
1439
-	}
1440
-
1441
-	if l, err = sockets.NewTcpSocket(addr, c, s.start); err != nil {
1435
+	if l, err = sockets.NewTcpSocket(addr, s.cfg.TLSConfig, s.start); err != nil {
1442 1436
 		return nil, err
1443 1437
 	}
1444 1438
 	if err := allocateDaemonPort(addr); err != nil {
1445 1439
 		return nil, err
1446 1440
 	}
1447
-
1448 1441
 	return
1449 1442
 }
1450 1443
 
... ...
@@ -3,6 +3,7 @@
3 3
 package main
4 4
 
5 5
 import (
6
+	"crypto/tls"
6 7
 	"fmt"
7 8
 	"io"
8 9
 	"os"
... ...
@@ -21,6 +22,7 @@ import (
21 21
 	"github.com/docker/docker/pkg/signal"
22 22
 	"github.com/docker/docker/pkg/system"
23 23
 	"github.com/docker/docker/pkg/timeutils"
24
+	"github.com/docker/docker/pkg/tlsconfig"
24 25
 	"github.com/docker/docker/registry"
25 26
 	"github.com/docker/docker/utils"
26 27
 )
... ...
@@ -112,11 +114,17 @@ func mainDaemon() {
112 112
 		CorsHeaders: daemonCfg.CorsHeaders,
113 113
 		Version:     dockerversion.VERSION,
114 114
 		SocketGroup: daemonCfg.SocketGroup,
115
-		Tls:         *flTls,
116
-		TlsVerify:   *flTlsVerify,
117
-		TlsCa:       *flCa,
118
-		TlsCert:     *flCert,
119
-		TlsKey:      *flKey,
115
+	}
116
+
117
+	if *flTls {
118
+		if *flTlsVerify {
119
+			tlsOptions.ClientAuth = tls.RequireAndVerifyClientCert
120
+		}
121
+		tlsConfig, err := tlsconfig.Server(tlsOptions)
122
+		if err != nil {
123
+			logrus.Fatal(err)
124
+		}
125
+		serverConfig.TLSConfig = tlsConfig
120 126
 	}
121 127
 
122 128
 	api := apiserver.New(serverConfig)
... ...
@@ -2,9 +2,7 @@ package main
2 2
 
3 3
 import (
4 4
 	"crypto/tls"
5
-	"crypto/x509"
6 5
 	"fmt"
7
-	"io/ioutil"
8 6
 	"os"
9 7
 	"runtime"
10 8
 	"strings"
... ...
@@ -16,6 +14,7 @@ import (
16 16
 	flag "github.com/docker/docker/pkg/mflag"
17 17
 	"github.com/docker/docker/pkg/reexec"
18 18
 	"github.com/docker/docker/pkg/term"
19
+	"github.com/docker/docker/pkg/tlsconfig"
19 20
 	"github.com/docker/docker/utils"
20 21
 )
21 22
 
... ...
@@ -85,6 +84,12 @@ func main() {
85 85
 
86 86
 	setDefaultConfFlag(flTrustKey, defaultTrustKeyFile)
87 87
 
88
+	// Regardless of whether the user sets it to true or false, if they
89
+	// specify --tlsverify at all then we need to turn on tls
90
+	if flag.IsSet("-tlsverify") {
91
+		*flTls = true
92
+	}
93
+
88 94
 	if *flDaemon {
89 95
 		if *flHelp {
90 96
 			flag.Usage()
... ...
@@ -94,59 +99,35 @@ func main() {
94 94
 		return
95 95
 	}
96 96
 
97
+	// From here on, we assume we're a client, not a server.
98
+
97 99
 	if len(flHosts) > 1 {
98 100
 		fmt.Fprintf(os.Stderr, "Please specify only one -H")
99 101
 		os.Exit(0)
100 102
 	}
101 103
 	protoAddrParts := strings.SplitN(flHosts[0], "://", 2)
102 104
 
103
-	var (
104
-		cli       *client.DockerCli
105
-		tlsConfig tls.Config
106
-	)
107
-	tlsConfig.InsecureSkipVerify = true
108
-
109
-	// Regardless of whether the user sets it to true or false, if they
110
-	// specify --tlsverify at all then we need to turn on tls
111
-	if flag.IsSet("-tlsverify") {
112
-		*flTls = true
113
-	}
114
-
115
-	// If we should verify the server, we need to load a trusted ca
116
-	if *flTlsVerify {
117
-		certPool := x509.NewCertPool()
118
-		file, err := ioutil.ReadFile(*flCa)
119
-		if err != nil {
120
-			fmt.Fprintf(os.Stderr, "Couldn't read ca cert %s: %s\n", *flCa, err)
121
-			os.Exit(1)
105
+	var tlsConfig *tls.Config
106
+	if *flTls {
107
+		tlsOptions.InsecureSkipVerify = !*flTlsVerify
108
+		if !flag.IsSet("-tlscert") {
109
+			if _, err := os.Stat(tlsOptions.CertFile); os.IsNotExist(err) {
110
+				tlsOptions.CertFile = ""
111
+			}
122 112
 		}
123
-		certPool.AppendCertsFromPEM(file)
124
-		tlsConfig.RootCAs = certPool
125
-		tlsConfig.InsecureSkipVerify = false
126
-	}
127
-
128
-	// If tls is enabled, try to load and send client certificates
129
-	if *flTls || *flTlsVerify {
130
-		_, errCert := os.Stat(*flCert)
131
-		_, errKey := os.Stat(*flKey)
132
-		if errCert == nil && errKey == nil {
133
-			*flTls = true
134
-			cert, err := tls.LoadX509KeyPair(*flCert, *flKey)
135
-			if err != nil {
136
-				fmt.Fprintf(os.Stderr, "Couldn't load X509 key pair: %q. Make sure the key is encrypted\n", err)
137
-				os.Exit(1)
113
+		if !flag.IsSet("-tlskey") {
114
+			if _, err := os.Stat(tlsOptions.KeyFile); os.IsNotExist(err) {
115
+				tlsOptions.KeyFile = ""
138 116
 			}
139
-			tlsConfig.Certificates = []tls.Certificate{cert}
140 117
 		}
141
-		// Avoid fallback to SSL protocols < TLS1.0
142
-		tlsConfig.MinVersion = tls.VersionTLS10
143
-	}
144
-
145
-	if *flTls || *flTlsVerify {
146
-		cli = client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
147
-	} else {
148
-		cli = client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], nil)
118
+		var err error
119
+		tlsConfig, err = tlsconfig.Client(tlsOptions)
120
+		if err != nil {
121
+			fmt.Fprintln(stderr, err)
122
+			os.Exit(1)
123
+		}
149 124
 	}
125
+	cli := client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], tlsConfig)
150 126
 
151 127
 	if err := cli.Cmd(flag.Args()...); err != nil {
152 128
 		if sterr, ok := err.(client.StatusError); ok {
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"github.com/docker/docker/opts"
11 11
 	"github.com/docker/docker/pkg/homedir"
12 12
 	flag "github.com/docker/docker/pkg/mflag"
13
+	"github.com/docker/docker/pkg/tlsconfig"
13 14
 )
14 15
 
15 16
 type command struct {
... ...
@@ -94,10 +95,8 @@ var (
94 94
 	flTlsVerify = flag.Bool([]string{"-tlsverify"}, dockerTlsVerify, "Use TLS and verify the remote")
95 95
 
96 96
 	// these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs
97
+	tlsOptions tlsconfig.Options
97 98
 	flTrustKey *string
98
-	flCa       *string
99
-	flCert     *string
100
-	flKey      *string
101 99
 	flHosts    []string
102 100
 )
103 101
 
... ...
@@ -116,9 +115,9 @@ func init() {
116 116
 	// TODO use flag flag.String([]string{"i", "-identity"}, "", "Path to libtrust key file")
117 117
 	flTrustKey = &placeholderTrustKey
118 118
 
119
-	flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust certs signed only by this CA")
120
-	flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file")
121
-	flKey = flag.String([]string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file")
119
+	flag.StringVar(&tlsOptions.CAFile, []string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust certs signed only by this CA")
120
+	flag.StringVar(&tlsOptions.CertFile, []string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file")
121
+	flag.StringVar(&tlsOptions.KeyFile, []string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file")
122 122
 	opts.HostListVar(&flHosts, []string{"H", "-host"}, "Daemon socket(s) to connect to")
123 123
 
124 124
 	flag.Usage = func() {
... ...
@@ -2,68 +2,19 @@ package sockets
2 2
 
3 3
 import (
4 4
 	"crypto/tls"
5
-	"crypto/x509"
6
-	"fmt"
7
-	"io/ioutil"
8 5
 	"net"
9
-	"os"
10 6
 
11 7
 	"github.com/docker/docker/pkg/listenbuffer"
12 8
 )
13 9
 
14
-type TlsConfig struct {
15
-	CA          string
16
-	Certificate string
17
-	Key         string
18
-	Verify      bool
19
-}
20
-
21
-func NewTlsConfig(tlsCert, tlsKey, tlsCA string, verify bool) *TlsConfig {
22
-	return &TlsConfig{
23
-		Verify:      verify,
24
-		Certificate: tlsCert,
25
-		Key:         tlsKey,
26
-		CA:          tlsCA,
27
-	}
28
-}
29
-
30
-func NewTcpSocket(addr string, config *TlsConfig, activate <-chan struct{}) (net.Listener, error) {
10
+func NewTcpSocket(addr string, tlsConfig *tls.Config, activate <-chan struct{}) (net.Listener, error) {
31 11
 	l, err := listenbuffer.NewListenBuffer("tcp", addr, activate)
32 12
 	if err != nil {
33 13
 		return nil, err
34 14
 	}
35
-	if config != nil {
36
-		if l, err = setupTls(l, config); err != nil {
37
-			return nil, err
38
-		}
15
+	if tlsConfig != nil {
16
+		tlsConfig.NextProtos = []string{"http/1.1"}
17
+		l = tls.NewListener(l, tlsConfig)
39 18
 	}
40 19
 	return l, nil
41 20
 }
42
-
43
-func setupTls(l net.Listener, config *TlsConfig) (net.Listener, error) {
44
-	tlsCert, err := tls.LoadX509KeyPair(config.Certificate, config.Key)
45
-	if err != nil {
46
-		if os.IsNotExist(err) {
47
-			return nil, fmt.Errorf("Could not load X509 key pair (%s, %s): %v", config.Certificate, config.Key, err)
48
-		}
49
-		return nil, fmt.Errorf("Error reading X509 key pair (%s, %s): %q. Make sure the key is encrypted.",
50
-			config.Certificate, config.Key, err)
51
-	}
52
-	tlsConfig := &tls.Config{
53
-		NextProtos:   []string{"http/1.1"},
54
-		Certificates: []tls.Certificate{tlsCert},
55
-		// Avoid fallback on insecure SSL protocols
56
-		MinVersion: tls.VersionTLS10,
57
-	}
58
-	if config.CA != "" {
59
-		certPool := x509.NewCertPool()
60
-		file, err := ioutil.ReadFile(config.CA)
61
-		if err != nil {
62
-			return nil, fmt.Errorf("Could not read CA certificate: %v", err)
63
-		}
64
-		certPool.AppendCertsFromPEM(file)
65
-		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
66
-		tlsConfig.ClientCAs = certPool
67
-	}
68
-	return tls.NewListener(l, tlsConfig), nil
69
-}
70 21
new file mode 100644
... ...
@@ -0,0 +1,107 @@
0
+// Package tlsconfig provides primitives to retrieve secure-enough TLS configurations for both clients and servers.
1
+//
2
+// As a reminder from https://golang.org/pkg/crypto/tls/#Config:
3
+//	A Config structure is used to configure a TLS client or server. After one has been passed to a TLS function it must not be modified.
4
+//	A Config may be reused; the tls package will also not modify it.
5
+package tlsconfig
6
+
7
+import (
8
+	"crypto/tls"
9
+	"crypto/x509"
10
+	"fmt"
11
+	"io/ioutil"
12
+	"os"
13
+
14
+	"github.com/Sirupsen/logrus"
15
+)
16
+
17
+// Options represents the information needed to create client and server TLS configurations.
18
+type Options struct {
19
+	InsecureSkipVerify bool
20
+	ClientAuth         tls.ClientAuthType
21
+	CAFile             string
22
+	CertFile           string
23
+	KeyFile            string
24
+}
25
+
26
+// Default is a secure-enough TLS configuration.
27
+var Default = tls.Config{
28
+	// Avoid fallback to SSL protocols < TLS1.0
29
+	MinVersion:               tls.VersionTLS10,
30
+	PreferServerCipherSuites: true,
31
+	CipherSuites: []uint16{
32
+		tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
33
+		tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
34
+		tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
35
+		tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
36
+		tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
37
+		tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
38
+		tls.TLS_RSA_WITH_AES_128_CBC_SHA,
39
+		tls.TLS_RSA_WITH_AES_256_CBC_SHA,
40
+	},
41
+}
42
+
43
+// certPool returns an X.509 certificate pool from `caFile`, the certificate file.
44
+func certPool(caFile string) (*x509.CertPool, error) {
45
+	// If we should verify the server, we need to load a trusted ca
46
+	certPool := x509.NewCertPool()
47
+	pem, err := ioutil.ReadFile(caFile)
48
+	if err != nil {
49
+		return nil, fmt.Errorf("Could not read CA certificate %s: %v", caFile, err)
50
+	}
51
+	if !certPool.AppendCertsFromPEM(pem) {
52
+		return nil, fmt.Errorf("failed to append certificates from PEM file: %s", caFile)
53
+	}
54
+	s := certPool.Subjects()
55
+	subjects := make([]string, len(s))
56
+	for i, subject := range s {
57
+		subjects[i] = string(subject)
58
+	}
59
+	logrus.Debugf("Trusting certs with subjects: %v", subjects)
60
+	return certPool, nil
61
+}
62
+
63
+// Client returns a TLS configuration meant to be used by a client.
64
+func Client(options Options) (*tls.Config, error) {
65
+	tlsConfig := Default
66
+	tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify
67
+	if !options.InsecureSkipVerify {
68
+		CAs, err := certPool(options.CAFile)
69
+		if err != nil {
70
+			return nil, err
71
+		}
72
+		tlsConfig.RootCAs = CAs
73
+	}
74
+
75
+	if options.CertFile != "" && options.KeyFile != "" {
76
+		tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile)
77
+		if err != nil {
78
+			return nil, fmt.Errorf("Could not load X509 key pair: %v. Make sure the key is not encrypted", err)
79
+		}
80
+		tlsConfig.Certificates = []tls.Certificate{tlsCert}
81
+	}
82
+
83
+	return &tlsConfig, nil
84
+}
85
+
86
+// Server returns a TLS configuration meant to be used by a server.
87
+func Server(options Options) (*tls.Config, error) {
88
+	tlsConfig := Default
89
+	tlsConfig.ClientAuth = options.ClientAuth
90
+	tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile)
91
+	if err != nil {
92
+		if os.IsNotExist(err) {
93
+			return nil, fmt.Errorf("Could not load X509 key pair (%s, %s): %v", options.CertFile, options.KeyFile, err)
94
+		}
95
+		return nil, fmt.Errorf("Error reading X509 key pair (%s, %s): %v. Make sure the key is not encrypted.", options.CertFile, options.KeyFile, err)
96
+	}
97
+	tlsConfig.Certificates = []tls.Certificate{tlsCert}
98
+	if options.ClientAuth >= tls.VerifyClientCertIfGiven {
99
+		CAs, err := certPool(options.CAFile)
100
+		if err != nil {
101
+			return nil, err
102
+		}
103
+		tlsConfig.ClientCAs = CAs
104
+	}
105
+	return &tlsConfig, nil
106
+}