Merge remote-tracking branch 'oss/master' into f-nomad

* oss/master:
  Add support for encrypted TLS key files (#3685)
This commit is contained in:
Chris Hoffman 2017-12-15 19:51:28 -05:00
commit 6c19fa3b78
8 changed files with 155 additions and 28 deletions

View file

@ -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",

View file

@ -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 {

View file

@ -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

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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},

View file

@ -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".