diff --git a/api/server/server.go b/api/server/server.go index a3edbdc636d14..abc30ff1aec69 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "crypto/tls" - "crypto/x509" "encoding/base64" "encoding/json" "expvar" @@ -15,6 +14,7 @@ import ( "net/http" "net/http/pprof" "os" + "path" "strconv" "strings" "syscall" @@ -33,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 ( @@ -1391,6 +1392,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 +} + // ListenAndServe sets up the required http.Server and gets it listening for // each addr passed in and does protocol specific checking. func ListenAndServe(proto, addr string, job *engine.Job) error { @@ -1428,24 +1446,75 @@ func ListenAndServe(proto, addr string, job *engine.Job) error { return err } - if proto != "unix" && (job.GetenvBool("Tls") || job.GetenvBool("TlsVerify")) { + if proto != "unix" && (!job.GetenvBool("Insecure")) { + trustKeyFile := job.Getenv("TrustKey") + err = os.MkdirAll(path.Dir(trustKeyFile), 0700) + if err != nil { + return fmt.Errorf("Error creating directory: %s", err) + } + trustKey, err := libtrust.LoadKeyFile(trustKeyFile) + if err == libtrust.ErrKeyFileDoesNotExist { + trustKey, err = libtrust.GenerateECP256PrivateKey() + if err != nil { + return fmt.Errorf("Error generating key: %s", err) + } + if err := libtrust.SaveKey(trustKeyFile, trustKey); err != nil { + return fmt.Errorf("Error saving key file: %s", err) + } + } else if err != nil { + return fmt.Errorf("Error loading key file: %s", err) + } + + var cert tls.Certificate tlsCert := job.Getenv("TlsCert") tlsKey := job.Getenv("TlsKey") - cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey) - if err != nil { - return fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?", - tlsCert, tlsKey, err) + if tlsCert != "" && tlsKey != "" { + var err error + cert, err = tls.LoadX509KeyPair(tlsCert, tlsKey) + if err != nil { + if !os.IsNotExist(err) { + return 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 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 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, } + + // Load authorized keys file + clients, err := libtrust.LoadKeySetFile(job.Getenv("TrustClients")) + if err != nil { + return fmt.Errorf("unable to load authorized keys: %s", err) + } + if job.GetenvBool("TlsVerify") { - certPool := x509.NewCertPool() + certPool, poolErr := libtrust.GenerateCACertPool(trustKey, clients) + if poolErr != nil { + return fmt.Errorf("CA pool generation error: %s", poolErr) + } file, err := ioutil.ReadFile(job.Getenv("TlsCa")) - if err != nil { + if err != nil && !os.IsNotExist(err) { return fmt.Errorf("Couldn't read CA certificate: %s", err) } certPool.AppendCertsFromPEM(file) @@ -1459,8 +1528,8 @@ func ListenAndServe(proto, addr string, job *engine.Job) error { // Basic error and sanity checking switch proto { case "tcp": - 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 /!\\") } case "unix": socketGroup := job.Getenv("SocketGroup") diff --git a/docker/daemon.go b/docker/daemon.go index 2f658784723a3..87f52b3670938 100644 --- a/docker/daemon.go +++ b/docker/daemon.go @@ -73,11 +73,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 cb780b2443332..36ba428a91f3c 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" "github.com/docker/docker/api" "github.com/docker/docker/api/client" @@ -15,13 +18,16 @@ import ( flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/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() { @@ -51,6 +57,7 @@ func main() { } flHosts = append(flHosts, defaultHost) } + *flTlsVerify = true if *flDaemon { mainDaemon() @@ -62,45 +69,145 @@ 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) + } // If we should verify the server, we need to load a trusted ca if *flTlsVerify { *flTls = true - 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 { @@ -114,6 +221,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 61081ec996313..8ed9a39a75425 100644 --- a/docker/flags.go +++ b/docker/flags.go @@ -28,20 +28,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 tls-verify flags") 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/docs/sources/articles/https.md b/docs/sources/articles/https.md index c8873bcbe4dd0..371d0909b54e4 100644 --- a/docs/sources/articles/https.md +++ b/docs/sources/articles/https.md @@ -5,27 +5,94 @@ 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. +optionally communicate using a HTTP socket. Enabling communication +through HTTP will by default use TLS and require daemon configuration +to allow client connections. -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. + +There are two different ways of authenticating connections between Docker +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 +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-based authentication + +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 +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 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. + - `--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, 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: @@ -116,60 +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! -## 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 - -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, @@ -177,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` -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/runtime_test.go b/integration/runtime_test.go index b17d132f8aa31..ab017561663e8 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" @@ -159,6 +160,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) } @@ -214,6 +216,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) }