diff --git a/.gitignore b/.gitignore index 40c314a9..6cdb2e7b 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ prog/weaveexec/weavewait prog/weaveexec/sigproxy prog/weaveexec/docker*.tgz Vagrantfile.local +test/tls/*.pem diff --git a/Makefile b/Makefile index c9c6579c..07b84f91 100644 --- a/Makefile +++ b/Makefile @@ -111,6 +111,7 @@ publish: $(PUBLISH) clean: -$(SUDO) docker rmi $(IMAGES) rm -f $(EXES) $(EXPORTS) + rm -f test/tls/*.pem build: $(SUDO) go clean -i net diff --git a/prog/weaveproxy/main.go b/prog/weaveproxy/main.go index 8b43ee6f..bc19d365 100644 --- a/prog/weaveproxy/main.go +++ b/prog/weaveproxy/main.go @@ -10,21 +10,33 @@ import ( ) var ( - version = "(unreleased version)" - defaultTarget = "unix:///var/run/docker.sock" - defaultListen = ":12375" + version = "(unreleased version)" + defaultDockerAddr = "unix:///var/run/docker.sock" + defaultListenAddr = ":12375" ) func main() { - var target, listen string - var withDNS, withIPAM, debug, justVersion bool + var ( + debug bool + justVersion bool + c = proxy.Config{ + DockerAddr: defaultDockerAddr, + ListenAddr: defaultListenAddr, + } + ) + c.Version = version getopt.BoolVarLong(&debug, "debug", 'd', "log debugging information") getopt.BoolVarLong(&justVersion, "version", 0, "print version and exit") - getopt.StringVar(&target, 'H', fmt.Sprintf("docker daemon URL to proxy (default %s)", defaultTarget)) - getopt.StringVar(&listen, 'L', fmt.Sprintf("address on which to listen (default %s)", defaultListen)) - getopt.BoolVarLong(&withDNS, "with-dns", 'w', "instruct created containers to use weaveDNS as their nameserver") - getopt.BoolVarLong(&withIPAM, "with-ipam", 'i', "automatically allocate addresses for containers without a WEAVE_CIDR") + getopt.StringVar(&c.ListenAddr, 'L', fmt.Sprintf("address on which to listen (default %s)", defaultListenAddr)) + getopt.StringVar(&c.DockerAddr, 'H', fmt.Sprintf("docker daemon URL to proxy (default %s)", defaultDockerAddr)) + getopt.StringVarLong(&c.TLSConfig.CACert, "tlscacert", 0, "Trust certs signed only by this CA") + getopt.StringVarLong(&c.TLSConfig.Cert, "tlscert", 0, "Path to TLS certificate file") + getopt.BoolVarLong(&c.TLSConfig.Enabled, "tls", 0, "Use TLS; implied by --tlsverify") + getopt.StringVarLong(&c.TLSConfig.Key, "tlskey", 0, "Path to TLS key file") + getopt.BoolVarLong(&c.TLSConfig.Verify, "tlsverify", 0, "Use TLS and verify the remote") + getopt.BoolVarLong(&c.WithDNS, "with-dns", 'w', "instruct created containers to use weaveDNS as their nameserver") + getopt.BoolVarLong(&c.WithIPAM, "with-ipam", 'i', "automatically allocate addresses for containers without a WEAVE_CIDR") getopt.Parse() if justVersion { @@ -32,27 +44,16 @@ func main() { os.Exit(0) } - if target == "" { - target = defaultTarget - } - - if listen == "" { - listen = defaultListen - } - if debug { InitDefaultLogging(true) } - p, err := proxy.NewProxy(version, target, listen, withDNS, withIPAM) + p, err := proxy.NewProxy(c) if err != nil { Error.Fatalf("Could not start proxy: %s", err) } - Info.Printf("Listening on %s", listen) - Info.Printf("Proxying %s", target) - if err := p.ListenAndServe(); err != nil { - Error.Fatalf("Could not listen on %s: %s", listen, err) + Error.Fatalf("Could not listen on %s: %s", p.ListenAddr, err) } } diff --git a/proxy/proxy.go b/proxy/proxy.go index 492a2cdb..67f5b87e 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -2,6 +2,7 @@ package proxy import ( "bytes" + "crypto/tls" "fmt" "net" "net/http" @@ -13,6 +14,12 @@ import ( . "github.com/weaveworks/weave/common" ) +const ( + defaultCaFile = "ca.pem" + defaultKeyFile = "key.pem" + defaultCertFile = "cert.pem" +) + var ( containerCreateRegexp = regexp.MustCompile("^(/v[0-9\\.]*)?/containers/create$") containerStartRegexp = regexp.MustCompile("^(/v[0-9\\.]*)?/containers/[^/]*/(re)?start$") @@ -20,31 +27,24 @@ var ( ) type Proxy struct { - Dial func() (net.Conn, error) - version string + Config + + dial func() (net.Conn, error) client *docker.Client - dockerAddr string - listenAddr string - withDNS bool dockerBridgeIP string - withIPAM bool } -func NewProxy(version, dockerAddr, listenAddr string, withDNS, withIPAM bool) (*Proxy, error) { - u, err := url.Parse(dockerAddr) - if err != nil { - return nil, err - } +type Config struct { + DockerAddr string + ListenAddr string + TLSConfig TLSConfig + Version string + WithDNS bool + WithIPAM bool +} - var dockerBridgeIP []byte - if withDNS { - dockerBridgeIP, err = callWeave("docker-bridge-ip") - if err != nil { - return nil, err - } - } - - client, err := docker.NewClient(dockerAddr) +func NewProxy(c Config) (*Proxy, error) { + u, err := url.Parse(c.DockerAddr) if err != nil { return nil, err } @@ -57,18 +57,31 @@ func NewProxy(version, dockerAddr, listenAddr string, withDNS, withIPAM bool) (* targetAddr = u.Path } - return &Proxy{ - Dial: func() (net.Conn, error) { + p := &Proxy{ + Config: c, + dial: func() (net.Conn, error) { return net.Dial(u.Scheme, targetAddr) }, - version: version, - client: client, - dockerAddr: dockerAddr, - listenAddr: listenAddr, - withDNS: withDNS, - dockerBridgeIP: string(dockerBridgeIP), - withIPAM: withIPAM, - }, nil + } + + if p.WithDNS { + dockerBridgeIP, err := callWeave("docker-bridge-ip") + if err != nil { + return nil, err + } + p.dockerBridgeIP = string(dockerBridgeIP) + } + + if err := p.TLSConfig.loadCerts(); err != nil { + Error.Fatalf("Could not configure tls for proxy: %s", err) + } + + p.client, err = docker.NewClient(p.DockerAddr) + if err != nil { + return nil, err + } + + return p, nil } func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -76,13 +89,13 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := r.URL.Path switch { case containerCreateRegexp.MatchString(path): - proxy.serveWithInterceptor(&createContainerInterceptor{proxy.client, proxy.withDNS, proxy.dockerBridgeIP, proxy.withIPAM}, w, r) + proxy.serveWithInterceptor(&createContainerInterceptor{proxy.client, proxy.WithDNS, proxy.dockerBridgeIP, proxy.WithIPAM}, w, r) case containerStartRegexp.MatchString(path): - proxy.serveWithInterceptor(&startContainerInterceptor{proxy.client, proxy.withDNS, proxy.withIPAM}, w, r) + proxy.serveWithInterceptor(&startContainerInterceptor{proxy.client, proxy.WithDNS, proxy.WithIPAM}, w, r) case execCreateRegexp.MatchString(path): - proxy.serveWithInterceptor(&createExecInterceptor{proxy.client, proxy.withIPAM}, w, r) + proxy.serveWithInterceptor(&createExecInterceptor{proxy.client, proxy.WithIPAM}, w, r) case strings.HasPrefix(path, "/status"): - fmt.Fprintln(w, "weave proxy", proxy.version) + fmt.Fprintln(w, "weave proxy", proxy.Version) fmt.Fprintln(w, proxy.Status()) default: proxy.serveWithInterceptor(&nullInterceptor{}, w, r) @@ -90,20 +103,28 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (proxy *Proxy) serveWithInterceptor(i interceptor, w http.ResponseWriter, r *http.Request) { - newClient(proxy.Dial, i).ServeHTTP(w, r) + newClient(proxy.dial, i).ServeHTTP(w, r) } // Return status string func (proxy *Proxy) Status() string { var buf bytes.Buffer - fmt.Fprintln(&buf, "Listen address is", proxy.listenAddr) - fmt.Fprintln(&buf, "Docker address is", proxy.dockerAddr) - if proxy.withDNS { + fmt.Fprintln(&buf, "Listen address is", proxy.ListenAddr) + fmt.Fprintln(&buf, "Docker address is", proxy.DockerAddr) + switch { + case proxy.TLSConfig.Verify: + fmt.Fprintln(&buf, "TLS verify") + case proxy.TLSConfig.Enabled: + fmt.Fprintln(&buf, "TLS on") + default: + fmt.Fprintln(&buf, "TLS off") + } + if proxy.WithDNS { fmt.Fprintln(&buf, "DNS on") } else { fmt.Fprintln(&buf, "DNS off") } - if proxy.withIPAM { + if proxy.WithIPAM { fmt.Fprintln(&buf, "IPAM on") } else { fmt.Fprintln(&buf, "IPAM off") @@ -112,8 +133,18 @@ func (proxy *Proxy) Status() string { } func (proxy *Proxy) ListenAndServe() error { - return (&http.Server{ - Addr: proxy.listenAddr, - Handler: proxy, - }).ListenAndServe() + listener, err := net.Listen("tcp", proxy.ListenAddr) + if err != nil { + return err + } + + if proxy.TLSConfig.enabled() { + listener = tls.NewListener(listener, proxy.TLSConfig.Config) + Info.Println("TLS Enabled") + } + + Info.Printf("Listening on %s", proxy.ListenAddr) + Info.Printf("Proxying %s", proxy.DockerAddr) + + return (&http.Server{Handler: proxy}).Serve(listener) } diff --git a/proxy/tls.go b/proxy/tls.go new file mode 100644 index 00000000..229e0ce3 --- /dev/null +++ b/proxy/tls.go @@ -0,0 +1,76 @@ +package proxy + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/docker/docker/pkg/homedir" +) + +type TLSConfig struct { + Enabled, Verify bool + Cert, Key, CACert string + *tls.Config +} + +func (c *TLSConfig) enabled() bool { + if c == nil { + return false + } + return c.Enabled || c.Verify +} + +func (c *TLSConfig) loadCerts() error { + if !c.enabled() { + return nil + } + + dockerCertPath := os.Getenv("DOCKER_CERT_PATH") + if dockerCertPath == "" { + dockerCertPath = filepath.Join(homedir.Get(), ".docker") + } + + if c.CACert == "" { + c.CACert = filepath.Join(dockerCertPath, defaultCaFile) + } + if c.Cert == "" { + c.Cert = filepath.Join(dockerCertPath, defaultCertFile) + } + if c.Key == "" { + c.Key = filepath.Join(dockerCertPath, defaultKeyFile) + } + + tlsConfig := &tls.Config{ + NextProtos: []string{"http/1.1"}, + // Avoid fallback on insecure SSL protocols + MinVersion: tls.VersionTLS10, + } + + if c.Verify { + certPool := x509.NewCertPool() + file, err := ioutil.ReadFile(c.CACert) + if err != nil { + return fmt.Errorf("Couldn't read CA certificate: %v", err) + } + certPool.AppendCertsFromPEM(file) + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.ClientCAs = certPool + } + + _, errCert := os.Stat(c.Cert) + _, errKey := os.Stat(c.Key) + if errCert == nil && errKey == nil { + cert, err := tls.LoadX509KeyPair(c.Cert, c.Key) + if err != nil { + return fmt.Errorf("Couldn't load X509 key pair: %q. Make sure the key is encrypted", err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + + c.Config = tlsConfig + return nil +} diff --git a/site/proxy.md b/site/proxy.md index ed1727d1..9712b288 100644 --- a/site/proxy.md +++ b/site/proxy.md @@ -5,6 +5,16 @@ layout: default # Weave Proxy + * [Advantages](#advantages) + * [Setup](#setup) + * [Usage](#usage) + * [Usage with IPAM](#usage-with-ipam) + * [Usage with WeaveDNS](#usage-with-weavedns) + * [Usage with TLS](#usage-with-tls) + * [Limitations](#limitations) + +## Advantages + Instead of the `weave` command-line utility, you may prefer to use the standard [Docker command-line interface](https://docs.docker.com/reference/commandline/cli/), or the @@ -20,7 +30,7 @@ starting a container's application process. Furthermore, containers can be started in foreground mode, and can be automatically removed (with the ususal `--rm`). -## Setup +## Setup Start the proxy with @@ -35,7 +45,7 @@ your `DOCKER_HOST`. host1$ export DOCKER_HOST=tcp://host1:12375 host1$ docker ps -## Usage +## Usage When containers are created via the weave proxy, their entrypoint will be modified to wait for the weave network interface to become @@ -56,7 +66,7 @@ Multiple IP addresses and networks can be supplied in the `WEAVE_CIDR` variable by space-separating them, as in `WEAVE_CIDR="10.2.1.1/24 10.2.2.1/24"`. -## Usage with IPAM +## Usage with IPAM To automatically assign a unique IP address to a container, weave must be told on startup what range of addresses to allocate from. For @@ -86,7 +96,7 @@ and attached to the weave network when we start them. For example: More details on IPAM can be found in the [IPAM documentation](ipam.html). -## Usage with WeaveDNS +## Usage with WeaveDNS Containers started via the proxy can be automatically configured to use WeaveDNS for name resolution. To accomplish this we need to launch @@ -102,7 +112,41 @@ resolution. WeaveDNS is used in addition to any dns servers specified via the `--dns` option. More details on weaveDNS can be found in the [weaveDNS documentation](weavedns.html). -## Limitations +## Usage with TLS + +If you are using docker with TLS, you probably want to talk to +the proxy over TLS. Launching the proxy with TLS is done +with the same command-line flags as the docker daemon. For example, if +you have generated your certificates and keys into the docker host's +`/tls` directory, we can launch the proxy with: + + host1$ weave launch-proxy --tlsverify --tlscacert=/tls/ca.pem \ + --tlscert=/tls/server-cert.pem --tlskey=/tls/server-key.pem + +The paths to your certificates and key must be provided as absolute +paths which exist on the docker host. + +Because the proxy connects to the docker daemon at +`unix:///var/run/docker.sock`, you must ensure that the daemon is +listening there. To do this, you will need to pass the `-H +unix:///var/run/docker.sock` option when starting the docker daemon, in +addition to the `-H` options for configuring the TCP listener. See +[the Docker +documentation](https://docs.docker.com/articles/basics/#bind-docker-to-another-hostport-or-a-unix-socket) for an example. + +With the proxy running over TLS, we can configure our regular docker +client + + host1$ docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem \ + --tlskey=key.pem -H=tcp://host1:12375 version + +The certificates and keys used by the client will need to be present +on your client machine. + +More details on generating certificates, and configuring docker with +TLS can be found in the [docker +documentation](https://docs.docker.com/articles/https/). + +## Limitations -* The proxy does not currently support TLS. * If you have a firewall, you will need to make sure port 12375 is open. diff --git a/test/670_proxy_tls_test.sh b/test/670_proxy_tls_test.sh new file mode 100755 index 00000000..1d284133 --- /dev/null +++ b/test/670_proxy_tls_test.sh @@ -0,0 +1,18 @@ +#! /bin/bash + +. ./config.sh + +start_suite "Boot the proxy with TLS-enabled Docker support" + +PWD=$($SSH $HOST1 pwd) + +weave_on $HOST1 launch +weave_on $HOST1 launch-proxy \ + --tlsverify \ + --tlscacert $PWD/tls/ca.pem \ + --tlscert $PWD/tls/$HOST1.pem \ + --tlskey $PWD/tls/$HOST1-key.pem + +assert_raises "DOCKER_CERT_PATH=./tls proxy docker_on $HOST1 --tlsverify ps" + +end_suite diff --git a/test/setup.sh b/test/setup.sh index 9aae4c46..be7da148 100755 --- a/test/setup.sh +++ b/test/setup.sh @@ -4,7 +4,9 @@ set -e . ./config.sh -echo Copying weave images and script to hosts +(cd ./tls && go get ./... && go run generate_certs.go $HOSTS) + +echo Copying weave images, scripts, and certificates to hosts for HOST in $HOSTS; do docker_on $HOST load -i ../weave.tar docker_on $HOST load -i ../weavedns.tar @@ -13,5 +15,6 @@ for HOST in $HOSTS; do cat ../bin/docker-ns | run_on $HOST sh -c "cat > $DOCKER_NS" cat ../weave | run_on $HOST sh -c "cat > ./weave" run_on $HOST chmod a+x $DOCKER_NS ./weave + rsync -az -e "$SSH" ./tls/ $HOST:~/tls run_on $HOST sudo service docker restart done diff --git a/test/tls/generate_certs.go b/test/tls/generate_certs.go new file mode 100644 index 00000000..d2389baf --- /dev/null +++ b/test/tls/generate_certs.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/docker/machine/utils" +) + +const ( + org = "vagrant" + bits = 2048 + caKeyPath = "ca-key.pem" + caCertPath = "ca.pem" + clientKeyPath = "key.pem" + clientCertPath = "cert.pem" + serverKeyPath = "server-key.pem" + serverCertPath = "server.pem" +) + +func main() { + if _, err := os.Stat(caCertPath); os.IsNotExist(err) { + log.Printf("Creating CA: %s", caCertPath) + + // check if the key path exists; if so, error + if _, err := os.Stat(caKeyPath); err == nil { + log.Fatalf("The CA key already exists. Please remove it or specify a different key/cert.") + } + + if err := utils.GenerateCACertificate(caCertPath, caKeyPath, org, bits); err != nil { + log.Printf("Error generating CA certificate: %s", err) + } + } + + if _, err := os.Stat(clientCertPath); os.IsNotExist(err) { + log.Printf("Creating client certificate: %s", clientCertPath) + + // check if the key path exists; if so, error + if _, err := os.Stat(clientKeyPath); err == nil { + log.Fatalf("The client key already exists. Please remove it or specify a different key/cert.") + } + + err = utils.GenerateCert( + []string{""}, + clientCertPath, + clientKeyPath, + caCertPath, + caKeyPath, + org, + bits, + ) + if err != nil { + log.Fatalf("Error generating client certificate: %s", err) + } + } + + if len(os.Args) <= 1 { + return + } + for _, ip := range os.Args[1:] { + serverKeyPath := fmt.Sprintf("%s-key.pem", ip) + serverCertPath := fmt.Sprintf("%s.pem", ip) + + if _, err := os.Stat(serverCertPath); os.IsNotExist(err) { + log.Printf("Creating server certificate: %s", serverCertPath) + err = utils.GenerateCert( + []string{ip}, + serverCertPath, + serverKeyPath, + caCertPath, + caKeyPath, + org, + bits, + ) + if err != nil { + log.Fatalf("error generating server cert: %s", err) + } + } + } +} diff --git a/weave b/weave index b4d57a72..3933c2d3 100755 --- a/weave +++ b/weave @@ -746,6 +746,53 @@ get_ipam_cidr() { fi } +###################################################################### +# weave proxy helpers +###################################################################### + + +# TODO: Handle relative paths for args +# TODO: Handle args with spaces +tls_arg() { + filename=${3:-$2} + PROXY_VOLUMES="$PROXY_VOLUMES -v $1:/home/weave/tls/$filename.pem" + PROXY_ARGS="$PROXY_ARGS --tls$2 /home/weave/tls/$filename.pem" +} + +proxy_args() { + PROXY_VOLUMES="" + PROXY_ARGS="" + while [ $# -gt 0 ]; do + case "$1" in + --tlscacert) + shift + tls_arg "$1" cacert ca + ;; + --tlscacert=*) + tls_arg "${1#*=}" cacert ca + ;; + --tlscert) + shift + tls_arg "$1" cert + ;; + --tlscert=*) + tls_arg "${1#*=}" cert + ;; + --tlskey) + shift + tls_arg "$1" key + ;; + --tlskey=*) + tls_arg "${1#*=}" key + ;; + *) + PROXY_ARGS="$PROXY_ARGS $1" + ;; + esac + shift + done +} + ########################################################################################## [ $(id -u) = 0 ] || { @@ -880,12 +927,15 @@ case "$COMMAND" in # Set WEAVEPROXY_DOCKER_ARGS in the environment in order to supply # additional parameters, such as resource limits, to docker # when launching the weaveproxy container. + proxy_args "$@" PROXY_CONTAINER=$(docker run --privileged -d --name=$PROXY_CONTAINER_NAME --net=host \ -p $PROXY_PORT:$PROXY_PORT/tcp \ - -v /var/run/docker.sock:/var/run/docker.sock -v /proc:/hostproc \ + $PROXY_VOLUMES \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /proc:/hostproc \ -e PROCFS=/hostproc \ --entrypoint=/home/weave/weaveproxy \ - $WEAVEPROXY_DOCKER_ARGS $EXEC_IMAGE "$@") + $WEAVEPROXY_DOCKER_ARGS $EXEC_IMAGE $PROXY_ARGS) wait_for_status_ip $PROXY_CONTAINER_NAME 127.0.0.1 $PROXY_PORT echo $PROXY_CONTAINER ;;