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:
parent
ccee88180b
commit
7f6a1739a3
|
@ -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
|
||||
```
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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]",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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}
|
BIN
plugins/database/cassandra/test-fixtures/with_tls/keystore.jks (Stored with Git LFS)
BIN
plugins/database/cassandra/test-fixtures/with_tls/keystore.jks (Stored with Git LFS)
Binary file not shown.
BIN
plugins/database/cassandra/test-fixtures/with_tls/stores/keystore (Stored with Git LFS)
Normal file
BIN
plugins/database/cassandra/test-fixtures/with_tls/stores/keystore (Stored with Git LFS)
Normal file
Binary file not shown.
BIN
plugins/database/cassandra/test-fixtures/with_tls/stores/server.p12 (Stored with Git LFS)
Normal file
BIN
plugins/database/cassandra/test-fixtures/with_tls/stores/server.p12 (Stored with Git LFS)
Normal file
Binary file not shown.
BIN
plugins/database/cassandra/test-fixtures/with_tls/stores/truststore (Stored with Git LFS)
Normal file
BIN
plugins/database/cassandra/test-fixtures/with_tls/stores/truststore (Stored with Git LFS)
Normal file
Binary file not shown.
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue