Merge pull request #363 from jefferai/f-logical-cassandra

Cassandra logical backend
This commit is contained in:
Armon Dadgar 2015-06-30 09:38:22 -07:00
commit 5aa4537389
10 changed files with 1266 additions and 0 deletions

View File

@ -0,0 +1,109 @@
package cassandra
import (
"fmt"
"strings"
"sync"
"github.com/gocql/gocql"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// Factory creates a new backend
func Factory(map[string]string) (logical.Backend, error) {
return Backend(), nil
}
// Backend contains the base information for the backend's functionality
func Backend() *framework.Backend {
var b backend
b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),
PathsSpecial: &logical.Paths{
Root: []string{
"config/*",
},
},
Paths: []*framework.Path{
pathConfigConnection(&b),
pathRoles(&b),
pathCredsCreate(&b),
},
Secrets: []*framework.Secret{
secretCreds(&b),
},
}
return b.Backend
}
type backend struct {
*framework.Backend
// Session is goroutine safe, however, since we reinitialize
// it when connection info changes, we want to make sure we
// can close it and use a new connection; hence the lock
session *gocql.Session
lock sync.Mutex
}
type sessionConfig struct {
Hosts string `json:"hosts" structs:"hosts"`
Username string `json:"username" structs:"username"`
Password string `json:"password" structs:"password"`
TLS bool `json:"tls" structs:"tls"`
InsecureTLS bool `json:"insecure_tls" structs:"insecure_tls"`
Certificate string `json:"certificate" structs:"certificate"`
PrivateKey string `json:"private_key" structs:"private_key"`
IssuingCA string `json:"issuing_ca" structs:"issuing_ca"`
}
// DB returns the database connection.
func (b *backend) DB(s logical.Storage) (*gocql.Session, error) {
b.lock.Lock()
defer b.lock.Unlock()
// If we already have a DB, we got it!
if b.session != nil {
return b.session, nil
}
entry, err := s.Get("config/connection")
if err != nil {
return nil, err
}
if entry == nil {
return nil,
fmt.Errorf("Configure the DB connection with config/connection first")
}
config := &sessionConfig{}
if err := entry.DecodeJSON(config); err != nil {
return nil, err
}
return createSession(config, s)
}
// ResetDB forces a connection next time DB() is called.
func (b *backend) ResetDB(newSession *gocql.Session) {
b.lock.Lock()
defer b.lock.Unlock()
if b.session != nil {
b.session.Close()
}
b.session = newSession
}
const backendHelp = `
The Cassandra backend dynamically generates database users.
After mounting this backend, configure it using the endpoints within
the "config/" path.
`

View File

@ -0,0 +1,128 @@
package cassandra
import (
"fmt"
"log"
"os"
"testing"
"github.com/hashicorp/vault/logical"
logicaltest "github.com/hashicorp/vault/logical/testing"
"github.com/mitchellh/mapstructure"
)
func TestBackend_basic(t *testing.T) {
b := Backend()
logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t),
testAccStepRole(t),
testAccStepReadCreds(t, "test"),
},
})
}
func TestBackend_roleCrud(t *testing.T) {
b := Backend()
logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t),
testAccStepRole(t),
testAccStepReadRole(t, "test", testRole),
testAccStepDeleteRole(t, "test"),
testAccStepReadRole(t, "test", ""),
},
})
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("CASSANDRA_HOST"); v == "" {
t.Fatal("CASSANDRA_HOST must be set for acceptance tests")
}
}
func testAccStepConfig(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "config/connection",
Data: map[string]interface{}{
"hosts": os.Getenv("CASSANDRA_HOST"),
"username": "cassandra",
"password": "cassandra",
},
}
}
func testAccStepRole(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "roles/test",
Data: map[string]interface{}{
"creation_cql": testRole,
},
}
}
func testAccStepDeleteRole(t *testing.T, n string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.DeleteOperation,
Path: "roles/" + n,
}
}
func testAccStepReadCreds(t *testing.T, name string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "creds/" + name,
Check: func(resp *logical.Response) error {
var d struct {
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}
log.Printf("[WARN] Generated credentials: %v", d)
return nil
},
}
}
func testAccStepReadRole(t *testing.T, name string, cql string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "roles/" + name,
Check: func(resp *logical.Response) error {
if resp == nil {
if cql == "" {
return nil
}
return fmt.Errorf("response is nil")
}
var d struct {
CreationCQL string `mapstructure:"creation_cql"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}
if d.CreationCQL != cql {
return fmt.Errorf("bad: %#v\n%#v\n%#v\n", resp, cql, d.CreationCQL)
}
return nil
},
}
}
const testRole = `CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER;
GRANT ALL PERMISSIONS ON ALL KEYSPACES TO '{{username}}';`

View File

@ -0,0 +1,209 @@
package cassandra
import (
"fmt"
"github.com/fatih/structs"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathConfigConnection(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/connection",
Fields: map[string]*framework.FieldSchema{
"hosts": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Comma-separated list of hosts",
},
"username": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The username to use for connecting to the cluster",
},
"password": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The password to use for connecting to the cluster",
},
"tls": &framework.FieldSchema{
Type: framework.TypeBool,
Description: `Whether to use TLS. If pem_bundle or pem_json are
set, this is automatically set to true`,
},
"insecure_tls": &framework.FieldSchema{
Type: framework.TypeBool,
Description: `Whether to use TLS but skip verification; has no
effect if a CA certificate is provided`,
},
"pem_bundle": &framework.FieldSchema{
Type: framework.TypeString,
Description: `PEM-format, concatenated unencrypted secret key
and certificate, with optional CA certificate`,
},
"pem_json": &framework.FieldSchema{
Type: framework.TypeString,
Description: `JSON containing a PEM-format, unencrypted secret
key and certificate, with optional CA certificate.
The JSON output of a certificate issued with the PKI
backend can be directly passed into this parameter.
If both this and "pem_bundle" are specified, this will
take precedence.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathConnectionRead,
logical.WriteOperation: b.pathConnectionWrite,
},
HelpSynopsis: pathConfigConnectionHelpSyn,
HelpDescription: pathConfigConnectionHelpDesc,
}
}
func (b *backend) pathConnectionRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
entry, err := req.Storage.Get("config/connection")
if err != nil {
return nil, err
}
if entry == nil {
return logical.ErrorResponse(fmt.Sprintf("Configure the DB connection with config/connection first")), nil
}
config := &sessionConfig{}
if err := entry.DecodeJSON(config); err != nil {
return nil, err
}
config.Password = "**********"
if len(config.PrivateKey) > 0 {
config.PrivateKey = "**********"
}
return &logical.Response{
Data: structs.New(config).Map(),
}, nil
}
func (b *backend) pathConnectionWrite(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
hosts := data.Get("hosts").(string)
username := data.Get("username").(string)
password := data.Get("password").(string)
switch {
case len(hosts) == 0:
return logical.ErrorResponse("Hosts cannot be empty"), nil
case len(username) == 0:
return logical.ErrorResponse("Username cannot be empty"), nil
case len(password) == 0:
return logical.ErrorResponse("Password cannot be empty"), nil
}
config := &sessionConfig{
Hosts: hosts,
Username: username,
Password: password,
TLS: data.Get("tls").(bool),
InsecureTLS: data.Get("insecure_tls").(bool),
}
if config.InsecureTLS {
config.TLS = true
}
pemBundle := data.Get("pem_bundle").(string)
pemJSON := data.Get("pem_json").(string)
var certBundle *certutil.CertBundle
var parsedCertBundle *certutil.ParsedCertBundle
var err error
switch {
case len(pemJSON) != 0:
parsedCertBundle, err = certutil.ParsePKIJSON([]byte(pemJSON))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Could not parse given JSON; it must be in the format of the output of the PKI backend certificate issuing command: %s", err)), nil
}
certBundle, err = parsedCertBundle.ToCertBundle()
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error marshaling PEM information: %s", err)), nil
}
config.Certificate = certBundle.Certificate
config.PrivateKey = certBundle.PrivateKey
config.IssuingCA = certBundle.IssuingCA
config.TLS = true
case len(pemBundle) != 0:
parsedCertBundle, err = certutil.ParsePEMBundle(pemBundle)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error parsing the given PEM information: %s", err)), nil
}
certBundle, err = parsedCertBundle.ToCertBundle()
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error marshaling PEM information: %s", err)), nil
}
config.Certificate = certBundle.Certificate
config.PrivateKey = certBundle.PrivateKey
config.IssuingCA = certBundle.IssuingCA
config.TLS = true
}
session, err := createSession(config, req.Storage)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Store it
entry, err := logical.StorageEntryJSON("config/connection", config)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
// Reset the DB connection
b.ResetDB(session)
return nil, nil
}
const pathConfigConnectionHelpSyn = `
Configure the connection information to talk to Cassandra.
`
const pathConfigConnectionHelpDesc = `
This path configures the connection information used to connect to Cassandra.
"hosts" is a comma-deliniated list of hostnames in the Cassandra cluster.
"username" and "password" are self-explanatory, although the given user
must have superuser access within Cassandra. Note that since this backend
issues username/password credentials, Cassandra must be configured to use
PasswordAuthenticator or a similar backend for its authentication. If you wish
to have no authorization in Cassandra and want to use TLS client certificates,
see the PKI backend.
TLS works as follows:
* If "tls" is set to true, the connection will use TLS; this happens automatically if "pem_bundle", "pem_json", or "insecure_tls" is set
* If "insecure_tls" is set to true, the connection will not perform verification of the server certificate; this also sets "tls" to true
* If only "issuing_ca" is set in "pem_json", or the only certificate in "pem_bundle" is a CA certificate, the given CA certificate will be used for server certificate verification; otherwise the system CA certificates will be used
* If "certificate" and "private_key" are set in "pem_bundle" or "pem_json", client auth will be turned on for the connection
"pem_bundle" should be a PEM-concatenated bundle of a private key + client certificate, an issuing CA certificate, or both. "pem_json" should contain the same information; for convenience, the JSON format is the same as that output by the issue command from the PKI backend.
When configuring the connection information, the backend will verify its
validity.
`

View File

@ -0,0 +1,92 @@
package cassandra
import (
"fmt"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathCredsCreate(b *backend) *framework.Path {
return &framework.Path{
Pattern: `creds/(?P<name>\w+)`,
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the role",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathCredsCreateRead,
},
HelpSynopsis: pathCredsCreateReadHelpSyn,
HelpDescription: pathCredsCreateReadHelpDesc,
}
}
func (b *backend) pathCredsCreateRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
// Get the role
role, err := getRole(req.Storage, name)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse(fmt.Sprintf("Unknown role: %s", name)), nil
}
displayName := req.DisplayName
username := fmt.Sprintf("vault-%s-%s-%s-%d", name, displayName, generateUUID(), time.Now().Unix())
password := generateUUID()
// Get our connection
session, err := b.DB(req.Storage)
if err != nil {
return nil, err
}
// Execute each query
for _, query := range splitSQL(role.CreationCQL) {
err = session.Query(substQuery(query, map[string]string{
"username": username,
"password": password,
})).Exec()
if err != nil {
for _, query := range splitSQL(role.RollbackCQL) {
session.Query(substQuery(query, map[string]string{
"username": username,
"password": password,
})).Exec()
}
return nil, err
}
}
// Return the secret
resp := b.Secret(SecretCredsType).Response(map[string]interface{}{
"username": username,
"password": password,
}, map[string]interface{}{
"username": username,
"role": name,
})
resp.Secret.Lease = role.Lease
resp.Secret.LeaseGracePeriod = role.LeaseGracePeriod
return resp, nil
}
const pathCredsCreateReadHelpSyn = `
Request database credentials for a certain role.
`
const pathCredsCreateReadHelpDesc = `
This path creates database credentials for a certain role. The
database credentials will be generated on demand and will be automatically
revoked when the lease is up.
`

View File

@ -0,0 +1,199 @@
package cassandra
import (
"fmt"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
const (
defaultCreationCQL = `CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER;`
defaultRollbackCQL = `DROP USER '{{username}}';`
)
func pathRoles(b *backend) *framework.Path {
return &framework.Path{
Pattern: "roles/(?P<name>\\w+)",
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the role",
},
"creation_cql": &framework.FieldSchema{
Type: framework.TypeString,
Default: defaultCreationCQL,
Description: `CQL to create a user and optionally grant
authorization. If not supplied, a default that
creates non-superuser accounts with the built-in
password authenticator will be used; no
authorization grants will be configured. Separate
statements by semicolons; use @file to load from a
file. Valid template values are '{{username}}' and
'{{password}}' -- the single quotes are important!`,
},
"rollback_cql": &framework.FieldSchema{
Type: framework.TypeString,
Default: defaultRollbackCQL,
Description: `CQL to roll back an account operation. This will
be used if there is an error during execution of a
statement passed in via the "creation_cql" parameter
parameter. The default simply drops the user, which
should generally be sufficient. Separate statements
by semicolons; use @file to load from a file. Valid
template values are '{{username}}' and
'{{password}}' -- the single quotes are important!`,
},
"lease": &framework.FieldSchema{
Type: framework.TypeString,
Default: "4h",
Description: "The lease length; defaults to 4 hours",
},
"lease_grace_period": &framework.FieldSchema{
Type: framework.TypeString,
Default: "1h",
Description: `Grace period for secret renewal; defaults to
one hour`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathRoleRead,
logical.WriteOperation: b.pathRoleCreate,
logical.DeleteOperation: b.pathRoleDelete,
},
HelpSynopsis: pathRoleHelpSyn,
HelpDescription: pathRoleHelpDesc,
}
}
func getRole(s logical.Storage, n string) (*roleEntry, error) {
entry, err := s.Get("role/" + n)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result roleEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathRoleDelete(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
err := req.Storage.Delete("role/" + data.Get("name").(string))
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathRoleRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
role, err := getRole(req.Storage, data.Get("name").(string))
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(role).Map(),
}, nil
}
func (b *backend) pathRoleCreate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string)
creationCQL := data.Get("creation_cql").(string)
rollbackCQL := data.Get("rollback_cql").(string)
leaseRaw := data.Get("lease").(string)
lease, err := time.ParseDuration(leaseRaw)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Error parsing lease value of %s: %s", leaseRaw, err)), nil
}
leaseGracePeriodRaw := data.Get("lease_grace_period").(string)
leaseGracePeriod, err := time.ParseDuration(leaseGracePeriodRaw)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Error parsing lease_grace value of %s: %s", leaseGracePeriodRaw, err)), nil
}
entry := &roleEntry{
Lease: lease,
LeaseGracePeriod: leaseGracePeriod,
CreationCQL: creationCQL,
RollbackCQL: rollbackCQL,
}
// Store it
entryJSON, err := logical.StorageEntryJSON("role/"+name, entry)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entryJSON); err != nil {
return nil, err
}
return nil, nil
}
type roleEntry struct {
CreationCQL string `json:"creation_cql" structs:"creation_cql"`
Lease time.Duration `json:"lease" structs:"lease"`
LeaseGracePeriod time.Duration `json:"lease_grace_period" structs:"lease_grace_period"`
RollbackCQL string `json:"rollback_cql" structs:"rollback_cql"`
}
const pathRoleHelpSyn = `
Manage the roles that can be created with this backend.
`
const pathRoleHelpDesc = `
This path lets you manage the roles that can be created with this backend.
The "creation_cql" parameter customizes the CQL string used to create users
and assign them grants. This can be a sequence of CQL queries separated by
semicolons. Some substitution will be done to the CQL string for certain keys.
The names of the variables must be surrounded by '{{' and '}}' to be replaced.
Note that it is important that single quotes are used, not double quotes.
* "username" - The random username generated for the DB user.
* "password" - The random password generated for the DB user.
If no "creation_cql" parameter is given, a default will be used:
` + defaultCreationCQL + `
This default should be suitable for Cassandra installations using the password
authenticator but not configured to use authorization.
Similarly, the "rollback_cql" is used if user creation fails, in the absense of
Cassandra transactions. The default should be suitable for almost any
instance of Cassandra:
` + defaultRollbackCQL + `
"lease" and "lease_grace_period" control the lease time and the allowed grace
period past lease expiration, respectively.
`

View File

@ -0,0 +1,85 @@
package cassandra
import (
"fmt"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// SecretCredsType is the type of creds issued from this backend
const SecretCredsType = "cassandra"
func secretCreds(b *backend) *framework.Secret {
return &framework.Secret{
Type: SecretCredsType,
Fields: map[string]*framework.FieldSchema{
"username": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Username",
},
"password": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Password",
},
},
DefaultDuration: 1 * time.Hour,
DefaultGracePeriod: 10 * time.Minute,
Renew: b.secretCredsRenew,
Revoke: b.secretCredsRevoke,
}
}
func (b *backend) secretCredsRenew(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// Get the lease information
roleRaw, ok := req.Secret.InternalData["role"]
if !ok {
return nil, fmt.Errorf("Secret is missing role internal data")
}
roleName, ok := roleRaw.(string)
if !ok {
return nil, fmt.Errorf("Error converting role internal data to string")
}
role, err := getRole(req.Storage, roleName)
if err != nil {
return nil, fmt.Errorf("Unable to load role: %s", err)
}
return framework.LeaseExtend(role.Lease, 0, false)(req, d)
}
func (b *backend) secretCredsRevoke(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// Get the username from the internal data
usernameRaw, ok := req.Secret.InternalData["username"]
if !ok {
return nil, fmt.Errorf("Secret is missing username internal data")
}
username, ok := usernameRaw.(string)
if !ok {
return nil, fmt.Errorf("Error converting username internal data to string")
}
session, err := b.DB(req.Storage)
if err != nil {
return nil, fmt.Errorf("Error getting session")
}
err = session.Query(fmt.Sprintf("REVOKE ALL PERMISSIONS ON ALL KEYSPACES FROM '%s'", username)).Exec()
if err != nil {
return nil, fmt.Errorf("Error revoking permissions for user %s", username)
}
err = session.Query(fmt.Sprintf("DROP USER '%s'", username)).Exec()
if err != nil {
return nil, fmt.Errorf("Error removing user %s", username)
}
return nil, nil
}

View File

@ -0,0 +1,106 @@
package cassandra
import (
"crypto/rand"
"crypto/tls"
"fmt"
"strings"
"github.com/gocql/gocql"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
)
// SplitSQL is used to split a series of SQL statements
func splitSQL(sql string) []string {
parts := strings.Split(sql, ";")
out := make([]string, 0, len(parts))
for _, p := range parts {
clean := strings.TrimSpace(p)
if len(clean) > 0 {
out = append(out, clean)
}
}
return out
}
// Query templates a query for us.
func substQuery(tpl string, data map[string]string) string {
for k, v := range data {
tpl = strings.Replace(tpl, fmt.Sprintf("{{%s}}", k), v, -1)
}
return tpl
}
func createSession(cfg *sessionConfig, s logical.Storage) (*gocql.Session, error) {
clusterConfig := gocql.NewCluster(strings.Split(cfg.Hosts, ",")...)
clusterConfig.Authenticator = gocql.PasswordAuthenticator{
Username: cfg.Username,
Password: cfg.Password,
}
if cfg.TLS {
tlsConfig := &tls.Config{
InsecureSkipVerify: cfg.InsecureTLS,
}
if len(cfg.Certificate) > 0 || len(cfg.IssuingCA) > 0 {
if len(cfg.Certificate) > 0 && len(cfg.PrivateKey) == 0 {
return nil, fmt.Errorf("Found certificate for TLS authentication but no private key")
}
certBundle := &certutil.CertBundle{}
if len(cfg.Certificate) > 0 {
certBundle.Certificate = cfg.Certificate
certBundle.PrivateKey = cfg.PrivateKey
}
if len(cfg.IssuingCA) > 0 {
certBundle.IssuingCA = cfg.IssuingCA
tlsConfig.InsecureSkipVerify = false
}
parsedCertBundle, err := certBundle.ToParsedCertBundle()
if err != nil {
return nil, fmt.Errorf("Error parsing certificate bundle: %s", err)
}
tlsConfig, err = parsedCertBundle.GetTLSConfig(certutil.TLSClient)
if err != nil {
return nil, fmt.Errorf("Error getting TLS configuration: %s", err)
}
}
clusterConfig.SslOpts = &gocql.SslOptions{
Config: *tlsConfig,
}
}
session, err := clusterConfig.CreateSession()
if err != nil {
return nil, fmt.Errorf("Error creating session: %s", err)
}
// Verify the info
err = session.Query(`LIST USERS`).Exec()
if err != nil {
return nil, fmt.Errorf("Error validating connection info: %s", err)
}
return session, nil
}
// generateUUID is used to generate a random UUID
func generateUUID() string {
buf := make([]byte, 16)
if _, err := rand.Read(buf); err != nil {
panic(fmt.Errorf("failed to read random bytes: %v", err))
}
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
buf[0:4],
buf[4:6],
buf[6:8],
buf[8:10],
buf[10:16])
}

View File

@ -15,6 +15,7 @@ import (
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
"github.com/hashicorp/vault/builtin/logical/aws"
"github.com/hashicorp/vault/builtin/logical/cassandra"
"github.com/hashicorp/vault/builtin/logical/consul"
"github.com/hashicorp/vault/builtin/logical/mysql"
"github.com/hashicorp/vault/builtin/logical/pki"
@ -68,6 +69,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
"aws": aws.Factory,
"consul": consul.Factory,
"postgresql": postgresql.Factory,
"cassandra": cassandra.Factory,
"pki": pki.Factory,
"transit": transit.Factory,
"mysql": mysql.Factory,

View File

@ -0,0 +1,332 @@
---
layout: "docs"
page_title: "Secret Backend: Cassandra"
sidebar_current: "docs-secrets-cassandra"
description: |-
The Cassandra secret backend for Vault generates database credentials to access Cassandra.
---
# Cassandra Secret Backend
Name: `cassandra`
The Cassandra secret backend for Vault generates database credentials
dynamically based on configured roles. This means that services that need
to access a database no longer need to hardcode credentials: they can request
them from Vault, and use Vault's leasing mechanism to more easily roll keys.
Additionally, it introduces a new ability: with every service accessing
the database with unique credentials, it makes auditing much easier when
questionable data access is discovered: you can track it down to the specific
instance of a service based on the Cassandra username.
This page will show a quick start for this backend. For detailed documentation
on every path, use `vault path-help` after mounting the backend.
## Quick Start
The first step to using the Cassandra backend is to mount it.
Unlike the `generic` backend, the `cassandra` backend is not mounted by default.
```text
$ vault mount cassandra
Successfully mounted 'cassandra' at 'cassandra'!
```
Next, Vault must be configured to connect to Cassandra. This is done by
writing one or more hosts, a username, and a password:
```text
$ vault write cassandra/config/connection \
host=localhost username=cassandra password=cassandra
```
In this case, we've configured Vault with the user "cassandra" and password "cassandra",
It is important that the Vault user is a superuser, in order to manage other user accounts.
The next step is to configure a role. A role is a logical name that maps
to a policy used to generated those credentials. For example, lets create
a "readonly" role:
```text
$ vault write cassandra/roles/readonly \
creation_cql="CREATE USER '{{username}}' WITH PASSWORD '{{password}}' NOSUPERUSER; \
GRANT SELECT ON ALL KEYSPACES TO '{{username}}';"
Success! Data written to: cassandra/roles/readonly
```
By writing to the `roles/readonly` path we are defining the `readonly` role.
This role will be created by evaluating the given `creation_cql` statements. By
default, the `{{username}}` and `{{password}}` fields will be populated by
Vault with dynamically generated values. This CQL statement is creating
the named user, and then granting it `SELECT` or read-only privileges
to keyspaces. More complex `GRANT` queries can be used to
customize the privileges of the role. See the [CQL Reference Manual](http://docs.datastax.com/en/cql/3.1/cql/cql_reference/grant_r.html)
for more information.
To generate a new set of credentials, we simply read from that role:
Vault is now configured to create and manage credentials for Cassandra!
```text
$ vault read cassandra/creds/readonly
Key Value
lease_id cassandra/creds/test/7a23e890-3a26-531d-529b-92d18d1fa63f
lease_duration 3600
lease_renewable true
password dfa80eea-ccbe-b228-ebf7-e2f62b245e71
username vault-root-1434647667-9313
```
By reading from the `creds/readonly` path, Vault has generated a new
set of credentials using the `readonly` role configuration. Here we
see the dynamically generated username and password, along with a one
hour lease.
Using ACLs, it is possible to restrict using the `cassandra` backend such
that trusted operators can manage the role definitions, and both
users and applications are restricted in the credentials they are
allowed to read.
If you get stuck at any time, simply run `vault help cassandra` or with a
subpath for interactive help output.
## API
### /cassandra/config/connection
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Configures the connection information used to communicate with Cassandra.
TLS works as follows:<br /><br />
<ul>
<li>
• If `tls` is set to true, the connection will use TLS; this happens automatically if `pem_bundle`, `pem_json`, or `insecure_tls` is set
</li>
<li>
• If `insecure_tls` is set to true, the connection will not perform verification of the server certificate; this also sets `tls` to true
</li>
<li>
• If only `issuing_ca` is set in `pem_json`, or the only certificate in `pem_bundle` is a CA certificate, the given CA certificate will be used for server certificate verification; otherwise the system CA certificates will be used
</li>
<li>
• If `certificate` and `private_key` are set in `pem_bundle` or `pem_json`, client auth will be turned on for the connection
</li>
</ul>
`pem_bundle` should be a PEM-concatenated bundle of a private key + client certificate, an issuing CA certificate, or both. `pem_json` should contain the same information; for convenience, the JSON format is the same as that output by the issue command from the PKI backend.<br /><br />
This is a root protected endpoint.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/cassandra/config/connection`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">hosts</span>
<span class="param-flags">required</span>
A set of comma-deliniated Cassandra hosts to connect to.
</li>
<li>
<span class="param">username</span>
<span class="param-flags">required</span>
The username to use for superuser access.
</li>
<li>
<span class="param">password</span>
<span class="param-flags">required</span>
The password corresponding to the given username.
</li>
<li>
<span class="param">tls</span>
<span class="param-flags">optional</span>
Whether to use TLS when connecting to Cassandra.
</li>
<li>
<span class="param">insecure_tls</span>
<span class="param-flags">optional</span>
Whether to skip verification of the server certificate when using TLS.
</li>
<li>
<span class="param">pem_bundle</span>
<span class="param-flags">optional</span>
Concatenated PEM blocks containing a certificate and private key;
a certificate, private key, and issuing CA certificate; or just a CA
certificate.
</li>
<li>
<span class="param">pem_json</span>
<span class="param-flags">optional</span>
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` backend; see [the pki documentation](https://vaultproject.io/docs/secrets/pki/index.html).
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
</dl>
### /cassandra/roles/
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Creates or updates the role definition.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/cassandra/roles/<name>`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">creation_cql</span>
<span class="param-flags">optional</span>
The CQL statements executed to create and configure the new user.
Must be semi-colon separated. The '{{username}}' and '{{password}}'
values will be substituted; it is required that these parameters are
in single quotes. The default creates a non-superuser user with
no authorization grants.
</li>
<li>
<span class="param">rollback_cql</span>
<span class="param-flags">optional</span>
The CQL statements executed to attempt a rollback if an error is
encountered during user creation. The default is to delete the user.
Must be semi-colon separated. The '{{username}}' and '{{password}}'
values will be substituted; it is required that these parameters are
in single quotes.
</li>
<li>
<span class="param">lease</span>
<span class="param-flags">optional</span>
The lease value provided as a string duration
with time suffix. Hour is the largest suffix.
</li>
<li>
<span class="param">lease_grace_period</span>
<span class="param-flags">optional</span>
The lease grace period (time before revocation after the lease has
expired) provided as a string duration with time suffix. Hour is the
largest suffix.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
</dl>
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Queries the role definition.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/cassandra/roles/<name>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"creation_cql": "CREATE USER...",
"revocation_cql": "DROP USER...",
"lease": "12h",
"lease_grace_period": "1h"
}
}
```
</dd>
</dl>
#### DELETE
<dl class="api">
<dt>Description</dt>
<dd>
Deletes the role definition.
</dd>
<dt>Method</dt>
<dd>DELETE</dd>
<dt>URL</dt>
<dd>`/cassandra/roles/<name>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
</dl>
### /cassandra/creds/
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Generates a new set of dynamic credentials based on the named role.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/cassandra/creds/<name>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"username": "vault-root-1430158508-126",
"password": "132ae3ef-5a64-7499-351e-bfe59f3a2a21"
}
}
```
</dd>
</dl>

View File

@ -118,6 +118,10 @@
<a href="/docs/secrets/mysql/index.html">MySQL</a>
</li>
<li<%= sidebar_current("docs-secrets-cassandra") %>>
<a href="/docs/secrets/cassandra/index.html">Cassandra</a>
</li>
<li<%= sidebar_current("docs-secrets-transit") %>>
<a href="/docs/secrets/transit/index.html">Transit</a>
</li>