Merge pull request #363 from jefferai/f-logical-cassandra
Cassandra logical backend
This commit is contained in:
commit
5aa4537389
|
@ -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.
|
||||
`
|
|
@ -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}}';`
|
|
@ -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.
|
||||
`
|
|
@ -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.
|
||||
`
|
|
@ -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.
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue