From 098c66a6240442db1f7ae49e4a2ccc1b71b0a04f Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Fri, 15 Dec 2017 17:33:55 -0500 Subject: [PATCH] Add support for encrypted TLS key files (#3685) --- command/server.go | 2 +- command/server/listener.go | 36 ++++++--- command/server/listener_tcp.go | 11 +-- command/server/listener_tcp_test.go | 10 ++- helper/reload/reload.go | 43 +++++++++-- helper/reload/reload_test.go | 74 +++++++++++++++++++ vault/testing.go | 2 +- .../docs/configuration/listener/tcp.html.md | 5 +- 8 files changed, 155 insertions(+), 28 deletions(-) create mode 100644 helper/reload/reload_test.go diff --git a/command/server.go b/command/server.go index 39928a2eaac0..94c566daf57e 100644 --- a/command/server.go +++ b/command/server.go @@ -475,7 +475,7 @@ CLUSTER_SYNTHESIS_COMPLETE: c.reloadFuncsLock.Lock() lns := make([]net.Listener, 0, len(config.Listeners)) for i, lnConfig := range config.Listeners { - ln, props, reloadFunc, err := server.NewListener(lnConfig.Type, lnConfig.Config, c.logGate) + ln, props, reloadFunc, err := server.NewListener(lnConfig.Type, lnConfig.Config, c.logGate, c.Ui) if err != nil { c.Ui.Output(fmt.Sprintf( "Error initializing listener of type %s: %s", diff --git a/command/server/listener.go b/command/server/listener.go index be83cba956d7..e8ba4236653d 100644 --- a/command/server/listener.go +++ b/command/server/listener.go @@ -1,6 +1,7 @@ package server import ( + "github.com/hashicorp/errwrap" // We must import sha512 so that it registers with the runtime so that // certificates that use it can be parsed. _ "crypto/sha512" @@ -15,10 +16,11 @@ import ( "github.com/hashicorp/vault/helper/proxyutil" "github.com/hashicorp/vault/helper/reload" "github.com/hashicorp/vault/helper/tlsutil" + "github.com/mitchellh/cli" ) // ListenerFactory is the factory function to create a listener. -type ListenerFactory func(map[string]interface{}, io.Writer) (net.Listener, map[string]string, reload.ReloadFunc, error) +type ListenerFactory func(map[string]interface{}, io.Writer, cli.Ui) (net.Listener, map[string]string, reload.ReloadFunc, error) // BuiltinListeners is the list of built-in listener types. var BuiltinListeners = map[string]ListenerFactory{ @@ -27,13 +29,13 @@ var BuiltinListeners = map[string]ListenerFactory{ // NewListener creates a new listener of the given type with the given // configuration. The type is looked up in the BuiltinListeners map. -func NewListener(t string, config map[string]interface{}, logger io.Writer) (net.Listener, map[string]string, reload.ReloadFunc, error) { +func NewListener(t string, config map[string]interface{}, logger io.Writer, ui cli.Ui) (net.Listener, map[string]string, reload.ReloadFunc, error) { f, ok := BuiltinListeners[t] if !ok { return nil, nil, nil, fmt.Errorf("unknown listener type: %s", t) } - return f(config, logger) + return f(config, logger, ui) } func listenerWrapProxy(ln net.Listener, config map[string]interface{}) (net.Listener, error) { @@ -70,7 +72,8 @@ func listenerWrapProxy(ln net.Listener, config map[string]interface{}) (net.List func listenerWrapTLS( ln net.Listener, props map[string]string, - config map[string]interface{}) (net.Listener, map[string]string, reload.ReloadFunc, error) { + config map[string]interface{}, + ui cli.Ui) (net.Listener, map[string]string, reload.ReloadFunc, error) { props["tls"] = "disabled" if v, ok := config["tls_disable"]; ok { @@ -83,22 +86,35 @@ func listenerWrapTLS( } } - _, ok := config["tls_cert_file"] + certFileRaw, ok := config["tls_cert_file"] if !ok { return nil, nil, nil, fmt.Errorf("'tls_cert_file' must be set") } - - _, ok = config["tls_key_file"] + certFile := certFileRaw.(string) + keyFileRaw, ok := config["tls_key_file"] if !ok { return nil, nil, nil, fmt.Errorf("'tls_key_file' must be set") } + keyFile := keyFileRaw.(string) - cg := reload.NewCertificateGetter(config["tls_cert_file"].(string), config["tls_key_file"].(string)) - + cg := reload.NewCertificateGetter(certFile, keyFile, "") if err := cg.Reload(config); err != nil { - return nil, nil, nil, fmt.Errorf("error loading TLS cert: %s", err) + // We try the key without a passphrase first and if we get an incorrect + // passphrase response, try again after prompting for a passphrase + if errwrap.Contains(err, x509.IncorrectPasswordError.Error()) { + var passphrase string + passphrase, err = ui.AskSecret(fmt.Sprintf("Enter passphrase for %s:", keyFile)) + if err == nil { + cg = reload.NewCertificateGetter(certFile, keyFile, passphrase) + if err = cg.Reload(config); err == nil { + goto PASSPHRASECORRECT + } + } + } + return nil, nil, nil, errwrap.Wrapf("error loading TLS cert: {{err}}", err) } +PASSPHRASECORRECT: var tlsvers string tlsversRaw, ok := config["tls_min_version"] if !ok { diff --git a/command/server/listener_tcp.go b/command/server/listener_tcp.go index b0ab68764806..bf39615a69f9 100644 --- a/command/server/listener_tcp.go +++ b/command/server/listener_tcp.go @@ -7,10 +7,11 @@ import ( "time" "github.com/hashicorp/vault/helper/reload" + "github.com/mitchellh/cli" ) -func tcpListenerFactory(config map[string]interface{}, _ io.Writer) (net.Listener, map[string]string, reload.ReloadFunc, error) { - bind_proto := "tcp" +func tcpListenerFactory(config map[string]interface{}, _ io.Writer, ui cli.Ui) (net.Listener, map[string]string, reload.ReloadFunc, error) { + bindProto := "tcp" var addr string addrRaw, ok := config["address"] if !ok { @@ -22,10 +23,10 @@ func tcpListenerFactory(config map[string]interface{}, _ io.Writer) (net.Listene // If they've passed 0.0.0.0, we only want to bind on IPv4 // rather than golang's dual stack default if strings.HasPrefix(addr, "0.0.0.0:") { - bind_proto = "tcp4" + bindProto = "tcp4" } - ln, err := net.Listen(bind_proto, addr) + ln, err := net.Listen(bindProto, addr) if err != nil { return nil, nil, nil, err } @@ -38,7 +39,7 @@ func tcpListenerFactory(config map[string]interface{}, _ io.Writer) (net.Listene } props := map[string]string{"addr": addr} - return listenerWrapTLS(ln, props, config) + return listenerWrapTLS(ln, props, config, ui) } // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted diff --git a/command/server/listener_tcp_test.go b/command/server/listener_tcp_test.go index ff28afa4ca6f..9d5d11895c98 100644 --- a/command/server/listener_tcp_test.go +++ b/command/server/listener_tcp_test.go @@ -10,13 +10,15 @@ import ( "os" "testing" "time" + + "github.com/mitchellh/cli" ) func TestTCPListener(t *testing.T) { ln, _, _, err := tcpListenerFactory(map[string]interface{}{ "address": "127.0.0.1:0", "tls_disable": "1", - }, nil) + }, nil, cli.NewMockUi()) if err != nil { t.Fatalf("err: %s", err) } @@ -54,7 +56,7 @@ func TestTCPListener_tls(t *testing.T) { "tls_key_file": wd + "reload_foo.key", "tls_require_and_verify_client_cert": "true", "tls_client_ca_file": wd + "reload_ca.pem", - }, nil) + }, nil, cli.NewMockUi()) if err != nil { t.Fatalf("err: %s", err) } @@ -93,7 +95,7 @@ func TestTCPListener_tls(t *testing.T) { "tls_require_and_verify_client_cert": "true", "tls_disable_client_certs": "true", "tls_client_ca_file": wd + "reload_ca.pem", - }, nil) + }, nil, cli.NewMockUi()) if err == nil { t.Fatal("expected error due to mutually exclusive client cert options") } @@ -104,7 +106,7 @@ func TestTCPListener_tls(t *testing.T) { "tls_key_file": wd + "reload_foo.key", "tls_disable_client_certs": "true", "tls_client_ca_file": wd + "reload_ca.pem", - }, nil) + }, nil, cli.NewMockUi()) if err != nil { t.Fatalf("err: %s", err) } diff --git a/helper/reload/reload.go b/helper/reload/reload.go index cc450b99a628..f3322adba8a1 100644 --- a/helper/reload/reload.go +++ b/helper/reload/reload.go @@ -2,8 +2,14 @@ package reload import ( "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" "fmt" + "io/ioutil" "sync" + + "github.com/hashicorp/errwrap" ) // ReloadFunc are functions that are called when a reload is requested @@ -17,19 +23,44 @@ type CertificateGetter struct { cert *tls.Certificate - certFile string - keyFile string + certFile string + keyFile string + passphrase string } -func NewCertificateGetter(certFile, keyFile string) *CertificateGetter { +func NewCertificateGetter(certFile, keyFile, passphrase string) *CertificateGetter { return &CertificateGetter{ - certFile: certFile, - keyFile: keyFile, + certFile: certFile, + keyFile: keyFile, + passphrase: passphrase, } } func (cg *CertificateGetter) Reload(_ map[string]interface{}) error { - cert, err := tls.LoadX509KeyPair(cg.certFile, cg.keyFile) + certPEMBlock, err := ioutil.ReadFile(cg.certFile) + if err != nil { + return err + } + keyPEMBlock, err := ioutil.ReadFile(cg.keyFile) + if err != nil { + return err + } + + // Check for encrypted pem block + keyBlock, _ := pem.Decode(keyPEMBlock) + if keyBlock == nil { + return errors.New("Decoded PEM is blank") + } + + if x509.IsEncryptedPEMBlock(keyBlock) { + keyBlock.Bytes, err = x509.DecryptPEMBlock(keyBlock, []byte(cg.passphrase)) + if err != nil { + return errwrap.Wrapf("Decrypting PEM block failed {{err}}", err) + } + keyPEMBlock = pem.EncodeToMemory(keyBlock) + } + + cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) if err != nil { return err } diff --git a/helper/reload/reload_test.go b/helper/reload/reload_test.go new file mode 100644 index 000000000000..811056da894f --- /dev/null +++ b/helper/reload/reload_test.go @@ -0,0 +1,74 @@ +package reload + +import ( + "crypto/x509" + "io/ioutil" + "testing" + + "github.com/hashicorp/errwrap" +) + +func TestReload_KeyWithPassphrase(t *testing.T) { + password := "password" + cert := []byte(`-----BEGIN CERTIFICATE----- +MIICLzCCAZgCCQCq27CeP4WhlDANBgkqhkiG9w0BAQUFADBcMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoM +CUhhc2hpQ29ycDEUMBIGA1UEAwwLbXl2YXVsdC5jb20wHhcNMTcxMjEzMjEzNTM3 +WhcNMTgxMjEzMjEzNTM3WjBcMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFjAU +BgNVBAcMDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoMCUhhc2hpQ29ycDEUMBIGA1UE +AwwLbXl2YXVsdC5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMvsz/9l +EJIlRG6DOw4fXdB/aJgJk2rR8cU0D8+vECIzb+MdDK0cBHtLiVpZC/RnZMdMzjGn +Z++Fp3dEnT6CD0IjKdJcD+qSyZSjHIuYpHjnjrVlM/Le0xST7egoG+fXkSt4myzG +ec2WK1jcZefRRGPycvMqx1yUWU76jDdFZSL5AgMBAAEwDQYJKoZIhvcNAQEFBQAD +gYEAQfYE26FLZ9SPPU8bHNDxoxDmGrn8yJ78C490Qpix/w6gdLaBtILenrZbhpnB +3L3okraM8mplaN2KdAcpnsr4wPv9hbYkam0coxCQEKs8ltHSBaXT6uKRWb00nkGu +yAXDRpuPdFRqbXW3ZFC5broUrz4ujxTDKfVeIn0zpPZkv24= +-----END CERTIFICATE-----`) + key := []byte(`-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,64B032D83BD6A6DC + +qVJ+mXEBKMkUPrQ8odHunMpPgChQUny4CX73/dAcm7O9iXIv9eXQSxj2qfgCOloj +vthg7jYNwtRb0ydzCEnEud35zWw38K/l19/pe4ULfNXlOddlsk4XIHarBiz+KUaX +WTbNk0H+DwdcEwhprPgpTk8gp88lZBiHCnTG/s8v/JNt+wkdqjfAp0Xbm9m+OZ7s +hlNxZin1OuBdprBqfKWBltUALZYiIBhspMTmh+jGQSyEKNTAIBejIiRH5+xYWuOy +xKencq8UpQMOMPR2ZiSw42dU9j8HHMgldI7KszU2FDIEFXG7aSjcxNyyybeBT+Uz +YPoxGxSdUYWqaz50UszvHg/QWR8NlPlQc3nFAUVpGKUF9MEQCIAK8HjcpMP+IAVO +ertp4cTa2Rpm9YeoFrY6tabvmXApXlQPw6rBn6o5KpceWG3ceOsDOsT+e3edHu9g +SGO4hjggbRpO+dBOuwfw4rMn9X1BbqXKJcREAmrgVVSf9/s942E4YOQ+IGJPdtmY +WHAFk8hiJepsVCA2NpwVlAD+QbPPaR2RtvYOtq3IKlWRuVQ+6dpxDsz5FlJhs2L+ +HsX6XqtwuQM8kk1hO8Gm3VeV7+b64r9kfbO8jCM18GexCYiCtig51mJW6IO42d1K +bS1axMx/KeDc/sy7LKEbHnjnYanpGz2Wa2EWhnWAeNXD1nUfUNFPp2SsIGbCMnat +mC4O4cO7YRl3+iJg3kHtTPGtgtCjrZcjlyBtxT2VC7SsTcTXZBWovczMIstyr4Ka +opM24uvQT3Bc0UM0WNh3tdRFuboxDeBDh7PX/2RIoiaMuCCiRZ3O0A== +-----END RSA PRIVATE KEY-----`) + tempDir, err := ioutil.TempDir("", "vault-test") + if err != nil { + t.Fatalf("Error creating temporary directory: %s", err) + } + keyFile := tempDir + "/server.key" + certFile := tempDir + "/server.crt" + + err = ioutil.WriteFile(certFile, cert, 0755) + if err != nil { + t.Fatalf("Error writing to temp file: %s", err) + } + err = ioutil.WriteFile(keyFile, key, 0755) + if err != nil { + t.Fatalf("Error writing to temp file: %s", err) + } + + cg := NewCertificateGetter(certFile, keyFile, "") + err = cg.Reload(nil) + if err == nil { + t.Fatal("error expected") + } + if !errwrap.Contains(err, x509.IncorrectPasswordError.Error()) { + t.Fatalf("expected incorrect password error, got %v", err) + } + + cg = NewCertificateGetter(certFile, keyFile, password) + if err := cg.Reload(nil); err != nil { + t.Fatalf("err: %v", err) + } +} diff --git a/vault/testing.go b/vault/testing.go index a1e469ed2b55..bb489c9fdc7b 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -1090,7 +1090,7 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te if err != nil { t.Fatal(err) } - certGetter := reload.NewCertificateGetter(certFile, keyFile) + certGetter := reload.NewCertificateGetter(certFile, keyFile, "") certGetters = append(certGetters, certGetter) tlsConfig := &tls.Config{ Certificates: []tls.Certificate{tlsCert}, diff --git a/website/source/docs/configuration/listener/tcp.html.md b/website/source/docs/configuration/listener/tcp.html.md index cf677145ac15..c18684d72265 100644 --- a/website/source/docs/configuration/listener/tcp.html.md +++ b/website/source/docs/configuration/listener/tcp.html.md @@ -53,7 +53,10 @@ listener "tcp" { combined file. - `tls_key_file` `(string: , reloads-on-SIGHUP)` – - Specifies the path to the private key for the certificate. + Specifies the path to the private key for the certificate. If the key file + is encrypted, you will be prompted to enter the passphrase on server startup. + The passphrase must stay the same between key files when reloading your + configuration using SIGHUP. - `tls_min_version` `(string: "tls12")` – Specifies the minimum supported version of TLS. Accepted values are "tls10", "tls11" or "tls12".