mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 12:26:34 +02:00
merging with master
This commit is contained in:
commit
5cd9b7a7d8
@ -15,11 +15,13 @@ func Backend() *framework.Backend {
|
||||
PathsSpecial: &logical.Paths{
|
||||
Root: []string{
|
||||
"keys/*",
|
||||
"raw/*",
|
||||
},
|
||||
},
|
||||
|
||||
Paths: []*framework.Path{
|
||||
pathKeys(),
|
||||
pathRaw(),
|
||||
pathEncrypt(),
|
||||
pathDecrypt(),
|
||||
},
|
||||
|
||||
@ -21,10 +21,27 @@ func TestBackend_basic(t *testing.T) {
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepWritePolicy(t, "test"),
|
||||
testAccStepReadPolicy(t, "test", false),
|
||||
testAccStepReadRaw(t, "test", false),
|
||||
testAccStepEncrypt(t, "test", testPlaintext, decryptData),
|
||||
testAccStepDecrypt(t, "test", testPlaintext, decryptData),
|
||||
testAccStepDeletePolicy(t, "test"),
|
||||
testAccStepReadPolicy(t, "test", true),
|
||||
testAccStepReadRaw(t, "test", true),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestBackend_upsert(t *testing.T) {
|
||||
decryptData := make(map[string]interface{})
|
||||
logicaltest.Test(t, logicaltest.TestCase{
|
||||
Backend: Backend(),
|
||||
Steps: []logicaltest.TestStep{
|
||||
testAccStepReadPolicy(t, "test", true),
|
||||
testAccStepEncrypt(t, "test", testPlaintext, decryptData),
|
||||
testAccStepReadPolicy(t, "test", false),
|
||||
testAccStepDecrypt(t, "test", testPlaintext, decryptData),
|
||||
testAccStepDeletePolicy(t, "test"),
|
||||
testAccStepReadPolicy(t, "test", true),
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -65,6 +82,43 @@ func testAccStepReadPolicy(t *testing.T, name string, expectNone bool) logicalte
|
||||
return err
|
||||
}
|
||||
|
||||
if d.Name != name {
|
||||
return fmt.Errorf("bad: %#v", d)
|
||||
}
|
||||
if d.CipherMode != "aes-gcm" {
|
||||
return fmt.Errorf("bad: %#v", d)
|
||||
}
|
||||
// Should NOT get a key back
|
||||
if d.Key != nil {
|
||||
return fmt.Errorf("bad: %#v", d)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testAccStepReadRaw(t *testing.T, name string, expectNone bool) logicaltest.TestStep {
|
||||
return logicaltest.TestStep{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "raw/" + name,
|
||||
Check: func(resp *logical.Response) error {
|
||||
if resp == nil && !expectNone {
|
||||
return fmt.Errorf("missing response")
|
||||
} else if expectNone {
|
||||
if resp != nil {
|
||||
return fmt.Errorf("response when expecting none")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var d struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Key []byte `mapstructure:"key"`
|
||||
CipherMode string `mapstructure:"cipher_mode"`
|
||||
}
|
||||
if err := mapstructure.Decode(resp.Data, &d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.Name != name {
|
||||
return fmt.Errorf("bad: %#v", d)
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
@ -56,7 +57,10 @@ func pathEncryptWrite(
|
||||
|
||||
// Error if invalid policy
|
||||
if p == nil {
|
||||
return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest
|
||||
p, err = generatePolicy(req.Storage, name)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("failed to upsert policy: %v", err)), logical.ErrInvalidRequest
|
||||
}
|
||||
}
|
||||
|
||||
// Guard against a potentially invalid cipher-mode
|
||||
|
||||
@ -45,6 +45,41 @@ func getPolicy(req *logical.Request, name string) (*Policy, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// generatePolicy is used to create a new named policy with
|
||||
// a randomly generated key
|
||||
func generatePolicy(storage logical.Storage, name string) (*Policy, error) {
|
||||
// Create the policy object
|
||||
p := &Policy{
|
||||
Name: name,
|
||||
CipherMode: "aes-gcm",
|
||||
}
|
||||
|
||||
// Generate a 256bit key
|
||||
p.Key = make([]byte, 32)
|
||||
_, err := rand.Read(p.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Encode the policy
|
||||
buf, err := p.Serialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write the policy into storage
|
||||
err = storage.Put(&logical.StorageEntry{
|
||||
Key: "policy/" + name,
|
||||
Value: buf,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the policy
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func pathKeys() *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `keys/(?P<name>\w+)`,
|
||||
@ -79,34 +114,9 @@ func pathPolicyWrite(
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create the policy object
|
||||
p := &Policy{
|
||||
Name: name,
|
||||
CipherMode: "aes-gcm",
|
||||
}
|
||||
|
||||
// Generate a 256bit key
|
||||
p.Key = make([]byte, 32)
|
||||
_, err = rand.Read(p.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Encode the policy
|
||||
buf, err := p.Serialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write the policy into storage
|
||||
err = req.Storage.Put(&logical.StorageEntry{
|
||||
Key: "policy/" + name,
|
||||
Value: buf,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
// Generate the policy
|
||||
_, err = generatePolicy(req.Storage, name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func pathPolicyRead(
|
||||
@ -124,7 +134,6 @@ func pathPolicyRead(
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"name": p.Name,
|
||||
"key": p.Key,
|
||||
"cipher_mode": p.CipherMode,
|
||||
},
|
||||
}
|
||||
|
||||
54
builtin/logical/transit/path_raw.go
Normal file
54
builtin/logical/transit/path_raw.go
Normal file
@ -0,0 +1,54 @@
|
||||
package transit
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func pathRaw() *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: `raw/(?P<name>\w+)`,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the key",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: pathRawRead,
|
||||
},
|
||||
|
||||
HelpSynopsis: pathPolicyHelpSyn,
|
||||
HelpDescription: pathPolicyHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func pathRawRead(
|
||||
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
p, err := getPolicy(req, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Return the response
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"name": p.Name,
|
||||
"key": p.Key,
|
||||
"cipher_mode": p.CipherMode,
|
||||
},
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
const pathRawHelpSyn = `Fetch raw keys for named encrption keys`
|
||||
|
||||
const pathRawHelpDesc = `
|
||||
This path is used to get the underlying encryption keys used for the
|
||||
named keys that are available.
|
||||
`
|
||||
@ -2,6 +2,8 @@ package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
auditFile "github.com/hashicorp/vault/builtin/audit/file"
|
||||
auditSyslog "github.com/hashicorp/vault/builtin/audit/syslog"
|
||||
@ -70,6 +72,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
|
||||
"mysql": mysql.Factory,
|
||||
"ssh": ssh.Factory,
|
||||
},
|
||||
ShutdownCh: makeShutdownCh(),
|
||||
}, nil
|
||||
},
|
||||
|
||||
@ -79,8 +82,8 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
|
||||
}, nil
|
||||
},
|
||||
|
||||
"help": func() (cli.Command, error) {
|
||||
return &command.HelpCommand{
|
||||
"path-help": func() (cli.Command, error) {
|
||||
return &command.PathHelpCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
@ -276,3 +279,20 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// makeShutdownCh returns a channel that can be used for shutdown
|
||||
// notifications for commands. This channel will send a message for every
|
||||
// interrupt or SIGTERM received.
|
||||
func makeShutdownCh() <-chan struct{} {
|
||||
resultCh := make(chan struct{})
|
||||
|
||||
signalCh := make(chan os.Signal, 4)
|
||||
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
for {
|
||||
<-signalCh
|
||||
resultCh <- struct{}{}
|
||||
}
|
||||
}()
|
||||
return resultCh
|
||||
}
|
||||
|
||||
16
cli/help.go
16
cli/help.go
@ -13,14 +13,14 @@ import (
|
||||
// HelpFunc is a cli.HelpFunc that can is used to output the help for Vault.
|
||||
func HelpFunc(commands map[string]cli.CommandFactory) string {
|
||||
commonNames := map[string]struct{}{
|
||||
"delete": struct{}{},
|
||||
"help": struct{}{},
|
||||
"read": struct{}{},
|
||||
"renew": struct{}{},
|
||||
"revoke": struct{}{},
|
||||
"write": struct{}{},
|
||||
"server": struct{}{},
|
||||
"status": struct{}{},
|
||||
"delete": struct{}{},
|
||||
"path-help": struct{}{},
|
||||
"read": struct{}{},
|
||||
"renew": struct{}{},
|
||||
"revoke": struct{}{},
|
||||
"write": struct{}{},
|
||||
"server": struct{}{},
|
||||
"status": struct{}{},
|
||||
}
|
||||
|
||||
// Determine the maximum key length, and classify based on type
|
||||
|
||||
@ -114,6 +114,15 @@ func (c *AuthCommand) Run(args []string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Warn if the VAULT_TOKEN environment variable is set, as that will take
|
||||
// precedence
|
||||
if os.Getenv("VAULT_TOKEN") != "" {
|
||||
c.Ui.Output("==> WARNING: VAULT_TOKEN environment variable set!\n")
|
||||
c.Ui.Output(" The environment variable takes precedence over the value")
|
||||
c.Ui.Output(" set by the auth command. Either update the value of the")
|
||||
c.Ui.Output(" environment variable or unset it to use the new token.\n")
|
||||
}
|
||||
|
||||
var vars map[string]string
|
||||
if len(args) > 0 {
|
||||
builder := kvbuilder.Builder{Stdin: os.Stdin}
|
||||
|
||||
@ -5,12 +5,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HelpCommand is a Command that lists the mounts.
|
||||
type HelpCommand struct {
|
||||
// PathHelpCommand is a Command that lists the mounts.
|
||||
type PathHelpCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *HelpCommand) Run(args []string) int {
|
||||
func (c *PathHelpCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("help", FlagSetDefault)
|
||||
flags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
@ -35,8 +35,15 @@ func (c *HelpCommand) Run(args []string) int {
|
||||
|
||||
help, err := client.Help(path)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error reading help: %s", err))
|
||||
if strings.Contains(err.Error(), "Vault is sealed") {
|
||||
c.Ui.Error(`Error: Vault is sealed.
|
||||
|
||||
The path-help command requires the Vault to be unsealed so that
|
||||
mount points of secret backends are known.`)
|
||||
} else {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error reading help: %s", err))
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
@ -44,13 +51,13 @@ func (c *HelpCommand) Run(args []string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *HelpCommand) Synopsis() string {
|
||||
func (c *PathHelpCommand) Synopsis() string {
|
||||
return "Look up the help for a path"
|
||||
}
|
||||
|
||||
func (c *HelpCommand) Help() string {
|
||||
func (c *PathHelpCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: vault help [options] path
|
||||
Usage: vault path-help [options] path
|
||||
|
||||
Look up the help for a path.
|
||||
|
||||
@ -58,6 +65,9 @@ Usage: vault help [options] path
|
||||
providers provide built-in help. This command looks up and outputs that
|
||||
help.
|
||||
|
||||
The command requires that the Vault be unsealed, because otherwise
|
||||
the mount points of the backends are unknown.
|
||||
|
||||
General Options:
|
||||
|
||||
-address=addr The address of the Vault server.
|
||||
@ -14,7 +14,7 @@ func TestHelp(t *testing.T) {
|
||||
defer ln.Close()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &HelpCommand{
|
||||
c := &PathHelpCommand{
|
||||
Meta: Meta{
|
||||
ClientToken: token,
|
||||
Ui: ui,
|
||||
@ -22,12 +22,16 @@ func (c *ReadCommand) Run(args []string) int {
|
||||
}
|
||||
|
||||
args = flags.Args()
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
c.Ui.Error("read expects one or two arguments")
|
||||
if len(args) != 1 {
|
||||
c.Ui.Error("read expects one argument")
|
||||
flags.Usage()
|
||||
return 1
|
||||
}
|
||||
|
||||
path := args[0]
|
||||
if path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
@ -98,7 +102,7 @@ Read Options:
|
||||
-format=table The format for output. By default it is a whitespace-
|
||||
delimited table. This can also be json.
|
||||
|
||||
-field=field If included, the raw value of the specified field
|
||||
-field=field If included, the raw value of the specified field
|
||||
will be output raw to stdout.
|
||||
|
||||
`
|
||||
|
||||
@ -32,6 +32,7 @@ type ServerCommand struct {
|
||||
CredentialBackends map[string]logical.Factory
|
||||
LogicalBackends map[string]logical.Factory
|
||||
|
||||
ShutdownCh <-chan struct{}
|
||||
Meta
|
||||
}
|
||||
|
||||
@ -154,7 +155,7 @@ func (c *ServerCommand) Run(args []string) int {
|
||||
"immediately begin using the Vault CLI.\n\n"+
|
||||
"The only step you need to take is to set the following\n"+
|
||||
"environment variables:\n\n"+
|
||||
" export VAULT_ADDR='http://127.0.0.1:8200'\n"+
|
||||
" export VAULT_ADDR='http://127.0.0.1:8200'\n\n"+
|
||||
"The unseal key and root token are reproduced below in case you\n"+
|
||||
"want to seal/unseal the Vault or play with authentication.\n\n"+
|
||||
"Unseal Key: %s\nRoot Token: %s\n",
|
||||
@ -237,7 +238,14 @@ func (c *ServerCommand) Run(args []string) int {
|
||||
// Release the log gate.
|
||||
logGate.Flush()
|
||||
|
||||
<-make(chan struct{})
|
||||
// Wait for shutdown
|
||||
select {
|
||||
case <-c.ShutdownCh:
|
||||
c.Ui.Output("==> Vault shutdown triggered")
|
||||
if err := core.Shutdown(); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error with core shutdown: %s", err))
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -407,8 +415,8 @@ General Options:
|
||||
specified multiple times. If it is a directory, all
|
||||
files with a ".hcl" or ".json" suffix will be loaded.
|
||||
|
||||
-dev Enables Dev mode. In this mode, Vault is completely
|
||||
in-memory and unsealed. Do not run the Dev server in
|
||||
-dev Enables Dev mode. In this mode, Vault is completely
|
||||
in-memory and unsealed. Do not run the Dev server in
|
||||
production!
|
||||
|
||||
-log-level=info Log verbosity. Defaults to "info", will be outputted
|
||||
|
||||
@ -19,15 +19,18 @@ type WriteCommand struct {
|
||||
|
||||
func (c *WriteCommand) Run(args []string) int {
|
||||
var format string
|
||||
var force bool
|
||||
flags := c.Meta.FlagSet("write", FlagSetDefault)
|
||||
flags.StringVar(&format, "format", "table", "")
|
||||
flags.BoolVar(&force, "force", false, "")
|
||||
flags.BoolVar(&force, "f", false, "")
|
||||
flags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
args = flags.Args()
|
||||
if len(args) < 2 {
|
||||
if len(args) < 2 && !force {
|
||||
c.Ui.Error("write expects at least two arguments")
|
||||
flags.Usage()
|
||||
return 1
|
||||
@ -117,6 +120,12 @@ General Options:
|
||||
not recommended. This is especially not recommended
|
||||
for unsealing a vault.
|
||||
|
||||
Write Options:
|
||||
|
||||
-f | -force Force the write to continue without any data values
|
||||
specified. This allows writing to keys that do not
|
||||
need or expect any fields to be specified.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
@ -246,3 +246,26 @@ func TestWrite_Output(t *testing.T) {
|
||||
t.Fatalf("bad: %s", string(ui.OutputWriter.Bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite_force(t *testing.T) {
|
||||
core, _, token := vault.TestCoreUnsealed(t)
|
||||
ln, addr := http.TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &WriteCommand{
|
||||
Meta: Meta{
|
||||
ClientToken: token,
|
||||
Ui: ui,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-address", addr,
|
||||
"-force",
|
||||
"sys/rotate",
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
}
|
||||
}
|
||||
|
||||
175
physical/mysql.go
Normal file
175
physical/mysql.go
Normal file
@ -0,0 +1,175 @@
|
||||
package physical
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// MySQLBackend is a physical backend that stores data
|
||||
// within MySQL database.
|
||||
type MySQLBackend struct {
|
||||
dbTable string
|
||||
client *sql.DB
|
||||
statements map[string]*sql.Stmt
|
||||
}
|
||||
|
||||
// newMySQLBackend constructs a MySQL backend using the given API client and
|
||||
// server address and credential for accessing mysql database.
|
||||
func newMySQLBackend(conf map[string]string) (Backend, error) {
|
||||
// Get the MySQL credentials to perform read/write operations.
|
||||
username, ok := conf["username"]
|
||||
if !ok || username == "" {
|
||||
return nil, fmt.Errorf("missing username")
|
||||
}
|
||||
password, ok := conf["password"]
|
||||
if !ok || username == "" {
|
||||
return nil, fmt.Errorf("missing password")
|
||||
}
|
||||
|
||||
// Get or set MySQL server address. Defaults to localhost and default port(3306)
|
||||
address, ok := conf["address"]
|
||||
if !ok {
|
||||
address = "127.0.0.1:3306"
|
||||
}
|
||||
|
||||
// Get the MySQL database and table details.
|
||||
database, ok := conf["database"]
|
||||
if !ok {
|
||||
database = "vault"
|
||||
}
|
||||
table, ok := conf["table"]
|
||||
if !ok {
|
||||
table = "vault"
|
||||
}
|
||||
dbTable := database + "." + table
|
||||
|
||||
// Create MySQL handle for the database.
|
||||
dsn := username + ":" + password + "@tcp(" + address + ")/"
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to mysql: %v", err)
|
||||
}
|
||||
|
||||
// Create the required database if it doesn't exists.
|
||||
if _, err := db.Exec("CREATE DATABASE IF NOT EXISTS " + database); err != nil {
|
||||
return nil, fmt.Errorf("failed to create mysql database: %v", err)
|
||||
}
|
||||
|
||||
// Create the required table if it doesn't exists.
|
||||
create_query := "CREATE TABLE IF NOT EXISTS " + dbTable +
|
||||
" (vault_key varchar(512), vault_value mediumblob, PRIMARY KEY (vault_key))"
|
||||
if _, err := db.Exec(create_query); err != nil {
|
||||
return nil, fmt.Errorf("failed to create mysql table: %v", err)
|
||||
}
|
||||
|
||||
// Setup the backend.
|
||||
m := &MySQLBackend{
|
||||
dbTable: dbTable,
|
||||
client: db,
|
||||
statements: make(map[string]*sql.Stmt),
|
||||
}
|
||||
|
||||
// Prepare all the statements required
|
||||
statements := map[string]string{
|
||||
"put": "INSERT INTO " + dbTable +
|
||||
" VALUES( ?, ? ) ON DUPLICATE KEY UPDATE vault_value=VALUES(vault_value)",
|
||||
"get": "SELECT vault_value FROM " + dbTable + " WHERE vault_key = ?",
|
||||
"delete": "DELETE FROM " + dbTable + " WHERE vault_key = ?",
|
||||
"list": "SELECT vault_key FROM " + dbTable + " WHERE vault_key LIKE ?",
|
||||
}
|
||||
for name, query := range statements {
|
||||
if err := m.prepare(name, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// prepare is a helper to prepare a query for future execution
|
||||
func (m *MySQLBackend) prepare(name, query string) error {
|
||||
stmt, err := m.client.Prepare(query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare '%s': %v", name, err)
|
||||
}
|
||||
m.statements[name] = stmt
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put is used to insert or update an entry.
|
||||
func (m *MySQLBackend) Put(entry *Entry) error {
|
||||
defer metrics.MeasureSince([]string{"mysql", "put"}, time.Now())
|
||||
|
||||
_, err := m.statements["put"].Exec(entry.Key, entry.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get is used to fetch and entry.
|
||||
func (m *MySQLBackend) Get(key string) (*Entry, error) {
|
||||
defer metrics.MeasureSince([]string{"mysql", "get"}, time.Now())
|
||||
|
||||
var result []byte
|
||||
err := m.statements["get"].QueryRow(key).Scan(&result)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ent := &Entry{
|
||||
Key: key,
|
||||
Value: result,
|
||||
}
|
||||
return ent, nil
|
||||
}
|
||||
|
||||
// Delete is used to permanently delete an entry
|
||||
func (m *MySQLBackend) Delete(key string) error {
|
||||
defer metrics.MeasureSince([]string{"mysql", "delete"}, time.Now())
|
||||
|
||||
_, err := m.statements["delete"].Exec(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// List is used to list all the keys under a given
|
||||
// prefix, up to the next prefix.
|
||||
func (m *MySQLBackend) List(prefix string) ([]string, error) {
|
||||
defer metrics.MeasureSince([]string{"mysql", "list"}, time.Now())
|
||||
|
||||
// Add the % wildcard to the prefix to do the prefix search
|
||||
likePrefix := prefix + "%"
|
||||
rows, err := m.statements["list"].Query(likePrefix)
|
||||
|
||||
var keys []string
|
||||
for rows.Next() {
|
||||
var key string
|
||||
err = rows.Scan(&key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan rows: %v", err)
|
||||
}
|
||||
|
||||
key = strings.TrimPrefix(key, prefix)
|
||||
if i := strings.Index(key, "/"); i == -1 {
|
||||
// Add objects only from the current 'folder'
|
||||
keys = append(keys, key)
|
||||
} else if i != -1 {
|
||||
// Add truncated 'folder' paths
|
||||
keys = appendIfMissing(keys, string(key[:i+1]))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
return keys, nil
|
||||
}
|
||||
53
physical/mysql_test.go
Normal file
53
physical/mysql_test.go
Normal file
@ -0,0 +1,53 @@
|
||||
package physical
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func TestMySQLBackend(t *testing.T) {
|
||||
address := os.Getenv("MYSQL_ADDR")
|
||||
if address == "" {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
database := os.Getenv("MYSQL_DB")
|
||||
if database == "" {
|
||||
database = "test"
|
||||
}
|
||||
|
||||
table := os.Getenv("MYSQL_TABLE")
|
||||
if table == "" {
|
||||
table = "test"
|
||||
}
|
||||
|
||||
username := os.Getenv("MYSQL_USERNAME")
|
||||
password := os.Getenv("MYSQL_PASSWORD")
|
||||
|
||||
// Run vault tests
|
||||
b, err := NewBackend("mysql", map[string]string{
|
||||
"address": address,
|
||||
"database": database,
|
||||
"table": table,
|
||||
"username": username,
|
||||
"password": password,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new backend: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
mysql := b.(*MySQLBackend)
|
||||
_, err := mysql.client.Exec("DROP TABLE " + mysql.dbTable)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to drop table: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
testBackend(t, b)
|
||||
testBackend_ListPrefix(t, b)
|
||||
|
||||
}
|
||||
@ -84,4 +84,5 @@ var BuiltinBackends = map[string]Factory{
|
||||
"file": newFileBackend,
|
||||
"s3": newS3Backend,
|
||||
"etcd": newEtcdBackend,
|
||||
"mysql": newMySQLBackend,
|
||||
}
|
||||
|
||||
@ -328,6 +328,21 @@ func NewCore(conf *CoreConfig) (*Core, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Shutdown is invoked when the Vault instance is about to be terminated. It
|
||||
// should not be accessible as part of an API call as it will cause an availability
|
||||
// problem. It is only used to gracefully quit in the case of HA so that failover
|
||||
// happens as quickly as possible.
|
||||
func (c *Core) Shutdown() error {
|
||||
c.stateLock.Lock()
|
||||
defer c.stateLock.Unlock()
|
||||
if c.sealed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Seal the Vault, causes a leader stepdown
|
||||
return c.sealInternal()
|
||||
}
|
||||
|
||||
// HandleRequest is used to handle a new incoming request
|
||||
func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err error) {
|
||||
c.stateLock.RLock()
|
||||
@ -930,6 +945,14 @@ func (c *Core) Seal(token string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Seal the Vault
|
||||
return c.sealInternal()
|
||||
}
|
||||
|
||||
// sealInternal is an internal method used to seal the vault.
|
||||
// It does not do any authorization checking. The stateLock must
|
||||
// be held prior to calling.
|
||||
func (c *Core) sealInternal() error {
|
||||
// Enable that we are sealed to prevent furthur transactions
|
||||
c.sealed = true
|
||||
|
||||
|
||||
@ -348,6 +348,17 @@ func TestCore_SealUnseal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to shutdown after unseal
|
||||
func TestCore_Shutdown(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
if err := c.Shutdown(); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if sealed, err := c.Sealed(); err != nil || !sealed {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to seal bad token
|
||||
func TestCore_Seal_BadToken(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
|
||||
@ -635,7 +635,7 @@ func (l *leaseEntry) encode() ([]byte, error) {
|
||||
func (le *leaseEntry) renewable() error {
|
||||
// If there is no entry, cannot review
|
||||
if le == nil || le.ExpireTime.IsZero() {
|
||||
return fmt.Errorf("lease not found")
|
||||
return fmt.Errorf("lease not found or lease is not renewable")
|
||||
}
|
||||
|
||||
// Determine if the lease is expired
|
||||
|
||||
@ -152,7 +152,7 @@ func TestExpiration_RegisterAuth_NoLease(t *testing.T) {
|
||||
|
||||
// Should not be able to renew, no expiration
|
||||
_, err = exp.RenewToken("auth/github/login", root.ID, 0)
|
||||
if err.Error() != "lease not found" {
|
||||
if err.Error() != "lease not found or lease is not renewable" {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@ -202,7 +202,7 @@ func TestSystemBackend_renew_invalidID(t *testing.T) {
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if resp.Data["error"] != "lease not found" {
|
||||
if resp.Data["error"] != "lease not found or lease is not renewable" {
|
||||
t.Fatalf("bad: %v", resp)
|
||||
}
|
||||
}
|
||||
@ -250,7 +250,7 @@ func TestSystemBackend_revoke(t *testing.T) {
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if resp3.Data["error"] != "lease not found" {
|
||||
if resp3.Data["error"] != "lease not found or lease is not renewable" {
|
||||
t.Fatalf("bad: %v", resp)
|
||||
}
|
||||
}
|
||||
@ -312,7 +312,7 @@ func TestSystemBackend_revokePrefix(t *testing.T) {
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if resp3.Data["error"] != "lease not found" {
|
||||
if resp3.Data["error"] != "lease not found or lease is not renewable" {
|
||||
t.Fatalf("bad: %v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +76,8 @@ durability, etc.
|
||||
* `s3` - Store data within an S3 bucket [S3](http://aws.amazon.com/s3/).
|
||||
This backend does not support HA.
|
||||
|
||||
* `mysql` - Store data within MySQL. This backend does not support HA.
|
||||
|
||||
* `inmem` - Store data in-memory. This is only really useful for
|
||||
development and experimentation. Data is lost whenever Vault is
|
||||
restarted.
|
||||
@ -143,6 +145,21 @@ For S3, the following options are supported:
|
||||
|
||||
* `region` (optional) - The AWS region. It can be sourced from the AWS_DEFAULT_REGION environment variable and will default to "us-east-1" if not specified.
|
||||
|
||||
#### Backend Reference: MySQL
|
||||
|
||||
The MySQL backend has the following options:
|
||||
|
||||
* `username` (required) - The MySQL username to connect with.
|
||||
|
||||
* `password` (required) - The MySQL password to connect with.
|
||||
|
||||
* `address` (optional) - The address of the MySQL host. Defaults to
|
||||
"127.0.0.1:3306.
|
||||
|
||||
* `database` (optional) - The name of the database to use. Defaults to "vault".
|
||||
|
||||
* `table` (optional) - The name of the table to use. Defaults to "vault".
|
||||
|
||||
#### Backend Reference: Inmem
|
||||
|
||||
The in-memory backend has no configuration options.
|
||||
|
||||
@ -54,6 +54,15 @@ $ vault read transit/keys/foo
|
||||
Key Value
|
||||
name foo
|
||||
cipher_mode aes-gcm
|
||||
````
|
||||
|
||||
We can read from the `raw/` endpoint to see the encryption key itself:
|
||||
|
||||
```
|
||||
$ vault read transit/raw/foo
|
||||
Key Value
|
||||
name foo
|
||||
cipher_mode aes-gcm
|
||||
key PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8=
|
||||
````
|
||||
|
||||
@ -114,17 +123,7 @@ only encrypt or decrypt using the named keys they need access to.
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"data": {
|
||||
"name": "foo",
|
||||
"cipher_mode": "aes-gcm",
|
||||
"key": "PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8="
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A `204` response code.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
@ -156,7 +155,6 @@ only encrypt or decrypt using the named keys they need access to.
|
||||
"data": {
|
||||
"name": "foo",
|
||||
"cipher_mode": "aes-gcm",
|
||||
"key": "PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8="
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -196,7 +194,9 @@ only encrypt or decrypt using the named keys they need access to.
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Encrypts the provided plaintext using the named key.
|
||||
Encrypts the provided plaintext using the named key. If the named key
|
||||
does not already exist, it will be automatically generated for the given
|
||||
name with the default parameters.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
@ -269,3 +269,42 @@ only encrypt or decrypt using the named keys they need access to.
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
### /transit/raw/
|
||||
#### GET
|
||||
|
||||
<dl class="api">
|
||||
<dt>Description</dt>
|
||||
<dd>
|
||||
Returns raw information about a named encryption key,
|
||||
Including the underlying encryption key. This is a root protected endpoint.
|
||||
</dd>
|
||||
|
||||
<dt>Method</dt>
|
||||
<dd>GET</dd>
|
||||
|
||||
<dt>URL</dt>
|
||||
<dd>`/transit/raw/<name>`</dd>
|
||||
|
||||
<dt>Parameters</dt>
|
||||
<dd>
|
||||
None
|
||||
</dd>
|
||||
|
||||
<dt>Returns</dt>
|
||||
<dd>
|
||||
|
||||
```javascript
|
||||
{
|
||||
"data": {
|
||||
"name": "foo",
|
||||
"cipher_mode": "aes-gcm",
|
||||
"key": "PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8="
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user