Skip to content

Commit

Permalink
Add support for encrypted TLS key files (#3685)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrishoffman authored Dec 15, 2017
1 parent df653b6 commit 098c66a
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 28 deletions.
2 changes: 1 addition & 1 deletion command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 26 additions & 10 deletions command/server/listener.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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{
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
11 changes: 6 additions & 5 deletions command/server/listener_tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions command/server/listener_tcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
}
Expand All @@ -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)
}
Expand Down
43 changes: 37 additions & 6 deletions helper/reload/reload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
74 changes: 74 additions & 0 deletions helper/reload/reload_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion vault/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
5 changes: 4 additions & 1 deletion website/source/docs/configuration/listener/tcp.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ listener "tcp" {
combined file.

- `tls_key_file` `(string: <required-if-enabled>, 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".
Expand Down

0 comments on commit 098c66a

Please sign in to comment.