Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable TLS based communication with Zookeeper Backend #4856

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 163 additions & 1 deletion physical/zookeeper/zookeeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package zookeeper

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"path/filepath"
"sort"
"strings"
Expand All @@ -11,9 +15,11 @@ import (

"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/hashicorp/vault/physical"

metrics "github.com/armon/go-metrics"
"github.com/hashicorp/vault/helper/tlsutil"
"github.com/samuel/go-zookeeper/zk"
)

Expand Down Expand Up @@ -119,7 +125,7 @@ func NewZooKeeperBackend(conf map[string]string, logger log.Logger) (physical.Ba
}

// We have all of the configuration in hand - let's try and connect to ZK
client, _, err := zk.Connect(strings.Split(machines, ","), time.Second)
client, _, err := createClient(conf, machines, time.Second)
if err != nil {
return nil, errwrap.Wrapf("client setup failed: {{err}}", err)
}
Expand All @@ -142,6 +148,162 @@ func NewZooKeeperBackend(conf map[string]string, logger log.Logger) (physical.Ba
return c, nil
}

func caseInsenstiveContains(superset, val string) bool {
return strings.Contains(strings.ToUpper(superset), strings.ToUpper(val))
}

// Returns a client for ZK connection. Config value 'tls_enabled' determines if TLS is enabled or not.
func createClient(conf map[string]string, machines string, timeout time.Duration) (*zk.Conn, <-chan zk.Event, error) {
// 'tls_enabled' defaults to false
isTlsEnabled := false
isTlsEnabledStr, ok := conf["tls_enabled"]

if ok && isTlsEnabledStr != "" {
parsedBoolval, err := parseutil.ParseBool(isTlsEnabledStr)
if err != nil {
return nil, nil, errwrap.Wrapf("failed parsing tls_enabled parameter: {{err}}", err)
}
isTlsEnabled = parsedBoolval
}

if isTlsEnabled {
// Create a custom Dialer with cert configuration for TLS handshake.
tlsDialer := customTLSDial(conf, machines)
options := zk.WithDialer(tlsDialer)
return zk.Connect(strings.Split(machines, ","), timeout, options)
} else {
return zk.Connect(strings.Split(machines, ","), timeout)
}
}

// Vault config file properties:
// 1. tls_skip_verify: skip host name verification.
// 2. tls_min_version: minimum supported/acceptable tls version
// 3. tls_cert_file: Cert file Absolute path
// 4. tls_key_file: Key file Absolute path
// 5. tls_ca_file: ca file absolute path
// 6. tls_verify_ip: If set to true, server's IP is verified in certificate if tls_skip_verify is false.
func customTLSDial(conf map[string]string, machines string) zk.Dialer {
return func(network, addr string, timeout time.Duration) (net.Conn, error) {
// Sets the serverName. *Note* the addr field comes in as an IP address
serverName, _, sParseErr := net.SplitHostPort(addr)
if sParseErr != nil {
// If the address is only missing port, assign the full address anyway
if strings.Contains(sParseErr.Error(), "missing port") {
serverName = addr
} else {
return nil, errwrap.Wrapf("failed parsing the server address for 'serverName' setting {{err}}", sParseErr)
}
}

insecureSkipVerify := false
tlsSkipVerify, ok := conf["tls_skip_verify"]

if ok && tlsSkipVerify != "" {
b, err := parseutil.ParseBool(tlsSkipVerify)
if err != nil {
return nil, errwrap.Wrapf("failed parsing tls_skip_verify parameter: {{err}}", err)
}
insecureSkipVerify = b
}

if !insecureSkipVerify {
// If tls_verify_ip is set to false, Server's DNS name is verified in the CN/SAN of the certificate.
// if tls_verify_ip is true, Server's IP is verified in the CN/SAN of the certificate.
// These checks happen only when tls_skip_verify is set to false.
// This value defaults to false
ipSanCheck := false
configVal, lookupOk := conf["tls_verify_ip"]

if lookupOk && configVal != "" {
parsedIpSanCheck, ipSanErr := parseutil.ParseBool(configVal)
if ipSanErr != nil {
return nil, errwrap.Wrapf("failed parsing tls_verify_ip parameter: {{err}}", ipSanErr)
}
ipSanCheck = parsedIpSanCheck
}
// The addr/serverName parameter to this method comes in as an IP address.
// Here we lookup the DNS name and assign it to serverName if ipSanCheck is set to false
if !ipSanCheck {
lookupAddressMany, lookupErr := net.LookupAddr(serverName)
if lookupErr == nil {
for _, lookupAddress := range lookupAddressMany {
// strip the trailing '.' from lookupAddr
if lookupAddress[len(lookupAddress)-1] == '.' {
lookupAddress = lookupAddress[:len(lookupAddress)-1]
}
// Allow serverName to be replaced only if the lookupname is part of the
// supplied machine names
// If there is no match, the serverName will continue to be an IP value.
if caseInsenstiveContains(machines, lookupAddress) {
serverName = lookupAddress
break
}
}
}
}

}

tlsMinVersionStr, ok := conf["tls_min_version"]
if !ok {
// Set the default value
tlsMinVersionStr = "tls12"
}

tlsMinVersion, ok := tlsutil.TLSLookup[tlsMinVersionStr]
if !ok {
return nil, fmt.Errorf("invalid 'tls_min_version'")
}

tlsClientConfig := &tls.Config{
MinVersion: tlsMinVersion,
InsecureSkipVerify: insecureSkipVerify,
ServerName: serverName,
}

_, okCert := conf["tls_cert_file"]
_, okKey := conf["tls_key_file"]

if okCert && okKey {
tlsCert, err := tls.LoadX509KeyPair(conf["tls_cert_file"], conf["tls_key_file"])
if err != nil {
return nil, errwrap.Wrapf("client tls setup failed for ZK: {{err}}", err)
}

tlsClientConfig.Certificates = []tls.Certificate{tlsCert}
}

if tlsCaFile, ok := conf["tls_ca_file"]; ok {
caPool := x509.NewCertPool()

data, err := ioutil.ReadFile(tlsCaFile)
if err != nil {
return nil, errwrap.Wrapf("failed to read ZK CA file: {{err}}", err)
}

if !caPool.AppendCertsFromPEM(data) {
return nil, fmt.Errorf("failed to parse ZK CA certificate")
}
tlsClientConfig.RootCAs = caPool
}

if network != "tcp" {
return nil, fmt.Errorf("unsupported network %q", network)
}

tcpConn, err := net.DialTimeout("tcp", addr, timeout)
if err != nil {
return nil, err
}
conn := tls.Client(tcpConn, tlsClientConfig)
if err := conn.Handshake(); err != nil {
return nil, fmt.Errorf("Handshake failed with Zookeeper : %v", err)
}
return conn, nil
}
}

// ensurePath is used to create each node in the path hierarchy.
// We avoid calling this optimistically, and invoke it when we get
// an error during an operation
Expand Down
45 changes: 45 additions & 0 deletions website/source/docs/configuration/storage/zookeeper.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,30 @@ znodes and, potentially, take Vault out of service.
ip:70.95.0.0/16
```

- `tls_enabled` `(bool: false)` – Specifies if TLS communication with the Zookeeper
backend has to be enabled.

- `tls_ca_file` `(string: "")` – Specifies the path to the CA certificate file used
for Zookeeper communication. Multiple CA certificates can be provided in the same file.

- `tls_cert_file` `(string: "")` (optional) – Specifies the path to the
client certificate for Zookeeper communication.

- `tls_key_file` `(string: "")` – Specifies the path to the private key for
Zookeeper communication.

- `tls_min_version` `(string: "tls12")` – Specifies the minimum TLS version to
use. Accepted values are `"tls10"`, `"tls11"` or `"tls12"`.

- `tls_skip_verify` `(bool: false)` – Specifies if the TLS host verification
should be disabled. It is highly discouraged that you disable this option.

- `tls_verify_ip` `(bool: false)` - This property comes into play only when
'tls_skip_verify' is set to false. When 'tls_verify_ip' is set to 'true', the
zookeeper server's IP is verified in the presented certificates CN/SAN entry.
When set to 'false' the server's DNS name is verified in the certificates CN/SAN entry.


## `zookeeper` Examples

### Custom Address and Path
Expand Down Expand Up @@ -105,4 +129,25 @@ storage "zookeeper" {
}
```

### zNode connection over TLS.

This example instructs Vault to connect to Zookeeper using the provided TLS configuration. The host verification will happen with the presented certificate using the servers IP because 'tls_verify_ip' is set to true.

```hcl
storage "zookeeper" {
address = "host1.com:5200,host2.com:5200,host3.com:5200"
path = "vault_path_on_zk/"
znode_owner = "digest:vault_user:digestvalueforpassword="
auth_info = "digest:vault_user:thisisthepassword"
redirect_addr = "http://localhost:8200"
tls_verify_ip = "true"
tls_enabled= "true"
tls_min_version= "tls12"
tls_cert_file = "/path/to/the/cert/file/zkcert.pem"
tls_key_file = "/path/to/the/key/file/zkkey.pem"
tls_skip_verify= "false"
tls_ca_file= "/path/to/the/ca/file/ca.pem"
}
```

[zk]: https://zookeeper.apache.org/