From 1a3b84386d11ce2cd76d566db0f583acf60f1921 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 22 Oct 2014 11:07:03 -0700 Subject: [PATCH 01/18] Add default TLS using engine keys Signed-off-by: Derek McGowan (github: dmcgowan) --- api/server/server.go | 135 ++++++++++++++++++++++--------- docker/daemon.go | 3 + docker/docker.go | 157 +++++++++++++++++++++++++++++++----- docker/flags.go | 20 ++--- integration/runtime_test.go | 3 + 5 files changed, 251 insertions(+), 67 deletions(-) diff --git a/api/server/server.go b/api/server/server.go index b3cf0603bb9dd..8906afe809b47 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -3,7 +3,7 @@ package server import ( "bufio" "bytes" - + "crypto/tls" "encoding/base64" "encoding/json" "expvar" @@ -14,13 +14,11 @@ import ( "net/http" "net/http/pprof" "os" + "path" "strconv" "strings" "syscall" - "crypto/tls" - "crypto/x509" - "code.google.com/p/go.net/websocket" "github.com/docker/libcontainer/user" "github.com/gorilla/mux" @@ -35,6 +33,7 @@ import ( "github.com/docker/docker/pkg/version" "github.com/docker/docker/registry" "github.com/docker/docker/utils" + "github.com/docker/libtrust" ) var ( @@ -1408,33 +1407,6 @@ func lookupGidByName(nameOrGid string) (int, error) { return -1, fmt.Errorf("Group %s not found", nameOrGid) } -func setupTls(cert, key, ca string, l net.Listener) (net.Listener, error) { - tlsCert, 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) - } - tlsConfig := &tls.Config{ - NextProtos: []string{"http/1.1"}, - Certificates: []tls.Certificate{tlsCert}, - // Avoid fallback on insecure SSL protocols - MinVersion: tls.VersionTLS10, - } - - if ca != "" { - 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) - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - tlsConfig.ClientCAs = certPool - } - - return tls.NewListener(l, tlsConfig), nil -} - func newListener(proto, addr string, bufferRequests bool) (net.Listener, error) { if bufferRequests { return listenbuffer.NewListenBuffer(proto, addr, activationLock) @@ -1453,6 +1425,23 @@ func changeGroup(addr string, nameOrGid string) error { return os.Chown(addr, 0, gid) } +// parseAddr parses an address into an array of IPs and domains +func parseAddr(addr string) ([]net.IP, []string, error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, nil, err + } + var domains []string + var ips []net.IP + ip := net.ParseIP(host) + if ip != nil { + ips = []net.IP{ip} + } else { + domains = []string{host} + } + return ips, domains, nil +} + func setSocketGroup(addr, group string) error { if group == "" { return nil @@ -1497,8 +1486,8 @@ func setupUnixHttp(addr string, job *engine.Job) (*HttpServer, error) { } func setupTcpHttp(addr string, job *engine.Job) (*HttpServer, error) { - if !strings.HasPrefix(addr, "127.0.0.1") && !job.GetenvBool("TlsVerify") { - log.Infof("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") + if job.GetenvBool("Insecure") { + log.Infof("/!\\ DON'T BIND INSECURELY ON A TCP ADDRESS IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") } r, err := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version")) @@ -1511,15 +1500,83 @@ func setupTcpHttp(addr string, job *engine.Job) (*HttpServer, error) { return nil, err } - if job.GetenvBool("Tls") || job.GetenvBool("TlsVerify") { - var tlsCa string - if job.GetenvBool("TlsVerify") { - tlsCa = job.Getenv("TlsCa") + if !job.GetenvBool("Insecure") { + trustKeyFile := job.Getenv("TrustKey") + err = os.MkdirAll(path.Dir(trustKeyFile), 0700) + if err != nil { + return nil, fmt.Errorf("Error creating directory: %s", err) + } + trustKey, err := libtrust.LoadKeyFile(trustKeyFile) + if err == libtrust.ErrKeyFileDoesNotExist { + trustKey, err = libtrust.GenerateECP256PrivateKey() + if err != nil { + return nil, fmt.Errorf("Error generating key: %s", err) + } + if err := libtrust.SaveKey(trustKeyFile, trustKey); err != nil { + return nil, fmt.Errorf("Error saving key file: %s", err) + } + } else if err != nil { + return nil, fmt.Errorf("Error loading key file: %s", err) + } + + var cert tls.Certificate + tlsCert := job.Getenv("TlsCert") + tlsKey := job.Getenv("TlsKey") + if tlsCert != "" && tlsKey != "" { + var err error + cert, err = tls.LoadX509KeyPair(tlsCert, tlsKey) + if err != nil { + if !os.IsNotExist(err) { + return nil, fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?", + tlsCert, tlsKey, err) + } + ips, domains, err := parseAddr(addr) + if err != nil { + return nil, err + } + // add default docker domain for docker clients to look for + domains = append(domains, "docker") + x509Cert, err := libtrust.GenerateSelfSignedServerCert(trustKey, domains, ips) + if err != nil { + return nil, fmt.Errorf("certificate generation error: %s", err) + } + cert = tls.Certificate{ + Certificate: [][]byte{x509Cert.Raw}, + PrivateKey: trustKey.CryptoPrivateKey(), + Leaf: x509Cert, + } + } + + } + + tlsConfig := &tls.Config{ + NextProtos: []string{"http/1.1"}, + Certificates: []tls.Certificate{cert}, + // Avoid fallback on insecure SSL protocols + MinVersion: tls.VersionTLS10, } - l, err = setupTls(job.Getenv("TlsCert"), job.Getenv("TlsKey"), tlsCa, l) + + // Load authorized keys file + clients, err := libtrust.LoadKeySetFile(job.Getenv("TrustClients")) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to load authorized keys: %s", err) + } + + if job.GetenvBool("TlsVerify") { + certPool, poolErr := libtrust.GenerateCACertPool(trustKey, clients) + if poolErr != nil { + return nil, fmt.Errorf("CA pool generation error: %s", poolErr) + } + file, err := ioutil.ReadFile(job.Getenv("TlsCa")) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("Couldn't read CA certificate: %s", err) + } + certPool.AppendCertsFromPEM(file) + + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.ClientCAs = certPool } + l = tls.NewListener(l, tlsConfig) } return &HttpServer{&http.Server{Addr: addr, Handler: r}, l}, nil } diff --git a/docker/daemon.go b/docker/daemon.go index 3128f7ee55cf7..b37c0e6461b6b 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -82,11 +82,14 @@ func mainDaemon() { job.Setenv("Version", dockerversion.VERSION) job.Setenv("SocketGroup", *flSocketGroup) + job.SetenvBool("Insecure", *flInsecure) job.SetenvBool("Tls", *flTls) job.SetenvBool("TlsVerify", *flTlsVerify) job.Setenv("TlsCa", *flCa) job.Setenv("TlsCert", *flCert) job.Setenv("TlsKey", *flKey) + job.Setenv("TrustKey", *flTrustKey) + job.Setenv("TrustClients", *flTrustClients) job.SetenvBool("BufferRequests", true) if err := job.Run(); err != nil { log.Fatal(err) diff --git a/docker/docker.go b/docker/docker.go index 3137f5c99fc46..7eabdcd0b7af7 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -1,12 +1,15 @@ package main import ( + "bufio" "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "os" + "path" "strings" + "time" log "github.com/Sirupsen/logrus" "github.com/docker/docker/api" @@ -15,13 +18,16 @@ import ( flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/reexec" "github.com/docker/docker/utils" + "github.com/docker/libtrust" ) const ( - defaultTrustKeyFile = "key.json" - defaultCaFile = "ca.pem" - defaultKeyFile = "key.pem" - defaultCertFile = "cert.pem" + defaultTrustKeyFile = "key.json" + defaultHostKeysFile = "allowed_hosts.json" + defaultClientKeysFile = "authorized_keys.json" + defaultCaFile = "ca.pem" + defaultKeyFile = "key.pem" + defaultCertFile = "cert.pem" ) func main() { @@ -66,6 +72,7 @@ func main() { } flHosts = append(flHosts, defaultHost) } + *flTlsVerify = true if *flDaemon { mainDaemon() @@ -77,11 +84,33 @@ func main() { } protoAddrParts := strings.SplitN(flHosts[0], "://", 2) + err := os.MkdirAll(path.Dir(*flTrustKey), 0700) + if err != nil { + log.Fatal(err) + } + trustKey, err := libtrust.LoadKeyFile(*flTrustKey) + if err == libtrust.ErrKeyFileDoesNotExist { + trustKey, err = libtrust.GenerateECP256PrivateKey() + if err != nil { + log.Fatalf("Error generating key: %s", err) + } + if err := libtrust.SaveKey(*flTrustKey, trustKey); err != nil { + log.Fatalf("Error saving key file: %s", err) + } + } else if err != nil { + log.Fatalf("Error loading key file: %s", err) + } + var ( cli *client.DockerCli tlsConfig tls.Config ) - tlsConfig.InsecureSkipVerify = true + + // Load known hosts + knownHosts, err := libtrust.LoadKeySetFile(*flTrustHosts) + if err != nil { + log.Fatalf("Could not load trusted hosts file: %s", err) + } // Regardless of whether the user sets it to true or false, if they // specify --tlsverify at all then we need to turn on tls @@ -91,36 +120,114 @@ func main() { // If we should verify the server, we need to load a trusted ca if *flTlsVerify { - certPool := x509.NewCertPool() - file, err := ioutil.ReadFile(*flCa) + allowedHosts, err := libtrust.FilterByHosts(knownHosts, protoAddrParts[1], false) if err != nil { - log.Fatalf("Couldn't read ca cert %s: %s", *flCa, err) + log.Fatalf("Error filtering hosts: %s", err) + } + certPool, err := libtrust.GenerateCACertPool(trustKey, allowedHosts) + if err != nil { + log.Fatalf("Could not create CA pool: %s", err) + } + if *flCa != "" { + file, err := ioutil.ReadFile(*flCa) + if err != nil { + if !os.IsNotExist(err) { + log.Fatalf("Couldn't read ca cert %s: %s", *flCa, err) + } else { + tlsConfig.ServerName = "docker" + } + } else { + certPool.AppendCertsFromPEM(file) + } + } else { + tlsConfig.ServerName = "docker" } - certPool.AppendCertsFromPEM(file) tlsConfig.RootCAs = certPool tlsConfig.InsecureSkipVerify = false } // If tls is enabled, try to load and send client certificates if *flTls || *flTlsVerify { - _, errCert := os.Stat(*flCert) - _, errKey := os.Stat(*flKey) - if errCert == nil && errKey == nil { - *flTls = true - cert, err := tls.LoadX509KeyPair(*flCert, *flKey) - if err != nil { + *flTls = true + cert, err := tls.LoadX509KeyPair(*flCert, *flKey) + if err != nil { + if !os.IsNotExist(err) { log.Fatalf("Couldn't load X509 key pair: %s. Key encrypted?", err) + } else { + x509Cert, certErr := libtrust.GenerateSelfSignedClientCert(trustKey) + if certErr != nil { + log.Fatalf("Certificate generation error: %s", certErr) + } + cert = tls.Certificate{ + Certificate: [][]byte{x509Cert.Raw}, + PrivateKey: trustKey.CryptoPrivateKey(), + Leaf: x509Cert, + } } - tlsConfig.Certificates = []tls.Certificate{cert} } + tlsConfig.Certificates = []tls.Certificate{cert} + } + + if protoAddrParts[0] != "unix" && (!*flInsecure) { + savedInsecure := tlsConfig.InsecureSkipVerify + tlsConfig.InsecureSkipVerify = true + testConn, connErr := tls.Dial(protoAddrParts[0], protoAddrParts[1], &tlsConfig) + if connErr != nil { + log.Fatalf("TLS Handshake error: %s", connErr) + } + opts := x509.VerifyOptions{ + Roots: tlsConfig.RootCAs, + CurrentTime: time.Now(), + DNSName: tlsConfig.ServerName, + Intermediates: x509.NewCertPool(), + } + + certs := testConn.ConnectionState().PeerCertificates + for i, cert := range certs { + if i == 0 { + continue + } + opts.Intermediates.AddCert(cert) + } + _, err = certs[0].Verify(opts) + if err != nil { + if _, ok := err.(x509.UnknownAuthorityError); ok { + pubKey, err := libtrust.FromCryptoPublicKey(certs[0].PublicKey) + if err != nil { + log.Fatalf("Error extracting public key from certificate: %s", err) + } + + if promptUnknownKey(pubKey, protoAddrParts[1]) { + pubKey.AddExtendedField("hosts", []string{protoAddrParts[1]}) + err = libtrust.AddKeySetFile(*flTrustHosts, pubKey) + if err != nil { + log.Fatalf("Error saving updated host keys file: %s", err) + } + + ca, err := libtrust.GenerateCACert(trustKey, pubKey) + if err != nil { + log.Fatalf("Error generating CA: %s", err) + } + tlsConfig.RootCAs.AddCert(ca) + } else { + log.Fatalf("Cancelling request due to invalid certificate") + } + } else { + log.Fatalf("TLS verification error: %s", connErr) + } + } + + testConn.Close() + tlsConfig.InsecureSkipVerify = savedInsecure + // Avoid fallback to SSL protocols < TLS1.0 tlsConfig.MinVersion = tls.VersionTLS10 } - if *flTls || *flTlsVerify { - cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], &tlsConfig) + if protoAddrParts[0] == "unix" || *flInsecure { + cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], nil) } else { - cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, nil, protoAddrParts[0], protoAddrParts[1], nil) + cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig) } if err := cli.Cmd(flag.Args()...); err != nil { @@ -134,6 +241,18 @@ func main() { } } +func promptUnknownKey(key libtrust.PublicKey, host string) bool { + fmt.Printf("The authenticity of host %q can't be established.\nRemote key ID %s\n", host, key.KeyID()) + fmt.Printf("Are you sure you want to continue connecting (yes/no)? ") + reader := bufio.NewReader(os.Stdin) + line, _, err := reader.ReadLine() + if err != nil { + log.Fatalf("Error reading input: %s", err) + } + input := strings.TrimSpace(strings.ToLower(string(line))) + return input == "yes" || input == "y" +} + func showVersion() { fmt.Printf("Docker version %s, build %s\n", dockerversion.VERSION, dockerversion.GITCOMMIT) } diff --git a/docker/flags.go b/docker/flags.go index 6601b4fe8a9d4..4ca9652005efa 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -37,20 +37,22 @@ var ( flEnableCors = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API") flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by --tlsverify flag") flTlsVerify = flag.Bool([]string{"-tlsverify"}, dockerTlsVerify, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)") + flInsecure = flag.Bool([]string{"-insecure"}, false, "Use insecure non-TLS connections") // these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs - flTrustKey *string - flCa *string - flCert *string - flKey *string - flHosts []string + flTrustKey *string + flTrustHosts *string + flTrustClients *string + flCa *string + flCert *string + flKey *string + flHosts []string ) func init() { - // placeholder for trust key flag - trustKeyDefault := filepath.Join(dockerCertPath, defaultTrustKeyFile) - flTrustKey = &trustKeyDefault - + flTrustHosts = flag.String([]string{"-allowed-hosts-file"}, filepath.Join(dockerCertPath, defaultHostKeysFile), "Path to file containing allowed hosts") + flTrustClients = flag.String([]string{"-authorized-keys-file"}, filepath.Join(dockerCertPath, defaultClientKeysFile), "Path to file containing authorized keys") + flTrustKey = flag.String([]string{"i", "-identity"}, filepath.Join(dockerCertPath, defaultTrustKeyFile), "Path to libtrust key file") flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust only remotes providing a certificate signed by the CA given here") flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file") flKey = flag.String([]string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file") diff --git a/integration/runtime_test.go b/integration/runtime_test.go index d173af1f7fd88..95aacc1156296 100644 --- a/integration/runtime_test.go +++ b/integration/runtime_test.go @@ -8,6 +8,7 @@ import ( "net" "net/url" "os" + "path" "path/filepath" "runtime" "strconv" @@ -160,6 +161,7 @@ func spawnGlobalDaemon() { } job := eng.Job("serveapi", listenURL.String()) job.SetenvBool("Logging", true) + job.SetenvBool("Insecure", true) if err := job.Run(); err != nil { log.Fatalf("Unable to spawn the test daemon: %s", err) } @@ -215,6 +217,7 @@ func spawnHttpsDaemon(addr, cacert, cert, key string) *engine.Engine { job.Setenv("TlsCa", cacert) job.Setenv("TlsCert", cert) job.Setenv("TlsKey", key) + job.Setenv("TrustKey", path.Join(unitTestStoreBase, ".docker", "key.json")) if err := job.Run(); err != nil { log.Fatalf("Unable to spawn the test daemon: %s", err) } From c727ca7de7c0d62c65a626730c046458416c9249 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 30 Oct 2014 12:03:43 -0700 Subject: [PATCH 02/18] Update https documentation Signed-off-by: Derek McGowan (github: dmcgowan) --- docs/sources/articles/https.md | 84 +++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index c8873bcbe4dd0..33432f113d8c2 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -5,11 +5,58 @@ page_keywords: docker, docs, article, example, https, daemon, tls, ca, certifica # Running Docker with https By default, Docker runs via a non-networked Unix socket. It can also -optionally communicate using a HTTP socket. - -If you need Docker to be reachable via the network in a safe manner, you can -enable TLS by specifying the `tlsverify` flag and pointing Docker's -`tlscacert` flag to a trusted CA certificate. +optionally communicate using a HTTP socket. Enabling communication +through HTTP will by default use TLS and require daemon configuration +to allow client connections. + + +There are two different ways of authenticating connections between Docker +client and daemon, both of which use secure TLS connections. + + - **Host-based authentication** uses an authorized keys list on the daemon +to whitelist client connections. The client must also accept the daemon's key +and remember it for future connections. + - **Certificate-based authentication** uses a certificate authority to +authorize connections. Using this method requires additional setup to enable +client authentication. + +The authentication method is selected using the `--auth` flag with values + `host`, `cert`, or `none` . `host` is the default method and `none` +should only be used with caution. + +## Host-based authentication + +Host-based authentication is similar to how SSH does authentication. When +connecting to a daemon for the first time, a client will ask whether a user +trusts a fingerprint of the daemon’s public key. If they do, the public key will +be stored so it does not prompt on subsequent connections. For the daemon +to authenticate the client, each client automatically generates its own +key (~/.docker/key.json) which is presented to the daemon and checked +against a list of keys authorized to connect (~/.docker/authorized_keys.json). + +To enable host-based authentication, add the flag `--auth=host`. The default +identity and authorization files may be overridden through the flags: + + - `--identity` specifies the key file to use. This file contains the client's private +key and its fingerprint is used by the daemon to identify the client. This file +should be secured. + - `--auth-authorized-keys` - specifies the client whitelist. This is a daemon +configuration and should have its write permissions restricted. + - `--auth-allowed-host` - specifies the list of daemon public key fingerprints +which have been approved by the user and the host name associated with +each fingerprint. + +## Certificate-based authentication + +Certificate-based authentication uses TLS certificates provided by a +certificate authority. This is for advanced usage where you may want to +integrate Docker with other TLS-compatible tools or you may already use PKI +within your organisation. You can just get the client to verify the server’s +certificate against a CA, or do full two-way authentication by getting the +server to also verify the client’s certificate. + +To enable certificate-based authentication, add the flag `--auth=cert` and +point the `--tlscacert` flag to a trusted CA certificate. In the daemon mode, it will only allow connections from clients authenticated by a certificate signed by that CA. In the client mode, @@ -24,7 +71,7 @@ it will only connect to servers with a certificate signed by that CA. > Mac OS X comes with a version of OpenSSL that is incompatible with the > certificates that Docker requires. -## Create a CA, server and client keys with OpenSSL +### Create a CA, server and client keys with OpenSSL First, initialize the CA serial file and generate CA private and public keys: @@ -135,34 +182,17 @@ need to provide your client keys, certificates and trusted CA: > daemon, giving them root access to the machine hosting the daemon. Guard > these keys as you would a root password! -## Secure by default - -If you want to secure your Docker client connections by default, you can move -the files to the `.docker` directory in your home directory - and set the -`DOCKER_HOST` and `DOCKER_TLS_VERIFY` variables as well (instead of passing -`-H=tcp://:2376` and `--tlsverify` on every call). - - $ cp ca.pem ~/.docker/ca.pem - $ cp cert.pem ~/.docker/cert.pem - $ cp key.pem ~/.docker/key.pem - $ export DOCKER_HOST=tcp://:2376 - $ export DOCKER_TLS_VERIFY=1 - -Docker will now connect securely by default: - - $ sudo docker ps - -## Other modes +### Other certificate-based modes If you don't want to have complete two-way authentication, you can run Docker in various other modes by mixing the flags. -### Daemon modes +#### Daemon modes - `tlsverify`, `tlscacert`, `tlscert`, `tlskey` set: Authenticate clients - `tls`, `tlscert`, `tlskey`: Do not authenticate clients -### Client modes +#### Client modes - `tls`: Authenticate server based on public/default CA pool - `tlsverify`, `tlscacert`: Authenticate server based on given CA @@ -179,7 +209,7 @@ location using the environment variable `DOCKER_CERT_PATH`. $ export DOCKER_CERT_PATH=${HOME}/.docker/zone1/ $ sudo docker --tlsverify ps -### Connecting to the Secure Docker port using `curl` +#### Connecting to the Secure Docker port using `curl` To use `curl` to make test API requests, you need to use three extra command line flags: From 756bc274ce9dc9099549c156b22746b4e6dcdd89 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 7 Nov 2014 13:31:58 +0000 Subject: [PATCH 03/18] Fail integration CLI tests if daemon exits Signed-off-by: Ben Firshman --- integration-cli/docker_utils.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index ba1a0b1306b17..87549c5f6477e 100644 --- a/integration-cli/docker_utils.go +++ b/integration-cli/docker_utils.go @@ -154,6 +154,12 @@ func (d *Daemon) Start(arg ...string) error { d.t.Log("daemon started") return nil + case <-d.wait: + output, err := ioutil.ReadFile(d.logFile.Name()) + if err != nil { + return fmt.Errorf("Daemon exited before it start serving requests. Error reading output from daemon: %s", err) + } + return fmt.Errorf("Daemon exited before it started serving requests. Output:\n%s", output) } } } From 2c7732ca851ead0c6bbdeb738b99491bbf0363f7 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 5 Nov 2014 16:31:09 +0000 Subject: [PATCH 04/18] Add cli tests for existing TLS behaviour - Removed integration tests which are covered by new tests. - Disabled tests on Drone for same reason as 07cedaab8640a68c480cb90722cb08fcf0a2c043 Signed-off-by: Ben Firshman --- .drone.yml | 1 + integration-cli/docker_cli_auth_test.go | 183 ++++++++++++++++++ integration-cli/docker_cli_run_test.go | 25 --- .../fixtures/https/ca.pem | 0 .../fixtures/https/client-cert.pem | 0 .../fixtures/https/client-key.pem | 0 .../fixtures/https/client-rogue-cert.pem | 0 .../fixtures/https/client-rogue-key.pem | 0 .../fixtures/https/server-cert.pem | 0 .../fixtures/https/server-key.pem | 0 .../fixtures/https/server-rogue-cert.pem | 0 .../fixtures/https/server-rogue-key.pem | 0 integration/https_test.go | 97 ---------- integration/runtime_test.go | 88 ++------- 14 files changed, 196 insertions(+), 198 deletions(-) create mode 100644 integration-cli/docker_cli_auth_test.go rename {integration => integration-cli}/fixtures/https/ca.pem (100%) rename {integration => integration-cli}/fixtures/https/client-cert.pem (100%) rename {integration => integration-cli}/fixtures/https/client-key.pem (100%) rename {integration => integration-cli}/fixtures/https/client-rogue-cert.pem (100%) rename {integration => integration-cli}/fixtures/https/client-rogue-key.pem (100%) rename {integration => integration-cli}/fixtures/https/server-cert.pem (100%) rename {integration => integration-cli}/fixtures/https/server-key.pem (100%) rename {integration => integration-cli}/fixtures/https/server-rogue-cert.pem (100%) rename {integration => integration-cli}/fixtures/https/server-rogue-key.pem (100%) delete mode 100644 integration/https_test.go diff --git a/.drone.yml b/.drone.yml index 53e00e0fdae92..204e01549f9fc 100755 --- a/.drone.yml +++ b/.drone.yml @@ -7,6 +7,7 @@ script: # Setup the DockerInDocker environment. - hack/dind # Tests relying on StartWithBusybox make Drone time out. + - rm integration-cli/docker_cli_auth_test.go - rm integration-cli/docker_cli_daemon_test.go - rm integration-cli/docker_cli_exec_test.go # Validate and test. diff --git a/integration-cli/docker_cli_auth_test.go b/integration-cli/docker_cli_auth_test.go new file mode 100644 index 0000000000000..62629975e0dad --- /dev/null +++ b/integration-cli/docker_cli_auth_test.go @@ -0,0 +1,183 @@ +package main + +import ( + "os/exec" + "strings" + "testing" +) + +const testDaemonURL = "tcp://localhost:4272" + +func TestAuthTLS(t *testing.T) { + d := NewDaemon(t) + if err := d.Start( + "-H", testDaemonURL, + "--tls", + "--tlscert", "fixtures/https/server-cert.pem", + "--tlskey", "fixtures/https/server-key.pem", + ); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + defer d.Stop() + + // ensure basic TLS connection + cmd := exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--tls", + "info", + ) + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + // ensure basic TLS connection is triggered with --tlsverify=false + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--tlsverify=false", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + // ensure verification with CA works + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--tlsverify", + "--tlscacert", "fixtures/https/ca.pem", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + // ensure connecting to server without TLS enabled fails + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command without --tls should have failed: %s", out) + } + if !strings.Contains(out, "malformed HTTP response") { + t.Errorf("command output should have contained 'malformed HTTP response': %s", out) + } + + logDone("auth - client verifying server with TLS options") +} + +func TestAuthTLSVerifyWithBadCert(t *testing.T) { + d := NewDaemon(t) + if err := d.Start( + "-H", testDaemonURL, + "--tls", + "--tlscert", "fixtures/https/server-rogue-cert.pem", + "--tlskey", "fixtures/https/server-rogue-key.pem", + ); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + defer d.Stop() + + // ensure verification fails + cmd := exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--tlsverify", + "--tlscacert", "fixtures/https/ca.pem", + "info", + ) + out, _, err := runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command without cert and key should have failed: %s", out) + } + if !strings.Contains(out, "certificate signed by unknown authority") { + t.Errorf("command output should have contained 'certificate signed by unknown authority': %s", out) + } + + logDone("auth - client rejects bad certificate with TLS options") +} + +func TestAuthTLSClientCert(t *testing.T) { + d := NewDaemon(t) + if err := d.Start( + "-H", testDaemonURL, + "--tlsverify", + "--tlscert", "fixtures/https/server-cert.pem", + "--tlskey", "fixtures/https/server-key.pem", + "--tlscacert", "fixtures/https/ca.pem", + ); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + defer d.Stop() + + // ensure basic client cert works + cmd := exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--tls", + "--tlscert", "fixtures/https/client-cert.pem", + "--tlskey", "fixtures/https/client-key.pem", + "info", + ) + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + // ensure client cert with --tlsverify works + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--tlsverify", + "--tlscert", "fixtures/https/client-cert.pem", + "--tlskey", "fixtures/https/client-key.pem", + "--tlscacert", "fixtures/https/ca.pem", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + // ensure not passing certs fails + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--tls", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command should have failed: %s", out) + } + if !strings.Contains(out, "bad certificate") { + t.Errorf("command output should have contained 'bad certificate': %s", out) + } + + // ensure passing incorrect certs fails + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--tls", + "--tlscert", "fixtures/https/client-rogue-cert.pem", + "--tlskey", "fixtures/https/client-rogue-key.pem", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command should have failed: %s", out) + } + if !strings.Contains(out, "bad certificate") { + t.Errorf("command output should have contained 'bad certificate': %s", out) + } + + logDone("auth - client certificates with TLS options") +} diff --git a/integration-cli/docker_cli_run_test.go b/integration-cli/docker_cli_run_test.go index 9546af0014c78..ed9dcf472055c 100644 --- a/integration-cli/docker_cli_run_test.go +++ b/integration-cli/docker_cli_run_test.go @@ -2688,28 +2688,3 @@ func TestContainerNetworkMode(t *testing.T) { logDone("run - container shared network namespace") } - -func TestRunTLSverify(t *testing.T) { - cmd := exec.Command(dockerBinary, "ps") - out, ec, err := runCommandWithOutput(cmd) - if err != nil || ec != 0 { - t.Fatalf("Should have worked: %v:\n%v", err, out) - } - - // Regardless of whether we specify true or false we need to - // test to make sure tls is turned on if --tlsverify is specified at all - - cmd = exec.Command(dockerBinary, "--tlsverify=false", "ps") - out, ec, err = runCommandWithOutput(cmd) - if err == nil || ec == 0 || !strings.Contains(out, "trying to connect") { - t.Fatalf("Should have failed: \nec:%v\nout:%v\nerr:%v", ec, out, err) - } - - cmd = exec.Command(dockerBinary, "--tlsverify=true", "ps") - out, ec, err = runCommandWithOutput(cmd) - if err == nil || ec == 0 || !strings.Contains(out, "cert") { - t.Fatalf("Should have failed: \nec:%v\nout:%v\nerr:%v", ec, out, err) - } - - logDone("run - verify tls is set for --tlsverify") -} diff --git a/integration/fixtures/https/ca.pem b/integration-cli/fixtures/https/ca.pem similarity index 100% rename from integration/fixtures/https/ca.pem rename to integration-cli/fixtures/https/ca.pem diff --git a/integration/fixtures/https/client-cert.pem b/integration-cli/fixtures/https/client-cert.pem similarity index 100% rename from integration/fixtures/https/client-cert.pem rename to integration-cli/fixtures/https/client-cert.pem diff --git a/integration/fixtures/https/client-key.pem b/integration-cli/fixtures/https/client-key.pem similarity index 100% rename from integration/fixtures/https/client-key.pem rename to integration-cli/fixtures/https/client-key.pem diff --git a/integration/fixtures/https/client-rogue-cert.pem b/integration-cli/fixtures/https/client-rogue-cert.pem similarity index 100% rename from integration/fixtures/https/client-rogue-cert.pem rename to integration-cli/fixtures/https/client-rogue-cert.pem diff --git a/integration/fixtures/https/client-rogue-key.pem b/integration-cli/fixtures/https/client-rogue-key.pem similarity index 100% rename from integration/fixtures/https/client-rogue-key.pem rename to integration-cli/fixtures/https/client-rogue-key.pem diff --git a/integration/fixtures/https/server-cert.pem b/integration-cli/fixtures/https/server-cert.pem similarity index 100% rename from integration/fixtures/https/server-cert.pem rename to integration-cli/fixtures/https/server-cert.pem diff --git a/integration/fixtures/https/server-key.pem b/integration-cli/fixtures/https/server-key.pem similarity index 100% rename from integration/fixtures/https/server-key.pem rename to integration-cli/fixtures/https/server-key.pem diff --git a/integration/fixtures/https/server-rogue-cert.pem b/integration-cli/fixtures/https/server-rogue-cert.pem similarity index 100% rename from integration/fixtures/https/server-rogue-cert.pem rename to integration-cli/fixtures/https/server-rogue-cert.pem diff --git a/integration/fixtures/https/server-rogue-key.pem b/integration-cli/fixtures/https/server-rogue-key.pem similarity index 100% rename from integration/fixtures/https/server-rogue-key.pem rename to integration-cli/fixtures/https/server-rogue-key.pem diff --git a/integration/https_test.go b/integration/https_test.go deleted file mode 100644 index 0705dc812fe42..0000000000000 --- a/integration/https_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package docker - -import ( - "crypto/tls" - "crypto/x509" - "io/ioutil" - "strings" - "testing" - "time" - - "github.com/docker/docker/api/client" - "github.com/docker/libtrust" -) - -const ( - errBadCertificate = "remote error: bad certificate" - errCaUnknown = "x509: certificate signed by unknown authority" -) - -func getTlsConfig(certFile, keyFile string, t *testing.T) *tls.Config { - certPool := x509.NewCertPool() - file, err := ioutil.ReadFile("fixtures/https/ca.pem") - if err != nil { - t.Fatal(err) - } - certPool.AppendCertsFromPEM(file) - - cert, err := tls.LoadX509KeyPair("fixtures/https/"+certFile, "fixtures/https/"+keyFile) - if err != nil { - t.Fatalf("Couldn't load X509 key pair: %s", err) - } - tlsConfig := &tls.Config{ - RootCAs: certPool, - Certificates: []tls.Certificate{cert}, - } - return tlsConfig -} - -// TestHttpsInfo connects via two-way authenticated HTTPS to the info endpoint -func TestHttpsInfo(t *testing.T) { - key, err := libtrust.GenerateECP256PrivateKey() - if err != nil { - t.Fatal(err) - } - cli := client.NewDockerCli(nil, ioutil.Discard, ioutil.Discard, key, testDaemonProto, - testDaemonHttpsAddr, getTlsConfig("client-cert.pem", "client-key.pem", t)) - - setTimeout(t, "Reading command output time out", 10*time.Second, func() { - if err := cli.CmdInfo(); err != nil { - t.Fatal(err) - } - }) -} - -// TestHttpsInfoRogueCert connects via two-way authenticated HTTPS to the info endpoint -// by using a rogue client certificate and checks that it fails with the expected error. -func TestHttpsInfoRogueCert(t *testing.T) { - key, err := libtrust.GenerateECP256PrivateKey() - if err != nil { - t.Fatal(err) - } - cli := client.NewDockerCli(nil, ioutil.Discard, ioutil.Discard, key, testDaemonProto, - testDaemonHttpsAddr, getTlsConfig("client-rogue-cert.pem", "client-rogue-key.pem", t)) - - setTimeout(t, "Reading command output time out", 10*time.Second, func() { - err := cli.CmdInfo() - if err == nil { - t.Fatal("Expected error but got nil") - } - if !strings.Contains(err.Error(), errBadCertificate) { - t.Fatalf("Expected error: %s, got instead: %s", errBadCertificate, err) - } - }) -} - -// TestHttpsInfoRogueServerCert connects via two-way authenticated HTTPS to the info endpoint -// which provides a rogue server certificate and checks that it fails with the expected error -func TestHttpsInfoRogueServerCert(t *testing.T) { - key, err := libtrust.GenerateECP256PrivateKey() - if err != nil { - t.Fatal(err) - } - cli := client.NewDockerCli(nil, ioutil.Discard, ioutil.Discard, key, testDaemonProto, - testDaemonRogueHttpsAddr, getTlsConfig("client-cert.pem", "client-key.pem", t)) - - setTimeout(t, "Reading command output time out", 10*time.Second, func() { - err := cli.CmdInfo() - if err == nil { - t.Fatal("Expected error but got nil") - } - - if !strings.Contains(err.Error(), errCaUnknown) { - t.Fatalf("Expected error: %s, got instead: %s", errBadCertificate, err) - } - - }) -} diff --git a/integration/runtime_test.go b/integration/runtime_test.go index 95aacc1156296..3e806085e6e9d 100644 --- a/integration/runtime_test.go +++ b/integration/runtime_test.go @@ -8,7 +8,6 @@ import ( "net" "net/url" "os" - "path" "path/filepath" "runtime" "strconv" @@ -30,27 +29,22 @@ import ( ) const ( - unitTestImageName = "docker-test-image" - unitTestImageID = "83599e29c455eb719f77d799bc7c51521b9551972f5a850d7ad265bc1b5292f6" // 1.0 - unitTestImageIDShort = "83599e29c455" - unitTestNetworkBridge = "testdockbr0" - unitTestStoreBase = "/var/lib/docker/unit-tests" - unitTestDockerTmpdir = "/var/lib/docker/tmp" - testDaemonAddr = "127.0.0.1:4270" - testDaemonProto = "tcp" - testDaemonHttpsProto = "tcp" - testDaemonHttpsAddr = "localhost:4271" - testDaemonRogueHttpsAddr = "localhost:4272" + unitTestImageName = "docker-test-image" + unitTestImageID = "83599e29c455eb719f77d799bc7c51521b9551972f5a850d7ad265bc1b5292f6" // 1.0 + unitTestImageIDShort = "83599e29c455" + unitTestNetworkBridge = "testdockbr0" + unitTestStoreBase = "/var/lib/docker/unit-tests" + unitTestDockerTmpdir = "/var/lib/docker/tmp" + testDaemonAddr = "127.0.0.1:4270" + testDaemonProto = "tcp" ) var ( // FIXME: globalDaemon is deprecated by globalEngine. All tests should be converted. - globalDaemon *daemon.Daemon - globalEngine *engine.Engine - globalHttpsEngine *engine.Engine - globalRogueHttpsEngine *engine.Engine - startFds int - startGoroutines int + globalDaemon *daemon.Daemon + globalEngine *engine.Engine + startFds int + startGoroutines int ) // FIXME: nuke() is deprecated by Daemon.Nuke() @@ -122,8 +116,6 @@ func init() { // Create the "global daemon" with a long-running daemons for integration tests spawnGlobalDaemon() - spawnLegitHttpsDaemon() - spawnRogueHttpsDaemon() startFds, startGoroutines = utils.GetTotalUsedFds(), runtime.NumGoroutine() } @@ -176,62 +168,6 @@ func spawnGlobalDaemon() { } } -func spawnLegitHttpsDaemon() { - if globalHttpsEngine != nil { - return - } - globalHttpsEngine = spawnHttpsDaemon(testDaemonHttpsAddr, "fixtures/https/ca.pem", - "fixtures/https/server-cert.pem", "fixtures/https/server-key.pem") -} - -func spawnRogueHttpsDaemon() { - if globalRogueHttpsEngine != nil { - return - } - globalRogueHttpsEngine = spawnHttpsDaemon(testDaemonRogueHttpsAddr, "fixtures/https/ca.pem", - "fixtures/https/server-rogue-cert.pem", "fixtures/https/server-rogue-key.pem") -} - -func spawnHttpsDaemon(addr, cacert, cert, key string) *engine.Engine { - t := std_log.New(os.Stderr, "", 0) - root, err := newTestDirectory(unitTestStoreBase) - if err != nil { - t.Fatal(err) - } - // FIXME: here we don't use NewTestEngine because it configures the daemon with Autorestart=false, - // and we want to set it to true. - - eng := newTestEngine(t, true, root) - - // Spawn a Daemon - go func() { - log.Debugf("Spawning https daemon for integration tests") - listenURL := &url.URL{ - Scheme: testDaemonHttpsProto, - Host: addr, - } - job := eng.Job("serveapi", listenURL.String()) - job.SetenvBool("Logging", true) - job.SetenvBool("Tls", true) - job.SetenvBool("TlsVerify", true) - job.Setenv("TlsCa", cacert) - job.Setenv("TlsCert", cert) - job.Setenv("TlsKey", key) - job.Setenv("TrustKey", path.Join(unitTestStoreBase, ".docker", "key.json")) - if err := job.Run(); err != nil { - log.Fatalf("Unable to spawn the test daemon: %s", err) - } - }() - - // Give some time to ListenAndServer to actually start - time.Sleep(time.Second) - - if err := eng.Job("acceptconnections").Run(); err != nil { - log.Fatalf("Unable to accept connections for test api: %s", err) - } - return eng -} - // FIXME: test that ImagePull(json=true) send correct json output func GetTestImage(daemon *daemon.Daemon) *image.Image { From 1f16770dccf72392b069a065d25a9ef8869609d2 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 5 Nov 2014 15:00:34 +0000 Subject: [PATCH 05/18] Move default flag values to flags.go Signed-off-by: Ben Firshman --- docker/docker.go | 9 --------- docker/flags.go | 9 +++++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docker/docker.go b/docker/docker.go index 7eabdcd0b7af7..598879c59b791 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -21,15 +21,6 @@ import ( "github.com/docker/libtrust" ) -const ( - defaultTrustKeyFile = "key.json" - defaultHostKeysFile = "allowed_hosts.json" - defaultClientKeysFile = "authorized_keys.json" - defaultCaFile = "ca.pem" - defaultKeyFile = "key.pem" - defaultCertFile = "cert.pem" -) - func main() { if reexec.Init() { return diff --git a/docker/flags.go b/docker/flags.go index 4ca9652005efa..b4262a49d87d3 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -10,6 +10,15 @@ import ( flag "github.com/docker/docker/pkg/mflag" ) +const ( + defaultTrustKeyFile = "key.json" + defaultHostKeysFile = "allowed_hosts.json" + defaultClientKeysFile = "authorized_keys.json" + defaultCaFile = "ca.pem" + defaultKeyFile = "key.pem" + defaultCertFile = "cert.pem" +) + var ( dockerCertPath = os.Getenv("DOCKER_CERT_PATH") dockerTlsVerify = os.Getenv("DOCKER_TLS_VERIFY") != "" From 08b167413f5422ca41e6d3ee40cdf538f317c9e0 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 5 Nov 2014 15:23:42 +0000 Subject: [PATCH 06/18] Add auth option to switch between auth methods - Can switch between "identity", "cert" and "none" - Replaced --insecure with --auth=none - Moved logic to generate TLS configs to separate auth files - Renamed --tlscert -> --auth-cert - Renamed --tlskey -> --auth-key - Renamed --tlscacert -> --auth-ca - Add DOCKER_AUTH_{CERT,KEY,CA} envvars for configuring cert auth. - Added backwards compatibility for --tls... options. The existing tests for this have been left to ensure correct behaviour. The DOCKER_CERT_PATH environment variable has been disabled for these new options. Changing behaviour on whether files exist in the default location (~/.docker/cert.pem, etc) seems dangerous if these files are accidentally missing. This option was added in the first place to make boot2docker work with cert auth, but now we have identity auth, this is unnecessary. Signed-off-by: Ben Firshman --- api/client/auth.go | 159 +++++++++++ api/common.go | 2 +- api/server/auth.go | 102 +++++++ api/server/server.go | 114 ++------ docker/daemon.go | 10 +- docker/docker.go | 192 +++---------- docker/flags.go | 47 +++- docs/sources/articles/https.md | 81 +++--- integration-cli/docker_cli_auth_test.go | 257 ++++++++++++++++++ .../fixtures/https/private-key-1.json | 8 + .../fixtures/https/private-key-2.json | 8 + .../fixtures/https/private-key-3.json | 8 + .../fixtures/https/public-key-1.json | 12 + .../fixtures/https/public-key-2.json | 12 + .../fixtures/https/public-key-3.json | 12 + integration/runtime_test.go | 2 +- 16 files changed, 717 insertions(+), 309 deletions(-) create mode 100644 api/client/auth.go create mode 100644 api/server/auth.go create mode 100644 integration-cli/fixtures/https/private-key-1.json create mode 100644 integration-cli/fixtures/https/private-key-2.json create mode 100644 integration-cli/fixtures/https/private-key-3.json create mode 100644 integration-cli/fixtures/https/public-key-1.json create mode 100644 integration-cli/fixtures/https/public-key-2.json create mode 100644 integration-cli/fixtures/https/public-key-3.json diff --git a/api/client/auth.go b/api/client/auth.go new file mode 100644 index 0000000000000..a2124048c796d --- /dev/null +++ b/api/client/auth.go @@ -0,0 +1,159 @@ +package client + +import ( + "bufio" + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "log" + "os" + "strings" + "time" + + "github.com/docker/libtrust" +) + +// NewIdentityAuthTLSConfig creates a tls.Config for the client to use for +// libtrust identity authentication +func NewIdentityAuthTLSConfig(trustKey libtrust.PrivateKey, knownHostsPath, proto, addr string) (*tls.Config, error) { + tlsConfig := createTLSConfig() + + // Load known hosts + knownHosts, err := libtrust.LoadKeySetFile(knownHostsPath) + if err != nil { + return nil, fmt.Errorf("Could not load trusted hosts file: %s", err) + } + + // Generate CA pool from known hosts + allowedHosts, err := libtrust.FilterByHosts(knownHosts, addr, false) + if err != nil { + return nil, fmt.Errorf("Error filtering hosts: %s", err) + } + certPool, err := libtrust.GenerateCACertPool(trustKey, allowedHosts) + if err != nil { + return nil, fmt.Errorf("Could not create CA pool: %s", err) + } + tlsConfig.ServerName = "docker" + tlsConfig.RootCAs = certPool + + // Generate client cert from trust key + x509Cert, err := libtrust.GenerateSelfSignedClientCert(trustKey) + if err != nil { + return nil, fmt.Errorf("Certificate generation error: %s", err) + } + tlsConfig.Certificates = []tls.Certificate{{ + Certificate: [][]byte{x509Cert.Raw}, + PrivateKey: trustKey.CryptoPrivateKey(), + Leaf: x509Cert, + }} + + // Connect to server to see if it is a known host + tlsConfig.InsecureSkipVerify = true + testConn, err := tls.Dial(proto, addr, tlsConfig) + if err != nil { + return nil, fmt.Errorf("TLS Handshake error: %s", err) + } + opts := x509.VerifyOptions{ + Roots: tlsConfig.RootCAs, + CurrentTime: time.Now(), + DNSName: tlsConfig.ServerName, + Intermediates: x509.NewCertPool(), + } + + certs := testConn.ConnectionState().PeerCertificates + for i, cert := range certs { + if i == 0 { + continue + } + opts.Intermediates.AddCert(cert) + } + _, err = certs[0].Verify(opts) + if err != nil { + if _, ok := err.(x509.UnknownAuthorityError); ok { + pubKey, err := libtrust.FromCryptoPublicKey(certs[0].PublicKey) + if err != nil { + return nil, fmt.Errorf("Error extracting public key from certificate: %s", err) + } + + // If server is not a known host, prompt user to ask whether it should + // be trusted and add to the known hosts file + if promptUnknownKey(pubKey, addr) { + pubKey.AddExtendedField("hosts", []string{addr}) + err = libtrust.AddKeySetFile(knownHostsPath, pubKey) + if err != nil { + return nil, fmt.Errorf("Error saving updated host keys file: %s", err) + } + + ca, err := libtrust.GenerateCACert(trustKey, pubKey) + if err != nil { + return nil, fmt.Errorf("Error generating CA: %s", err) + } + tlsConfig.RootCAs.AddCert(ca) + } else { + return nil, fmt.Errorf("Cancelling request due to invalid certificate") + } + } else { + return nil, fmt.Errorf("TLS verification error: %s", err) + } + } + + testConn.Close() + tlsConfig.InsecureSkipVerify = false + + return tlsConfig, nil +} + +// NewCertAuthTLSConfig creates a tls.Config for the client to use for +// certificate authentication +func NewCertAuthTLSConfig(caPath, certPath, keyPath string) (*tls.Config, error) { + tlsConfig := createTLSConfig() + + // Verify the server against a CA certificate? + if caPath != "" { + certPool := x509.NewCertPool() + file, err := ioutil.ReadFile(caPath) + if err != nil { + return nil, fmt.Errorf("Couldn't read ca cert %s: %s", caPath, err) + } + certPool.AppendCertsFromPEM(file) + tlsConfig.RootCAs = certPool + } else { + tlsConfig.InsecureSkipVerify = true + } + + // Try to load and send client certificates + if certPath != "" && keyPath != "" { + _, errCert := os.Stat(certPath) + _, errKey := os.Stat(keyPath) + if errCert == nil && errKey == nil { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("Couldn't load X509 key pair: %s. Key encrypted?", err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + } + return tlsConfig, nil +} + +// createTLSConfig creates the base tls.Config used by auth methods with some +// sensible defaults +func createTLSConfig() *tls.Config { + return &tls.Config{ + // Avoid fallback to SSL protocols < TLS1.0 + MinVersion: tls.VersionTLS10, + } +} + +func promptUnknownKey(key libtrust.PublicKey, host string) bool { + fmt.Printf("The authenticity of host %q can't be established.\nRemote key ID %s\n", host, key.KeyID()) + fmt.Printf("Are you sure you want to continue connecting (yes/no)? ") + reader := bufio.NewReader(os.Stdin) + line, _, err := reader.ReadLine() + if err != nil { + log.Fatalf("Error reading input: %s", err) + } + input := strings.TrimSpace(strings.ToLower(string(line))) + return input == "yes" || input == "y" +} diff --git a/api/common.go b/api/common.go index 3a46a8a5237a2..71e72f69e0275 100644 --- a/api/common.go +++ b/api/common.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/engine" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/version" - "github.com/docker/docker/vendor/src/github.com/docker/libtrust" + "github.com/docker/libtrust" ) const ( diff --git a/api/server/auth.go b/api/server/auth.go new file mode 100644 index 0000000000000..a6497081562a1 --- /dev/null +++ b/api/server/auth.go @@ -0,0 +1,102 @@ +package server + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net" + + "github.com/docker/libtrust" +) + +// NewIdentityAuthTLSConfig creates a tls.Config for the server to use for +// libtrust identity authentication +func NewIdentityAuthTLSConfig(trustKey libtrust.PrivateKey, trustClientsPath, addr string) (*tls.Config, error) { + tlsConfig := createTLSConfig() + + // Load authorized keys file + clients, err := libtrust.LoadKeySetFile(trustClientsPath) + if err != nil { + return nil, fmt.Errorf("unable to load authorized keys: %s", err) + } + + // Create a CA pool from authorized keys + certPool, err := libtrust.GenerateCACertPool(trustKey, clients) + if err != nil { + return nil, fmt.Errorf("CA pool generation error: %s", err) + } + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.ClientCAs = certPool + + // Generate cert + ips, domains, err := parseAddr(addr) + if err != nil { + return nil, err + } + // add default docker domain for docker clients to look for + domains = append(domains, "docker") + x509Cert, err := libtrust.GenerateSelfSignedServerCert(trustKey, domains, ips) + if err != nil { + return nil, fmt.Errorf("certificate generation error: %s", err) + } + tlsConfig.Certificates = []tls.Certificate{{ + Certificate: [][]byte{x509Cert.Raw}, + PrivateKey: trustKey.CryptoPrivateKey(), + Leaf: x509Cert, + }} + + return tlsConfig, nil +} + +// NewCertAuthTLSConfig creates a tls.Config for the server to use for +// certificate authentication +func NewCertAuthTLSConfig(caPath, certPath, keyPath string) (*tls.Config, error) { + tlsConfig := createTLSConfig() + + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?", certPath, keyPath, err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + + // Verify client certificates against a CA? + if caPath != "" { + certPool := x509.NewCertPool() + file, err := ioutil.ReadFile(caPath) + if err != nil { + return nil, fmt.Errorf("Couldn't read CA certificate: %s", err) + } + certPool.AppendCertsFromPEM(file) + + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.ClientCAs = certPool + } + + return tlsConfig, nil +} + +func createTLSConfig() *tls.Config { + return &tls.Config{ + NextProtos: []string{"http/1.1"}, + // Avoid fallback on insecure SSL protocols + MinVersion: tls.VersionTLS10, + } +} + +// parseAddr parses an address into an array of IPs and domains +func parseAddr(addr string) ([]net.IP, []string, error) { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, nil, err + } + var domains []string + var ips []net.IP + ip := net.ParseIP(host) + if ip != nil { + ips = []net.IP{ip} + } else { + domains = []string{host} + } + return ips, domains, nil +} diff --git a/api/server/server.go b/api/server/server.go index 8906afe809b47..876be2ac18943 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -14,7 +14,6 @@ import ( "net/http" "net/http/pprof" "os" - "path" "strconv" "strings" "syscall" @@ -33,7 +32,6 @@ import ( "github.com/docker/docker/pkg/version" "github.com/docker/docker/registry" "github.com/docker/docker/utils" - "github.com/docker/libtrust" ) var ( @@ -1425,23 +1423,6 @@ func changeGroup(addr string, nameOrGid string) error { return os.Chown(addr, 0, gid) } -// parseAddr parses an address into an array of IPs and domains -func parseAddr(addr string) ([]net.IP, []string, error) { - host, _, err := net.SplitHostPort(addr) - if err != nil { - return nil, nil, err - } - var domains []string - var ips []net.IP - ip := net.ParseIP(host) - if ip != nil { - ips = []net.IP{ip} - } else { - domains = []string{host} - } - return ips, domains, nil -} - func setSocketGroup(addr, group string) error { if group == "" { return nil @@ -1486,10 +1467,6 @@ func setupUnixHttp(addr string, job *engine.Job) (*HttpServer, error) { } func setupTcpHttp(addr string, job *engine.Job) (*HttpServer, error) { - if job.GetenvBool("Insecure") { - log.Infof("/!\\ DON'T BIND INSECURELY ON A TCP ADDRESS IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") - } - r, err := createRouter(job.Eng, job.GetenvBool("Logging"), job.GetenvBool("EnableCors"), job.Getenv("Version")) if err != nil { return nil, err @@ -1500,84 +1477,33 @@ func setupTcpHttp(addr string, job *engine.Job) (*HttpServer, error) { return nil, err } - if !job.GetenvBool("Insecure") { - trustKeyFile := job.Getenv("TrustKey") - err = os.MkdirAll(path.Dir(trustKeyFile), 0700) - if err != nil { - return nil, fmt.Errorf("Error creating directory: %s", err) - } - trustKey, err := libtrust.LoadKeyFile(trustKeyFile) - if err == libtrust.ErrKeyFileDoesNotExist { - trustKey, err = libtrust.GenerateECP256PrivateKey() - if err != nil { - return nil, fmt.Errorf("Error generating key: %s", err) - } - if err := libtrust.SaveKey(trustKeyFile, trustKey); err != nil { - return nil, fmt.Errorf("Error saving key file: %s", err) - } - } else if err != nil { - return nil, fmt.Errorf("Error loading key file: %s", err) - } - - var cert tls.Certificate - tlsCert := job.Getenv("TlsCert") - tlsKey := job.Getenv("TlsKey") - if tlsCert != "" && tlsKey != "" { - var err error - cert, err = tls.LoadX509KeyPair(tlsCert, tlsKey) - if err != nil { - if !os.IsNotExist(err) { - return nil, fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?", - tlsCert, tlsKey, err) - } - ips, domains, err := parseAddr(addr) - if err != nil { - return nil, err - } - // add default docker domain for docker clients to look for - domains = append(domains, "docker") - x509Cert, err := libtrust.GenerateSelfSignedServerCert(trustKey, domains, ips) - if err != nil { - return nil, fmt.Errorf("certificate generation error: %s", err) - } - cert = tls.Certificate{ - Certificate: [][]byte{x509Cert.Raw}, - PrivateKey: trustKey.CryptoPrivateKey(), - Leaf: x509Cert, - } - } + var tlsConfig *tls.Config + switch job.Getenv("Auth") { + case "identity": + trustKey, err := api.LoadOrCreateTrustKey(job.Getenv("TrustKey")) + if err != nil { + return nil, err } - - tlsConfig := &tls.Config{ - NextProtos: []string{"http/1.1"}, - Certificates: []tls.Certificate{cert}, - // Avoid fallback on insecure SSL protocols - MinVersion: tls.VersionTLS10, + if tlsConfig, err = NewIdentityAuthTLSConfig(trustKey, job.Getenv("TrustClients"), addr); err != nil { + return nil, fmt.Errorf("Error creating TLS config: %s", err) } - - // Load authorized keys file - clients, err := libtrust.LoadKeySetFile(job.Getenv("TrustClients")) - if err != nil { - return nil, fmt.Errorf("unable to load authorized keys: %s", err) + case "cert": + if tlsConfig, err = NewCertAuthTLSConfig(job.Getenv("AuthCa"), job.Getenv("AuthCert"), job.Getenv("AuthKey")); err != nil { + return nil, fmt.Errorf("Error creating TLS config: %s", err) } + case "none": + tlsConfig = nil + default: + return nil, fmt.Errorf("Unknown auth method: %s", job.Getenv("Auth")) + } - if job.GetenvBool("TlsVerify") { - certPool, poolErr := libtrust.GenerateCACertPool(trustKey, clients) - if poolErr != nil { - return nil, fmt.Errorf("CA pool generation error: %s", poolErr) - } - file, err := ioutil.ReadFile(job.Getenv("TlsCa")) - if err != nil && !os.IsNotExist(err) { - return nil, fmt.Errorf("Couldn't read CA certificate: %s", err) - } - certPool.AppendCertsFromPEM(file) - - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - tlsConfig.ClientCAs = certPool - } + if tlsConfig == nil { + log.Infof("/!\\ DON'T BIND INSECURELY ON A TCP ADDRESS IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") + } else { l = tls.NewListener(l, tlsConfig) } + return &HttpServer{&http.Server{Addr: addr, Handler: r}, l}, nil } diff --git a/docker/daemon.go b/docker/daemon.go index b37c0e6461b6b..1f72e416d90ce 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -82,12 +82,10 @@ func mainDaemon() { job.Setenv("Version", dockerversion.VERSION) job.Setenv("SocketGroup", *flSocketGroup) - job.SetenvBool("Insecure", *flInsecure) - job.SetenvBool("Tls", *flTls) - job.SetenvBool("TlsVerify", *flTlsVerify) - job.Setenv("TlsCa", *flCa) - job.Setenv("TlsCert", *flCert) - job.Setenv("TlsKey", *flKey) + job.Setenv("Auth", *flAuth) + job.Setenv("AuthCa", *flAuthCa) + job.Setenv("AuthCert", *flAuthCert) + job.Setenv("AuthKey", *flAuthKey) job.Setenv("TrustKey", *flTrustKey) job.Setenv("TrustClients", *flTrustClients) job.SetenvBool("BufferRequests", true) diff --git a/docker/docker.go b/docker/docker.go index 598879c59b791..d9754ed32dd5d 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -1,15 +1,10 @@ package main import ( - "bufio" "crypto/tls" - "crypto/x509" "fmt" - "io/ioutil" "os" - "path" "strings" - "time" log "github.com/Sirupsen/logrus" "github.com/docker/docker/api" @@ -18,7 +13,6 @@ import ( flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/reexec" "github.com/docker/docker/utils" - "github.com/docker/libtrust" ) func main() { @@ -51,6 +45,29 @@ func main() { initLogging(log.DebugLevel) } + // Backwards compatibility for deprecated --tls and --tlsverify options + if *flTls || flag.IsSet("-tlsverify") { + *flAuth = "cert" + + // Backwards compatibility for --tlscacert + if *flCa != "" { + *flAuthCa = *flCa + } + // Backwards compatibility for --tlscert + if *flCert != "" { + *flAuthCert = *flCert + } + // Backwards compatibility for --tlskey + if *flKey != "" { + *flAuthKey = *flKey + } + + // Only verify against a CA if --tlsverify is set + if !*flTlsVerify { + *flAuthCa = "" + } + } + if len(flHosts) == 0 { defaultHost := os.Getenv("DOCKER_HOST") if defaultHost == "" || *flDaemon { @@ -74,152 +91,33 @@ func main() { log.Fatal("Please specify only one -H") } protoAddrParts := strings.SplitN(flHosts[0], "://", 2) + proto, addr := protoAddrParts[0], protoAddrParts[1] - err := os.MkdirAll(path.Dir(*flTrustKey), 0700) + trustKey, err := api.LoadOrCreateTrustKey(*flTrustKey) if err != nil { log.Fatal(err) } - trustKey, err := libtrust.LoadKeyFile(*flTrustKey) - if err == libtrust.ErrKeyFileDoesNotExist { - trustKey, err = libtrust.GenerateECP256PrivateKey() - if err != nil { - log.Fatalf("Error generating key: %s", err) - } - if err := libtrust.SaveKey(*flTrustKey, trustKey); err != nil { - log.Fatalf("Error saving key file: %s", err) - } - } else if err != nil { - log.Fatalf("Error loading key file: %s", err) - } - - var ( - cli *client.DockerCli - tlsConfig tls.Config - ) - - // Load known hosts - knownHosts, err := libtrust.LoadKeySetFile(*flTrustHosts) - if err != nil { - log.Fatalf("Could not load trusted hosts file: %s", err) - } - // Regardless of whether the user sets it to true or false, if they - // specify --tlsverify at all then we need to turn on tls - if flag.IsSet("-tlsverify") { - *flTls = true - } + var tlsConfig *tls.Config - // If we should verify the server, we need to load a trusted ca - if *flTlsVerify { - allowedHosts, err := libtrust.FilterByHosts(knownHosts, protoAddrParts[1], false) - if err != nil { - log.Fatalf("Error filtering hosts: %s", err) - } - certPool, err := libtrust.GenerateCACertPool(trustKey, allowedHosts) - if err != nil { - log.Fatalf("Could not create CA pool: %s", err) - } - if *flCa != "" { - file, err := ioutil.ReadFile(*flCa) - if err != nil { - if !os.IsNotExist(err) { - log.Fatalf("Couldn't read ca cert %s: %s", *flCa, err) - } else { - tlsConfig.ServerName = "docker" - } - } else { - certPool.AppendCertsFromPEM(file) + if proto != "unix" { + switch *flAuth { + case "identity": + if tlsConfig, err = client.NewIdentityAuthTLSConfig(trustKey, *flTrustHosts, proto, addr); err != nil { + log.Fatal(err) } - } else { - tlsConfig.ServerName = "docker" - } - tlsConfig.RootCAs = certPool - tlsConfig.InsecureSkipVerify = false - } - - // If tls is enabled, try to load and send client certificates - if *flTls || *flTlsVerify { - *flTls = true - cert, err := tls.LoadX509KeyPair(*flCert, *flKey) - if err != nil { - if !os.IsNotExist(err) { - log.Fatalf("Couldn't load X509 key pair: %s. Key encrypted?", err) - } else { - x509Cert, certErr := libtrust.GenerateSelfSignedClientCert(trustKey) - if certErr != nil { - log.Fatalf("Certificate generation error: %s", certErr) - } - cert = tls.Certificate{ - Certificate: [][]byte{x509Cert.Raw}, - PrivateKey: trustKey.CryptoPrivateKey(), - Leaf: x509Cert, - } - } - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - - if protoAddrParts[0] != "unix" && (!*flInsecure) { - savedInsecure := tlsConfig.InsecureSkipVerify - tlsConfig.InsecureSkipVerify = true - testConn, connErr := tls.Dial(protoAddrParts[0], protoAddrParts[1], &tlsConfig) - if connErr != nil { - log.Fatalf("TLS Handshake error: %s", connErr) - } - opts := x509.VerifyOptions{ - Roots: tlsConfig.RootCAs, - CurrentTime: time.Now(), - DNSName: tlsConfig.ServerName, - Intermediates: x509.NewCertPool(), - } - - certs := testConn.ConnectionState().PeerCertificates - for i, cert := range certs { - if i == 0 { - continue + case "cert": + if tlsConfig, err = client.NewCertAuthTLSConfig(*flAuthCa, *flAuthCert, *flAuthKey); err != nil { + log.Fatal(err) } - opts.Intermediates.AddCert(cert) + case "none": + tlsConfig = nil + default: + log.Fatalf("Unknown auth method: %s", *flAuth) } - _, err = certs[0].Verify(opts) - if err != nil { - if _, ok := err.(x509.UnknownAuthorityError); ok { - pubKey, err := libtrust.FromCryptoPublicKey(certs[0].PublicKey) - if err != nil { - log.Fatalf("Error extracting public key from certificate: %s", err) - } - - if promptUnknownKey(pubKey, protoAddrParts[1]) { - pubKey.AddExtendedField("hosts", []string{protoAddrParts[1]}) - err = libtrust.AddKeySetFile(*flTrustHosts, pubKey) - if err != nil { - log.Fatalf("Error saving updated host keys file: %s", err) - } - - ca, err := libtrust.GenerateCACert(trustKey, pubKey) - if err != nil { - log.Fatalf("Error generating CA: %s", err) - } - tlsConfig.RootCAs.AddCert(ca) - } else { - log.Fatalf("Cancelling request due to invalid certificate") - } - } else { - log.Fatalf("TLS verification error: %s", connErr) - } - } - - testConn.Close() - tlsConfig.InsecureSkipVerify = savedInsecure - - // Avoid fallback to SSL protocols < TLS1.0 - tlsConfig.MinVersion = tls.VersionTLS10 } - if protoAddrParts[0] == "unix" || *flInsecure { - cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], nil) - } else { - cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig) - } + cli := client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, trustKey, proto, addr, tlsConfig) if err := cli.Cmd(flag.Args()...); err != nil { if sterr, ok := err.(*utils.StatusError); ok { @@ -232,18 +130,6 @@ func main() { } } -func promptUnknownKey(key libtrust.PublicKey, host string) bool { - fmt.Printf("The authenticity of host %q can't be established.\nRemote key ID %s\n", host, key.KeyID()) - fmt.Printf("Are you sure you want to continue connecting (yes/no)? ") - reader := bufio.NewReader(os.Stdin) - line, _, err := reader.ReadLine() - if err != nil { - log.Fatalf("Error reading input: %s", err) - } - input := strings.TrimSpace(strings.ToLower(string(line))) - return input == "yes" || input == "y" -} - func showVersion() { fmt.Printf("Docker version %s, build %s\n", dockerversion.VERSION, dockerversion.GITCOMMIT) } diff --git a/docker/flags.go b/docker/flags.go index b4262a49d87d3..183ef3f7101e0 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -20,16 +20,16 @@ const ( ) var ( + dockerAuth = os.Getenv("DOCKER_AUTH") + dockerAuthCert = os.Getenv("DOCKER_AUTH_CERT") + dockerAuthKey = os.Getenv("DOCKER_AUTH_KEY") + dockerAuthCa = os.Getenv("DOCKER_AUTH_CA") + + // Deprecated TLS environment variables dockerCertPath = os.Getenv("DOCKER_CERT_PATH") dockerTlsVerify = os.Getenv("DOCKER_TLS_VERIFY") != "" ) -func init() { - if dockerCertPath == "" { - dockerCertPath = filepath.Join(getHomeDir(), ".docker") - } -} - func getHomeDir() string { if runtime.GOOS == "windows" { return os.Getenv("USERPROFILE") @@ -44,11 +44,15 @@ var ( flSocketGroup = flag.String([]string{"G", "-group"}, "docker", "Group to assign the unix socket specified by -H when running in daemon mode\nuse '' (the empty string) to disable setting of a group") flLogLevel = flag.String([]string{"l", "-log-level"}, "info", "Set the logging level") flEnableCors = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API") - flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by --tlsverify flag") - flTlsVerify = flag.Bool([]string{"-tlsverify"}, dockerTlsVerify, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)") - flInsecure = flag.Bool([]string{"-insecure"}, false, "Use insecure non-TLS connections") + flAuthCa = flag.String([]string{"-auth-ca"}, dockerAuthCa, "CA to verify remotes against in cert auth mode") + flAuthCert = flag.String([]string{"-auth-cert"}, dockerAuthCert, "Certificate to present to remote in cert auth mode") + flAuthKey = flag.String([]string{"-auth-key"}, dockerAuthKey, "Private key for cert auth") + // Deprecated TLS options + flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by --tlsverify=true") + flTlsVerify = flag.Bool([]string{"-tlsverify"}, dockerTlsVerify, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)") // these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs + flAuth *string flTrustKey *string flTrustHosts *string flTrustClients *string @@ -59,14 +63,27 @@ var ( ) func init() { - flTrustHosts = flag.String([]string{"-allowed-hosts-file"}, filepath.Join(dockerCertPath, defaultHostKeysFile), "Path to file containing allowed hosts") - flTrustClients = flag.String([]string{"-authorized-keys-file"}, filepath.Join(dockerCertPath, defaultClientKeysFile), "Path to file containing authorized keys") - flTrustKey = flag.String([]string{"i", "-identity"}, filepath.Join(dockerCertPath, defaultTrustKeyFile), "Path to libtrust key file") - flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust only remotes providing a certificate signed by the CA given here") - flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file") - flKey = flag.String([]string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file") + dockerHome := filepath.Join(getHomeDir(), ".docker") + + if dockerAuth == "" { + dockerAuth = "none" + } + if dockerCertPath == "" { + dockerCertPath = dockerHome + } + + flAuth = flag.String([]string{"-auth"}, dockerAuth, "Method used to authenticate the connection between client and daemon. Possible methods: identity, cert, none") + flTrustHosts = flag.String([]string{"-auth-known-hosts"}, filepath.Join(dockerHome, defaultHostKeysFile), "Path to file containing known hosts for identity auth") + flTrustClients = flag.String([]string{"-auth-authorized-keys"}, filepath.Join(dockerHome, defaultClientKeysFile), "Path to file containing authorized keys identity auth") + flTrustKey = flag.String([]string{"i", "-identity"}, filepath.Join(dockerHome, defaultTrustKeyFile), "Path to libtrust key file") + opts.HostListVar(&flHosts, []string{"H", "-host"}, "The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.") + // Deprecated TLS options + flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Deprecated: Trust only remotes providing a certificate signed by the CA given here") + flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Deprecated: Path to TLS certificate file") + flKey = flag.String([]string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Deprecated: Path to TLS key file") + flag.Usage = func() { fmt.Fprint(os.Stderr, "Usage: docker [OPTIONS] COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nOptions:\n") diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 33432f113d8c2..b063950684765 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -13,7 +13,7 @@ to allow client connections. There are two different ways of authenticating connections between Docker client and daemon, both of which use secure TLS connections. - - **Host-based authentication** uses an authorized keys list on the daemon + - **Identity-based authentication** uses an authorized keys list on the daemon to whitelist client connections. The client must also accept the daemon's key and remember it for future connections. - **Certificate-based authentication** uses a certificate authority to @@ -21,12 +21,12 @@ authorize connections. Using this method requires additional setup to enable client authentication. The authentication method is selected using the `--auth` flag with values - `host`, `cert`, or `none` . `host` is the default method and `none` + `identity`, `cert`, or `none` . `identity` is the default method and `none` should only be used with caution. -## Host-based authentication +## Identity-based authentication -Host-based authentication is similar to how SSH does authentication. When +Identity-based authentication is similar to how SSH does authentication. When connecting to a daemon for the first time, a client will ask whether a user trusts a fingerprint of the daemon’s public key. If they do, the public key will be stored so it does not prompt on subsequent connections. For the daemon @@ -34,12 +34,13 @@ to authenticate the client, each client automatically generates its own key (~/.docker/key.json) which is presented to the daemon and checked against a list of keys authorized to connect (~/.docker/authorized_keys.json). -To enable host-based authentication, add the flag `--auth=host`. The default -identity and authorization files may be overridden through the flags: +To enable identity-based authentication, add the flag `--auth=identity`. +The default identity and authorization files may be overridden through the +flags: - - `--identity` specifies the key file to use. This file contains the client's private -key and its fingerprint is used by the daemon to identify the client. This file -should be secured. + - `--identity` specifies the key file to use. This file contains the client's +private key and its fingerprint is used by the daemon to identify the client. +This file should be secured. - `--auth-authorized-keys` - specifies the client whitelist. This is a daemon configuration and should have its write permissions restricted. - `--auth-allowed-host` - specifies the list of daemon public key fingerprints @@ -56,23 +57,42 @@ certificate against a CA, or do full two-way authentication by getting the server to also verify the client’s certificate. To enable certificate-based authentication, add the flag `--auth=cert` and -point the `--tlscacert` flag to a trusted CA certificate. +point the `--auth-ca` flag to a trusted CA certificate. In the daemon mode, it will only allow connections from clients authenticated by a certificate signed by that CA. In the client mode, it will only connect to servers with a certificate signed by that CA. -> **Warning**: +### Client configuration + +To enable certificate-based authentication, use the `--auth=cert` option. By +default, this will use the public CA pool. You want to use a specific CA, +specify its path with the `--auth-ca` option. + +If the server requires client certificate authentication, you can provide this +with the `--auth-cert` and `--auth-key` options. + +### Daemon configuration + +When running the daemon with the `--auth=cert` option, it will serve a TLS +connection that the client can verify against its CA certificate. You must +provide a keypair for the client to check with the `--auth-cert` and +`--auth-key` options. + +If you also want the client to authenticate with a client certificate, you can +pass a CA to check the certificate against with the `--auth-ca` option. + +### Create a CA, server and client keys with OpenSSL + +> **Warning**: > Using TLS and managing a CA is an advanced topic. Please familiarize yourself > with OpenSSL, x509 and TLS before using it in production. > **Warning**: > These TLS commands will only generate a working set of certificates on Linux. -> Mac OS X comes with a version of OpenSSL that is incompatible with the +> Mac OS X comes with a version of OpenSSL that is incompatible with the > certificates that Docker requires. -### Create a CA, server and client keys with OpenSSL - First, initialize the CA serial file and generate CA private and public keys: @@ -163,43 +183,26 @@ Finally, you need to remove the passphrase from the client and server key: Now you can make the Docker daemon only accept connections from clients providing a certificate trusted by our CA: - $ sudo docker -d --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem \ + $ sudo docker -d --auth=cert --auth-ca=ca.pem --auth-cert=server-cert.pem --auth-key=server-key.pem \ -H=0.0.0.0:2376 To be able to connect to Docker and validate its certificate, you now need to provide your client keys, certificates and trusted CA: - $ sudo docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \ + $ sudo docker --auth=cert --auth-ca=ca.pem --auth-cert=cert.pem --auth-key=key.pem \ -H=dns-name-of-docker-host:2376 version > **Note**: > Docker over TLS should run on TCP port 2376. -> **Warning**: +> **Warning**: > As shown in the example above, you don't have to run the `docker` client > with `sudo` or the `docker` group when you use certificate authentication. > That means anyone with the keys can give any instructions to your Docker > daemon, giving them root access to the machine hosting the daemon. Guard > these keys as you would a root password! -### Other certificate-based modes - -If you don't want to have complete two-way authentication, you can run -Docker in various other modes by mixing the flags. - -#### Daemon modes - - - `tlsverify`, `tlscacert`, `tlscert`, `tlskey` set: Authenticate clients - - `tls`, `tlscert`, `tlskey`: Do not authenticate clients - -#### Client modes - - - `tls`: Authenticate server based on public/default CA pool - - `tlsverify`, `tlscacert`: Authenticate server based on given CA - - `tls`, `tlscert`, `tlskey`: Authenticate with client certificate, do not - authenticate server based on given CA - - `tlsverify`, `tlscacert`, `tlscert`, `tlskey`: Authenticate with client - certificate and authenticate server based on given CA +### Automatic configuration If found, the client will send its client certificate, so you just need to drop your keys into `~/.docker/.pem`. Alternatively, @@ -207,11 +210,11 @@ if you want to store your keys in another location, you can specify that location using the environment variable `DOCKER_CERT_PATH`. $ export DOCKER_CERT_PATH=${HOME}/.docker/zone1/ - $ sudo docker --tlsverify ps + $ sudo docker --auth=cert ps -#### Connecting to the Secure Docker port using `curl` +### Connecting to the Secure Docker port using `curl` -To use `curl` to make test API requests, you need to use three extra command line -flags: +To use `curl` to make test API requests, you need to use three extra command +line flags: $ curl --insecure --cert ~/.docker/cert.pem --key ~/.docker/key.pem https://boot2docker:2376/images/json` diff --git a/integration-cli/docker_cli_auth_test.go b/integration-cli/docker_cli_auth_test.go index 62629975e0dad..3318fdf3292b5 100644 --- a/integration-cli/docker_cli_auth_test.go +++ b/integration-cli/docker_cli_auth_test.go @@ -8,6 +8,262 @@ import ( const testDaemonURL = "tcp://localhost:4272" +func TestAuthIdentityWithGoodKeys(t *testing.T) { + d := NewDaemon(t) + if err := d.Start( + "-H", testDaemonURL, + "--auth", "identity", + "--identity", "fixtures/https/private-key-1.json", + "--auth-authorized-keys", "fixtures/https/public-key-2.json", + ); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + defer d.Stop() + + // ensure basic connection + cmd := exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "identity", + "--identity", "fixtures/https/private-key-2.json", + "--auth-known-hosts", "fixtures/https/public-key-1.json", + "info", + ) + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + logDone("auth - identity mode with good keys") +} + +func TestAuthIdentityWithBadServerKey(t *testing.T) { + d := NewDaemon(t) + if err := d.Start( + "-H", testDaemonURL, + "--auth", "identity", + "--identity", "fixtures/https/private-key-3.json", + "--auth-authorized-keys", "fixtures/https/public-key-2.json", + ); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + defer d.Stop() + + // ensure basic connection + cmd := exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "identity", + "--identity", "fixtures/https/private-key-2.json", + "--auth-known-hosts", "fixtures/https/public-key-1.json", + "info", + ) + out, _, err := runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command should have failed: %s", out) + } + if !strings.Contains(out, "The authenticity of host \"localhost:4272\" can't be established.") { + t.Errorf("command output should have contained 'The authenticity of host \"localhost:4272\" can't be established.': %s", out) + } + + logDone("auth - identity mode with bad server key") +} + +func TestAuthIdentityWithBadClientKey(t *testing.T) { + d := NewDaemon(t) + if err := d.Start( + "-H", testDaemonURL, + "--auth", "identity", + "--identity", "fixtures/https/private-key-1.json", + "--auth-authorized-keys", "fixtures/https/public-key-2.json", + ); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + defer d.Stop() + + // ensure basic connection + cmd := exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "identity", + "--identity", "fixtures/https/private-key-3.json", + "--auth-known-hosts", "fixtures/https/public-key-1.json", + "info", + ) + out, _, err := runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command should have failed: %s", out) + } + if !strings.Contains(out, "remote error: bad certificate") { + t.Errorf("command output should have contained 'remote error: bad certificate': %s", out) + } + + logDone("auth - identity mode with bad client key") +} + +func TestAuthCert(t *testing.T) { + d := NewDaemon(t) + if err := d.Start( + "-H", testDaemonURL, + "--auth", "cert", + "--auth-cert", "fixtures/https/server-cert.pem", + "--auth-key", "fixtures/https/server-key.pem", + ); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + defer d.Stop() + + // ensure basic TLS connection + cmd := exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "cert", + "info", + ) + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + // ensure verification with CA works + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "cert", + "--auth-ca", "fixtures/https/ca.pem", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + // ensure connecting to server without TLS enabled fails + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "none", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command with --auth=none should have failed: %s", out) + } + if !strings.Contains(out, "malformed HTTP response") { + t.Errorf("command output should have contained 'malformed HTTP response': %s", out) + } + + logDone("auth - cert mode, client verifying server") +} + +func TestAuthCertWithBadCert(t *testing.T) { + d := NewDaemon(t) + if err := d.Start( + "-H", testDaemonURL, + "--auth", "cert", + "--auth-cert", "fixtures/https/server-rogue-cert.pem", + "--auth-key", "fixtures/https/server-rogue-key.pem", + ); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + defer d.Stop() + + // ensure verification fails + cmd := exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "cert", + "--auth-ca", "fixtures/https/ca.pem", + "info", + ) + out, _, err := runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command without cert and key should have failed: %s", out) + } + if !strings.Contains(out, "certificate signed by unknown authority") { + t.Errorf("command output should have contained 'certificate signed by unknown authority': %s", out) + } + + logDone("auth - cert mode, client rejects bad certificate") +} + +func TestAuthCertClientCert(t *testing.T) { + d := NewDaemon(t) + if err := d.Start( + "-H", testDaemonURL, + "--auth", "cert", + "--auth-cert", "fixtures/https/server-cert.pem", + "--auth-key", "fixtures/https/server-key.pem", + "--auth-ca", "fixtures/https/ca.pem", + ); err != nil { + t.Fatalf("Could not start daemon: %v", err) + } + defer d.Stop() + + // ensure basic client cert works + cmd := exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "cert", + "--auth-cert", "fixtures/https/client-cert.pem", + "--auth-key", "fixtures/https/client-key.pem", + "info", + ) + out, _, err := runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + // ensure client cert with --tlsverify works + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "cert", + "--auth-cert", "fixtures/https/client-cert.pem", + "--auth-key", "fixtures/https/client-key.pem", + "--auth-ca", "fixtures/https/ca.pem", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err != nil { + t.Fatalf("failed to execute command: %s, %v", out, err) + } + + // ensure not passing certs fails + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "cert", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command should have failed: %s", out) + } + if !strings.Contains(out, "bad certificate") { + t.Errorf("command output should have contained 'bad certificate': %s", out) + } + + // ensure passing incorrect certs fails + cmd = exec.Command( + dockerBinary, + "-H", testDaemonURL, + "--auth", "cert", + "--auth-cert", "fixtures/https/client-rogue-cert.pem", + "--auth-key", "fixtures/https/client-rogue-key.pem", + "info", + ) + out, _, err = runCommandWithOutput(cmd) + if err == nil { + t.Fatalf("command should have failed: %s", out) + } + if !strings.Contains(out, "bad certificate") { + t.Errorf("command output should have contained 'bad certificate': %s", out) + } + + logDone("auth - cert mode, client certificates") +} + func TestAuthTLS(t *testing.T) { d := NewDaemon(t) if err := d.Start( @@ -61,6 +317,7 @@ func TestAuthTLS(t *testing.T) { cmd = exec.Command( dockerBinary, "-H", testDaemonURL, + "--auth", "none", "info", ) out, _, err = runCommandWithOutput(cmd) diff --git a/integration-cli/fixtures/https/private-key-1.json b/integration-cli/fixtures/https/private-key-1.json new file mode 100644 index 0000000000000..c686fa52ca2c0 --- /dev/null +++ b/integration-cli/fixtures/https/private-key-1.json @@ -0,0 +1,8 @@ +{ + "crv": "P-256", + "d": "AITQyfsSpfi4d6ewvcs7-MsVct5EIXnJ8ZT_2iNlv70", + "kid": "FDTR:VRAS:HWMP:MVLO:XVN4:OQRA:QWUS:PMZX:ANM5:6AB3:IZVG:MELL", + "kty": "EC", + "x": "qt6kkQOC8GCGPxjHPaHkvbou0gX_4YXWzA2ExvmM7co", + "y": "QIOEwDF1pbSrcC0XIAHx9QeZG7qBX0rS5AhGtw-5X4Y" +} \ No newline at end of file diff --git a/integration-cli/fixtures/https/private-key-2.json b/integration-cli/fixtures/https/private-key-2.json new file mode 100644 index 0000000000000..02b2aeeed65b7 --- /dev/null +++ b/integration-cli/fixtures/https/private-key-2.json @@ -0,0 +1,8 @@ +{ + "crv": "P-256", + "d": "7n2qxPU2_4CuFQKSoSlObO3IyAjN0tCuhJUPhv5uqHo", + "kid": "2IUQ:R3EY:7DDM:TAIV:653R:QQVO:W5OZ:DEMO:XBHS:4ET7:6ZFO:D4UP", + "kty": "EC", + "x": "0rgsH0zVAY61DgSACt7FwGiAsvHNQaWL6028Sg-5NUM", + "y": "kU58U2i97SVN8ETuXd87aMkKfroLYY5ICKiiyDlNwfI" +} \ No newline at end of file diff --git a/integration-cli/fixtures/https/private-key-3.json b/integration-cli/fixtures/https/private-key-3.json new file mode 100644 index 0000000000000..6eded791d5336 --- /dev/null +++ b/integration-cli/fixtures/https/private-key-3.json @@ -0,0 +1,8 @@ +{ + "crv": "P-256", + "d": "yonHc66-w9m0bhHkNRKAsc94OQDgYWx_A9ynHB0IA8Q", + "kid": "4DFB:Z4JA:GT3U:ZJ52:6MWJ:I75B:UROI:2IUY:EAIA:PUIH:VOP4:MZP5", + "kty": "EC", + "x": "OdLw0U3X9kXz3bKBEG_0JZZglNRGk-QAsVl_zDHay7c", + "y": "Jfh6okMAYdsDcO8JNK1Rrisp_beHID4wq7O8rp3F0K0" +} \ No newline at end of file diff --git a/integration-cli/fixtures/https/public-key-1.json b/integration-cli/fixtures/https/public-key-1.json new file mode 100644 index 0000000000000..e4f25dae28b30 --- /dev/null +++ b/integration-cli/fixtures/https/public-key-1.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "hosts": ["localhost:4272"], + "crv": "P-256", + "kid": "FDTR:VRAS:HWMP:MVLO:XVN4:OQRA:QWUS:PMZX:ANM5:6AB3:IZVG:MELL", + "kty": "EC", + "x": "qt6kkQOC8GCGPxjHPaHkvbou0gX_4YXWzA2ExvmM7co", + "y": "QIOEwDF1pbSrcC0XIAHx9QeZG7qBX0rS5AhGtw-5X4Y" + } + ] +} diff --git a/integration-cli/fixtures/https/public-key-2.json b/integration-cli/fixtures/https/public-key-2.json new file mode 100644 index 0000000000000..ce71d2dbb86e4 --- /dev/null +++ b/integration-cli/fixtures/https/public-key-2.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "hosts": ["localhost:4272"], + "crv": "P-256", + "kid": "2IUQ:R3EY:7DDM:TAIV:653R:QQVO:W5OZ:DEMO:XBHS:4ET7:6ZFO:D4UP", + "kty": "EC", + "x": "0rgsH0zVAY61DgSACt7FwGiAsvHNQaWL6028Sg-5NUM", + "y": "kU58U2i97SVN8ETuXd87aMkKfroLYY5ICKiiyDlNwfI" + } + ] +} diff --git a/integration-cli/fixtures/https/public-key-3.json b/integration-cli/fixtures/https/public-key-3.json new file mode 100644 index 0000000000000..eb48b38eb916c --- /dev/null +++ b/integration-cli/fixtures/https/public-key-3.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "hosts": ["localhost:4272"], + "crv": "P-256", + "kid": "4DFB:Z4JA:GT3U:ZJ52:6MWJ:I75B:UROI:2IUY:EAIA:PUIH:VOP4:MZP5", + "kty": "EC", + "x": "OdLw0U3X9kXz3bKBEG_0JZZglNRGk-QAsVl_zDHay7c", + "y": "Jfh6okMAYdsDcO8JNK1Rrisp_beHID4wq7O8rp3F0K0" + } + ] +} diff --git a/integration/runtime_test.go b/integration/runtime_test.go index 3e806085e6e9d..c5ce84b340be8 100644 --- a/integration/runtime_test.go +++ b/integration/runtime_test.go @@ -153,7 +153,7 @@ func spawnGlobalDaemon() { } job := eng.Job("serveapi", listenURL.String()) job.SetenvBool("Logging", true) - job.SetenvBool("Insecure", true) + job.Setenv("Auth", "none") if err := job.Run(); err != nil { log.Fatalf("Unable to spawn the test daemon: %s", err) } From 19e3a1b582072cd441c63ff99e2ff0872a68a768 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 13 Nov 2014 16:09:59 -0800 Subject: [PATCH 07/18] Update auth flags and documentation - Change allowed-hosts to known-hosts since known hosts is a more familiar concept than allowed hosts for similar functionality - Add deprecation warning to old tls flags - Use dash for key files instead of underscore for consistency with other key files Signed-off-by: Derek McGowan (github: dmcgowan) --- docker/docker.go | 1 - docker/flags.go | 14 +++++++------- docs/man/docker.1.md | 3 +++ docs/sources/articles/https.md | 8 ++++---- docs/sources/reference/commandline/cli.md | 17 ++++++++++------- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/docker/docker.go b/docker/docker.go index d9754ed32dd5d..c89e341c50ccd 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -80,7 +80,6 @@ func main() { } flHosts = append(flHosts, defaultHost) } - *flTlsVerify = true if *flDaemon { mainDaemon() diff --git a/docker/flags.go b/docker/flags.go index 183ef3f7101e0..751bad59b2d61 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -12,8 +12,8 @@ import ( const ( defaultTrustKeyFile = "key.json" - defaultHostKeysFile = "allowed_hosts.json" - defaultClientKeysFile = "authorized_keys.json" + defaultHostKeysFile = "known-hosts.json" + defaultClientKeysFile = "authorized-keys.json" defaultCaFile = "ca.pem" defaultKeyFile = "key.pem" defaultCertFile = "cert.pem" @@ -48,8 +48,8 @@ var ( flAuthCert = flag.String([]string{"-auth-cert"}, dockerAuthCert, "Certificate to present to remote in cert auth mode") flAuthKey = flag.String([]string{"-auth-key"}, dockerAuthKey, "Private key for cert auth") // Deprecated TLS options - flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by --tlsverify=true") - flTlsVerify = flag.Bool([]string{"-tlsverify"}, dockerTlsVerify, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)") + flTls = flag.Bool([]string{"#-tls"}, false, "Use TLS; implied by --tlsverify=true") + flTlsVerify = flag.Bool([]string{"#-tlsverify"}, dockerTlsVerify, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)") // these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs flAuth *string @@ -80,9 +80,9 @@ func init() { opts.HostListVar(&flHosts, []string{"H", "-host"}, "The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.") // Deprecated TLS options - flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Deprecated: Trust only remotes providing a certificate signed by the CA given here") - flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Deprecated: Path to TLS certificate file") - flKey = flag.String([]string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Deprecated: Path to TLS key file") + flCa = flag.String([]string{"#-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Deprecated: Trust only remotes providing a certificate signed by the CA given here") + flCert = flag.String([]string{"#-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Deprecated: Path to TLS certificate file") + flKey = flag.String([]string{"#-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Deprecated: Path to TLS key file") flag.Usage = func() { fmt.Fprint(os.Stderr, "Usage: docker [OPTIONS] COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nOptions:\n") diff --git a/docs/man/docker.1.md b/docs/man/docker.1.md index c8d28b2c238a9..0970dc7653584 100644 --- a/docs/man/docker.1.md +++ b/docs/man/docker.1.md @@ -34,6 +34,9 @@ unix://[/path/to/socket] to use. **--api-enable-cors**=*true*|*false* Enable CORS headers in the remote API. Default is false. +**--auth**=identity|cert|none + Set authentication method for tcp socket. Default is `none`. + **-b**="" Attach containers to a pre\-existing network bridge; use 'none' to disable container networking diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index b063950684765..2774b353a0f10 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -21,8 +21,8 @@ authorize connections. Using this method requires additional setup to enable client authentication. The authentication method is selected using the `--auth` flag with values - `identity`, `cert`, or `none` . `identity` is the default method and `none` -should only be used with caution. + `identity`, `cert`, or `none` . `none` is the current default method but +`identity` will become the default in a future version. ## Identity-based authentication @@ -32,7 +32,7 @@ trusts a fingerprint of the daemon’s public key. If they do, the public key wi be stored so it does not prompt on subsequent connections. For the daemon to authenticate the client, each client automatically generates its own key (~/.docker/key.json) which is presented to the daemon and checked -against a list of keys authorized to connect (~/.docker/authorized_keys.json). +against a list of keys authorized to connect (~/.docker/authorized-keys.json). To enable identity-based authentication, add the flag `--auth=identity`. The default identity and authorization files may be overridden through the @@ -43,7 +43,7 @@ private key and its fingerprint is used by the daemon to identify the client. This file should be secured. - `--auth-authorized-keys` - specifies the client whitelist. This is a daemon configuration and should have its write permissions restricted. - - `--auth-allowed-host` - specifies the list of daemon public key fingerprints + - `--auth-known-hosts` - specifies the list of daemon public key fingerprints which have been approved by the user and the host name associated with each fingerprint. diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 07cc578eb2f5a..0bea967aa942d 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -84,11 +84,14 @@ expect an integer, and they can only be specified once. -s, --storage-driver="" Force the Docker runtime to use a specific storage driver --selinux-enabled=false Enable selinux support. SELinux does not presently support the BTRFS storage driver --storage-opt=[] Set storage driver options - --tls=false Use TLS; implied by --tlsverify flag - --tlscacert="/home/sven/.docker/ca.pem" Trust only remotes providing a certificate signed by the CA given here - --tlscert="/home/sven/.docker/cert.pem" Path to TLS certificate file - --tlskey="/home/sven/.docker/key.pem" Path to TLS key file - --tlsverify=false Use TLS and verify the remote (daemon: verify client, client: verify daemon) + --auth="none" Set authentication method when using tcp socket host value in -H or --host (none, identity, cert) + --auth-ca="" Path to CA certificate when using `cert` auth, only remotes providing a certificate signed by this CA are trusted + --auth-cert="" Path to TLS certificate file when using `cert` auth + --auth-key="" Path to TLS key file when using `cert` auth + --auth-authorized-keys="" Path to authorized keys file containing client whitelist when using `identity` auth + defaults to "~/.docker/authorized-keys.json" + --auth-known-hosts="" Path to known hosts file containing list of daemon public key fingerprints when using `identity` auth + defaults to "~/.docker/known-hosts.json" -v, --version=false Print version information and quit Options with [] may be specified multiple times. @@ -146,9 +149,9 @@ the `-H` flag for the client. # both are equal Setting the `DOCKER_TLS_VERIFY` environment variable to any value other than the empty -string is equivalent to setting the `--tlsverify` flag. The following are equivalent: +string is equivalent to setting the `--auth=cert` flag. The following are equivalent: - $ sudo docker --tlsverify ps + $ sudo docker --auth=cert ps # or $ export DOCKER_TLS_VERIFY=1 $ sudo docker ps From 904cdf15951f21075d3a68285b3bd5c364097ae6 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 21 Nov 2014 13:38:10 -0800 Subject: [PATCH 08/18] Create public key file along with private key Signed-off-by: Derek McGowan (github: dmcgowan) --- api/common.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/common.go b/api/common.go index 71e72f69e0275..94bcc1b267d3e 100644 --- a/api/common.go +++ b/api/common.go @@ -67,6 +67,11 @@ func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) { if err := libtrust.SaveKey(trustKeyPath, trustKey); err != nil { return nil, fmt.Errorf("Error saving key file: %s", err) } + dir, file := path.Split(trustKeyPath) + // Save public key + if err := libtrust.SavePublicKey(path.Join(dir, "public-"+file), trustKey.PublicKey()); err != nil { + return nil, fmt.Errorf("Error saving public key file: %s", err) + } } else if err != nil { return nil, fmt.Errorf("Error loading key file: %s", err) } From 64c40d32982d800c5613f81c9e8f548d4b8ec350 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 21 Nov 2014 16:28:11 -0800 Subject: [PATCH 09/18] Add support for authorized key directory Signed-off-by: Derek McGowan (github: dmcgowan) --- api/server/auth.go | 91 ++++++++++++++++++++--- api/server/server.go | 6 +- docker/daemon.go | 1 + docker/flags.go | 3 + docs/sources/articles/https.md | 13 +++- docs/sources/reference/commandline/cli.md | 2 + 6 files changed, 103 insertions(+), 13 deletions(-) diff --git a/api/server/auth.go b/api/server/auth.go index a6497081562a1..fe0a407544595 100644 --- a/api/server/auth.go +++ b/api/server/auth.go @@ -6,29 +6,100 @@ import ( "fmt" "io/ioutil" "net" + "os" + "path" + "sync" "github.com/docker/libtrust" ) -// NewIdentityAuthTLSConfig creates a tls.Config for the server to use for -// libtrust identity authentication -func NewIdentityAuthTLSConfig(trustKey libtrust.PrivateKey, trustClientsPath, addr string) (*tls.Config, error) { - tlsConfig := createTLSConfig() +// ClientKeyManager manages client keys on the filesystem +type ClientKeyManager struct { + key libtrust.PrivateKey + clientFile string + clientDir string + + clientLock sync.RWMutex + clients []libtrust.PublicKey + + configLock sync.Mutex + configs []*tls.Config +} + +// NewClientKeyManager loads a new manager from a set of key files +// and managed by the given private key. +func NewClientKeyManager(trustKey libtrust.PrivateKey, clientFile, clientDir string) (*ClientKeyManager, error) { + m := &ClientKeyManager{ + key: trustKey, + clientFile: clientFile, + clientDir: clientDir, + } + if err := m.loadKeys(); err != nil { + return nil, err + } + // TODO Start watching file and directory + return m, nil +} +func (c *ClientKeyManager) loadKeys() error { // Load authorized keys file - clients, err := libtrust.LoadKeySetFile(trustClientsPath) + clients, err := libtrust.LoadKeySetFile(c.clientFile) if err != nil { - return nil, fmt.Errorf("unable to load authorized keys: %s", err) + return fmt.Errorf("unable to load authorized keys: %s", err) + } + + // Add clients from authorized keys directory + files, err := ioutil.ReadDir(c.clientDir) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("unable to open authorized keys directory: %s", err) + } + for _, f := range files { + if !f.IsDir() { + publicKey, err := libtrust.LoadPublicKeyFile(path.Join(c.clientDir, f.Name())) + if err != nil { + return fmt.Errorf("unable to load authorized key file: %s", err) + } + clients = append(clients, publicKey) + } } - // Create a CA pool from authorized keys - certPool, err := libtrust.GenerateCACertPool(trustKey, clients) + c.clientLock.Lock() + c.clients = clients + c.clientLock.Unlock() + + return nil +} + +// RegisterTLSConfig registers a tls configuration to manager +// such that any changes to the keys may be reflected in +// the tls client CA pool +func (c *ClientKeyManager) RegisterTLSConfig(tlsConfig *tls.Config) error { + c.clientLock.RLock() + certPool, err := libtrust.GenerateCACertPool(c.key, c.clients) if err != nil { - return nil, fmt.Errorf("CA pool generation error: %s", err) + return fmt.Errorf("CA pool generation error: %s", err) } - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + c.clientLock.RUnlock() + tlsConfig.ClientCAs = certPool + c.configLock.Lock() + c.configs = append(c.configs, tlsConfig) + c.configLock.Unlock() + + return nil +} + +// NewIdentityAuthTLSConfig creates a tls.Config for the server to use for +// libtrust identity authentication +func NewIdentityAuthTLSConfig(trustKey libtrust.PrivateKey, clients *ClientKeyManager, addr string) (*tls.Config, error) { + tlsConfig := createTLSConfig() + + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + if err := clients.RegisterTLSConfig(tlsConfig); err != nil { + return nil, err + } + // Generate cert ips, domains, err := parseAddr(addr) if err != nil { diff --git a/api/server/server.go b/api/server/server.go index 876be2ac18943..3595b16aa8c0b 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -1485,7 +1485,11 @@ func setupTcpHttp(addr string, job *engine.Job) (*HttpServer, error) { if err != nil { return nil, err } - if tlsConfig, err = NewIdentityAuthTLSConfig(trustKey, job.Getenv("TrustClients"), addr); err != nil { + manager, err := NewClientKeyManager(trustKey, job.Getenv("TrustClients"), job.Getenv("TrustDir")) + if err != nil { + return nil, err + } + if tlsConfig, err = NewIdentityAuthTLSConfig(trustKey, manager, addr); err != nil { return nil, fmt.Errorf("Error creating TLS config: %s", err) } case "cert": diff --git a/docker/daemon.go b/docker/daemon.go index 1f72e416d90ce..e160d53a62217 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -88,6 +88,7 @@ func mainDaemon() { job.Setenv("AuthKey", *flAuthKey) job.Setenv("TrustKey", *flTrustKey) job.Setenv("TrustClients", *flTrustClients) + job.Setenv("TrustDir", *flTrustDir) job.SetenvBool("BufferRequests", true) if err := job.Run(); err != nil { log.Fatal(err) diff --git a/docker/flags.go b/docker/flags.go index 751bad59b2d61..8033d442d275a 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -14,6 +14,7 @@ const ( defaultTrustKeyFile = "key.json" defaultHostKeysFile = "known-hosts.json" defaultClientKeysFile = "authorized-keys.json" + defaultClientKeysDir = "authorized-keys.d" defaultCaFile = "ca.pem" defaultKeyFile = "key.pem" defaultCertFile = "cert.pem" @@ -56,6 +57,7 @@ var ( flTrustKey *string flTrustHosts *string flTrustClients *string + flTrustDir *string flCa *string flCert *string flKey *string @@ -75,6 +77,7 @@ func init() { flAuth = flag.String([]string{"-auth"}, dockerAuth, "Method used to authenticate the connection between client and daemon. Possible methods: identity, cert, none") flTrustHosts = flag.String([]string{"-auth-known-hosts"}, filepath.Join(dockerHome, defaultHostKeysFile), "Path to file containing known hosts for identity auth") flTrustClients = flag.String([]string{"-auth-authorized-keys"}, filepath.Join(dockerHome, defaultClientKeysFile), "Path to file containing authorized keys identity auth") + flTrustDir = flag.String([]string{"-auth-authorized-dir"}, filepath.Join(dockerHome, defaultClientKeysDir), "Path to directory containing authorized public key files for identity auth") flTrustKey = flag.String([]string{"i", "-identity"}, filepath.Join(dockerHome, defaultTrustKeyFile), "Path to libtrust key file") opts.HostListVar(&flHosts, []string{"H", "-host"}, "The socket(s) to bind to in daemon mode or connect to in client mode, specified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.") diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 2774b353a0f10..a0dcfe57e21ce 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -32,7 +32,7 @@ trusts a fingerprint of the daemon’s public key. If they do, the public key wi be stored so it does not prompt on subsequent connections. For the daemon to authenticate the client, each client automatically generates its own key (~/.docker/key.json) which is presented to the daemon and checked -against a list of keys authorized to connect (~/.docker/authorized-keys.json). +against a list of keys authorized to connect (~/.docker/authorized-keys.d/). To enable identity-based authentication, add the flag `--auth=identity`. The default identity and authorization files may be overridden through the @@ -41,12 +41,21 @@ flags: - `--identity` specifies the key file to use. This file contains the client's private key and its fingerprint is used by the daemon to identify the client. This file should be secured. - - `--auth-authorized-keys` - specifies the client whitelist. This is a daemon + - `--auth-authorized-keys` - specifies the client whitelist. This is a daemon configuration and should have its write permissions restricted. + - `--auth-authorized-dir` - alternative method for specifying the whitelisted +client public keys. Any public key file in this directory will be part of the +client whitelist, therefore write access to this directory should be restricted. - `--auth-known-hosts` - specifies the list of daemon public key fingerprints which have been approved by the user and the host name associated with each fingerprint. +To setup a new client connection, copy the `~/.docker/public-key.json` +file on the client machine to the `~/.docker/authorized-keys.d/` directory on +the daemon machine. The copied file should keep the same suffix (e.g. .json +.jwk .pem) but otherwise the name may be changed to something which +meaningfully identities the client to the user. + ## Certificate-based authentication Certificate-based authentication uses TLS certificates provided by a diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 0bea967aa942d..330519acc7466 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -90,6 +90,8 @@ expect an integer, and they can only be specified once. --auth-key="" Path to TLS key file when using `cert` auth --auth-authorized-keys="" Path to authorized keys file containing client whitelist when using `identity` auth defaults to "~/.docker/authorized-keys.json" + --auth-authorized-dir="" Path to authorized keys directory containing public key files to whitelist when using `identity` auth + defaults to "~/.docker/authorized-keys.d/" --auth-known-hosts="" Path to known hosts file containing list of daemon public key fingerprints when using `identity` auth defaults to "~/.docker/known-hosts.json" -v, --version=false Print version information and quit From 72fcdd0a9eb4aa59f1b5926e4489302580315f55 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 24 Nov 2014 17:57:47 +0000 Subject: [PATCH 10/18] Add note to docs about deprecated options Signed-off-by: Ben Firshman --- docs/sources/articles/https.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index a0dcfe57e21ce..3ed849308495b 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -24,6 +24,10 @@ The authentication method is selected using the `--auth` flag with values `identity`, `cert`, or `none` . `none` is the current default method but `identity` will become the default in a future version. +*Note:* The `--tls` and `--tlsverify` options in Docker 1.3 and earlier have +been replaced by the `--auth=cert` option. The old options have been +deprecated. + ## Identity-based authentication Identity-based authentication is similar to how SSH does authentication. When From 8d3d5bf98945c130679dc18cdfe7f0aeb480ea76 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 26 Nov 2014 09:22:24 -0800 Subject: [PATCH 11/18] Remove flag and reference to authorized keys file Signed-off-by: Derek McGowan (github: dmcgowan) --- api/server/auth.go | 10 +++++--- docker/daemon.go | 1 - docker/flags.go | 31 ++++++++++------------- docs/sources/articles/https.md | 8 +++--- docs/sources/reference/commandline/cli.md | 4 +-- 5 files changed, 25 insertions(+), 29 deletions(-) diff --git a/api/server/auth.go b/api/server/auth.go index fe0a407544595..75c292ecf75f8 100644 --- a/api/server/auth.go +++ b/api/server/auth.go @@ -43,9 +43,13 @@ func NewClientKeyManager(trustKey libtrust.PrivateKey, clientFile, clientDir str } func (c *ClientKeyManager) loadKeys() error { // Load authorized keys file - clients, err := libtrust.LoadKeySetFile(c.clientFile) - if err != nil { - return fmt.Errorf("unable to load authorized keys: %s", err) + var clients []libtrust.PublicKey + if c.clientFile != "" { + fileClients, err := libtrust.LoadKeySetFile(c.clientFile) + if err != nil { + return fmt.Errorf("unable to load authorized keys: %s", err) + } + clients = fileClients } // Add clients from authorized keys directory diff --git a/docker/daemon.go b/docker/daemon.go index e160d53a62217..0e6c68754e760 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -87,7 +87,6 @@ func mainDaemon() { job.Setenv("AuthCert", *flAuthCert) job.Setenv("AuthKey", *flAuthKey) job.Setenv("TrustKey", *flTrustKey) - job.Setenv("TrustClients", *flTrustClients) job.Setenv("TrustDir", *flTrustDir) job.SetenvBool("BufferRequests", true) if err := job.Run(); err != nil { diff --git a/docker/flags.go b/docker/flags.go index 8033d442d275a..f1b732de23d34 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -11,13 +11,12 @@ import ( ) const ( - defaultTrustKeyFile = "key.json" - defaultHostKeysFile = "known-hosts.json" - defaultClientKeysFile = "authorized-keys.json" - defaultClientKeysDir = "authorized-keys.d" - defaultCaFile = "ca.pem" - defaultKeyFile = "key.pem" - defaultCertFile = "cert.pem" + defaultTrustKeyFile = "key.json" + defaultHostKeysFile = "known-hosts.json" + defaultClientKeysDir = "authorized-keys.d" + defaultCaFile = "ca.pem" + defaultKeyFile = "key.pem" + defaultCertFile = "cert.pem" ) var ( @@ -53,15 +52,14 @@ var ( flTlsVerify = flag.Bool([]string{"#-tlsverify"}, dockerTlsVerify, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)") // these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs - flAuth *string - flTrustKey *string - flTrustHosts *string - flTrustClients *string - flTrustDir *string - flCa *string - flCert *string - flKey *string - flHosts []string + flAuth *string + flTrustKey *string + flTrustHosts *string + flTrustDir *string + flCa *string + flCert *string + flKey *string + flHosts []string ) func init() { @@ -76,7 +74,6 @@ func init() { flAuth = flag.String([]string{"-auth"}, dockerAuth, "Method used to authenticate the connection between client and daemon. Possible methods: identity, cert, none") flTrustHosts = flag.String([]string{"-auth-known-hosts"}, filepath.Join(dockerHome, defaultHostKeysFile), "Path to file containing known hosts for identity auth") - flTrustClients = flag.String([]string{"-auth-authorized-keys"}, filepath.Join(dockerHome, defaultClientKeysFile), "Path to file containing authorized keys identity auth") flTrustDir = flag.String([]string{"-auth-authorized-dir"}, filepath.Join(dockerHome, defaultClientKeysDir), "Path to directory containing authorized public key files for identity auth") flTrustKey = flag.String([]string{"i", "-identity"}, filepath.Join(dockerHome, defaultTrustKeyFile), "Path to libtrust key file") diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 3ed849308495b..21e85655bdc77 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -45,11 +45,9 @@ flags: - `--identity` specifies the key file to use. This file contains the client's private key and its fingerprint is used by the daemon to identify the client. This file should be secured. - - `--auth-authorized-keys` - specifies the client whitelist. This is a daemon -configuration and should have its write permissions restricted. - - `--auth-authorized-dir` - alternative method for specifying the whitelisted -client public keys. Any public key file in this directory will be part of the -client whitelist, therefore write access to this directory should be restricted. + - `--auth-authorized-keys` - specifies the directory containing the client public +key files to whitelist. This is a daemon configuration and the directory should +have its write permissions restricted. - `--auth-known-hosts` - specifies the list of daemon public key fingerprints which have been approved by the user and the host name associated with each fingerprint. diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 330519acc7466..3fa8f1e6943fc 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -88,9 +88,7 @@ expect an integer, and they can only be specified once. --auth-ca="" Path to CA certificate when using `cert` auth, only remotes providing a certificate signed by this CA are trusted --auth-cert="" Path to TLS certificate file when using `cert` auth --auth-key="" Path to TLS key file when using `cert` auth - --auth-authorized-keys="" Path to authorized keys file containing client whitelist when using `identity` auth - defaults to "~/.docker/authorized-keys.json" - --auth-authorized-dir="" Path to authorized keys directory containing public key files to whitelist when using `identity` auth + --auth-authorized-keys="" Path to authorized keys directory containing public key files to whitelist when using `identity` auth defaults to "~/.docker/authorized-keys.d/" --auth-known-hosts="" Path to known hosts file containing list of daemon public key fingerprints when using `identity` auth defaults to "~/.docker/known-hosts.json" From 01064f3b2101a0b27a80b07ffefd36ea25b53e38 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 26 Nov 2014 09:51:31 -0800 Subject: [PATCH 12/18] Fix documentation style Signed-off-by: Derek McGowan (github: dmcgowan) --- docs/sources/articles/https.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 21e85655bdc77..30c78da2e81ca 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -16,17 +16,18 @@ client and daemon, both of which use secure TLS connections. - **Identity-based authentication** uses an authorized keys list on the daemon to whitelist client connections. The client must also accept the daemon's key and remember it for future connections. - - **Certificate-based authentication** uses a certificate authority to + - **Certificate-based authentication** uses a Certificate Authority to authorize connections. Using this method requires additional setup to enable client authentication. The authentication method is selected using the `--auth` flag with values - `identity`, `cert`, or `none` . `none` is the current default method but + `identity`, `cert`, or `none` . The `none` method is currently the default but `identity` will become the default in a future version. -*Note:* The `--tls` and `--tlsverify` options in Docker 1.3 and earlier have -been replaced by the `--auth=cert` option. The old options have been -deprecated. +> **Note**: +> The `--tls` and `--tlsverify` options in Docker 1.3 and earlier have +> been replaced by the `--auth=cert` option. The old options have been +> deprecated. ## Identity-based authentication @@ -35,8 +36,10 @@ connecting to a daemon for the first time, a client will ask whether a user trusts a fingerprint of the daemon’s public key. If they do, the public key will be stored so it does not prompt on subsequent connections. For the daemon to authenticate the client, each client automatically generates its own -key (~/.docker/key.json) which is presented to the daemon and checked -against a list of keys authorized to connect (~/.docker/authorized-keys.d/). +key (`~/.docker/key.json`) which is presented to the daemon and checked +against a list of keys authorized to connect (`~/.docker/authorized-keys.d/`). +Every public key file in the authorized key directory represents a client which +is authorized to connect using that key. To enable identity-based authentication, add the flag `--auth=identity`. The default identity and authorization files may be overridden through the @@ -45,23 +48,23 @@ flags: - `--identity` specifies the key file to use. This file contains the client's private key and its fingerprint is used by the daemon to identify the client. This file should be secured. - - `--auth-authorized-keys` - specifies the directory containing the client public -key files to whitelist. This is a daemon configuration and the directory should -have its write permissions restricted. + - `--auth-authorized-keys` - specifies the directory containing the client +public key files to whitelist. This is a daemon configuration and the directory +should have its write permissions restricted. - `--auth-known-hosts` - specifies the list of daemon public key fingerprints which have been approved by the user and the host name associated with each fingerprint. To setup a new client connection, copy the `~/.docker/public-key.json` file on the client machine to the `~/.docker/authorized-keys.d/` directory on -the daemon machine. The copied file should keep the same suffix (e.g. .json -.jwk .pem) but otherwise the name may be changed to something which +the daemon machine. The copied file should keep the same suffix (e.g. `.json` +`.jwk` `.pem`) but otherwise the name may be changed to something which meaningfully identities the client to the user. ## Certificate-based authentication Certificate-based authentication uses TLS certificates provided by a -certificate authority. This is for advanced usage where you may want to +Certificate Authority (CA). This is for advanced usage where you may want to integrate Docker with other TLS-compatible tools or you may already use PKI within your organisation. You can just get the client to verify the server’s certificate against a CA, or do full two-way authentication by getting the From 0065d9b5d767a9d888f6bc8b5df35d8c4d0bdc43 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 26 Nov 2014 18:07:09 +0000 Subject: [PATCH 13/18] Add commas to list of extensions Signed-off-by: Ben Firshman --- docs/sources/articles/https.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/articles/https.md b/docs/sources/articles/https.md index 30c78da2e81ca..314b1bc9747cb 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -57,8 +57,8 @@ each fingerprint. To setup a new client connection, copy the `~/.docker/public-key.json` file on the client machine to the `~/.docker/authorized-keys.d/` directory on -the daemon machine. The copied file should keep the same suffix (e.g. `.json` -`.jwk` `.pem`) but otherwise the name may be changed to something which +the daemon machine. The copied file should keep the same suffix (e.g. `.json`, +`.jwk` or `.pem`) but otherwise the name may be changed to something which meaningfully identities the client to the user. ## Certificate-based authentication From 989fd89afd156ccd594fee35f245b8c6a6e00617 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 26 Nov 2014 17:58:32 +0000 Subject: [PATCH 14/18] Correctly document new auth environment variables Signed-off-by: Ben Firshman --- docs/sources/reference/commandline/cli.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 3fa8f1e6943fc..34eab24440cdf 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -148,12 +148,15 @@ the `-H` flag for the client. $ sudo docker ps # both are equal -Setting the `DOCKER_TLS_VERIFY` environment variable to any value other than the empty -string is equivalent to setting the `--auth=cert` flag. The following are equivalent: +Setting the `DOCKER_AUTH` environment variable to any value other than the empty +string is equivalent to setting the `--auth` flag. The environment variables +`DOCKER_AUTH_CA`, `DOCKER_AUTH_CERT` and `DOCKER_AUTH_KEY` also correspond with +flags of the same name. The following are equivalent: - $ sudo docker --auth=cert ps + $ sudo docker --auth=cert --auth-ca=ca.pem ps # or - $ export DOCKER_TLS_VERIFY=1 + $ export DOCKER_AUTH=cert + $ export DOCKER_AUTH_CA=ca.pem $ sudo docker ps ### Daemon storage-driver option From 134dcd12a7575b7c29b3d5c695e5943cc0044ce6 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 26 Nov 2014 18:03:49 +0000 Subject: [PATCH 15/18] Rename HTTPS docs to authentication Signed-off-by: Ben Firshman --- docs/mkdocs.yml | 2 +- .../articles/{https.md => authentication.md} | 6 +++--- docs/sources/articles/certificates.md | 13 +++++++------ docs/sources/articles/security.md | 2 +- docs/sources/reference/commandline/cli.md | 5 ++--- docs/sources/release-notes.md | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) rename docs/sources/articles/{https.md => authentication.md} (98%) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index f5ea845e95754..10fb386c41a85 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -85,7 +85,7 @@ pages: - ['articles/basics.md', 'Articles', 'Docker basics'] - ['articles/networking.md', 'Articles', 'Advanced networking'] - ['articles/security.md', 'Articles', 'Security'] -- ['articles/https.md', 'Articles', 'Running Docker with HTTPS'] +- ['articles/authentication.md', 'Articles', 'Authenticating to the Docker daemon'] - ['articles/host_integration.md', 'Articles', 'Automatically starting containers'] - ['articles/baseimages.md', 'Articles', 'Creating a base image'] - ['articles/dockerfile_best-practices.md', 'Articles', 'Best practices for writing Dockerfiles'] diff --git a/docs/sources/articles/https.md b/docs/sources/articles/authentication.md similarity index 98% rename from docs/sources/articles/https.md rename to docs/sources/articles/authentication.md index 314b1bc9747cb..73c996c9e25e5 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/authentication.md @@ -1,6 +1,6 @@ -page_title: Running Docker with HTTPS -page_description: How to setup and run Docker with HTTPS -page_keywords: docker, docs, article, example, https, daemon, tls, ca, certificate +page_title: Authenticating to the Docker daemon +page_description: How to setup the Docker daemon with encryption and authentication +page_keywords: docker, docs, article, example, https, daemon, tls, ca, certificate, authentication # Running Docker with https diff --git a/docs/sources/articles/certificates.md b/docs/sources/articles/certificates.md index e031676402f0d..807e100b0a5aa 100644 --- a/docs/sources/articles/certificates.md +++ b/docs/sources/articles/certificates.md @@ -4,9 +4,10 @@ page_keywords: Usage, registry, repository, client, root, certificate, docker, a # Using certificates for repository client verification -In [Running Docker with HTTPS](/articles/https), you learned that, by default, -Docker runs via a non-networked Unix socket and TLS must be enabled in order -to have the Docker client and the daemon communicate securely over HTTPS. +In [Authenticating to the Docker daemon](/articles/authentication), you learned +that, by default, Docker runs via a non-networked Unix socket and authentication +must be enabled in order to have the Docker client and the daemon communicate +securely. Now, you will see how to allow the Docker registry (i.e., *a server*) to verify that the Docker daemon (i.e., *a client*) has the right to access the @@ -45,15 +46,15 @@ Our example is set up like this: ## Creating the client certificates You will use OpenSSL's `genrsa` and `req` commands to first generate an RSA -key and then use the key to create the certificate request. +key and then use the key to create the certificate request. $ openssl genrsa -out client.key 1024 $ openssl req -new -x509 -text -key client.key -out client.cert -> **Warning:**: +> **Warning:**: > Using TLS and managing a CA is an advanced topic. > You should be familiar with OpenSSL, x509, and TLS before -> attempting to use them in production. +> attempting to use them in production. > **Warning:** > These TLS commands will only generate a working set of certificates on Linux. diff --git a/docs/sources/articles/security.md b/docs/sources/articles/security.md index 12f7b350ec185..0cd26ddfa484e 100644 --- a/docs/sources/articles/security.md +++ b/docs/sources/articles/security.md @@ -110,7 +110,7 @@ However, if you do that, being aware of the above mentioned security implication, you should ensure that it will be reachable only from a trusted network or VPN; or protected with e.g., `stunnel` and client SSL certificates. You can also secure them with [HTTPS and -certificates](/articles/https/). +certificates](/articles/authentication/). Recent improvements in Linux namespaces will soon allow to run full-featured containers without root privileges, thanks to the new user diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 34eab24440cdf..d218fa8106a89 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -114,7 +114,7 @@ requiring either `root` permission, or `docker` group membership. If you need to access the Docker daemon remotely, you need to enable the `tcp` Socket. Beware that the default setup provides un-encrypted and un-authenticated direct access to the Docker daemon - and should be secured either using the -[built in HTTPS encrypted socket](/articles/https/), or by putting a secure web +[built in authentication](/articles/authentication/), or by putting a secure web proxy in front of it. You can listen on port `2375` on all network interfaces with `-H tcp://0.0.0.0:2375`, or on a particular network interface using its IP address: `-H tcp://192.168.59.103:2375`. It is conventional to use port `2375` @@ -220,7 +220,7 @@ verification failed (i.e., wrong CA). By default, Docker assumes all, but local (see local registries below), registries are secure. Communicating with an insecure registry is not possible if Docker assumes that registry is secure. In order to communicate with an insecure registry, the Docker daemon requires `--insecure-registry` -in one of the following two forms: +in one of the following two forms: * `--insecure-registry myregistry:5000` tells the Docker daemon that myregistry:5000 should be considered insecure. * `--insecure-registry 10.1.0.0/16` tells the Docker daemon that all registries whose domain resolve to an IP address is part @@ -1669,4 +1669,3 @@ both Docker client and daemon. Usage: docker wait CONTAINER [CONTAINER...] Block until a container stops, then print its exit code. - diff --git a/docs/sources/release-notes.md b/docs/sources/release-notes.md index b1b3b2bfdf44e..71e018b35c065 100644 --- a/docs/sources/release-notes.md +++ b/docs/sources/release-notes.md @@ -90,7 +90,7 @@ line reference](/reference/commandline/cli/#run). * You can now set a `DOCKER_TLS_VERIFY` environment variable to secure connections by default (rather than having to pass the `--tlsverify` flag on -every call). For more information, see the [https guide](/articles/https). +every call). * Three security issues have been addressed in this release: [CVE-2014-5280, CVE-2014-5270, and From 831d09f8b535a1e2939bdacb02032d238e8dd249 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Thu, 27 Nov 2014 09:42:14 +1000 Subject: [PATCH 16/18] add redirect for https docs to authentication Docker-DCO-1.1-Signed-off-by: Sven Dowideit (github: SvenDowideit) --- docs/s3_website.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/s3_website.json b/docs/s3_website.json index 224ba816e28c4..f79de4cc9f474 100644 --- a/docs/s3_website.json +++ b/docs/s3_website.json @@ -26,7 +26,8 @@ { "Condition": { "KeyPrefixEquals": "use/host_integration/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "articles/host_integration/" } }, { "Condition": { "KeyPrefixEquals": "docker-io/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "docker-hub/" } }, { "Condition": { "KeyPrefixEquals": "examples/cfengine_process_management/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "articles/cfengine_process_management/" } }, - { "Condition": { "KeyPrefixEquals": "examples/https/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "articles/https/" } }, + { "Condition": { "KeyPrefixEquals": "examples/https/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "articles/authentication/" } }, + { "Condition": { "KeyPrefixEquals": "articles/https/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "articles/authentication/" } }, { "Condition": { "KeyPrefixEquals": "examples/ambassador_pattern_linking/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "articles/ambassador_pattern_linking/" } }, { "Condition": { "KeyPrefixEquals": "examples/using_supervisord/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "articles/using_supervisord/" } }, { "Condition": { "KeyPrefixEquals": "reference/api/registry_index_spec/" }, "Redirect": { "HostName": "$BUCKET", "ReplaceKeyPrefixWith": "reference/api/hub_registry_spec/" } }, From bbf1d8e71ebd88bda5d6fecfcf572da5348d9203 Mon Sep 17 00:00:00 2001 From: John Gossman Date: Wed, 10 Dec 2014 12:50:48 -0800 Subject: [PATCH 17/18] Use path/filepath instead of path (for Windows) the code is using path.Split, path.Join etc. instead of filepath.Split, filepath.Join. path assumes '/' separators, but Windows of course uses '\'. Talk about the gift that keeps on giving...I've been fixing bugs like this in portable code since 1983. See https://github.com/docker/machine/issues/76 --- api/common.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/common.go b/api/common.go index 94bcc1b267d3e..07aed0499d134 100644 --- a/api/common.go +++ b/api/common.go @@ -4,7 +4,7 @@ import ( "fmt" "mime" "os" - "path" + "path/filepath" "strings" log "github.com/Sirupsen/logrus" @@ -54,7 +54,7 @@ func MatchesContentType(contentType, expectedType string) bool { // LoadOrCreateTrustKey attempts to load the libtrust key at the given path, // otherwise generates a new one func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) { - err := os.MkdirAll(path.Dir(trustKeyPath), 0700) + err := os.MkdirAll(filepath.Dir(trustKeyPath), 0700) if err != nil { return nil, err } @@ -67,9 +67,9 @@ func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) { if err := libtrust.SaveKey(trustKeyPath, trustKey); err != nil { return nil, fmt.Errorf("Error saving key file: %s", err) } - dir, file := path.Split(trustKeyPath) + dir, file := filepath.Split(trustKeyPath) // Save public key - if err := libtrust.SavePublicKey(path.Join(dir, "public-"+file), trustKey.PublicKey()); err != nil { + if err := libtrust.SavePublicKey(filepath.Join(dir, "public-"+file), trustKey.PublicKey()); err != nil { return nil, fmt.Errorf("Error saving public key file: %s", err) } } else if err != nil { From 6e2f738d466fac8a8902445ca069fc405254daf5 Mon Sep 17 00:00:00 2001 From: John Gossman Date: Wed, 10 Dec 2014 12:50:48 -0800 Subject: [PATCH 18/18] Use path/filepath instead of path (for Windows) the code is using path.Split, path.Join etc. instead of filepath.Split, filepath.Join. path assumes '/' separators, but Windows of course uses '\'. Talk about the gift that keeps on giving...I've been fixing bugs like this in portable code since 1983. See https://github.com/docker/machine/issues/76 Signed-off-by: John Gossman --- api/common.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/common.go b/api/common.go index 94bcc1b267d3e..07aed0499d134 100644 --- a/api/common.go +++ b/api/common.go @@ -4,7 +4,7 @@ import ( "fmt" "mime" "os" - "path" + "path/filepath" "strings" log "github.com/Sirupsen/logrus" @@ -54,7 +54,7 @@ func MatchesContentType(contentType, expectedType string) bool { // LoadOrCreateTrustKey attempts to load the libtrust key at the given path, // otherwise generates a new one func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) { - err := os.MkdirAll(path.Dir(trustKeyPath), 0700) + err := os.MkdirAll(filepath.Dir(trustKeyPath), 0700) if err != nil { return nil, err } @@ -67,9 +67,9 @@ func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) { if err := libtrust.SaveKey(trustKeyPath, trustKey); err != nil { return nil, fmt.Errorf("Error saving key file: %s", err) } - dir, file := path.Split(trustKeyPath) + dir, file := filepath.Split(trustKeyPath) // Save public key - if err := libtrust.SavePublicKey(path.Join(dir, "public-"+file), trustKey.PublicKey()); err != nil { + if err := libtrust.SavePublicKey(filepath.Join(dir, "public-"+file), trustKey.PublicKey()); err != nil { return nil, fmt.Errorf("Error saving public key file: %s", err) } } else if err != nil {