diff --git a/.gitignore b/.gitignore index 6f39f896..15550993 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ weaveexec/weavewait weaveexec/sigproxy weaveexec/docker*.tgz Vagrantfile.local +test/tls/*.pem +test/tls/*.srl +test/tls/*.csr diff --git a/proxy/listener.go b/proxy/listener.go new file mode 100644 index 00000000..04251108 --- /dev/null +++ b/proxy/listener.go @@ -0,0 +1,18 @@ +package proxy + +import ( + "crypto/tls" + "net" +) + +func (proxy *Proxy) listener(addr string) (net.Listener, error) { + listener, err := net.Listen("tcp", addr) + if err != nil { + return nil, err + } + + if proxy.tlsConfig.enabled() { + listener = tls.NewListener(listener, proxy.tlsConfig.Config) + } + return listener, nil +} 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..8d06089b --- /dev/null +++ b/proxy/tls.go @@ -0,0 +1,78 @@ +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{ + InsecureSkipVerify: true, + 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 cert %s: %s", c.CACert, err) + } + certPool.AppendCertsFromPEM(file) + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.InsecureSkipVerify = false + tlsConfig.RootCAs = 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..fbc4c853 --- /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 \ + --tls \ + --tlscacert $PWD/tls/$HOST1.ca.pem \ + --tlscert $PWD/tls/$HOST1.server.pem \ + --tlskey $PWD/tls/server-key.pem + +assert_raises "proxy docker_on $HOST1 --tls --tlscacert ./tls/$HOST1.ca.pem ps" + +end_suite diff --git a/test/setup.sh b/test/setup.sh index 9aae4c46..d9b350fd 100755 --- a/test/setup.sh +++ b/test/setup.sh @@ -15,3 +15,10 @@ for HOST in $HOSTS; do run_on $HOST chmod a+x $DOCKER_NS ./weave run_on $HOST sudo service docker restart done + +echo Setting up Docker TLS on hosts +(cd ./tls && make) +chmod 0600 tls/* +for HOST in $HOSTS; do + rsync -az -e "$SSH" ./tls/ $HOST:~/tls/ +done diff --git a/test/tls/Makefile b/test/tls/Makefile new file mode 100644 index 00000000..bfd8028c --- /dev/null +++ b/test/tls/Makefile @@ -0,0 +1,36 @@ +SERVERS = 192.168.48.11 192.168.48.12 +SERVER_CAS = $(SERVERS:=.ca.pem) +SERVER_CSRS = $(SERVERS:=.server.csr) +SERVER_PEMS = $(SERVERS:=.server.pem) +CLIENT_PEMS = $(SERVERS:=.client.pem) + +.DEFAULT: all +.PHONY: all clean $(SERVERS) + +SHELL=/bin/bash + +all: ca-key.pem server-key.pem client-key.pem $(SERVER_CAS) $(SERVER_CSRS) $(SERVER_PEMS) client.csr $(CLIENT_PEMS) + +ca-key.pem server-key.pem client-key.pem: + openssl genrsa -out $@ 1024 + +$(SERVER_CAS): ca-key.pem + openssl req -subj "/CN=$(@:.ca.pem=)" -new -x509 -days 3650 -key ca-key.pem -sha256 -out $@ + +$(SERVER_CSRS): server-key.pem + openssl req -subj "/CN=$(@:.server.csr=)" -new -key server-key.pem -out $@ + +$(SERVER_PEMS): %.server.pem: %.server.csr ca-key.pem %.ca.pem + openssl x509 -req -days 3650 -in $(@:.pem=.csr) \ + -CA $(@:.server.pem=.ca.pem) -CAkey ca-key.pem -CAcreateserial -out $@ + +client.csr: client-key.pem + openssl req -subj '/CN=client' -new -key client-key.pem -out $@ + +$(CLIENT_PEMS): %.client.pem: client.csr ca-key.pem %.ca.pem + openssl x509 -req -days 3650 -in client.csr \ + -CA $(@:.client.pem=.ca.pem) -CAkey ca-key.pem -CAcreateserial \ + -extfile <(echo "extendedKeyUsage = clientAuth") -out $@ + +clean: + rm -f *.pem *.csr *.srl diff --git a/weave b/weave index 4d4ea1e3..4ca7bb6a 100755 --- a/weave +++ b/weave @@ -722,6 +722,40 @@ get_ipam_cidr() { fi } +###################################################################### +# weave proxy helpers +###################################################################### + +# TODO: Handle args with spaces +# TODO: Handle non-absolute paths for certs +proxy_args() { + PROXY_VOLUMES="" + PROXY_ARGS="" + while [ $# -gt 0 ]; do + case $1 in + --tlscacert|--tlscacert=*) + [[ "${1#*=}" != "$1" ]] || shift + PROXY_VOLUMES="$PROXY_VOLUMES -v ${1#--tlscacert=}:/home/weave/tls/ca.pem" + PROXY_ARGS="$PROXY_ARGS --tlscacert /home/weave/tls/ca.pem" + ;; + --tlscert|--tlscert=*) + [[ "${1#*=}" != "$1" ]] || shift + PROXY_VOLUMES="$PROXY_VOLUMES -v ${1#--tlscert=}:/home/weave/tls/cert.pem" + PROXY_ARGS="$PROXY_ARGS --tlscert /home/weave/tls/cert.pem" + ;; + --tlskey|--tlskey=*) + [[ "${1#*=}" != "$1" ]] || shift + PROXY_VOLUMES="$PROXY_VOLUMES -v ${1#--tlskey=}:/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 ] || { @@ -855,14 +889,16 @@ 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 --net=host -d \ --name=$PROXY_CONTAINER_NAME \ -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 "$@") + $EXEC_IMAGE $PROXY_ARGS) wait_for_status_ip 127.0.0.1 $PROXY_PORT echo $PROXY_CONTAINER diff --git a/weaveproxy/main.go b/weaveproxy/main.go index 8b43ee6f..2ef29777 100644 --- a/weaveproxy/main.go +++ b/weaveproxy/main.go @@ -16,13 +16,29 @@ var ( ) func main() { - var target, listen string - var withDNS, withIPAM, debug, justVersion bool + var ( + debug bool + justVersion bool + listen string + target string + tlsCACert string + tlsCert string + tlsEnabled bool + tlsKey string + tlsVerify bool + 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(&tlsCACert, "tlscacert", 0, "Trust certs signed only by this CA") + getopt.StringVarLong(&tlsCert, "tlscert", 0, "Path to TLS certificate file") + getopt.BoolVarLong(&tlsEnabled, "tls", 0, "Use TLS; implied by --tlsverify") + getopt.StringVarLong(&tlsKey, "tlskey", 0, "Path to TLS key file") + getopt.BoolVarLong(&tlsVerify, "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 +48,10 @@ func main() { os.Exit(0) } + if debug { + InitDefaultLogging(true) + } + if target == "" { target = defaultTarget } @@ -40,18 +60,24 @@ 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, + &proxy.TLSConfig{ + Enabled: tlsEnabled, + Verify: tlsVerify, + Cert: tlsCert, + Key: tlsKey, + CACert: tlsCACert, + }, + ) 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) }