Browse code

Merge pull request #9100 from tiborvass/insecure-registry-cidr

Add the possibility of specifying a subnet for --insecure-registry

Tibor Vass authored on 2014/11/15 06:45:48
Showing 5 changed files
... ...
@@ -56,7 +56,7 @@ func (config *Config) InstallFlags() {
56 56
 	flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b")
57 57
 	flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking")
58 58
 	flag.StringVar(&config.FixedCIDR, []string{"-fixed-cidr"}, "", "IPv4 subnet for fixed IPs (ex: 10.20.0.0/16)\nthis subnet must be nested in the bridge subnet (which is defined by -b or --bip)")
59
-	opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback)")
59
+	opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)")
60 60
 	flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Enable inter-container communication")
61 61
 	flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver")
62 62
 	flag.StringVar(&config.ExecDriver, []string{"e", "-exec-driver"}, "native", "Force the Docker runtime to use a specific exec driver")
... ...
@@ -68,6 +68,14 @@ func (config *Config) InstallFlags() {
68 68
 	opts.IPListVar(&config.Dns, []string{"#dns", "-dns"}, "Force Docker to use specific DNS servers")
69 69
 	opts.DnsSearchListVar(&config.DnsSearch, []string{"-dns-search"}, "Force Docker to use specific DNS search domains")
70 70
 	opts.MirrorListVar(&config.Mirrors, []string{"-registry-mirror"}, "Specify a preferred Docker registry mirror")
71
+
72
+	// Localhost is by default considered as an insecure registry
73
+	// This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker).
74
+	//
75
+	// TODO: should we deprecate this once it is easier for people to set up a TLS registry or change
76
+	// daemon flags on boot2docker?
77
+	// If so, do not forget to check the TODO in TestIsSecure
78
+	config.InsecureRegistries = append(config.InsecureRegistries, "127.0.0.0/8")
71 79
 }
72 80
 
73 81
 func getDefaultNetworkMtu() int {
... ...
@@ -70,7 +70,7 @@ expect an integer, and they can only be specified once.
70 70
       -g, --graph="/var/lib/docker"              Path to use as the root of the Docker runtime
71 71
       -H, --host=[]                              The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.
72 72
       --icc=true                                 Enable inter-container communication
73
-      --insecure-registry=[]                     Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback)
73
+      --insecure-registry=[]                     Enable insecure communication with specified registries (disables certificate verification for HTTPS and enables HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)
74 74
       --ip=0.0.0.0                               Default IP address to use when binding container ports
75 75
       --ip-forward=true                          Enable net.ipv4.ip_forward
76 76
       --ip-masq=true                             Enable IP masquerading for bridge's IP range
... ...
@@ -193,24 +193,44 @@ To set the DNS server for all Docker containers, use
193 193
 To set the DNS search domain for all Docker containers, use
194 194
 `docker -d --dns-search example.com`.
195 195
 
196
+### Insecure registries
197
+
198
+Docker considers a private registry either secure or insecure.
199
+In the rest of this section, *registry* is used for *private registry*, and `myregistry:5000`
200
+is a placeholder example for a private registry.
201
+
202
+A secure registry uses TLS and a copy of its CA certificate is placed on the Docker host at
203
+`/etc/docker/certs.d/myregistry:5000/ca.crt`.
204
+An insecure registry is either not using TLS (i.e., listening on plain text HTTP), or is using
205
+TLS with a CA certificate not known by the Docker daemon. The latter can happen when the
206
+certificate was not found under `/etc/docker/certs.d/myregistry:5000/`, or if the certificate
207
+verification failed (i.e., wrong CA).
208
+
209
+By default, Docker assumes all, but local (see local registries below), registries are secure.
210
+Communicating with an insecure registry is not possible if Docker assumes that registry is secure.
211
+In order to communicate with an insecure registry, the Docker daemon requires `--insecure-registry`
212
+in one of the following two forms: 
213
+
214
+* `--insecure-registry myregistry:5000` tells the Docker daemon that myregistry:5000 should be considered insecure.
215
+* `--insecure-registry 10.1.0.0/16` tells the Docker daemon that all registries whose domain resolve to an IP address is part
216
+of the subnet described by the CIDR syntax, should be considered insecure.
217
+
218
+The flag can be used multiple times to allow multiple registries to be marked as insecure.
219
+
220
+If an insecure registry is not marked as insecure, `docker pull`, `docker push`, and `docker search`
221
+will result in an error message prompting the user to either secure or pass the `--insecure-registry`
222
+flag to the Docker daemon as described above.
223
+
224
+Local registries, whose IP address falls in the 127.0.0.0/8 range, are automatically marked as insecure
225
+as of Docker 1.3.2. It is not recommended to rely on this, as it may change in the future.
226
+
227
+
196 228
 ### Miscellaneous options
197 229
 
198 230
 IP masquerading uses address translation to allow containers without a public IP to talk
199 231
 to other machines on the Internet. This may interfere with some network topologies and
200 232
 can be disabled with --ip-masq=false.
201 233
 
202
-
203
-By default, Docker will assume all registries are secured via TLS with certificate verification
204
-enabled. Prior versions of Docker used an auto fallback if a registry did not support TLS
205
-(or if the TLS connection failed). This introduced the opportunity for Man In The Middle (MITM)
206
-attacks, so as of Docker 1.3.1, the user must now specify the `--insecure-registry` daemon flag
207
-for each insecure registry. An insecure registry is either not using TLS (i.e. plain text HTTP),
208
-or is using TLS with a CA certificate not known by the Docker daemon (i.e. certification
209
-verification disabled). For example, if there is a registry listening for HTTP at 127.0.0.1:5000,
210
-as of Docker 1.3.1 you are required to specify `--insecure-registry 127.0.0.1:5000` when starting
211
-the Docker daemon.
212
-
213
-
214 234
 Docker supports softlinks for the Docker data directory
215 235
 (`/var/lib/docker`) and for `/var/lib/docker/tmp`. The `DOCKER_TMPDIR` and the data directory can be set like this:
216 236
 
... ...
@@ -12,6 +12,9 @@ import (
12 12
 	log "github.com/Sirupsen/logrus"
13 13
 )
14 14
 
15
+// for mocking in unit tests
16
+var lookupIP = net.LookupIP
17
+
15 18
 // scans string for api version in the URL path. returns the trimmed hostname, if version found, string and API version.
16 19
 func scanForAPIVersion(hostname string) (string, APIVersion) {
17 20
 	var (
... ...
@@ -79,7 +82,10 @@ func newEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error
79 79
 	if err != nil {
80 80
 		return nil, err
81 81
 	}
82
-	endpoint.secure = isSecure(endpoint.URL.Host, insecureRegistries)
82
+	endpoint.secure, err = isSecure(endpoint.URL.Host, insecureRegistries)
83
+	if err != nil {
84
+		return nil, err
85
+	}
83 86
 	return &endpoint, nil
84 87
 }
85 88
 
... ...
@@ -152,30 +158,56 @@ func (e Endpoint) Ping() (RegistryInfo, error) {
152 152
 
153 153
 // isSecure returns false if the provided hostname is part of the list of insecure registries.
154 154
 // Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
155
-func isSecure(hostname string, insecureRegistries []string) bool {
155
+//
156
+// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet.
157
+// If the subnet contains one of the IPs of the registry specified by hostname, the latter is considered
158
+// insecure.
159
+//
160
+// hostname should be a URL.Host (`host:port` or `host`)
161
+func isSecure(hostname string, insecureRegistries []string) (bool, error) {
156 162
 	if hostname == IndexServerURL.Host {
157
-		return true
163
+		return true, nil
158 164
 	}
159 165
 
160 166
 	host, _, err := net.SplitHostPort(hostname)
161
-
162 167
 	if err != nil {
168
+		// assume hostname is of the form `host` without the port and go on.
163 169
 		host = hostname
164 170
 	}
165
-
166
-	if host == "127.0.0.1" || host == "localhost" {
167
-		return false
168
-	}
169
-
170
-	if len(insecureRegistries) == 0 {
171
-		return true
172
-	}
173
-
174
-	for _, h := range insecureRegistries {
175
-		if hostname == h {
176
-			return false
171
+	addrs, err := lookupIP(host)
172
+	if err != nil {
173
+		ip := net.ParseIP(host)
174
+		if ip == nil {
175
+			// if resolving `host` fails, error out, since host is to be net.Dial-ed anyway
176
+			return true, fmt.Errorf("issecure: could not resolve %q: %v", host, err)
177
+		}
178
+		addrs = []net.IP{ip}
179
+	}
180
+	if len(addrs) == 0 {
181
+		return true, fmt.Errorf("issecure: could not resolve %q", host)
182
+	}
183
+
184
+	for _, addr := range addrs {
185
+		for _, r := range insecureRegistries {
186
+			// hostname matches insecure registry
187
+			if hostname == r {
188
+				return false, nil
189
+			}
190
+
191
+			// now assume a CIDR was passed to --insecure-registry
192
+			_, ipnet, err := net.ParseCIDR(r)
193
+			if err != nil {
194
+				// if could not parse it as a CIDR, even after removing
195
+				// assume it's not a CIDR and go on with the next candidate
196
+				continue
197
+			}
198
+
199
+			// check if the addr falls in the subnet
200
+			if ipnet.Contains(addr) {
201
+				return false, nil
202
+			}
177 203
 		}
178 204
 	}
179 205
 
180
-	return true
206
+	return true, nil
181 207
 }
... ...
@@ -2,9 +2,11 @@ package registry
2 2
 
3 3
 import (
4 4
 	"encoding/json"
5
+	"errors"
5 6
 	"fmt"
6 7
 	"io"
7 8
 	"io/ioutil"
9
+	"net"
8 10
 	"net/http"
9 11
 	"net/http/httptest"
10 12
 	"net/url"
... ...
@@ -80,6 +82,11 @@ var (
80 80
 			"latest": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
81 81
 		},
82 82
 	}
83
+	mockHosts = map[string][]net.IP{
84
+		"":            {net.ParseIP("0.0.0.0")},
85
+		"localhost":   {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
86
+		"example.com": {net.ParseIP("42.42.42.42")},
87
+	}
83 88
 )
84 89
 
85 90
 func init() {
... ...
@@ -106,6 +113,25 @@ func init() {
106 106
 		panic(err)
107 107
 	}
108 108
 	insecureRegistries = []string{URL.Host}
109
+
110
+	// override net.LookupIP
111
+	lookupIP = func(host string) ([]net.IP, error) {
112
+		if host == "127.0.0.1" {
113
+			// I believe in future Go versions this will fail, so let's fix it later
114
+			return net.LookupIP(host)
115
+		}
116
+		for h, addrs := range mockHosts {
117
+			if host == h {
118
+				return addrs, nil
119
+			}
120
+			for _, addr := range addrs {
121
+				if addr.String() == host {
122
+					return []net.IP{addr}, nil
123
+				}
124
+			}
125
+		}
126
+		return nil, errors.New("lookup: no such host")
127
+	}
109 128
 }
110 129
 
111 130
 func handlerAccessLog(handler http.Handler) http.Handler {
... ...
@@ -333,19 +333,26 @@ func TestIsSecure(t *testing.T) {
333 333
 		{"localhost:5000", []string{"localhost:5000"}, false},
334 334
 		{"localhost", []string{"example.com"}, false},
335 335
 		{"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false},
336
-		{"localhost", []string{}, false},
337
-		{"localhost:5000", []string{}, false},
338
-		{"127.0.0.1", []string{}, false},
336
+		{"localhost", nil, false},
337
+		{"localhost:5000", nil, false},
338
+		{"127.0.0.1", nil, false},
339 339
 		{"localhost", []string{"example.com"}, false},
340 340
 		{"127.0.0.1", []string{"example.com"}, false},
341
-		{"example.com", []string{}, true},
341
+		{"example.com", nil, true},
342 342
 		{"example.com", []string{"example.com"}, false},
343 343
 		{"127.0.0.1", []string{"example.com"}, false},
344 344
 		{"127.0.0.1:5000", []string{"example.com"}, false},
345
+		{"example.com:5000", []string{"42.42.0.0/16"}, false},
346
+		{"example.com", []string{"42.42.0.0/16"}, false},
347
+		{"example.com:5000", []string{"42.42.42.42/8"}, false},
348
+		{"127.0.0.1:5000", []string{"127.0.0.0/8"}, false},
349
+		{"42.42.42.42:5000", []string{"42.1.1.1/8"}, false},
345 350
 	}
346 351
 	for _, tt := range tests {
347
-		if sec := isSecure(tt.addr, tt.insecureRegistries); sec != tt.expected {
348
-			t.Errorf("isSecure failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec)
352
+		// TODO: remove this once we remove localhost insecure by default
353
+		insecureRegistries := append(tt.insecureRegistries, "127.0.0.0/8")
354
+		if sec, err := isSecure(tt.addr, insecureRegistries); err != nil || sec != tt.expected {
355
+			t.Fatalf("isSecure failed for %q %v, expected %v got %v. Error: %v", tt.addr, insecureRegistries, tt.expected, sec, err)
349 356
 		}
350 357
 	}
351 358
 }