From c31358259f4dc94ce49aa85642178becaec0f79b Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Tue, 2 Jun 2015 12:53:15 +0100 Subject: [PATCH] [proxy] Add tls support Caveats: * TLS files must be present on the target server (and readable) * TLS file paths cannot contain spaces * TLS file paths must be absolute (i.e. cannot be relative) * Bash argument parsing is terrible --- .gitignore | 1 + Makefile | 1 + prog/weaveproxy/main.go | 38 ++++++++++++------ proxy/proxy.go | 39 ++++++++++++++---- proxy/tls.go | 76 +++++++++++++++++++++++++++++++++++ test/670_proxy_tls_test.sh | 18 +++++++++ test/setup.sh | 5 ++- test/tls/generate_certs.go | 81 ++++++++++++++++++++++++++++++++++++++ weave | 52 +++++++++++++++++++++++- 9 files changed, 290 insertions(+), 21 deletions(-) create mode 100644 proxy/tls.go create mode 100755 test/670_proxy_tls_test.sh create mode 100644 test/tls/generate_certs.go 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..eaa669b8 100644 --- a/prog/weaveproxy/main.go +++ b/prog/weaveproxy/main.go @@ -16,13 +16,25 @@ var ( ) func main() { - var target, listen string - var withDNS, withIPAM, debug, justVersion bool + var ( + debug bool + justVersion bool + listen string + target string + tlsConfig proxy.TLSConfig + withDNS bool + withIPAM bool + ) 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.StringVar(&target, 'H', fmt.Sprintf("docker daemon URL to proxy (default %s)", defaultTarget)) + getopt.StringVarLong(&tlsConfig.CACert, "tlscacert", 0, "Trust certs signed only by this CA") + getopt.StringVarLong(&tlsConfig.Cert, "tlscert", 0, "Path to TLS certificate file") + getopt.BoolVarLong(&tlsConfig.Enabled, "tls", 0, "Use TLS; implied by --tlsverify") + getopt.StringVarLong(&tlsConfig.Key, "tlskey", 0, "Path to TLS key file") + getopt.BoolVarLong(&tlsConfig.Verify, "tlsverify", 0, "Use TLS and verify the remote") 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.Parse() @@ -32,6 +44,10 @@ func main() { os.Exit(0) } + if debug { + InitDefaultLogging(true) + } + if target == "" { target = defaultTarget } @@ -40,18 +56,18 @@ func main() { listen = defaultListen } - if debug { - InitDefaultLogging(true) - } - - p, err := proxy.NewProxy(version, target, listen, withDNS, withIPAM) + p, err := proxy.NewProxy( + version, + target, + listen, + withDNS, + withIPAM, + &tlsConfig, + ) 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) } diff --git a/proxy/proxy.go b/proxy/proxy.go index 492a2cdb..42c4be69 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$") @@ -24,13 +31,14 @@ type Proxy struct { version string client *docker.Client dockerAddr string - listenAddr string - withDNS bool dockerBridgeIP string + listenAddr string + tlsConfig *TLSConfig + withDNS bool withIPAM bool } -func NewProxy(version, dockerAddr, listenAddr string, withDNS, withIPAM bool) (*Proxy, error) { +func NewProxy(version, dockerAddr, listenAddr string, withDNS, withIPAM bool, tlsConfig *TLSConfig) (*Proxy, error) { u, err := url.Parse(dockerAddr) if err != nil { return nil, err @@ -44,6 +52,10 @@ func NewProxy(version, dockerAddr, listenAddr string, withDNS, withIPAM bool) (* } } + if err := tlsConfig.loadCerts(); err != nil { + Error.Fatalf("Could not configure tls for proxy: %s", err) + } + client, err := docker.NewClient(dockerAddr) if err != nil { return nil, err @@ -65,8 +77,9 @@ func NewProxy(version, dockerAddr, listenAddr string, withDNS, withIPAM bool) (* client: client, dockerAddr: dockerAddr, listenAddr: listenAddr, - withDNS: withDNS, dockerBridgeIP: string(dockerBridgeIP), + tlsConfig: tlsConfig, + withDNS: withDNS, withIPAM: withIPAM, }, nil } @@ -112,8 +125,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/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 9811680b..8aecd8c1 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 +proxy_args() { + PROXY_VOLUMES="" + PROXY_ARGS="" + while [ $# -gt 0 ]; do + case "$1" in + --tlscacert) + shift + PROXY_VOLUMES="$PROXY_VOLUMES -v $1:/home/weave/tls/ca.pem" + PROXY_ARGS="$PROXY_ARGS --tlscacert /home/weave/tls/ca.pem" + ;; + --tlscacert=*) + PROXY_VOLUMES="$PROXY_VOLUMES -v ${1#*=}:/home/weave/tls/ca.pem" + PROXY_ARGS="$PROXY_ARGS --tlscacert /home/weave/tls/ca.pem" + ;; + --tlscert) + shift + PROXY_VOLUMES="$PROXY_VOLUMES -v $1:/home/weave/tls/cert.pem" + PROXY_ARGS="$PROXY_ARGS --tlscert /home/weave/tls/cert.pem" + ;; + --tlscert=*) + PROXY_VOLUMES="$PROXY_VOLUMES -v ${1#*=}:/home/weave/tls/cert.pem" + PROXY_ARGS="$PROXY_ARGS --tlscert /home/weave/tls/cert.pem" + ;; + --tlskey) + shift + PROXY_VOLUMES="$PROXY_VOLUMES -v $1:/home/weave/tls/key.pem" + PROXY_ARGS="$PROXY_ARGS --tlskey /home/weave/tls/key.pem" + ;; + --tlskey=*) + PROXY_VOLUMES="$PROXY_VOLUMES -v ${1#*=}:/home/weave/tls/key.pem" + PROXY_ARGS="$PROXY_ARGS --tlskey /home/weave/tls/key.pem" + ;; + *) + 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 \ -e PROCFS=/hostproc \ + $PROXY_VOLUMES \ --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 ;;