mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 20:17:59 +00:00
Adding mssql secret backend
This commit is contained in:
138
builtin/logical/mssql/backend.go
Normal file
138
builtin/logical/mssql/backend.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
_ "github.com/denisenkom/go-mssqldb"
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
|
||||||
|
return Backend().Setup(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
pathConfigLease(&b),
|
||||||
|
pathListRoles(&b),
|
||||||
|
pathRoles(&b),
|
||||||
|
pathCredsCreate(&b),
|
||||||
|
},
|
||||||
|
|
||||||
|
Secrets: []*framework.Secret{
|
||||||
|
secretCreds(&b),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Backend
|
||||||
|
}
|
||||||
|
|
||||||
|
type backend struct {
|
||||||
|
*framework.Backend
|
||||||
|
|
||||||
|
db *sql.DB
|
||||||
|
defaultDb string
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB returns the default database connection.
|
||||||
|
func (b *backend) DB(s logical.Storage) (*sql.DB, error) {
|
||||||
|
b.lock.Lock()
|
||||||
|
defer b.lock.Unlock()
|
||||||
|
|
||||||
|
// If we already have a DB, we got it!
|
||||||
|
if b.db != nil {
|
||||||
|
return b.db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, attempt to make connection
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
var connConfig connectionConfig
|
||||||
|
if err := entry.DecodeJSON(&connConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conn := connConfig.ConnectionParams
|
||||||
|
|
||||||
|
b.db, err = sql.Open("mssql", BuildDsn(conn))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set some connection pool settings. We don't need much of this,
|
||||||
|
// since the request rate shouldn't be high.
|
||||||
|
b.db.SetMaxOpenConns(connConfig.MaxOpenConnections)
|
||||||
|
|
||||||
|
stmt, err := b.db.Prepare("SELECT db_name();")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
err = stmt.QueryRow().Scan(&b.defaultDb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetDB forces a connection next time DB() is called.
|
||||||
|
func (b *backend) ResetDB() {
|
||||||
|
b.lock.Lock()
|
||||||
|
defer b.lock.Unlock()
|
||||||
|
|
||||||
|
if b.db != nil {
|
||||||
|
b.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
b.db = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lease returns the lease information
|
||||||
|
func (b *backend) Lease(s logical.Storage) (*configLease, error) {
|
||||||
|
entry, err := s.Get("config/lease")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if entry == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result configLease
|
||||||
|
if err := entry.DecodeJSON(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendHelp = `
|
||||||
|
The MSSQL backend dynamically generates database users.
|
||||||
|
|
||||||
|
After mounting this backend, configure it using the endpoints within
|
||||||
|
the "config/" path.
|
||||||
|
|
||||||
|
This backend does not support Azure SQL Databases
|
||||||
|
`
|
||||||
169
builtin/logical/mssql/backend_test.go
Normal file
169
builtin/logical/mssql/backend_test.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package mssql
|
||||||
|
|
||||||
|
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, _ := Factory(logical.TestBackendConfig())
|
||||||
|
|
||||||
|
logicaltest.Test(t, logicaltest.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Backend: b,
|
||||||
|
Steps: []logicaltest.TestStep{
|
||||||
|
testAccStepConfig(t),
|
||||||
|
testAccStepRole(t),
|
||||||
|
testAccStepReadCreds(t, "web"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "web", testRoleSQL),
|
||||||
|
testAccStepDeleteRole(t, "web"),
|
||||||
|
testAccStepReadRole(t, "web", ""),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackend_leaseWriteRead(t *testing.T) {
|
||||||
|
b := Backend()
|
||||||
|
|
||||||
|
logicaltest.Test(t, logicaltest.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Backend: b,
|
||||||
|
Steps: []logicaltest.TestStep{
|
||||||
|
testAccStepConfig(t),
|
||||||
|
testAccStepWriteLease(t),
|
||||||
|
testAccStepReadLease(t),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccPreCheck(t *testing.T) {
|
||||||
|
if v := os.Getenv("MSSQL_PARAMS"); v == "" {
|
||||||
|
t.Fatal("MSSQL_PARAMS must be set for acceptance tests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccStepConfig(t *testing.T) logicaltest.TestStep {
|
||||||
|
return logicaltest.TestStep{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/connection",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"connection_params": os.Getenv("MSSQL_PARAMS"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccStepRole(t *testing.T) logicaltest.TestStep {
|
||||||
|
return logicaltest.TestStep{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "roles/web",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"sql": testRoleSQL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, sql string) logicaltest.TestStep {
|
||||||
|
return logicaltest.TestStep{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "roles/" + name,
|
||||||
|
Check: func(resp *logical.Response) error {
|
||||||
|
if resp == nil {
|
||||||
|
if sql == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("bad: %#v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
var d struct {
|
||||||
|
SQL string `mapstructure:"sql"`
|
||||||
|
}
|
||||||
|
if err := mapstructure.Decode(resp.Data, &d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.SQL != sql {
|
||||||
|
return fmt.Errorf("bad: %#v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccStepWriteLease(t *testing.T) logicaltest.TestStep {
|
||||||
|
return logicaltest.TestStep{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "config/lease",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"lease": "1h5m",
|
||||||
|
"lease_max": "24h",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccStepReadLease(t *testing.T) logicaltest.TestStep {
|
||||||
|
return logicaltest.TestStep{
|
||||||
|
Operation: logical.ReadOperation,
|
||||||
|
Path: "config/lease",
|
||||||
|
Check: func(resp *logical.Response) error {
|
||||||
|
if resp.Data["lease"] != "1h5m0s" || resp.Data["lease_max"] != "24h0m0s" {
|
||||||
|
return fmt.Errorf("bad: %#v", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testRoleSQL = `
|
||||||
|
CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}';
|
||||||
|
CREATE USER [{{name}}] FOR LOGIN [{{name}}];
|
||||||
|
GRANT SELECT ON SCHEMA::dbo TO [{{name}}]
|
||||||
|
`
|
||||||
88
builtin/logical/mssql/path_config_connection.go
Normal file
88
builtin/logical/mssql/path_config_connection.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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{
|
||||||
|
"connection_params": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "DB connection parameters",
|
||||||
|
},
|
||||||
|
"max_open_connections": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeInt,
|
||||||
|
Description: "Maximum number of open connections to database",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.UpdateOperation: b.pathConnectionWrite,
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathConfigConnectionHelpSyn,
|
||||||
|
HelpDescription: pathConfigConnectionHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathConnectionWrite(
|
||||||
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
connParams := data.Get("connection_params").(string)
|
||||||
|
|
||||||
|
maxOpenConns := data.Get("max_open_connections").(int)
|
||||||
|
if maxOpenConns == 0 {
|
||||||
|
maxOpenConns = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the string
|
||||||
|
db, err := sql.Open("mssql", BuildDsn(connParams))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
|
"Error validating connection info: %s", err)), nil
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
|
"Error validating connection info: %s", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store it
|
||||||
|
entry, err := logical.StorageEntryJSON("config/connection", connectionConfig{
|
||||||
|
ConnectionParams: connParams,
|
||||||
|
MaxOpenConnections: maxOpenConns,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := req.Storage.Put(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the DB connection
|
||||||
|
b.ResetDB()
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type connectionConfig struct {
|
||||||
|
ConnectionParams string `json:"connection_params"`
|
||||||
|
MaxOpenConnections int `json:"max_open_connections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathConfigConnectionHelpSyn = `
|
||||||
|
Configure the connection string to talk to Microsoft Sql Server.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathConfigConnectionHelpDesc = `
|
||||||
|
This path configures the connection string used to connect to Sql Server.
|
||||||
|
The value of the string is a Data Source Name (DSN). An example is
|
||||||
|
using "server=<hostname>;port=<port>;user id=<username>;password=<password>;database=<database>;"
|
||||||
|
|
||||||
|
When configuring the connection string, the backend will verify its validity.
|
||||||
|
`
|
||||||
103
builtin/logical/mssql/path_config_lease.go
Normal file
103
builtin/logical/mssql/path_config_lease.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pathConfigLease(b *backend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: "config/lease",
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"lease": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Default lease for roles.",
|
||||||
|
},
|
||||||
|
|
||||||
|
"lease_max": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Maximum time a credential is valid for.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.ReadOperation: b.pathLeaseRead,
|
||||||
|
logical.UpdateOperation: b.pathLeaseWrite,
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathConfigLeaseHelpSyn,
|
||||||
|
HelpDescription: pathConfigLeaseHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathLeaseWrite(
|
||||||
|
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
leaseRaw := d.Get("lease").(string)
|
||||||
|
leaseMaxRaw := d.Get("lease_max").(string)
|
||||||
|
|
||||||
|
lease, err := time.ParseDuration(leaseRaw)
|
||||||
|
if err != nil {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
|
"Invalid lease: %s", err)), nil
|
||||||
|
}
|
||||||
|
leaseMax, err := time.ParseDuration(leaseMaxRaw)
|
||||||
|
if err != nil {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
|
"Invalid lease: %s", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store it
|
||||||
|
entry, err := logical.StorageEntryJSON("config/lease", &configLease{
|
||||||
|
Lease: lease,
|
||||||
|
LeaseMax: leaseMax,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := req.Storage.Put(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathLeaseRead(
|
||||||
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
lease, err := b.Lease(req.Storage)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lease == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"lease": lease.Lease.String(),
|
||||||
|
"lease_max": lease.LeaseMax.String(),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type configLease struct {
|
||||||
|
Lease time.Duration
|
||||||
|
LeaseMax time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathConfigLeaseHelpSyn = `
|
||||||
|
Configure the default lease information for generated credentials.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathConfigLeaseHelpDesc = `
|
||||||
|
This configures the default lease information used for credentials
|
||||||
|
generated by this backend. The lease specifies the duration that a
|
||||||
|
credential will be valid for, as well as the maximum session for
|
||||||
|
a set of credentials.
|
||||||
|
|
||||||
|
The format for the lease is "1h" or integer and then unit. The longest
|
||||||
|
unit is hour.
|
||||||
|
`
|
||||||
123
builtin/logical/mssql/path_creds_create.go
Normal file
123
builtin/logical/mssql/path_creds_create.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-uuid"
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pathCredsCreate(b *backend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: "creds/" + framework.GenericNameRegex("name"),
|
||||||
|
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: pathCredsCreateHelpSyn,
|
||||||
|
HelpDescription: pathCredsCreateHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathCredsCreateRead(
|
||||||
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
name := data.Get("name").(string)
|
||||||
|
|
||||||
|
// Get the role
|
||||||
|
role, err := b.Role(req.Storage, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if role == nil {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if we have a lease
|
||||||
|
lease, err := b.Lease(req.Storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lease == nil {
|
||||||
|
lease = &configLease{Lease: 1 * time.Hour}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate our username and password
|
||||||
|
displayName := req.DisplayName
|
||||||
|
if len(displayName) > 10 {
|
||||||
|
displayName = displayName[:10]
|
||||||
|
}
|
||||||
|
userUUID, err := uuid.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
username := fmt.Sprintf("%s-%s", displayName, userUUID)
|
||||||
|
password, err := uuid.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get our connection
|
||||||
|
db, err := b.DB(req.Storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
// Always reset database to default db of connection. Since it is in a
|
||||||
|
// transaction, all statements will be on the same connection in the pool.
|
||||||
|
roleSQL := fmt.Sprintf("USE [%s]; %s", b.defaultDb, role.SQL)
|
||||||
|
|
||||||
|
// Execute each query
|
||||||
|
for _, query := range SplitSQL(roleSQL) {
|
||||||
|
stmt, err := db.Prepare(Query(query, map[string]string{
|
||||||
|
"name": username,
|
||||||
|
"password": password,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the transaction
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the secret
|
||||||
|
resp := b.Secret(SecretCredsType).Response(map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}, map[string]interface{}{
|
||||||
|
"username": username,
|
||||||
|
})
|
||||||
|
resp.Secret.TTL = lease.Lease
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathCredsCreateHelpSyn = `
|
||||||
|
Request database credentials for a certain role.
|
||||||
|
`
|
||||||
|
|
||||||
|
const pathCredsCreateHelpDesc = `
|
||||||
|
This path reads database credentials for a certain role. The
|
||||||
|
database credentials will be generated on demand and will be automatically
|
||||||
|
revoked when the lease is up.
|
||||||
|
`
|
||||||
167
builtin/logical/mssql/path_roles.go
Normal file
167
builtin/logical/mssql/path_roles.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
func pathListRoles(b *backend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: "roles/?$",
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.ListOperation: b.pathRoleList,
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathRoleHelpSyn,
|
||||||
|
HelpDescription: pathRoleHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathRoles(b *backend) *framework.Path {
|
||||||
|
return &framework.Path{
|
||||||
|
Pattern: "roles/" + framework.GenericNameRegex("name"),
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"name": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "Name of the role.",
|
||||||
|
},
|
||||||
|
|
||||||
|
"sql": &framework.FieldSchema{
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "SQL string to create a role. See help for more info.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
logical.ReadOperation: b.pathRoleRead,
|
||||||
|
logical.UpdateOperation: b.pathRoleCreate,
|
||||||
|
logical.DeleteOperation: b.pathRoleDelete,
|
||||||
|
},
|
||||||
|
|
||||||
|
HelpSynopsis: pathRoleHelpSyn,
|
||||||
|
HelpDescription: pathRoleHelpDesc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) Role(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 := b.Role(req.Storage, data.Get("name").(string))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if role == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"sql": role.SQL,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathRoleList(
|
||||||
|
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
entries, err := req.Storage.List("role/")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return logical.ListResponse(entries), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) pathRoleCreate(
|
||||||
|
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
name := data.Get("name").(string)
|
||||||
|
sql := data.Get("sql").(string)
|
||||||
|
|
||||||
|
// Get our connection
|
||||||
|
db, err := b.DB(req.Storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the query by trying to prepare it
|
||||||
|
for _, query := range SplitSQL(sql) {
|
||||||
|
stmt, err := db.Prepare(Query(query, map[string]string{
|
||||||
|
"name": "foo",
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
|
"Error testing query: %s", err)), nil
|
||||||
|
}
|
||||||
|
stmt.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store it
|
||||||
|
entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{
|
||||||
|
SQL: sql,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := req.Storage.Put(entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type roleEntry struct {
|
||||||
|
SQL string `json:"sql"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "sql" parameter customizes the SQL string used to create the login to
|
||||||
|
the server. The parameter can be a sequence of SQL queries, each semi-colon
|
||||||
|
seperated. Some substitution will be done to the SQL string for certain keys.
|
||||||
|
The names of the variables must be surrounded by "{{" and "}}" to be replaced.
|
||||||
|
|
||||||
|
* "name" - The random username generated for the DB user.
|
||||||
|
|
||||||
|
* "password" - The random password generated for the DB user.
|
||||||
|
|
||||||
|
Example SQL query to use:
|
||||||
|
|
||||||
|
CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}';
|
||||||
|
CREATE USER [{{name}}] FROM LOGIN [{{name}}];
|
||||||
|
GRANT SELECT, UPDATE, DELETE, INSERT on SCHEMA::dbo TO [{{name}}];
|
||||||
|
|
||||||
|
Please see the Microsoft SQL Server manual on the GRANT command to learn how to
|
||||||
|
do more fine grained access.
|
||||||
|
`
|
||||||
173
builtin/logical/mssql/secret_creds.go
Normal file
173
builtin/logical/mssql/secret_creds.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/logical"
|
||||||
|
"github.com/hashicorp/vault/logical/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SecretCredsType = "creds"
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Renew: b.secretCredsRenew,
|
||||||
|
Revoke: b.secretCredsRevoke,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *backend) secretCredsRenew(
|
||||||
|
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
|
// Get the lease information
|
||||||
|
lease, err := b.Lease(req.Storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lease == nil {
|
||||||
|
lease = &configLease{}
|
||||||
|
}
|
||||||
|
|
||||||
|
f := framework.LeaseExtend(lease.Lease, lease.LeaseMax, b.System())
|
||||||
|
return f(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)
|
||||||
|
|
||||||
|
// Get our connection
|
||||||
|
db, err := b.DB(req.Storage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// First disable server login
|
||||||
|
stmt, err := db.Prepare(fmt.Sprintf("ALTER LOGIN [%s] DISABLE;", username))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for sessions for the login so that we can kill any outstanding
|
||||||
|
// sessions. There cannot be any active sessions before we drop the logins
|
||||||
|
// This isn't done in a transaction because even if we fail along the way,
|
||||||
|
// we want to remove as much access as possible
|
||||||
|
stmt, err = db.Prepare(fmt.Sprintf(
|
||||||
|
"SELECT session_id FROM sys.dm_exec_sessions WHERE login_name = '%s';", username))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.Query()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var revokeStmts []string
|
||||||
|
for rows.Next() {
|
||||||
|
var sessionID int
|
||||||
|
err = rows.Scan(&sessionID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
revokeStmts = append(revokeStmts, fmt.Sprintf("KILL %d;", sessionID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for database users using undocumented stored procedure for now since
|
||||||
|
// it is the easiest way to get this information;
|
||||||
|
// we need to drop the database users before we can drop the login and the role
|
||||||
|
// This isn't done in a transaction because even if we fail along the way,
|
||||||
|
// we want to remove as much access as possible
|
||||||
|
stmt, err = db.Prepare(fmt.Sprintf("EXEC sp_msloginmappings '%s';", username))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err = stmt.Query()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var loginName, dbName, qUsername, aliasName string
|
||||||
|
err = rows.Scan(&loginName, &dbName, &qUsername, &aliasName)
|
||||||
|
if err != nil {
|
||||||
|
// keep going; remove as many permissions as possible right now
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
revokeStmts = append(revokeStmts, fmt.Sprintf(
|
||||||
|
"USE [%s]; DROP USER IF EXISTS [%s];", dbName, username))
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do not stop on error, as we want to remove as
|
||||||
|
// many permissions as possible right now
|
||||||
|
var lastStmtError error
|
||||||
|
for _, query := range revokeStmts {
|
||||||
|
stmt, err := db.Prepare(query)
|
||||||
|
if err != nil {
|
||||||
|
lastStmtError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err = stmt.Exec()
|
||||||
|
if err != nil {
|
||||||
|
lastStmtError = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// can't drop if not all database users are dropped
|
||||||
|
if rows.Err() != nil {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
|
"could not generate sql statements for all rows: %v", rows.Err())), nil
|
||||||
|
}
|
||||||
|
if lastStmtError != nil {
|
||||||
|
return logical.ErrorResponse(fmt.Sprintf(
|
||||||
|
"could not perform all sql statements: %v", lastStmtError)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop this login
|
||||||
|
stmt, err = db.Prepare(fmt.Sprintf(dropLoginSQL, username, username))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
if _, err := stmt.Exec(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropLoginSQL = `
|
||||||
|
IF EXISTS
|
||||||
|
(SELECT name
|
||||||
|
FROM master.sys.server_principals
|
||||||
|
WHERE name = '%s')
|
||||||
|
BEGIN
|
||||||
|
DROP LOGIN [%s]
|
||||||
|
END
|
||||||
|
`
|
||||||
60
builtin/logical/mssql/util.go
Normal file
60
builtin/logical/mssql/util.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package mssql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildDsn creates a DSN with some default and overriden values
|
||||||
|
func BuildDsn(dsn string) string {
|
||||||
|
dsnParts := make(map[string]string)
|
||||||
|
parts := strings.Split(dsn, ";")
|
||||||
|
for _, part := range parts {
|
||||||
|
if len(part) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lst := strings.SplitN(part, "=", 2)
|
||||||
|
name := strings.TrimSpace(strings.ToLower(lst[0]))
|
||||||
|
if len(name) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var value string
|
||||||
|
if len(lst) > 1 {
|
||||||
|
value = strings.TrimSpace(lst[1])
|
||||||
|
}
|
||||||
|
dsnParts[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default app name to vault
|
||||||
|
if _, exists := dsnParts["app name"]; !exists {
|
||||||
|
dsnParts["app name"] = "vault"
|
||||||
|
}
|
||||||
|
|
||||||
|
var newDsn string
|
||||||
|
for k, v := range dsnParts {
|
||||||
|
newDsn = newDsn + k + "=" + v + ";"
|
||||||
|
}
|
||||||
|
return newDsn
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 Query(tpl string, data map[string]string) string {
|
||||||
|
for k, v := range data {
|
||||||
|
tpl = strings.Replace(tpl, fmt.Sprintf("{{%s}}", k), v, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tpl
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/hashicorp/vault/builtin/logical/aws"
|
"github.com/hashicorp/vault/builtin/logical/aws"
|
||||||
"github.com/hashicorp/vault/builtin/logical/cassandra"
|
"github.com/hashicorp/vault/builtin/logical/cassandra"
|
||||||
"github.com/hashicorp/vault/builtin/logical/consul"
|
"github.com/hashicorp/vault/builtin/logical/consul"
|
||||||
|
"github.com/hashicorp/vault/builtin/logical/mssql"
|
||||||
"github.com/hashicorp/vault/builtin/logical/mysql"
|
"github.com/hashicorp/vault/builtin/logical/mysql"
|
||||||
"github.com/hashicorp/vault/builtin/logical/pki"
|
"github.com/hashicorp/vault/builtin/logical/pki"
|
||||||
"github.com/hashicorp/vault/builtin/logical/postgresql"
|
"github.com/hashicorp/vault/builtin/logical/postgresql"
|
||||||
@@ -73,6 +74,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
|
|||||||
"cassandra": cassandra.Factory,
|
"cassandra": cassandra.Factory,
|
||||||
"pki": pki.Factory,
|
"pki": pki.Factory,
|
||||||
"transit": transit.Factory,
|
"transit": transit.Factory,
|
||||||
|
"mssql": mssql.Factory,
|
||||||
"mysql": mysql.Factory,
|
"mysql": mysql.Factory,
|
||||||
"ssh": ssh.Factory,
|
"ssh": ssh.Factory,
|
||||||
},
|
},
|
||||||
|
|||||||
358
website/source/docs/secrets/mssql/index.html.md
Normal file
358
website/source/docs/secrets/mssql/index.html.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Secret Backend: mssql"
|
||||||
|
sidebar_current: "docs-secrets-mssql"
|
||||||
|
description: |-
|
||||||
|
The MSSQL secret backend for Vault generates database credentials to access Microsoft Sql Server.
|
||||||
|
---
|
||||||
|
|
||||||
|
# MSSQL Secret Backend
|
||||||
|
|
||||||
|
Name: `mssql`
|
||||||
|
|
||||||
|
The MSSQL 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 SQL username.
|
||||||
|
|
||||||
|
Vault makes use of its own internal revocation system to ensure that users
|
||||||
|
become invalid within a reasonable time of the lease expiring.
|
||||||
|
|
||||||
|
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 mssql backend is to mount it.
|
||||||
|
Unlike the `generic` backend, the `mssql` backend is not mounted by default.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault mount mssql
|
||||||
|
Successfully mounted 'mssql' at 'mssql'!
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we must configure Vault to know how to connect to the MSSQL
|
||||||
|
instance. This is done by providing a DSN (Data Source Name):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write mssql/config/connection \
|
||||||
|
value="server=localhost;port=1433;user id=sa;password=Password!;database=AdventureWorks;/"
|
||||||
|
Success! Data written to: mssql/config/connection
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, we've configured Vault with the user "sa" and password "Password!",
|
||||||
|
connecting to an instance at "localhost" on port 1433. It is not necessary
|
||||||
|
that Vault has the sa login, but the user must have privileges to create
|
||||||
|
logins, create users on the database, and manage processes. The server roles
|
||||||
|
`securityadmin` and `processadmin` are examples of built-in roles that grant
|
||||||
|
these permissions.
|
||||||
|
|
||||||
|
Optionally, we can configure the lease settings for credentials generated
|
||||||
|
by Vault. This is done by writing to the `config/lease` key:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write mssql/config/lease \
|
||||||
|
lease=1h \
|
||||||
|
lease_max=24h
|
||||||
|
Success! Data written to: mssql`/config/lease
|
||||||
|
```
|
||||||
|
|
||||||
|
This restricts each credential to being valid or leased for 1 hour
|
||||||
|
at a time, with a maximum use period of 24 hours. This forces an
|
||||||
|
application to renew their credentials at least hourly, and to recycle
|
||||||
|
them once per day.
|
||||||
|
|
||||||
|
The next step is to configure a role. A role is a logical name that maps
|
||||||
|
to a policy used to generate those credentials. For example, lets create
|
||||||
|
a "readonly" role:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault write mssql/roles/readonly \
|
||||||
|
sql="CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}'; CREATE USER [{{name}}] FOR LOGIN [{{name}}]; GRANT SELECT ON SCHEMA::dbo TO [{{name}}]"
|
||||||
|
Success! Data written to: mssql/roles/readonly
|
||||||
|
```
|
||||||
|
|
||||||
|
By writing to the `roles/readonly` path we are defining the `readonly` role.
|
||||||
|
This role will be created by evaluating the given `sql` statements. By
|
||||||
|
default, the `{{name}}` and `{{password}}` fields will be populated by
|
||||||
|
Vault with dynamically generated values. This SQL statement is creating
|
||||||
|
the named login on the server and user on the default database for the
|
||||||
|
connection, and then granting it `SELECT` on the `dbo` schema. More complex
|
||||||
|
`GRANT` queries can be used to customize the privileges of the role.
|
||||||
|
|
||||||
|
To generate a new set of credentials, we simply read from that role:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ vault read mssql/creds/readonly
|
||||||
|
Key Value
|
||||||
|
lease_id mssql/creds/readonly/cdf23ac8-6dbd-4bf9-9919-6acaaa86ba6c
|
||||||
|
lease_duration 3600
|
||||||
|
password ee202d0d-e4fd-4410-8d14-2a78c5c8cb76
|
||||||
|
username root-a147d529-e7d6-4a16-8930-4c3e72170b19
|
||||||
|
```
|
||||||
|
|
||||||
|
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 mssql 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.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### /mssql/config/connection
|
||||||
|
#### POST
|
||||||
|
|
||||||
|
<dl class="api">
|
||||||
|
<dt>Description</dt>
|
||||||
|
<dd>
|
||||||
|
Configures the connection DSN used to communicate with Sql Server.
|
||||||
|
This is a root protected endpoint.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Method</dt>
|
||||||
|
<dd>POST</dd>
|
||||||
|
|
||||||
|
<dt>URL</dt>
|
||||||
|
<dd>`/mssql/config/connection`</dd>
|
||||||
|
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="param">value</span>
|
||||||
|
<span class="param-flags">required</span>
|
||||||
|
The MSSQL DSN
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
<dd>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="param">max_open_connections</span>
|
||||||
|
<span class="param-flags">optional</span>
|
||||||
|
Maximum number of open connections to the database.
|
||||||
|
Defaults to 2.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Returns</dt>
|
||||||
|
<dd>
|
||||||
|
A `204` response code.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
### /mssql/config/lease
|
||||||
|
#### POST
|
||||||
|
|
||||||
|
<dl class="api">
|
||||||
|
<dt>Description</dt>
|
||||||
|
<dd>
|
||||||
|
Configures the lease settings for generated credentials.
|
||||||
|
If not configured, leases default to 1 hour. This is a root
|
||||||
|
protected endpoint.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Method</dt>
|
||||||
|
<dd>POST</dd>
|
||||||
|
|
||||||
|
<dt>URL</dt>
|
||||||
|
<dd>`/mssql/config/lease`</dd>
|
||||||
|
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="param">lease</span>
|
||||||
|
<span class="param-flags">required</span>
|
||||||
|
The lease value provided as a string duration
|
||||||
|
with time suffix. Hour is the largest suffix.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="param">lease_max</span>
|
||||||
|
<span class="param-flags">required</span>
|
||||||
|
The maximum lease value 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>
|
||||||
|
|
||||||
|
### /mssql/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>`/mssql/roles/<name>`</dd>
|
||||||
|
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="param">sql</span>
|
||||||
|
<span class="param-flags">required</span>
|
||||||
|
The SQL statements executed to create and configure the role.
|
||||||
|
Must be semi-colon separated. The '{{name}}' and '{{password}}'
|
||||||
|
values will be substituted.
|
||||||
|
</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>`/mssql/roles/<name>`</dd>
|
||||||
|
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>
|
||||||
|
None
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Returns</dt>
|
||||||
|
<dd>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"sql": "CREATE LOGIN..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
#### LIST
|
||||||
|
|
||||||
|
<dl class="api">
|
||||||
|
<dt>Description</dt>
|
||||||
|
<dd>
|
||||||
|
Returns a list of available roles. Only the role names are returned, not
|
||||||
|
any values.
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Method</dt>
|
||||||
|
<dd>GET</dd>
|
||||||
|
|
||||||
|
<dt>URL</dt>
|
||||||
|
<dd>`/roles/?list=true`</dd>
|
||||||
|
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>
|
||||||
|
None
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Returns</dt>
|
||||||
|
<dd>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"auth": null,
|
||||||
|
"data": {
|
||||||
|
"keys": ["dev", "prod"]
|
||||||
|
},
|
||||||
|
"lease_duration": 2592000,
|
||||||
|
"lease_id": "",
|
||||||
|
"renewable": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</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>`/mssql/roles/<name>`</dd>
|
||||||
|
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>
|
||||||
|
None
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Returns</dt>
|
||||||
|
<dd>
|
||||||
|
A `204` response code.
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
### /mssql/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>`/mssql/creds/<name>`</dd>
|
||||||
|
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>
|
||||||
|
None
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt>Returns</dt>
|
||||||
|
<dd>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"username": "root-a147d529-e7d6-4a16-8930-4c3e72170b19",
|
||||||
|
"password": "ee202d0d-e4fd-4410-8d14-2a78c5c8cb76"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
Reference in New Issue
Block a user