Cassandra: Refactor PEM parsing logic (#11861)

* Refactor TLS parsing

The ParsePEMBundle and ParsePKIJSON functions in the certutil package assumes
both a client certificate and a custom CA are specified. Cassandra needs to
allow for either a client certificate, a custom CA, or both. This revamps the
parsing of pem_json and pem_bundle to accomodate for any of these configurations
This commit is contained in:
Michael Golowka 2021-06-21 11:38:08 -06:00 committed by GitHub
parent ccee88180b
commit 7f6a1739a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 467 additions and 1469 deletions

3
changelog/11861.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
secrets/database/cassandra: Fixed issue where the PEM parsing logic of `pem_bundle` and `pem_json` didn't work for CA-only configurations
```

View File

@ -14,13 +14,34 @@ import (
)
type containerConfig struct {
version string
copyFromTo map[string]string
sslOpts *gocql.SslOptions
containerName string
imageName string
version string
copyFromTo map[string]string
env []string
sslOpts *gocql.SslOptions
}
type ContainerOpt func(*containerConfig)
func ContainerName(name string) ContainerOpt {
return func(cfg *containerConfig) {
cfg.containerName = name
}
}
func Image(imageName string, version string) ContainerOpt {
return func(cfg *containerConfig) {
cfg.imageName = imageName
cfg.version = version
// Reset the environment because there's a very good chance the default environment doesn't apply to the
// non-default image being used
cfg.env = nil
}
}
func Version(version string) ContainerOpt {
return func(cfg *containerConfig) {
cfg.version = version
@ -33,6 +54,12 @@ func CopyFromTo(copyFromTo map[string]string) ContainerOpt {
}
}
func Env(keyValue string) ContainerOpt {
return func(cfg *containerConfig) {
cfg.env = append(cfg.env, keyValue)
}
}
func SslOpts(sslOpts *gocql.SslOptions) ContainerOpt {
return func(cfg *containerConfig) {
cfg.sslOpts = sslOpts
@ -63,7 +90,9 @@ func PrepareTestContainer(t *testing.T, opts ...ContainerOpt) (Host, func()) {
}
containerCfg := &containerConfig{
version: "3.11",
imageName: "cassandra",
version: "3.11",
env: []string{"CASSANDRA_BROADCAST_ADDRESS=127.0.0.1"},
}
for _, opt := range opts {
@ -79,13 +108,15 @@ func PrepareTestContainer(t *testing.T, opts ...ContainerOpt) (Host, func()) {
copyFromTo[absFrom] = to
}
runner, err := docker.NewServiceRunner(docker.RunOptions{
ImageRepo: "cassandra",
ImageTag: containerCfg.version,
Ports: []string{"9042/tcp"},
CopyFromTo: copyFromTo,
Env: []string{"CASSANDRA_BROADCAST_ADDRESS=127.0.0.1"},
})
runOpts := docker.RunOptions{
ContainerName: containerCfg.containerName,
ImageRepo: containerCfg.imageName,
ImageTag: containerCfg.version,
Ports: []string{"9042/tcp"},
CopyFromTo: copyFromTo,
Env: containerCfg.env,
}
runner, err := docker.NewServiceRunner(runOpts)
if err != nil {
t.Fatalf("Could not start docker cassandra: %s", err)
}

View File

@ -55,16 +55,27 @@ func getCassandra(t *testing.T, protocolVersion interface{}) (*Cassandra, func()
}
func TestInitialize(t *testing.T) {
db, cleanup := getCassandra(t, 4)
defer cleanup()
t.Run("integer protocol version", func(t *testing.T) {
// getCassandra performs an Initialize call
db, cleanup := getCassandra(t, 4)
t.Cleanup(cleanup)
err := db.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
err := db.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
})
db, cleanup = getCassandra(t, "4")
defer cleanup()
t.Run("string protocol version", func(t *testing.T) {
// getCassandra performs an Initialize call
db, cleanup := getCassandra(t, "4")
t.Cleanup(cleanup)
err := db.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
})
}
func TestCreateUser(t *testing.T) {
@ -74,7 +85,7 @@ func TestCreateUser(t *testing.T) {
newUserReq dbplugin.NewUserRequest
expectErr bool
expectedUsernameRegex string
assertCreds func(t testing.TB, address string, port int, username, password string, timeout time.Duration)
assertCreds func(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions, timeout time.Duration)
}
tests := map[string]testCase{
@ -160,7 +171,7 @@ func TestCreateUser(t *testing.T) {
t.Fatalf("no error expected, got: %s", err)
}
require.Regexp(t, test.expectedUsernameRegex, newUserResp.Username)
test.assertCreds(t, db.Hosts, db.Port, newUserResp.Username, test.newUserReq.Password, 5*time.Second)
test.assertCreds(t, db.Hosts, db.Port, newUserResp.Username, test.newUserReq.Password, nil, 5*time.Second)
})
}
}
@ -184,7 +195,7 @@ func TestUpdateUserPassword(t *testing.T) {
createResp := dbtesting.AssertNewUser(t, db, createReq)
assertCreds(t, db.Hosts, db.Port, createResp.Username, password, 5*time.Second)
assertCreds(t, db.Hosts, db.Port, createResp.Username, password, nil, 5*time.Second)
newPassword := "somenewpassword"
updateReq := dbplugin.UpdateUserRequest{
@ -198,7 +209,7 @@ func TestUpdateUserPassword(t *testing.T) {
dbtesting.AssertUpdateUser(t, db, updateReq)
assertCreds(t, db.Hosts, db.Port, createResp.Username, newPassword, 5*time.Second)
assertCreds(t, db.Hosts, db.Port, createResp.Username, newPassword, nil, 5*time.Second)
}
func TestDeleteUser(t *testing.T) {
@ -220,7 +231,7 @@ func TestDeleteUser(t *testing.T) {
createResp := dbtesting.AssertNewUser(t, db, createReq)
assertCreds(t, db.Hosts, db.Port, createResp.Username, password, 5*time.Second)
assertCreds(t, db.Hosts, db.Port, createResp.Username, password, nil, 5*time.Second)
deleteReq := dbplugin.DeleteUserRequest{
Username: createResp.Username,
@ -228,13 +239,13 @@ func TestDeleteUser(t *testing.T) {
dbtesting.AssertDeleteUser(t, db, deleteReq)
assertNoCreds(t, db.Hosts, db.Port, createResp.Username, password, 5*time.Second)
assertNoCreds(t, db.Hosts, db.Port, createResp.Username, password, nil, 5*time.Second)
}
func assertCreds(t testing.TB, address string, port int, username, password string, timeout time.Duration) {
func assertCreds(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions, timeout time.Duration) {
t.Helper()
op := func() error {
return connect(t, address, port, username, password)
return connect(t, address, port, username, password, sslOpts)
}
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = timeout
@ -248,7 +259,7 @@ func assertCreds(t testing.TB, address string, port int, username, password stri
}
}
func connect(t testing.TB, address string, port int, username, password string) error {
func connect(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions) error {
t.Helper()
clusterConfig := gocql.NewCluster(address)
clusterConfig.Authenticator = gocql.PasswordAuthenticator{
@ -257,6 +268,7 @@ func connect(t testing.TB, address string, port int, username, password string)
}
clusterConfig.ProtoVersion = 4
clusterConfig.Port = port
clusterConfig.SslOpts = sslOpts
session, err := clusterConfig.CreateSession()
if err != nil {
@ -266,12 +278,12 @@ func connect(t testing.TB, address string, port int, username, password string)
return nil
}
func assertNoCreds(t testing.TB, address string, port int, username, password string, timeout time.Duration) {
func assertNoCreds(t testing.TB, address string, port int, username, password string, sslOpts *gocql.SslOptions, timeout time.Duration) {
t.Helper()
op := func() error {
// "Invert" the error so the backoff logic sees a failure to connect as a success
err := connect(t, address, port, username, password)
err := connect(t, address, port, username, password, sslOpts)
if err != nil {
return nil
}

View File

@ -12,7 +12,6 @@ import (
dbplugin "github.com/hashicorp/vault/sdk/database/dbplugin/v5"
"github.com/hashicorp/vault/sdk/database/helper/connutil"
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/parseutil"
"github.com/hashicorp/vault/sdk/helper/tlsutil"
"github.com/mitchellh/mapstructure"
@ -40,7 +39,7 @@ type cassandraConnectionProducer struct {
connectTimeout time.Duration
socketKeepAlive time.Duration
certBundle *certutil.CertBundle
sslOpts *gocql.SslOptions
rawConfig map[string]interface{}
Initialized bool
@ -83,38 +82,46 @@ func (c *cassandraConnectionProducer) Initialize(ctx context.Context, req dbplug
return fmt.Errorf("username cannot be empty")
case len(c.Password) == 0:
return fmt.Errorf("password cannot be empty")
case len(c.PemJSON) > 0 && len(c.PemBundle) > 0:
return fmt.Errorf("cannot specify both pem_json and pem_bundle")
}
var tlsMinVersion uint16 = tls.VersionTLS12
if c.TLSMinVersion != "" {
ver, exists := tlsutil.TLSLookup[c.TLSMinVersion]
if !exists {
return fmt.Errorf("unrecognized TLS version [%s]", c.TLSMinVersion)
}
tlsMinVersion = ver
}
var certBundle *certutil.CertBundle
var parsedCertBundle *certutil.ParsedCertBundle
switch {
case len(c.PemJSON) != 0:
parsedCertBundle, err = certutil.ParsePKIJSON([]byte(c.PemJSON))
cfg, err := jsonBundleToTLSConfig(c.PemJSON, tlsMinVersion, c.TLSServerName, c.InsecureTLS)
if err != nil {
return fmt.Errorf("could not parse given JSON; it must be in the format of the output of the PKI backend certificate issuing command: %w", err)
return fmt.Errorf("failed to parse pem_json: %w", err)
}
certBundle, err = parsedCertBundle.ToCertBundle()
if err != nil {
return fmt.Errorf("error marshaling PEM information: %w", err)
c.sslOpts = &gocql.SslOptions{
Config: cfg,
EnableHostVerification: !cfg.InsecureSkipVerify,
}
c.certBundle = certBundle
c.TLS = true
case len(c.PemBundle) != 0:
parsedCertBundle, err = certutil.ParsePEMBundle(c.PemBundle)
cfg, err := pemBundleToTLSConfig(c.PemBundle, tlsMinVersion, c.TLSServerName, c.InsecureTLS)
if err != nil {
return fmt.Errorf("error parsing the given PEM information: %w", err)
return fmt.Errorf("failed to parse pem_bundle: %w", err)
}
certBundle, err = parsedCertBundle.ToCertBundle()
if err != nil {
return fmt.Errorf("error marshaling PEM information: %w", err)
c.sslOpts = &gocql.SslOptions{
Config: cfg,
EnableHostVerification: !cfg.InsecureSkipVerify,
}
c.certBundle = certBundle
c.TLS = true
}
if c.InsecureTLS {
c.TLS = true
case c.InsecureTLS:
c.sslOpts = &gocql.SslOptions{
EnableHostVerification: !c.InsecureTLS,
}
}
// Set initialized to true at this point since all fields are set,
@ -183,14 +190,7 @@ func (c *cassandraConnectionProducer) createSession(ctx context.Context) (*gocql
clusterConfig.Timeout = c.connectTimeout
clusterConfig.SocketKeepalive = c.socketKeepAlive
if c.TLS {
sslOpts, err := getSslOpts(c.certBundle, c.TLSMinVersion, c.TLSServerName, c.InsecureTLS)
if err != nil {
return nil, err
}
clusterConfig.SslOpts = sslOpts
}
clusterConfig.SslOpts = c.sslOpts
if c.LocalDatacenter != "" {
clusterConfig.PoolConfig.HostSelectionPolicy = gocql.DCAwareRoundRobinPolicy(c.LocalDatacenter)
@ -231,52 +231,6 @@ func (c *cassandraConnectionProducer) createSession(ctx context.Context) (*gocql
return session, nil
}
func getSslOpts(certBundle *certutil.CertBundle, minTLSVersion, serverName string, insecureSkipVerify bool) (*gocql.SslOptions, error) {
tlsConfig := &tls.Config{}
if certBundle != nil {
if certBundle.Certificate == "" && certBundle.PrivateKey != "" {
return nil, fmt.Errorf("found private key for TLS authentication but no certificate")
}
if certBundle.Certificate != "" && certBundle.PrivateKey == "" {
return nil, fmt.Errorf("found certificate for TLS authentication but no private key")
}
parsedCertBundle, err := certBundle.ToParsedCertBundle()
if err != nil {
return nil, fmt.Errorf("failed to parse certificate bundle: %w", err)
}
tlsConfig, err = parsedCertBundle.GetTLSConfig(certutil.TLSClient)
if err != nil {
return nil, fmt.Errorf("failed to get TLS configuration: tlsConfig:%#v err:%w", tlsConfig, err)
}
}
tlsConfig.InsecureSkipVerify = insecureSkipVerify
if serverName != "" {
tlsConfig.ServerName = serverName
}
if minTLSVersion != "" {
var ok bool
tlsConfig.MinVersion, ok = tlsutil.TLSLookup[minTLSVersion]
if !ok {
return nil, fmt.Errorf("invalid 'tls_min_version' in config")
}
} else {
// MinVersion was not being set earlier. Reset it to
// zero to gracefully handle upgrades.
tlsConfig.MinVersion = 0
}
opts := &gocql.SslOptions{
Config: tlsConfig,
EnableHostVerification: !insecureSkipVerify,
}
return opts, nil
}
func (c *cassandraConnectionProducer) secretValues() map[string]string {
return map[string]string{
c.Password: "[password]",

View File

@ -3,46 +3,127 @@ package cassandra
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"io/ioutil"
"testing"
"time"
"github.com/gocql/gocql"
"github.com/hashicorp/vault/helper/testhelpers/cassandra"
"github.com/hashicorp/vault/sdk/database/dbplugin/v5"
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/stretchr/testify/require"
)
var (
insecureFileMounts = map[string]string{
"test-fixtures/no_tls/cassandra.yaml": "/etc/cassandra/cassandra.yaml",
}
secureFileMounts = map[string]string{
"test-fixtures/with_tls/cassandra.yaml": "/etc/cassandra/cassandra.yaml",
"test-fixtures/with_tls/keystore.jks": "/etc/cassandra/keystore.jks",
"test-fixtures/with_tls/.cassandra": "/root/.cassandra/",
}
)
func TestTLSConnection(t *testing.T) {
func TestSelfSignedCA(t *testing.T) {
copyFromTo := map[string]string{
"test-fixtures/with_tls/stores": "/bitnami/cassandra/secrets/",
"test-fixtures/with_tls/cqlshrc": "/.cassandra/cqlshrc",
}
tlsConfig := loadServerCA(t, "test-fixtures/with_tls/ca.pem")
// Note about CI behavior: when running these tests locally, they seem to pass without issue. However, if the
// ServerName is not set, the tests fail within CI. It's not entirely clear to me why they are failing in CI
// however by manually setting the ServerName we can get around the hostname/DNS issue and get them passing.
// Setting the ServerName isn't the ideal solution, but it was the only reliable one I was able to find
tlsConfig.ServerName = "cassandra"
sslOpts := &gocql.SslOptions{
Config: tlsConfig,
EnableHostVerification: true,
}
host, cleanup := cassandra.PrepareTestContainer(t,
cassandra.ContainerName("cassandra"),
cassandra.Image("bitnami/cassandra", "latest"),
cassandra.CopyFromTo(copyFromTo),
cassandra.SslOpts(sslOpts),
cassandra.Env("CASSANDRA_KEYSTORE_PASSWORD=cassandra"),
cassandra.Env("CASSANDRA_TRUSTSTORE_PASSWORD=cassandra"),
cassandra.Env("CASSANDRA_INTERNODE_ENCRYPTION=none"),
cassandra.Env("CASSANDRA_CLIENT_ENCRYPTION=true"),
)
t.Cleanup(cleanup)
type testCase struct {
config map[string]interface{}
expectErr bool
}
caPEM := loadFile(t, "test-fixtures/with_tls/ca.pem")
badCAPEM := loadFile(t, "test-fixtures/with_tls/bad_ca.pem")
tests := map[string]testCase{
"tls not specified": {
config: map[string]interface{}{},
// ///////////////////////
// pem_json tests
"pem_json/ca only": {
config: map[string]interface{}{
"pem_json": toJSON(t, certutil.CertBundle{
CAChain: []string{caPEM},
}),
},
expectErr: false,
},
"pem_json/bad ca": {
config: map[string]interface{}{
"pem_json": toJSON(t, certutil.CertBundle{
CAChain: []string{badCAPEM},
}),
},
expectErr: true,
},
"unrecognized certificate": {
"pem_json/missing ca": {
config: map[string]interface{}{
"pem_json": "",
},
expectErr: true,
},
// ///////////////////////
// pem_bundle tests
"pem_bundle/ca only": {
config: map[string]interface{}{
"pem_bundle": caPEM,
},
expectErr: false,
},
"pem_bundle/unrecognized CA": {
config: map[string]interface{}{
"pem_bundle": badCAPEM,
},
expectErr: true,
},
"pem_bundle/missing ca": {
config: map[string]interface{}{
"pem_bundle": "",
},
expectErr: true,
},
// ///////////////////////
// no cert data provided
"no cert data/tls=true": {
config: map[string]interface{}{
"tls": "true",
},
expectErr: true,
},
"insecure TLS": {
"no cert data/tls=false": {
config: map[string]interface{}{
"tls": "true",
"insecure_tls": true,
"tls": "false",
},
expectErr: true,
},
"no cert data/insecure_tls": {
config: map[string]interface{}{
"insecure_tls": "true",
},
expectErr: false,
},
@ -50,26 +131,24 @@ func TestTLSConnection(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
host, cleanup := cassandra.PrepareTestContainer(t,
cassandra.Version("3.11.9"),
cassandra.CopyFromTo(secureFileMounts),
cassandra.SslOpts(&gocql.SslOptions{
Config: &tls.Config{InsecureSkipVerify: true},
EnableHostVerification: false,
}),
)
defer cleanup()
// Set values that we don't know until the cassandra container is started
config := map[string]interface{}{
"hosts": host.ConnectionURL(),
"hosts": host.Name,
"port": host.Port,
"username": "cassandra",
"password": "cassandra",
"protocol_version": "3",
"connect_timeout": "20s",
"protocol_version": "4",
"connect_timeout": "30s",
"tls": "true",
// Note about CI behavior: when running these tests locally, they seem to pass without issue. However, if the
// tls_server_name is not set, the tests fail within CI. It's not entirely clear to me why they are failing in CI
// however by manually setting the tls_server_name we can get around the hostname/DNS issue and get them passing.
// Setting the tls_server_name isn't the ideal solution, but it was the only reliable one I was able to find
"tls_server_name": "cassandra",
}
// Then add any values specified in the test config. Generally for these tests they shouldn't overlap
// Apply the generated & common fields to the config to be sent to the DB
for k, v := range test.config {
config[k] = v
}
@ -90,6 +169,64 @@ func TestTLSConnection(t *testing.T) {
if !test.expectErr && err != nil {
t.Fatalf("no error expected, got: %s", err)
}
// If no error expected, run a NewUser query to make sure the connection
// actually works in case Initialize doesn't catch it
if !test.expectErr {
assertNewUser(t, db, sslOpts)
}
})
}
}
func assertNewUser(t *testing.T, db *Cassandra, sslOpts *gocql.SslOptions) {
newUserReq := dbplugin.NewUserRequest{
UsernameConfig: dbplugin.UsernameMetadata{
DisplayName: "dispname",
RoleName: "rolename",
},
Statements: dbplugin.Statements{
Commands: []string{
"create user '{{username}}' with password '{{password}}'",
},
},
RollbackStatements: dbplugin.Statements{},
Password: "gh8eruajASDFAsgy89svn",
Expiration: time.Now().Add(5 * time.Second),
}
newUserResp := dbtesting.AssertNewUser(t, db, newUserReq)
t.Logf("Username: %s", newUserResp.Username)
assertCreds(t, db.Hosts, db.Port, newUserResp.Username, newUserReq.Password, sslOpts, 5*time.Second)
}
func loadServerCA(t *testing.T, file string) *tls.Config {
t.Helper()
pemData, err := ioutil.ReadFile(file)
require.NoError(t, err)
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pemData)
config := &tls.Config{
RootCAs: pool,
}
return config
}
func loadFile(t *testing.T, filename string) string {
t.Helper()
contents, err := ioutil.ReadFile(filename)
require.NoError(t, err)
return string(contents)
}
func toJSON(t *testing.T, val interface{}) string {
t.Helper()
b, err := json.Marshal(val)
require.NoError(t, err)
return string(b)
}

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEFjCCAv6gAwIBAgIUHNknw0iUWaMC5UCpiribG8DQhZYwDQYJKoZIhvcNAQEL
BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
Ew1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl
c3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEwNjE0MjAyNDAwWhcNMjYwNjEz
MjAyNDAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE
CxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw
ZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBANc0MEZOJ7xm4JrCceerX0kWcdPIczXFIIZTJYdTB7YPHTiL
PFSZ9ugu8W6R7wOMLUazcD7Ugw0hjt+JkiRIY1AOvuZRX7DR3Q0sGy9qFb1y2kOk
lTSAFOV96FxxAg9Fn23mcvjV1TDO1dlxvOuAo0NMjk82TzHk7LVuYOKuJ/Sc9i8a
Ba4vndbiwkSGpytymCu0X4T4ZEARLUZ4feGhr5RbYRehq2Nb8kw/KNLZZyzlzJbr
8OkVizW796bkVJwRfCFubZPl8EvRslxZ2+sMFSozoofoFlB1FsGAvlnEfkxqTJJo
WafmsYnOVnbNfwOogDP0+bp8WAZrAxJqTAWm/LMCAwEAAaNCMEAwDgYDVR0PAQH/
BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHyfBUnvAULGlcFSljTI
DegUVLB5MA0GCSqGSIb3DQEBCwUAA4IBAQBOdVqZpMCKq+X2TBi3nJmz6kjePVBh
ocHUG02nRkL533x+PUxRpDG3AMzWF3niPxtMuVIZDfpi27zlm2QCh9b3sQi83w+9
UX1/j3dUoUyiVi/U0iZeZmuDY3ne59DNFdOgGY9p3FvJ+b9WfPg8+v2w26rGoSMz
21XKNZcRFcjOJ5LJ3i9+liaCkpXLfErA+AtqNeraHOorJ5UO4mA7OlFowV8adOQq
SinFIoXCExBTxqMv0lVzEhGN6Wd261CmKY5e4QLqASCO+s7zwGhHyzwjdA0pCNtI
PmHIk13m0p56G8hpz+M/5hBQFb0MIIR3Je6QVzfRty2ipUO91E9Ydm7C
-----END CERTIFICATE-----

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEFjCCAv6gAwIBAgIUWd8FZSev3ygjhWE7O8orqHPQ4IEwDQYJKoZIhvcNAQEL
BQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
Ew1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl
c3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEwNjEwMjAwNDAwWhcNMjYwNjA5
MjAwNDAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU
BgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE
CxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw
ZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAMXTnIDpOXXiHuKyI9EZxv7qg81DmelOB+iAzhvRsigMSuka
qZH29Aaf4PBvKLlSVN6sVP16cXRvk48qa0C78tP0kTPKWdEyE1xQUZb270SZ6Tm3
T7sNRTRwWTsgeC1n6SHlBUn3MviQgA1dZM1CbZIXQpBxtuPg+p9eu3YP/CZJFJjT
LYVKT6kRumBQEX/UUesNfUnUpVIOxxOwbVeF6a/wGxeLY6/fOQ+TJhVUjSy/pvaI
6NnycrwD/4ck6gusV5HKakidCID9MwV610Vc7AFi070VGYCjKfiv6EYMMnjycYqi
KHz623Ca4rO4qtWWvT1K/+GkryDKXeI3KHuEsdsCAwEAAaNCMEAwDgYDVR0PAQH/
BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIy8cvyabFclVWwcZ4rl
ADoLEdyAMA0GCSqGSIb3DQEBCwUAA4IBAQCzn9QbsOpBuvhhgdH/Jk0q7H0kmpVS
rbLhcQyWv9xiyopYbbUfh0Hud15rnqAkyT9nd2Kvo8T/X9rc1OXa6oDO6aoXjIm1
aKOFikET8fc/81rT81E7TVPO7TZW5s9Cej30zCOJQWZ+ibHNyequuyihtImNacXF
+1pAAldj/JMu+Ky1YFrs2iccGOpGCGbsWfLQt+wYKwya7dpSz1ceqigKavIJSOMV
CNsyC59UtFbvdk139FyEvCmecsCbWuo0JVg3do5n6upwqrgvLRNP8EHzm17DWu5T
aNtsBbv85uUgMmF7kzxr+t6VdtG9u+q0HCmW1/1VVK3ZsA+UTB7UBddD
-----END CERTIFICATE-----

View File

@ -0,0 +1,3 @@
{
"ca_chain": ["-----BEGIN CERTIFICATE-----\nMIIEFjCCAv6gAwIBAgIUWd8FZSev3ygjhWE7O8orqHPQ4IEwDQYJKoZIhvcNAQEL\nBQAwgaIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH\nEw1TYW4gRnJhbmNpc2NvMRIwEAYDVQQKEwlIYXNoaUNvcnAxIzAhBgNVBAsTGlRl\nc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MS0wKwYDVQQDEyRQcm90b3R5cGUgVGVz\ndCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMjEwNjEwMjAwNDAwWhcNMjYwNjA5\nMjAwNDAwWjCBojELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAU\nBgNVBAcTDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoTCUhhc2hpQ29ycDEjMCEGA1UE\nCxMaVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxLTArBgNVBAMTJFByb3RvdHlw\nZSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAMXTnIDpOXXiHuKyI9EZxv7qg81DmelOB+iAzhvRsigMSuka\nqZH29Aaf4PBvKLlSVN6sVP16cXRvk48qa0C78tP0kTPKWdEyE1xQUZb270SZ6Tm3\nT7sNRTRwWTsgeC1n6SHlBUn3MviQgA1dZM1CbZIXQpBxtuPg+p9eu3YP/CZJFJjT\nLYVKT6kRumBQEX/UUesNfUnUpVIOxxOwbVeF6a/wGxeLY6/fOQ+TJhVUjSy/pvaI\n6NnycrwD/4ck6gusV5HKakidCID9MwV610Vc7AFi070VGYCjKfiv6EYMMnjycYqi\nKHz623Ca4rO4qtWWvT1K/+GkryDKXeI3KHuEsdsCAwEAAaNCMEAwDgYDVR0PAQH/\nBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIy8cvyabFclVWwcZ4rl\nADoLEdyAMA0GCSqGSIb3DQEBCwUAA4IBAQCzn9QbsOpBuvhhgdH/Jk0q7H0kmpVS\nrbLhcQyWv9xiyopYbbUfh0Hud15rnqAkyT9nd2Kvo8T/X9rc1OXa6oDO6aoXjIm1\naKOFikET8fc/81rT81E7TVPO7TZW5s9Cej30zCOJQWZ+ibHNyequuyihtImNacXF\n+1pAAldj/JMu+Ky1YFrs2iccGOpGCGbsWfLQt+wYKwya7dpSz1ceqigKavIJSOMV\nCNsyC59UtFbvdk139FyEvCmecsCbWuo0JVg3do5n6upwqrgvLRNP8EHzm17DWu5T\naNtsBbv85uUgMmF7kzxr+t6VdtG9u+q0HCmW1/1VVK3ZsA+UTB7UBddD\n-----END CERTIFICATE-----\n"]
}

View File

@ -1,46 +0,0 @@
#!/bin/sh
################################################################
# Usage: ./gencert.sh
#
# Generates a keystore.jks file that can be used with a
# Cassandra server for TLS connections. This does not update
# a cassandra config file.
################################################################
set -e
KEYFILE="key.pem"
CERTFILE="cert.pem"
PKCSFILE="keystore.p12"
JKSFILE="keystore.jks"
HOST="127.0.0.1"
NAME="cassandra"
ALIAS="cassandra"
PASSWORD="cassandra"
echo "# Generating certificate keypair..."
go run /usr/local/go/src/crypto/tls/generate_cert.go --host=${HOST}
echo "# Creating keystore..."
openssl pkcs12 -export -in ${CERTFILE} -inkey ${KEYFILE} -name ${NAME} -password pass:${PASSWORD} > ${PKCSFILE}
echo "# Creating Java key store"
if [ -e "${JKSFILE}" ]; then
echo "# Removing old key store"
rm ${JKSFILE}
fi
set +e
keytool -importkeystore \
-srckeystore ${PKCSFILE} \
-srcstoretype PKCS12 \
-srcstorepass ${PASSWORD} \
-destkeystore ${JKSFILE} \
-deststorepass ${PASSWORD} \
-destkeypass ${PASSWORD} \
-alias ${ALIAS}
echo "# Removing intermediate files"
rm ${KEYFILE} ${CERTFILE} ${PKCSFILE}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,117 @@
package cassandra
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/errutil"
)
func jsonBundleToTLSConfig(rawJSON string, tlsMinVersion uint16, serverName string, insecureSkipVerify bool) (*tls.Config, error) {
var certBundle certutil.CertBundle
err := json.Unmarshal([]byte(rawJSON), &certBundle)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
if certBundle.IssuingCA != "" && len(certBundle.CAChain) > 0 {
return nil, fmt.Errorf("issuing_ca and ca_chain cannot both be specified")
}
if certBundle.IssuingCA != "" {
certBundle.CAChain = []string{certBundle.IssuingCA}
certBundle.IssuingCA = ""
}
return toClientTLSConfig(certBundle.Certificate, certBundle.PrivateKey, certBundle.CAChain, tlsMinVersion, serverName, insecureSkipVerify)
}
func pemBundleToTLSConfig(pemBundle string, tlsMinVersion uint16, serverName string, insecureSkipVerify bool) (*tls.Config, error) {
if len(pemBundle) == 0 {
return nil, errutil.UserError{Err: "empty pem bundle"}
}
pemBytes := []byte(pemBundle)
var pemBlock *pem.Block
certificate := ""
privateKey := ""
caChain := []string{}
for len(pemBytes) > 0 {
pemBlock, pemBytes = pem.Decode(pemBytes)
if pemBlock == nil {
return nil, errutil.UserError{Err: "no data found in PEM block"}
}
blockBytes := pem.EncodeToMemory(pemBlock)
switch pemBlock.Type {
case "CERTIFICATE":
// Parse the cert so we know if it's a CA or not
cert, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
if cert.IsCA {
caChain = append(caChain, string(blockBytes))
continue
}
// Only one leaf certificate supported
if certificate != "" {
return nil, errutil.UserError{Err: "multiple leaf certificates not supported"}
}
certificate = string(blockBytes)
case "RSA PRIVATE KEY", "EC PRIVATE KEY", "PRIVATE KEY":
if privateKey != "" {
return nil, errutil.UserError{Err: "multiple private keys not supported"}
}
privateKey = string(blockBytes)
default:
return nil, fmt.Errorf("unsupported PEM block type [%s]", pemBlock.Type)
}
}
return toClientTLSConfig(certificate, privateKey, caChain, tlsMinVersion, serverName, insecureSkipVerify)
}
func toClientTLSConfig(certificatePEM string, privateKeyPEM string, caChainPEMs []string, tlsMinVersion uint16, serverName string, insecureSkipVerify bool) (*tls.Config, error) {
if certificatePEM != "" && privateKeyPEM == "" {
return nil, fmt.Errorf("found certificate for client-side TLS authentication but no private key")
} else if certificatePEM == "" && privateKeyPEM != "" {
return nil, fmt.Errorf("found private key for client-side TLS authentication but no certificate")
}
var certificates []tls.Certificate
if certificatePEM != "" {
certificate, err := tls.X509KeyPair([]byte(certificatePEM), []byte(privateKeyPEM))
if err != nil {
return nil, fmt.Errorf("failed to parse certificate and private key pair: %w", err)
}
certificates = append(certificates, certificate)
}
var rootCAs *x509.CertPool
if len(caChainPEMs) > 0 {
rootCAs = x509.NewCertPool()
for _, caBlock := range caChainPEMs {
ok := rootCAs.AppendCertsFromPEM([]byte(caBlock))
if !ok {
return nil, fmt.Errorf("failed to add CA certificate to certificate pool: it may be malformed or empty")
}
}
}
config := &tls.Config{
Certificates: certificates,
RootCAs: rootCAs,
ServerName: serverName,
InsecureSkipVerify: insecureSkipVerify,
MinVersion: tlsMinVersion,
}
return config, nil
}

View File

@ -42,23 +42,35 @@ has a number of parameters to further configure a connection.
- `insecure_tls` `(bool: false)` Specifies whether to skip verification of the
server certificate when using TLS.
- `tls_server_name` `(string: "")` Specifies the name to use as the SNI host when
- `tls_server_name` `(string: "")` Specifies the name to use as the SNI host when
connecting to the Cassandra server via TLS.
- `pem_bundle` `(string: "")` Specifies concatenated PEM blocks containing a
certificate and private key; a certificate, private key, and issuing CA
certificate; or just a CA certificate.
!> **Known Issue:** There is a known issue when using `pem_bundle` with only a CA (no client certificate & key)
where Vault will not parse the CA certificate correctly. To work around this, use `pem_json` with the
following structure: `{"ca_chain": ["-----BEGIN CERTIFICATE-----\nMIIEFjC...FNYakP7I\n-----END CERTIFICATE-----"]}`
Also make sure the PEM data is properly JSON encoded with `\n` instead of newlines.
certificate; or just a CA certificate. Only one of `pem_bundle` or `pem_json` can be specified.
- `pem_json` `(string: "")` Specifies JSON containing a certificate and
private key; a certificate, private key, and issuing CA certificate; or just a
CA certificate. For convenience format is the same as the output of the
`issue` command from the `pki` secrets engine; see
[the pki documentation](/docs/secrets/pki).
CA certificate. The value in this field must be an encoded JSON object. For convenience format is the
same as the output of the `issue` command from the `pki` secrets engine; see
[the pki documentation](/docs/secrets/pki). Only one of `pem_bundle` or `pem_json` can be specified.
<details>
<summary>`pem_json` example</summary>
```json
{
"certificate": "<client certificate as a PEM>",
"private_key": "<private key as a PEM>",
"ca_chain": ["<CA as a PEM>", "<Additional PEM for the CA chain if needed"]
}
```
If using the Vault CLI, it's probably easiest to write the JSON to a file and then reference the file:
```shell
vault write database/config/cassandra-example <...other fields> pem_json=@/path/to/file.json
```
</details>
- `skip_verification` `(bool: false)` - Skip permissions checks when a connection to Cassandra
is first created. These checks ensure that Vault is able to create roles, but can be resource
@ -75,7 +87,7 @@ has a number of parameters to further configure a connection.
- `socket_keep_alive` `(string: "0s")` the keep-alive period for an active
network connection. If zero, keep-alives are not enabled.
- `consistency` `(string: "")`  Specifies the consistency option to use. See
- `consistency` `(string: "")` Specifies the consistency option to use. See
the [gocql
definition](https://github.com/gocql/gocql/blob/master/frame.go#L188) for
valid options.