浏览代码

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

Add the possibility of specifying a subnet for --insecure-registry
Tibor Vass 10 年之前
父节点
当前提交
36503981f0
共有 5 个文件被更改,包括 130 次插入37 次删除
  1. 9 1
      daemon/config.go
  2. 33 13
      docs/sources/reference/commandline/cli.md
  3. 49 17
      registry/endpoint.go
  4. 26 0
      registry/registry_mock_test.go
  5. 13 6
      registry/registry_test.go

+ 9 - 1
daemon/config.go

@@ -56,7 +56,7 @@ func (config *Config) InstallFlags() {
 	flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b")
 	flag.StringVar(&config.BridgeIP, []string{"#bip", "-bip"}, "", "Use this CIDR notation address for the network bridge's IP, not compatible with -b")
 	flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking")
 	flag.StringVar(&config.BridgeIface, []string{"b", "-bridge"}, "", "Attach containers to a pre-existing network bridge\nuse 'none' to disable container networking")
 	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)")
 	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)")
-	opts.ListVar(&config.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback)")
+	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)")
 	flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Enable inter-container communication")
 	flag.BoolVar(&config.InterContainerCommunication, []string{"#icc", "-icc"}, true, "Enable inter-container communication")
 	flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver")
 	flag.StringVar(&config.GraphDriver, []string{"s", "-storage-driver"}, "", "Force the Docker runtime to use a specific storage driver")
 	flag.StringVar(&config.ExecDriver, []string{"e", "-exec-driver"}, "native", "Force the Docker runtime to use a specific exec driver")
 	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() {
 	opts.IPListVar(&config.Dns, []string{"#dns", "-dns"}, "Force Docker to use specific DNS servers")
 	opts.IPListVar(&config.Dns, []string{"#dns", "-dns"}, "Force Docker to use specific DNS servers")
 	opts.DnsSearchListVar(&config.DnsSearch, []string{"-dns-search"}, "Force Docker to use specific DNS search domains")
 	opts.DnsSearchListVar(&config.DnsSearch, []string{"-dns-search"}, "Force Docker to use specific DNS search domains")
 	opts.MirrorListVar(&config.Mirrors, []string{"-registry-mirror"}, "Specify a preferred Docker registry mirror")
 	opts.MirrorListVar(&config.Mirrors, []string{"-registry-mirror"}, "Specify a preferred Docker registry mirror")
+
+	// Localhost is by default considered as an insecure registry
+	// This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker).
+	//
+	// TODO: should we deprecate this once it is easier for people to set up a TLS registry or change
+	// daemon flags on boot2docker?
+	// If so, do not forget to check the TODO in TestIsSecure
+	config.InsecureRegistries = append(config.InsecureRegistries, "127.0.0.0/8")
 }
 }
 
 
 func getDefaultNetworkMtu() int {
 func getDefaultNetworkMtu() int {

+ 33 - 13
docs/sources/reference/commandline/cli.md

@@ -70,7 +70,7 @@ expect an integer, and they can only be specified once.
       -g, --graph="/var/lib/docker"              Path to use as the root of the Docker runtime
       -g, --graph="/var/lib/docker"              Path to use as the root of the Docker runtime
       -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.
       -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.
       --icc=true                                 Enable inter-container communication
       --icc=true                                 Enable inter-container communication
-      --insecure-registry=[]                     Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback)
+      --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)
       --ip=0.0.0.0                               Default IP address to use when binding container ports
       --ip=0.0.0.0                               Default IP address to use when binding container ports
       --ip-forward=true                          Enable net.ipv4.ip_forward
       --ip-forward=true                          Enable net.ipv4.ip_forward
       --ip-masq=true                             Enable IP masquerading for bridge's IP range
       --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
 To set the DNS search domain for all Docker containers, use
 To set the DNS search domain for all Docker containers, use
 `docker -d --dns-search example.com`.
 `docker -d --dns-search example.com`.
 
 
+### Insecure registries
+
+Docker considers a private registry either secure or insecure.
+In the rest of this section, *registry* is used for *private registry*, and `myregistry:5000`
+is a placeholder example for a private registry.
+
+A secure registry uses TLS and a copy of its CA certificate is placed on the Docker host at
+`/etc/docker/certs.d/myregistry:5000/ca.crt`.
+An insecure registry is either not using TLS (i.e., listening on plain text HTTP), or is using
+TLS with a CA certificate not known by the Docker daemon. The latter can happen when the
+certificate was not found under `/etc/docker/certs.d/myregistry:5000/`, or if the certificate
+verification failed (i.e., wrong CA).
+
+By default, Docker assumes all, but local (see local registries below), registries are secure.
+Communicating with an insecure registry is not possible if Docker assumes that registry is secure.
+In order to communicate with an insecure registry, the Docker daemon requires `--insecure-registry`
+in one of the following two forms: 
+
+* `--insecure-registry myregistry:5000` tells the Docker daemon that myregistry:5000 should be considered insecure.
+* `--insecure-registry 10.1.0.0/16` tells the Docker daemon that all registries whose domain resolve to an IP address is part
+of the subnet described by the CIDR syntax, should be considered insecure.
+
+The flag can be used multiple times to allow multiple registries to be marked as insecure.
+
+If an insecure registry is not marked as insecure, `docker pull`, `docker push`, and `docker search`
+will result in an error message prompting the user to either secure or pass the `--insecure-registry`
+flag to the Docker daemon as described above.
+
+Local registries, whose IP address falls in the 127.0.0.0/8 range, are automatically marked as insecure
+as of Docker 1.3.2. It is not recommended to rely on this, as it may change in the future.
+
+
 ### Miscellaneous options
 ### Miscellaneous options
 
 
 IP masquerading uses address translation to allow containers without a public IP to talk
 IP masquerading uses address translation to allow containers without a public IP to talk
 to other machines on the Internet. This may interfere with some network topologies and
 to other machines on the Internet. This may interfere with some network topologies and
 can be disabled with --ip-masq=false.
 can be disabled with --ip-masq=false.
 
 
-
-By default, Docker will assume all registries are secured via TLS with certificate verification
-enabled. Prior versions of Docker used an auto fallback if a registry did not support TLS
-(or if the TLS connection failed). This introduced the opportunity for Man In The Middle (MITM)
-attacks, so as of Docker 1.3.1, the user must now specify the `--insecure-registry` daemon flag
-for each insecure registry. An insecure registry is either not using TLS (i.e. plain text HTTP),
-or is using TLS with a CA certificate not known by the Docker daemon (i.e. certification
-verification disabled). For example, if there is a registry listening for HTTP at 127.0.0.1:5000,
-as of Docker 1.3.1 you are required to specify `--insecure-registry 127.0.0.1:5000` when starting
-the Docker daemon.
-
-
 Docker supports softlinks for the Docker data directory
 Docker supports softlinks for the Docker data directory
 (`/var/lib/docker`) and for `/var/lib/docker/tmp`. The `DOCKER_TMPDIR` and the data directory can be set like this:
 (`/var/lib/docker`) and for `/var/lib/docker/tmp`. The `DOCKER_TMPDIR` and the data directory can be set like this:
 
 

+ 49 - 17
registry/endpoint.go

@@ -12,6 +12,9 @@ import (
 	log "github.com/Sirupsen/logrus"
 	log "github.com/Sirupsen/logrus"
 )
 )
 
 
+// for mocking in unit tests
+var lookupIP = net.LookupIP
+
 // scans string for api version in the URL path. returns the trimmed hostname, if version found, string and API version.
 // scans string for api version in the URL path. returns the trimmed hostname, if version found, string and API version.
 func scanForAPIVersion(hostname string) (string, APIVersion) {
 func scanForAPIVersion(hostname string) (string, APIVersion) {
 	var (
 	var (
@@ -79,7 +82,10 @@ func newEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	endpoint.secure = isSecure(endpoint.URL.Host, insecureRegistries)
+	endpoint.secure, err = isSecure(endpoint.URL.Host, insecureRegistries)
+	if err != nil {
+		return nil, err
+	}
 	return &endpoint, nil
 	return &endpoint, nil
 }
 }
 
 
@@ -152,30 +158,56 @@ func (e Endpoint) Ping() (RegistryInfo, error) {
 
 
 // isSecure returns false if the provided hostname is part of the list of insecure registries.
 // isSecure returns false if the provided hostname is part of the list of insecure registries.
 // Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
 // Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
-func isSecure(hostname string, insecureRegistries []string) bool {
+//
+// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet.
+// If the subnet contains one of the IPs of the registry specified by hostname, the latter is considered
+// insecure.
+//
+// hostname should be a URL.Host (`host:port` or `host`)
+func isSecure(hostname string, insecureRegistries []string) (bool, error) {
 	if hostname == IndexServerURL.Host {
 	if hostname == IndexServerURL.Host {
-		return true
+		return true, nil
 	}
 	}
 
 
 	host, _, err := net.SplitHostPort(hostname)
 	host, _, err := net.SplitHostPort(hostname)
-
 	if err != nil {
 	if err != nil {
+		// assume hostname is of the form `host` without the port and go on.
 		host = hostname
 		host = hostname
 	}
 	}
-
-	if host == "127.0.0.1" || host == "localhost" {
-		return false
-	}
-
-	if len(insecureRegistries) == 0 {
-		return true
-	}
-
-	for _, h := range insecureRegistries {
-		if hostname == h {
-			return false
+	addrs, err := lookupIP(host)
+	if err != nil {
+		ip := net.ParseIP(host)
+		if ip == nil {
+			// if resolving `host` fails, error out, since host is to be net.Dial-ed anyway
+			return true, fmt.Errorf("issecure: could not resolve %q: %v", host, err)
+		}
+		addrs = []net.IP{ip}
+	}
+	if len(addrs) == 0 {
+		return true, fmt.Errorf("issecure: could not resolve %q", host)
+	}
+
+	for _, addr := range addrs {
+		for _, r := range insecureRegistries {
+			// hostname matches insecure registry
+			if hostname == r {
+				return false, nil
+			}
+
+			// now assume a CIDR was passed to --insecure-registry
+			_, ipnet, err := net.ParseCIDR(r)
+			if err != nil {
+				// if could not parse it as a CIDR, even after removing
+				// assume it's not a CIDR and go on with the next candidate
+				continue
+			}
+
+			// check if the addr falls in the subnet
+			if ipnet.Contains(addr) {
+				return false, nil
+			}
 		}
 		}
 	}
 	}
 
 
-	return true
+	return true, nil
 }
 }

+ 26 - 0
registry/registry_mock_test.go

@@ -2,9 +2,11 @@ package registry
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"io/ioutil"
 	"io/ioutil"
+	"net"
 	"net/http"
 	"net/http"
 	"net/http/httptest"
 	"net/http/httptest"
 	"net/url"
 	"net/url"
@@ -80,6 +82,11 @@ var (
 			"latest": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
 			"latest": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
 		},
 		},
 	}
 	}
+	mockHosts = map[string][]net.IP{
+		"":            {net.ParseIP("0.0.0.0")},
+		"localhost":   {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
+		"example.com": {net.ParseIP("42.42.42.42")},
+	}
 )
 )
 
 
 func init() {
 func init() {
@@ -106,6 +113,25 @@ func init() {
 		panic(err)
 		panic(err)
 	}
 	}
 	insecureRegistries = []string{URL.Host}
 	insecureRegistries = []string{URL.Host}
+
+	// override net.LookupIP
+	lookupIP = func(host string) ([]net.IP, error) {
+		if host == "127.0.0.1" {
+			// I believe in future Go versions this will fail, so let's fix it later
+			return net.LookupIP(host)
+		}
+		for h, addrs := range mockHosts {
+			if host == h {
+				return addrs, nil
+			}
+			for _, addr := range addrs {
+				if addr.String() == host {
+					return []net.IP{addr}, nil
+				}
+			}
+		}
+		return nil, errors.New("lookup: no such host")
+	}
 }
 }
 
 
 func handlerAccessLog(handler http.Handler) http.Handler {
 func handlerAccessLog(handler http.Handler) http.Handler {

+ 13 - 6
registry/registry_test.go

@@ -333,19 +333,26 @@ func TestIsSecure(t *testing.T) {
 		{"localhost:5000", []string{"localhost:5000"}, false},
 		{"localhost:5000", []string{"localhost:5000"}, false},
 		{"localhost", []string{"example.com"}, false},
 		{"localhost", []string{"example.com"}, false},
 		{"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false},
 		{"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false},
-		{"localhost", []string{}, false},
-		{"localhost:5000", []string{}, false},
-		{"127.0.0.1", []string{}, false},
+		{"localhost", nil, false},
+		{"localhost:5000", nil, false},
+		{"127.0.0.1", nil, false},
 		{"localhost", []string{"example.com"}, false},
 		{"localhost", []string{"example.com"}, false},
 		{"127.0.0.1", []string{"example.com"}, false},
 		{"127.0.0.1", []string{"example.com"}, false},
-		{"example.com", []string{}, true},
+		{"example.com", nil, true},
 		{"example.com", []string{"example.com"}, false},
 		{"example.com", []string{"example.com"}, false},
 		{"127.0.0.1", []string{"example.com"}, false},
 		{"127.0.0.1", []string{"example.com"}, false},
 		{"127.0.0.1:5000", []string{"example.com"}, false},
 		{"127.0.0.1:5000", []string{"example.com"}, false},
+		{"example.com:5000", []string{"42.42.0.0/16"}, false},
+		{"example.com", []string{"42.42.0.0/16"}, false},
+		{"example.com:5000", []string{"42.42.42.42/8"}, false},
+		{"127.0.0.1:5000", []string{"127.0.0.0/8"}, false},
+		{"42.42.42.42:5000", []string{"42.1.1.1/8"}, false},
 	}
 	}
 	for _, tt := range tests {
 	for _, tt := range tests {
-		if sec := isSecure(tt.addr, tt.insecureRegistries); sec != tt.expected {
-			t.Errorf("isSecure failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec)
+		// TODO: remove this once we remove localhost insecure by default
+		insecureRegistries := append(tt.insecureRegistries, "127.0.0.0/8")
+		if sec, err := isSecure(tt.addr, insecureRegistries); err != nil || sec != tt.expected {
+			t.Fatalf("isSecure failed for %q %v, expected %v got %v. Error: %v", tt.addr, insecureRegistries, tt.expected, sec, err)
 		}
 		}
 	}
 	}
 }
 }