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
;;