diff --git a/commands.go b/commands.go index 3161b2ff76..42a1fa0c4b 100644 --- a/commands.go +++ b/commands.go @@ -36,8 +36,11 @@ type machineConfig struct { machineName string machineDir string caCertPath string + caKeyPath string clientCertPath string clientKeyPath string + serverCertPath string + serverKeyPath string machineUrl string swarmMaster bool swarmHost string @@ -68,6 +71,25 @@ func (h hostListItemByName) Less(i, j int) bool { return strings.ToLower(h[i].Name) < strings.ToLower(h[j].Name) } +func confirmInput(msg string) bool { + fmt.Printf("%s (y/n): ", msg) + var resp string + _, err := fmt.Scanln(&resp) + + if err != nil { + log.Fatal(err) + + } + + if strings.Index(strings.ToLower(resp), "y") == 0 { + return true + + } + + return false + +} + func setupCertificates(caCertPath, caKeyPath, clientCertPath, clientKeyPath string) error { org := utils.GetUsername() bits := 2048 @@ -207,6 +229,18 @@ var Commands = []cli.Command{ Usage: "List machines", Action: cmdLs, }, + { + Name: "regenerate-certs", + Usage: "Regenerate TLS Certificates for a machine", + Description: "Argument(s) are one or more machine names. Will use the active machine if none is provided.", + Action: cmdRegenerateCerts, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "force, f", + Usage: "Force rebuild and do not prompt", + }, + }, + }, { Name: "restart", Usage: "Restart a machine", @@ -369,6 +403,33 @@ func cmdConfig(c *cli.Context) { dockerHost = fmt.Sprintf("tcp://%s:%s", machineIp, swarmPort) } + + u, err := url.Parse(cfg.machineUrl) + if err != nil { + log.Fatal(err) + } + + if u.Scheme != "unix" { + // validate cert and regenerate if needed + valid, err := utils.ValidateCertificate( + u.Host, + cfg.caCertPath, + cfg.serverCertPath, + cfg.serverKeyPath, + ) + if err != nil { + log.Fatal(err) + } + + if !valid { + log.Debugf("invalid certs detected; regenerating for %s", u.Host) + + if err := runActionWithContext("configureAuth", c); err != nil { + log.Fatal(err) + } + } + } + fmt.Printf("--tlsverify --tlscacert=%s --tlscert=%s --tlskey=%s -H=%s", cfg.caCertPath, cfg.clientCertPath, cfg.clientKeyPath, dockerHost) } @@ -459,6 +520,16 @@ func cmdLs(c *cli.Context) { w.Flush() } +func cmdRegenerateCerts(c *cli.Context) { + force := c.Bool("force") + if force || confirmInput("Regenerate TLS machine certs? Warning: this is irreversible.") { + log.Infof("Regenerating TLS certificates") + if err := runActionWithContext("configureAuth", c); err != nil { + log.Fatal(err) + } + } +} + func cmdRm(c *cli.Context) { if len(c.Args()) == 0 { cli.ShowCommandHelp(c, "rm") @@ -521,6 +592,32 @@ func cmdEnv(c *cli.Context) { dockerHost = fmt.Sprintf("tcp://%s:%s", machineIp, swarmPort) } + u, err := url.Parse(cfg.machineUrl) + if err != nil { + log.Fatal(err) + } + + if u.Scheme != "unix" { + // validate cert and regenerate if needed + valid, err := utils.ValidateCertificate( + u.Host, + cfg.caCertPath, + cfg.serverCertPath, + cfg.serverKeyPath, + ) + if err != nil { + log.Fatal(err) + } + + if !valid { + log.Debugf("invalid certs detected; regenerating for %s", u.Host) + + if err := runActionWithContext("configureAuth", c); err != nil { + log.Fatal(err) + } + } + } + switch userShell { case "fish": fmt.Printf("set -x DOCKER_TLS_VERIFY 1;\nset -x DOCKER_CERT_PATH %s;\nset -x DOCKER_HOST %s;\n", @@ -574,11 +671,12 @@ func cmdSsh(c *cli.Context) { // We run commands concurrently and communicate back an error if there was one. func machineCommand(actionName string, machine *Host, errorChan chan<- error) { commands := map[string](func() error){ - "start": machine.Start, - "stop": machine.Stop, - "restart": machine.Restart, - "kill": machine.Kill, - "upgrade": machine.Upgrade, + "configureAuth": machine.ConfigureAuth, + "start": machine.Start, + "stop": machine.Stop, + "restart": machine.Restart, + "kill": machine.Kill, + "upgrade": machine.Upgrade, } log.Debugf("command=%s machine=%s", actionName, machine.Name) @@ -811,8 +909,11 @@ func getMachineConfig(c *cli.Context) (*machineConfig, error) { machineDir := filepath.Join(utils.GetMachineDir(), machine.Name) caCert := filepath.Join(machineDir, "ca.pem") + caKey := filepath.Join(utils.GetMachineCertDir(), "ca-key.pem") clientCert := filepath.Join(machineDir, "cert.pem") clientKey := filepath.Join(machineDir, "key.pem") + serverCert := filepath.Join(machineDir, "server.pem") + serverKey := filepath.Join(machineDir, "server-key.pem") machineUrl, err := machine.GetURL() if err != nil { if err == drivers.ErrHostIsNotRunning { @@ -825,8 +926,11 @@ func getMachineConfig(c *cli.Context) (*machineConfig, error) { machineName: name, machineDir: machineDir, caCertPath: caCert, + caKeyPath: caKey, clientCertPath: clientCert, clientKeyPath: clientKey, + serverCertPath: serverCert, + serverKeyPath: serverKey, machineUrl: machineUrl, swarmMaster: machine.SwarmMaster, swarmHost: machine.SwarmHost, diff --git a/docs/index.md b/docs/index.md index 632ccec2f2..8570cfd73e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -460,6 +460,16 @@ foo3 virtualbox Running tcp://192.168.99.108:2376 foo4 * virtualbox Running tcp://192.168.99.109:2376 ``` +#### regenerate-certs + +Regenerate TLS certificates and update the machine with new certs. + +``` +$ docker-machine regenerate-certs +Regenerate TLS machine certs? Warning: this is irreversible. (y/n): y +INFO[0013] Regenerating TLS certificates +``` + #### restart Restart a machine. Oftentimes this is equivalent to diff --git a/host.go b/host.go index 75383ffc61..417ebc7502 100644 --- a/host.go +++ b/host.go @@ -245,7 +245,7 @@ func (h *Host) StopDocker() error { switch h.Driver.GetProviderType() { case provider.Local: - cmd, err = h.GetSSHCommand("if [ -e /var/run/docker.pid ]; then sudo /etc/init.d/docker stop ; fi") + cmd, err = h.GetSSHCommand("if [ -e /var/run/docker.pid ] && [ -d /proc/$(cat /var/run/docker.pid) ]; then sudo /etc/init.d/docker stop ; exit 0; fi") case provider.Remote: cmd, err = h.GetSSHCommand("sudo service docker stop") default: @@ -364,7 +364,7 @@ func (h *Host) ConfigureAuth() error { } machineServerKeyPath := path.Join(dockerDir, "server-key.pem") - cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee -a %s", string(caCert), machineCaCertPath)) + cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee %s", string(caCert), machineCaCertPath)) if err != nil { return err } @@ -372,7 +372,7 @@ func (h *Host) ConfigureAuth() error { return err } - cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee -a %s", string(serverKey), machineServerKeyPath)) + cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee %s", string(serverKey), machineServerKeyPath)) if err != nil { return err } @@ -380,7 +380,7 @@ func (h *Host) ConfigureAuth() error { return err } - cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee -a %s", string(serverCert), machineServerCertPath)) + cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee %s", string(serverCert), machineServerCertPath)) if err != nil { return err } diff --git a/utils/certs.go b/utils/certs.go index 76c1bce2a3..e2c22ad31d 100644 --- a/utils/certs.go +++ b/utils/certs.go @@ -7,12 +7,33 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "io/ioutil" "math/big" "net" "os" "time" ) +func getTLSConfig(caCert, cert, key []byte, allowInsecure bool) (*tls.Config, error) { + // TLS config + var tlsConfig tls.Config + tlsConfig.InsecureSkipVerify = allowInsecure + certPool := x509.NewCertPool() + + certPool.AppendCertsFromPEM(caCert) + tlsConfig.RootCAs = certPool + keypair, err := tls.X509KeyPair(cert, key) + if err != nil { + return &tlsConfig, err + } + tlsConfig.Certificates = []tls.Certificate{keypair} + if allowInsecure { + tlsConfig.InsecureSkipVerify = true + } + + return &tlsConfig, nil +} + func newCertificate(org string) (*x509.Certificate, error) { now := time.Now() // need to set notBefore slightly in the past to account for time @@ -149,3 +170,32 @@ func GenerateCert(hosts []string, certFile, keyFile, caFile, caKeyFile, org stri return nil } + +func ValidateCertificate(addr, caCertPath, serverCertPath, serverKeyPath string) (bool, error) { + caCert, err := ioutil.ReadFile(caCertPath) + if err != nil { + return false, err + } + + serverCert, err := ioutil.ReadFile(serverCertPath) + if err != nil { + return false, err + } + + serverKey, err := ioutil.ReadFile(serverKeyPath) + if err != nil { + return false, err + } + + tlsConfig, err := getTLSConfig(caCert, serverCert, serverKey, false) + if err != nil { + return false, err + } + + _, err = tls.Dial("tcp", addr, tlsConfig) + if err != nil { + return false, nil + } + + return true, nil +} diff --git a/utils/certs_test.go b/utils/certs_test.go index 042f47e035..1201e63d28 100644 --- a/utils/certs_test.go +++ b/utils/certs_test.go @@ -12,6 +12,8 @@ func TestGenerateCACertificate(t *testing.T) { if err != nil { t.Fatal(err) } + // cleanup + defer os.RemoveAll(tmpDir) os.Setenv("MACHINE_DIR", tmpDir) caCertPath := filepath.Join(tmpDir, "ca.pem") @@ -29,9 +31,6 @@ func TestGenerateCACertificate(t *testing.T) { t.Fatal(err) } os.Setenv("MACHINE_DIR", "") - - // cleanup - _ = os.RemoveAll(tmpDir) } func TestGenerateCert(t *testing.T) { @@ -39,6 +38,8 @@ func TestGenerateCert(t *testing.T) { if err != nil { t.Fatal(err) } + // cleanup + defer os.RemoveAll(tmpDir) os.Setenv("MACHINE_DIR", tmpDir) caCertPath := filepath.Join(tmpDir, "ca.pem") @@ -70,7 +71,4 @@ func TestGenerateCert(t *testing.T) { if _, err := os.Stat(keyPath); err != nil { t.Fatalf("key not created at %s", keyPath) } - - // cleanup - _ = os.RemoveAll(tmpDir) }