Browse code

Merge pull request #3070 from alexlarsson/certificates

Solomon Hykes authored on 2014/07/19 10:27:00
Showing 4 changed files
... ...
@@ -83,6 +83,7 @@ pages:
83 83
 - ['articles/security.md', 'Articles', 'Security']
84 84
 - ['articles/https.md', 'Articles', 'Running Docker with HTTPS']
85 85
 - ['articles/host_integration.md', 'Articles', 'Automatically starting Containers']
86
+- ['articles/certificates.md', 'Articles', 'Using certificates for repository client verification']
86 87
 - ['articles/using_supervisord.md', 'Articles', 'Using Supervisor']
87 88
 - ['articles/cfengine_process_management.md', 'Articles', 'Process management with CFEngine']
88 89
 - ['articles/puppet.md', 'Articles', 'Using Puppet']
89 90
new file mode 100644
... ...
@@ -0,0 +1,83 @@
0
+page_title: Using certificates for repository client verification
1
+page_description: How to set up per-repository client certificates
2
+page_keywords: Usage, repository, certificate, root, docker, documentation, examples
3
+
4
+# Using certificates for repository client verification
5
+
6
+This lets you specify custom client TLS certificates and CA root for a
7
+specific registry hostname. Docker will then verify the registry
8
+against the CA and present the client cert when talking to that
9
+registry. This allows the registry to verify that the client has a
10
+proper key, indicating that the client is allowed to access the
11
+images.
12
+
13
+A custom cert is configured by creating a directory in
14
+`/etc/docker/certs.d` with the same name as the registry hostname. Inside
15
+this directory all .crt files are added as CA Roots (if none exists,
16
+the system default is used) and pair of files `$filename.key` and
17
+`$filename.cert` indicate a custom certificate to present to the
18
+registry.
19
+
20
+If there are multiple certificates each one will be tried in
21
+alphabetical order, proceeding to the next if we get a 403 of 5xx
22
+response.
23
+
24
+So, an example setup would be::
25
+
26
+    /etc/docker/certs.d/
27
+    └── localhost
28
+       ├── client.cert
29
+       ├── client.key
30
+       └── localhost.crt
31
+
32
+A simple way to test this setup is to use an apache server to host a
33
+registry. Just copy a registry tree into the apache root,
34
+[here](http://people.gnome.org/~alexl/v1.tar.gz) is an example one
35
+containing the busybox image.
36
+
37
+Then add this conf file as `/etc/httpd/conf.d/registry.conf`:
38
+
39
+    # This must be in the root context, otherwise it causes a re-negotiation
40
+    # which is not supported by the tls implementation in go
41
+    SSLVerifyClient optional_no_ca
42
+
43
+    <Location /v1>
44
+    Action cert-protected /cgi-bin/cert.cgi
45
+    SetHandler cert-protected
46
+
47
+    Header set x-docker-registry-version "0.6.2"
48
+    SetEnvIf Host (.*) custom_host=$1
49
+    Header set X-Docker-Endpoints "%{custom_host}e"
50
+    </Location>
51
+
52
+And this as `/var/www/cgi-bin/cert.cgi`:
53
+
54
+    #!/bin/bash
55
+    if [ "$HTTPS" != "on" ]; then
56
+        echo "Status: 403 Not using SSL"
57
+        echo "x-docker-registry-version: 0.6.2"
58
+        echo
59
+        exit 0
60
+    fi
61
+    if [ "$SSL_CLIENT_VERIFY" == "NONE" ]; then
62
+        echo "Status: 403 Client certificate invalid"
63
+        echo "x-docker-registry-version: 0.6.2"
64
+        echo
65
+        exit 0
66
+    fi
67
+    echo "Content-length: $(stat --printf='%s' $PATH_TRANSLATED)"
68
+    echo "x-docker-registry-version: 0.6.2"
69
+    echo "X-Docker-Endpoints: $SERVER_NAME"
70
+    echo "X-Docker-Size: 0"
71
+    echo
72
+
73
+    cat $PATH_TRANSLATED
74
+
75
+This will return 403 for all accessed to `/v1` unless any client cert is
76
+presented. Obviously a real implementation would verify more details
77
+about the certificate.
78
+
79
+Example client certs can be generated with::
80
+
81
+    openssl genrsa -out client.key 1024
82
+    openssl req -new -x509 -text -key client.key -out client.cert
0 83
new file mode 100644
... ...
@@ -0,0 +1,14 @@
0
+# Use
1
+
2
+## Contents:
3
+
4
+ - [First steps with Docker](basics/)
5
+ - [Share Images via Repositories](workingwithrepository/)
6
+ - [Redirect Ports](port_redirection/)
7
+ - [Configure Networking](networking/)
8
+ - [Automatically Start Containers](host_integration/)
9
+ - [Share Directories via Volumes](working_with_volumes/)
10
+ - [Link Containers](working_with_links_names/)
11
+ - [Link via an Ambassador Container](ambassador_pattern_linking/)
12
+ - [Using Puppet](puppet/)
13
+ - [Using certificates for repository client verification](certificates/)
... ...
@@ -4,6 +4,8 @@ import (
4 4
 	"bytes"
5 5
 	"crypto/sha256"
6 6
 	_ "crypto/sha512"
7
+	"crypto/tls"
8
+	"crypto/x509"
7 9
 	"encoding/json"
8 10
 	"errors"
9 11
 	"fmt"
... ...
@@ -13,6 +15,8 @@ import (
13 13
 	"net/http"
14 14
 	"net/http/cookiejar"
15 15
 	"net/url"
16
+	"os"
17
+	"path"
16 18
 	"regexp"
17 19
 	"runtime"
18 20
 	"strconv"
... ...
@@ -29,31 +33,155 @@ var (
29 29
 	errLoginRequired         = errors.New("Authentication is required.")
30 30
 )
31 31
 
32
+type TimeoutType uint32
33
+
34
+const (
35
+	NoTimeout TimeoutType = iota
36
+	ReceiveTimeout
37
+	ConnectTimeout
38
+)
39
+
40
+func newClient(jar http.CookieJar, roots *x509.CertPool, cert *tls.Certificate, timeout TimeoutType) *http.Client {
41
+	tlsConfig := tls.Config{RootCAs: roots}
42
+
43
+	if cert != nil {
44
+		tlsConfig.Certificates = append(tlsConfig.Certificates, *cert)
45
+	}
46
+
47
+	httpTransport := &http.Transport{
48
+		DisableKeepAlives: true,
49
+		Proxy:             http.ProxyFromEnvironment,
50
+		TLSClientConfig:   &tlsConfig,
51
+	}
52
+
53
+	switch timeout {
54
+	case ConnectTimeout:
55
+		httpTransport.Dial = func(proto string, addr string) (net.Conn, error) {
56
+			// Set the connect timeout to 5 seconds
57
+			conn, err := net.DialTimeout(proto, addr, 5*time.Second)
58
+			if err != nil {
59
+				return nil, err
60
+			}
61
+			// Set the recv timeout to 10 seconds
62
+			conn.SetDeadline(time.Now().Add(10 * time.Second))
63
+			return conn, nil
64
+		}
65
+	case ReceiveTimeout:
66
+		httpTransport.Dial = func(proto string, addr string) (net.Conn, error) {
67
+			conn, err := net.Dial(proto, addr)
68
+			if err != nil {
69
+				return nil, err
70
+			}
71
+			conn = utils.NewTimeoutConn(conn, 1*time.Minute)
72
+			return conn, nil
73
+		}
74
+	}
75
+
76
+	return &http.Client{
77
+		Transport:     httpTransport,
78
+		CheckRedirect: AddRequiredHeadersToRedirectedRequests,
79
+		Jar:           jar,
80
+	}
81
+}
82
+
83
+func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType) (*http.Response, *http.Client, error) {
84
+	hasFile := func(files []os.FileInfo, name string) bool {
85
+		for _, f := range files {
86
+			if f.Name() == name {
87
+				return true
88
+			}
89
+		}
90
+		return false
91
+	}
92
+
93
+	hostDir := path.Join("/etc/docker/certs.d", req.URL.Host)
94
+	fs, err := ioutil.ReadDir(hostDir)
95
+	if err != nil && !os.IsNotExist(err) {
96
+		return nil, nil, err
97
+	}
98
+
99
+	var (
100
+		pool  *x509.CertPool
101
+		certs []*tls.Certificate
102
+	)
103
+
104
+	for _, f := range fs {
105
+		if strings.HasSuffix(f.Name(), ".crt") {
106
+			if pool == nil {
107
+				pool = x509.NewCertPool()
108
+			}
109
+			data, err := ioutil.ReadFile(path.Join(hostDir, f.Name()))
110
+			if err != nil {
111
+				return nil, nil, err
112
+			} else {
113
+				pool.AppendCertsFromPEM(data)
114
+			}
115
+		}
116
+		if strings.HasSuffix(f.Name(), ".cert") {
117
+			certName := f.Name()
118
+			keyName := certName[:len(certName)-5] + ".key"
119
+			if !hasFile(fs, keyName) {
120
+				return nil, nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
121
+			} else {
122
+				cert, err := tls.LoadX509KeyPair(path.Join(hostDir, certName), path.Join(hostDir, keyName))
123
+				if err != nil {
124
+					return nil, nil, err
125
+				}
126
+				certs = append(certs, &cert)
127
+			}
128
+		}
129
+		if strings.HasSuffix(f.Name(), ".key") {
130
+			keyName := f.Name()
131
+			certName := keyName[:len(keyName)-4] + ".cert"
132
+			if !hasFile(fs, certName) {
133
+				return nil, nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
134
+			}
135
+		}
136
+	}
137
+
138
+	if len(certs) == 0 {
139
+		client := newClient(jar, pool, nil, timeout)
140
+		res, err := client.Do(req)
141
+		if err != nil {
142
+			return nil, nil, err
143
+		}
144
+		return res, client, nil
145
+	} else {
146
+		for i, cert := range certs {
147
+			client := newClient(jar, pool, cert, timeout)
148
+			res, err := client.Do(req)
149
+			if i == len(certs)-1 {
150
+				// If this is the last cert, always return the result
151
+				return res, client, err
152
+			} else {
153
+				// Otherwise, continue to next cert if 403 or 5xx
154
+				if err == nil && res.StatusCode != 403 && !(res.StatusCode >= 500 && res.StatusCode < 600) {
155
+					return res, client, err
156
+				}
157
+			}
158
+		}
159
+	}
160
+
161
+	return nil, nil, nil
162
+}
163
+
32 164
 func pingRegistryEndpoint(endpoint string) (RegistryInfo, error) {
33 165
 	if endpoint == IndexServerAddress() {
34 166
 		// Skip the check, we now this one is valid
35 167
 		// (and we never want to fallback to http in case of error)
36 168
 		return RegistryInfo{Standalone: false}, nil
37 169
 	}
38
-	httpDial := func(proto string, addr string) (net.Conn, error) {
39
-		// Set the connect timeout to 5 seconds
40
-		conn, err := net.DialTimeout(proto, addr, 5*time.Second)
41
-		if err != nil {
42
-			return nil, err
43
-		}
44
-		// Set the recv timeout to 10 seconds
45
-		conn.SetDeadline(time.Now().Add(10 * time.Second))
46
-		return conn, nil
47
-	}
48
-	httpTransport := &http.Transport{
49
-		Dial:  httpDial,
50
-		Proxy: http.ProxyFromEnvironment,
170
+
171
+	req, err := http.NewRequest("GET", endpoint+"_ping", nil)
172
+	if err != nil {
173
+		return RegistryInfo{Standalone: false}, err
51 174
 	}
52
-	client := &http.Client{Transport: httpTransport}
53
-	resp, err := client.Get(endpoint + "_ping")
175
+
176
+	resp, _, err := doRequest(req, nil, ConnectTimeout)
54 177
 	if err != nil {
55 178
 		return RegistryInfo{Standalone: false}, err
56 179
 	}
180
+
57 181
 	defer resp.Body.Close()
58 182
 
59 183
 	jsonString, err := ioutil.ReadAll(resp.Body)
... ...
@@ -171,6 +299,10 @@ func setTokenAuth(req *http.Request, token []string) {
171 171
 	}
172 172
 }
173 173
 
174
+func (r *Registry) doRequest(req *http.Request) (*http.Response, *http.Client, error) {
175
+	return doRequest(req, r.jar, r.timeout)
176
+}
177
+
174 178
 // Retrieve the history of a given image from the Registry.
175 179
 // Return a list of the parent's json (requested image included)
176 180
 func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]string, error) {
... ...
@@ -179,7 +311,7 @@ func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]s
179 179
 		return nil, err
180 180
 	}
181 181
 	setTokenAuth(req, token)
182
-	res, err := r.client.Do(req)
182
+	res, _, err := r.doRequest(req)
183 183
 	if err != nil {
184 184
 		return nil, err
185 185
 	}
... ...
@@ -214,7 +346,7 @@ func (r *Registry) LookupRemoteImage(imgID, registry string, token []string) boo
214 214
 		return false
215 215
 	}
216 216
 	setTokenAuth(req, token)
217
-	res, err := r.client.Do(req)
217
+	res, _, err := r.doRequest(req)
218 218
 	if err != nil {
219 219
 		utils.Errorf("Error in LookupRemoteImage %s", err)
220 220
 		return false
... ...
@@ -231,7 +363,7 @@ func (r *Registry) GetRemoteImageJSON(imgID, registry string, token []string) ([
231 231
 		return nil, -1, fmt.Errorf("Failed to download json: %s", err)
232 232
 	}
233 233
 	setTokenAuth(req, token)
234
-	res, err := r.client.Do(req)
234
+	res, _, err := r.doRequest(req)
235 235
 	if err != nil {
236 236
 		return nil, -1, fmt.Errorf("Failed to download json: %s", err)
237 237
 	}
... ...
@@ -260,6 +392,7 @@ func (r *Registry) GetRemoteImageLayer(imgID, registry string, token []string, i
260 260
 	var (
261 261
 		retries   = 5
262 262
 		headRes   *http.Response
263
+		client    *http.Client
263 264
 		hasResume bool = false
264 265
 		imageURL       = fmt.Sprintf("%simages/%s/layer", registry, imgID)
265 266
 	)
... ...
@@ -267,9 +400,10 @@ func (r *Registry) GetRemoteImageLayer(imgID, registry string, token []string, i
267 267
 	if err != nil {
268 268
 		return nil, fmt.Errorf("Error while getting from the server: %s\n", err)
269 269
 	}
270
+
270 271
 	setTokenAuth(headReq, token)
271 272
 	for i := 1; i <= retries; i++ {
272
-		headRes, err = r.client.Do(headReq)
273
+		headRes, client, err = r.doRequest(headReq)
273 274
 		if err != nil && i == retries {
274 275
 			return nil, fmt.Errorf("Eror while making head request: %s\n", err)
275 276
 		} else if err != nil {
... ...
@@ -290,10 +424,10 @@ func (r *Registry) GetRemoteImageLayer(imgID, registry string, token []string, i
290 290
 	setTokenAuth(req, token)
291 291
 	if hasResume {
292 292
 		utils.Debugf("server supports resume")
293
-		return utils.ResumableRequestReader(r.client, req, 5, imgSize), nil
293
+		return utils.ResumableRequestReader(client, req, 5, imgSize), nil
294 294
 	}
295 295
 	utils.Debugf("server doesn't support resume")
296
-	res, err := r.client.Do(req)
296
+	res, _, err := r.doRequest(req)
297 297
 	if err != nil {
298 298
 		return nil, err
299 299
 	}
... ...
@@ -319,7 +453,7 @@ func (r *Registry) GetRemoteTags(registries []string, repository string, token [
319 319
 			return nil, err
320 320
 		}
321 321
 		setTokenAuth(req, token)
322
-		res, err := r.client.Do(req)
322
+		res, _, err := r.doRequest(req)
323 323
 		if err != nil {
324 324
 			return nil, err
325 325
 		}
... ...
@@ -380,7 +514,7 @@ func (r *Registry) GetRepositoryData(remote string) (*RepositoryData, error) {
380 380
 	}
381 381
 	req.Header.Set("X-Docker-Token", "true")
382 382
 
383
-	res, err := r.client.Do(req)
383
+	res, _, err := r.doRequest(req)
384 384
 	if err != nil {
385 385
 		return nil, err
386 386
 	}
... ...
@@ -448,13 +582,13 @@ func (r *Registry) PushImageChecksumRegistry(imgData *ImgData, registry string,
448 448
 	req.Header.Set("X-Docker-Checksum", imgData.Checksum)
449 449
 	req.Header.Set("X-Docker-Checksum-Payload", imgData.ChecksumPayload)
450 450
 
451
-	res, err := r.client.Do(req)
451
+	res, _, err := r.doRequest(req)
452 452
 	if err != nil {
453 453
 		return fmt.Errorf("Failed to upload metadata: %s", err)
454 454
 	}
455 455
 	defer res.Body.Close()
456 456
 	if len(res.Cookies()) > 0 {
457
-		r.client.Jar.SetCookies(req.URL, res.Cookies())
457
+		r.jar.SetCookies(req.URL, res.Cookies())
458 458
 	}
459 459
 	if res.StatusCode != 200 {
460 460
 		errBody, err := ioutil.ReadAll(res.Body)
... ...
@@ -484,7 +618,7 @@ func (r *Registry) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, regis
484 484
 	req.Header.Add("Content-type", "application/json")
485 485
 	setTokenAuth(req, token)
486 486
 
487
-	res, err := r.client.Do(req)
487
+	res, _, err := r.doRequest(req)
488 488
 	if err != nil {
489 489
 		return fmt.Errorf("Failed to upload metadata: %s", err)
490 490
 	}
... ...
@@ -525,7 +659,7 @@ func (r *Registry) PushImageLayerRegistry(imgID string, layer io.Reader, registr
525 525
 	req.ContentLength = -1
526 526
 	req.TransferEncoding = []string{"chunked"}
527 527
 	setTokenAuth(req, token)
528
-	res, err := r.client.Do(req)
528
+	res, _, err := r.doRequest(req)
529 529
 	if err != nil {
530 530
 		return "", "", fmt.Errorf("Failed to upload layer: %s", err)
531 531
 	}
... ...
@@ -562,7 +696,7 @@ func (r *Registry) PushRegistryTag(remote, revision, tag, registry string, token
562 562
 	req.Header.Add("Content-type", "application/json")
563 563
 	setTokenAuth(req, token)
564 564
 	req.ContentLength = int64(len(revision))
565
-	res, err := r.client.Do(req)
565
+	res, _, err := r.doRequest(req)
566 566
 	if err != nil {
567 567
 		return err
568 568
 	}
... ...
@@ -610,7 +744,7 @@ func (r *Registry) PushImageJSONIndex(remote string, imgList []*ImgData, validat
610 610
 		req.Header["X-Docker-Endpoints"] = regs
611 611
 	}
612 612
 
613
-	res, err := r.client.Do(req)
613
+	res, _, err := r.doRequest(req)
614 614
 	if err != nil {
615 615
 		return nil, err
616 616
 	}
... ...
@@ -629,7 +763,7 @@ func (r *Registry) PushImageJSONIndex(remote string, imgList []*ImgData, validat
629 629
 		if validate {
630 630
 			req.Header["X-Docker-Endpoints"] = regs
631 631
 		}
632
-		res, err = r.client.Do(req)
632
+		res, _, err := r.doRequest(req)
633 633
 		if err != nil {
634 634
 			return nil, err
635 635
 		}
... ...
@@ -688,7 +822,7 @@ func (r *Registry) SearchRepositories(term string) (*SearchResults, error) {
688 688
 		req.SetBasicAuth(r.authConfig.Username, r.authConfig.Password)
689 689
 	}
690 690
 	req.Header.Set("X-Docker-Token", "true")
691
-	res, err := r.client.Do(req)
691
+	res, _, err := r.doRequest(req)
692 692
 	if err != nil {
693 693
 		return nil, err
694 694
 	}
... ...
@@ -750,10 +884,11 @@ type RegistryInfo struct {
750 750
 }
751 751
 
752 752
 type Registry struct {
753
-	client        *http.Client
754 753
 	authConfig    *AuthConfig
755 754
 	reqFactory    *utils.HTTPRequestFactory
756 755
 	indexEndpoint string
756
+	jar           *cookiejar.Jar
757
+	timeout       TimeoutType
757 758
 }
758 759
 
759 760
 func trustedLocation(req *http.Request) bool {
... ...
@@ -791,30 +926,16 @@ func AddRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque
791 791
 }
792 792
 
793 793
 func NewRegistry(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, indexEndpoint string, timeout bool) (r *Registry, err error) {
794
-	httpTransport := &http.Transport{
795
-		DisableKeepAlives: true,
796
-		Proxy:             http.ProxyFromEnvironment,
797
-	}
798
-	if timeout {
799
-		httpTransport.Dial = func(proto string, addr string) (net.Conn, error) {
800
-			conn, err := net.Dial(proto, addr)
801
-			if err != nil {
802
-				return nil, err
803
-			}
804
-			conn = utils.NewTimeoutConn(conn, 1*time.Minute)
805
-			return conn, nil
806
-		}
807
-	}
808 794
 	r = &Registry{
809
-		authConfig: authConfig,
810
-		client: &http.Client{
811
-			Transport:     httpTransport,
812
-			CheckRedirect: AddRequiredHeadersToRedirectedRequests,
813
-		},
795
+		authConfig:    authConfig,
814 796
 		indexEndpoint: indexEndpoint,
815 797
 	}
816 798
 
817
-	r.client.Jar, err = cookiejar.New(nil)
799
+	if timeout {
800
+		r.timeout = ReceiveTimeout
801
+	}
802
+
803
+	r.jar, err = cookiejar.New(nil)
818 804
 	if err != nil {
819 805
 		return nil, err
820 806
 	}