From 39fe73a23139280af92f5469b308f91fcef1cb61 Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Mon, 8 Dec 2014 18:09:06 -0800 Subject: [PATCH] TLS support. TLS authentication support between the CLI and Swarm but also between Swarm and the Docker nodes. Closes #148. Signed-off-by: Andrea Luzzardi --- README.md | 13 +++++++++++++ api/api.go | 16 ++++++++++++++-- main.go | 27 +++++++++++++++++++++++++-- manage.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 07e43c3f17..eecb26082c 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,19 @@ $ swarm list --token=6856663cdefdec325839a4b7e1de38e8 http:// ``` +### TLS + +Swarm supports TLS authentication between the CLI and Swarm but also between Swarm and the Docker nodes. + +In order to enable TLS, the same command line options as Docker can be specified: + +`swarm manage --tlsverify --tlscacert= --tlscert= --tlskey= [...]` + +Please refer to the [Docker documentation](https://docs.docker.com/articles/https/) for more information on how +to set up TLS authentication on Docker and generating the certificates. + +Note that Swarm certificates must be generated with`extendedKeyUsage = clientAuth,serverAuth`. + ## Participating We welcome pull requests and patches; come say hi on IRC, #docker-swarm on freenode. diff --git a/api/api.go b/api/api.go index 188e9526d2..12dc0d0c43 100644 --- a/api/api.go +++ b/api/api.go @@ -2,9 +2,11 @@ package api import ( "bytes" + "crypto/tls" "encoding/json" "fmt" "io/ioutil" + "net" "net/http" "net/url" "regexp" @@ -362,7 +364,7 @@ func createRouter(c *context, enableCors bool) (*mux.Router, error) { return r, nil } -func ListenAndServe(c *cluster.Cluster, s *scheduler.Scheduler, addr, version string, enableCors bool) error { +func ListenAndServe(c *cluster.Cluster, s *scheduler.Scheduler, addr, version string, enableCors bool, tlsConfig *tls.Config) error { context := &context{ cluster: c, scheduler: s, @@ -374,9 +376,19 @@ func ListenAndServe(c *cluster.Cluster, s *scheduler.Scheduler, addr, version st if err != nil { return err } + server := &http.Server{ Addr: addr, Handler: r, } - return server.ListenAndServe() + + l, err := net.Listen("tcp", addr) + if err != nil { + return err + } + if tlsConfig != nil { + tlsConfig.NextProtos = []string{"http/1.1"} + l = tls.NewListener(l, tlsConfig) + } + return server.Serve(l) } diff --git a/main.go b/main.go index b94fd193b6..5673a10194 100644 --- a/main.go +++ b/main.go @@ -54,6 +54,26 @@ func main() { Name: "api-enable-cors, cors", Usage: "enable CORS headers in the remote API", } + flTls := cli.BoolFlag{ + Name: "tls", + Usage: "Use TLS; implied by --tlsverify=true", + } + flTlsCaCert := cli.StringFlag{ + Name: "tlscacert", + Usage: "Trust only remotes providing a certificate signed by the CA given here", + } + flTlsCert := cli.StringFlag{ + Name: "tlscert", + Usage: "Path to TLS certificate file", + } + flTlsKey := cli.StringFlag{ + Name: "tlskey", + Usage: "Path to TLS key file", + } + flTlsVerify := cli.BoolFlag{ + Name: "tlsverify", + Usage: "Use TLS and verify the remote", + } app.Commands = []cli.Command{ { @@ -91,8 +111,11 @@ func main() { Name: "manage", ShortName: "m", Usage: "manage a docker cluster", - Flags: []cli.Flag{flToken, flAddr, flHeartBeat, flEnableCors}, - Action: manage, + Flags: []cli.Flag{ + flToken, flAddr, flHeartBeat, + flTls, flTlsCaCert, flTlsCert, flTlsKey, flTlsVerify, + flEnableCors}, + Action: manage, }, { Name: "join", diff --git a/manage.go b/manage.go index 9dc56b37d5..8375152250 100644 --- a/manage.go +++ b/manage.go @@ -1,6 +1,10 @@ package main import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" "strings" "time" @@ -22,7 +26,52 @@ func (h *logHandler) Handle(e *cluster.Event) error { return nil } +// Load the TLS certificates/keys and, if verify is true, the CA. +func loadTlsConfig(ca, cert, key string, verify bool) (*tls.Config, error) { + c, err := tls.LoadX509KeyPair(cert, key) + if err != nil { + return nil, fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?", + cert, key, err) + } + + config := &tls.Config{ + Certificates: []tls.Certificate{c}, + MinVersion: tls.VersionTLS10, + } + + if verify { + certPool := x509.NewCertPool() + file, err := ioutil.ReadFile(ca) + if err != nil { + return nil, fmt.Errorf("Couldn't read CA certificate: %s", err) + } + certPool.AppendCertsFromPEM(file) + config.RootCAs = certPool + } else { + // If --tlsverify is not supplied, disable CA validation. + config.InsecureSkipVerify = true + } + + return config, nil +} + func manage(c *cli.Context) { + var ( + tlsConfig *tls.Config = nil + err error + ) + + // If either --tls or --tlsverify are specified, load the certificates. + if c.Bool("tls") || c.Bool("tlsverify") { + tlsConfig, err = loadTlsConfig( + c.String("tlscacert"), + c.String("tlscert"), + c.String("tlskey"), + c.Bool("tlsverify")) + if err != nil { + log.Fatal(err) + } + } refresh := func(c *cluster.Cluster, nodes []string) { for _, addr := range nodes { @@ -32,7 +81,7 @@ func manage(c *cli.Context) { } if c.Node(addr) == nil { n := cluster.NewNode(addr) - if err := n.Connect(nil); err != nil { + if err := n.Connect(tlsConfig); err != nil { log.Error(err) return } @@ -82,5 +131,5 @@ func manage(c *cli.Context) { }, ) - log.Fatal(api.ListenAndServe(cluster, s, c.String("addr"), c.App.Version, c.Bool("cors"))) + log.Fatal(api.ListenAndServe(cluster, s, c.String("addr"), c.App.Version, c.Bool("cors"), tlsConfig)) }