blob: 9a4edb45dcdcea74479cde04d9e78eb5da226be2 [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package listenerutil
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"os"
osuser "os/user"
"strconv"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-secure-stdlib/reloadutil"
"github.com/hashicorp/go-secure-stdlib/tlsutil"
"github.com/hashicorp/vault/internalshared/configutil"
"github.com/jefferai/isbadcipher"
"github.com/mitchellh/cli"
)
type Listener struct {
net.Listener
Config *configutil.Listener
}
type UnixSocketsConfig struct {
User string `hcl:"user"`
Mode string `hcl:"mode"`
Group string `hcl:"group"`
}
// rmListener is an implementation of net.Listener that forwards most
// calls to the listener but also removes a file as part of the close. We
// use this to cleanup the unix domain socket on close.
type rmListener struct {
net.Listener
Path string
}
func (l *rmListener) Close() error {
// Close the listener itself
if err := l.Listener.Close(); err != nil {
return err
}
// Remove the file
return os.Remove(l.Path)
}
func UnixSocketListener(path string, unixSocketsConfig *UnixSocketsConfig) (net.Listener, error) {
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to remove socket file: %v", err)
}
ln, err := net.Listen("unix", path)
if err != nil {
return nil, err
}
if unixSocketsConfig != nil {
err = setFilePermissions(path, unixSocketsConfig.User, unixSocketsConfig.Group, unixSocketsConfig.Mode)
if err != nil {
return nil, fmt.Errorf("failed to set file system permissions on the socket file: %s", err)
}
}
// Wrap the listener in rmListener so that the Unix domain socket file is
// removed on close.
return &rmListener{
Listener: ln,
Path: path,
}, nil
}
func TLSConfig(
l *configutil.Listener,
props map[string]string,
ui cli.Ui,
) (*tls.Config, reloadutil.ReloadFunc, error) {
props["tls"] = "disabled"
if l.TLSDisable {
return nil, nil, nil
}
cg := reloadutil.NewCertificateGetter(l.TLSCertFile, l.TLSKeyFile, "")
if err := cg.Reload(); err != nil {
// 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:", l.TLSKeyFile))
if err == nil {
cg = reloadutil.NewCertificateGetter(l.TLSCertFile, l.TLSKeyFile, passphrase)
if err = cg.Reload(); err == nil {
goto PASSPHRASECORRECT
}
}
}
return nil, nil, fmt.Errorf("error loading TLS cert: %w", err)
}
PASSPHRASECORRECT:
tlsConf := &tls.Config{
GetCertificate: cg.GetCertificate,
NextProtos: []string{"h2", "http/1.1"},
ClientAuth: tls.RequestClientCert,
}
if l.TLSMinVersion == "" {
l.TLSMinVersion = "tls12"
}
if l.TLSMaxVersion == "" {
l.TLSMaxVersion = "tls13"
}
var ok bool
tlsConf.MinVersion, ok = tlsutil.TLSLookup[l.TLSMinVersion]
if !ok {
return nil, nil, fmt.Errorf("'tls_min_version' value %q not supported, please specify one of [tls10,tls11,tls12,tls13]", l.TLSMinVersion)
}
tlsConf.MaxVersion, ok = tlsutil.TLSLookup[l.TLSMaxVersion]
if !ok {
return nil, nil, fmt.Errorf("'tls_max_version' value %q not supported, please specify one of [tls10,tls11,tls12,tls13]", l.TLSMaxVersion)
}
if tlsConf.MaxVersion < tlsConf.MinVersion {
return nil, nil, fmt.Errorf("'tls_max_version' must be greater than or equal to 'tls_min_version'")
}
if len(l.TLSCipherSuites) > 0 {
// HTTP/2 with TLS 1.2 blacklists several cipher suites.
// https://tools.ietf.org/html/rfc7540#appendix-A
//
// Since the CLI (net/http) automatically uses HTTP/2 with TLS 1.2,
// we check here if all or some specified cipher suites are blacklisted.
badCiphers := []string{}
for _, cipher := range l.TLSCipherSuites {
if isbadcipher.IsBadCipher(cipher) {
// Get the name of the current cipher.
cipherStr, err := tlsutil.GetCipherName(cipher)
if err != nil {
return nil, nil, fmt.Errorf("invalid value for 'tls_cipher_suites': %w", err)
}
badCiphers = append(badCiphers, cipherStr)
}
}
if len(badCiphers) == len(l.TLSCipherSuites) {
ui.Warn(`WARNING! All cipher suites defined by 'tls_cipher_suites' are blacklisted by the
HTTP/2 specification. HTTP/2 communication with TLS 1.2 will not work as intended
and Vault will be unavailable via the CLI.
Please see https://tools.ietf.org/html/rfc7540#appendix-A for further information.`)
} else if len(badCiphers) > 0 {
ui.Warn(fmt.Sprintf(`WARNING! The following cipher suites defined by 'tls_cipher_suites' are
blacklisted by the HTTP/2 specification:
%v
Please see https://tools.ietf.org/html/rfc7540#appendix-A for further information.`, badCiphers))
}
tlsConf.CipherSuites = l.TLSCipherSuites
}
if l.TLSRequireAndVerifyClientCert {
tlsConf.ClientAuth = tls.RequireAndVerifyClientCert
if l.TLSClientCAFile != "" {
caPool := x509.NewCertPool()
data, err := ioutil.ReadFile(l.TLSClientCAFile)
if err != nil {
return nil, nil, fmt.Errorf("failed to read tls_client_ca_file: %w", err)
}
if !caPool.AppendCertsFromPEM(data) {
return nil, nil, fmt.Errorf("failed to parse CA certificate in tls_client_ca_file")
}
tlsConf.ClientCAs = caPool
}
}
if l.TLSDisableClientCerts {
if l.TLSRequireAndVerifyClientCert {
return nil, nil, fmt.Errorf("'tls_disable_client_certs' and 'tls_require_and_verify_client_cert' are mutually exclusive")
}
tlsConf.ClientAuth = tls.NoClientCert
}
props["tls"] = "enabled"
return tlsConf, cg.Reload, nil
}
// setFilePermissions handles configuring ownership and permissions
// settings on a given file. All permission/ownership settings are
// optional. If no user or group is specified, the current user/group
// will be used. Mode is optional, and has no default (the operation is
// not performed if absent). User may be specified by name or ID, but
// group may only be specified by ID.
func setFilePermissions(path string, user, group, mode string) error {
var err error
uid, gid := os.Getuid(), os.Getgid()
if user != "" {
if uid, err = strconv.Atoi(user); err == nil {
goto GROUP
}
// Try looking up the user by name
u, err := osuser.Lookup(user)
if err != nil {
return fmt.Errorf("failed to look up user %q: %v", user, err)
}
uid, _ = strconv.Atoi(u.Uid)
}
GROUP:
if group != "" {
if gid, err = strconv.Atoi(group); err == nil {
goto OWN
}
// Try looking up the user by name
g, err := osuser.LookupGroup(group)
if err != nil {
return fmt.Errorf("failed to look up group %q: %v", user, err)
}
gid, _ = strconv.Atoi(g.Gid)
}
OWN:
if err := os.Chown(path, uid, gid); err != nil {
return fmt.Errorf("failed setting ownership to %d:%d on %q: %v",
uid, gid, path, err)
}
if mode != "" {
mode, err := strconv.ParseUint(mode, 8, 32)
if err != nil {
return fmt.Errorf("invalid mode specified: %v", mode)
}
if err := os.Chmod(path, os.FileMode(mode)); err != nil {
return fmt.Errorf("failed setting permissions to %d on %q: %v",
mode, path, err)
}
}
return nil
}