mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-08 16:24:51 -04:00
Merge branch 'master' into jo-ie11-fixes
This commit is contained in:
commit
981427cbff
49 changed files with 1932 additions and 383 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -1,12 +1,20 @@
|
|||
## 0.10.1 (Unreleased)
|
||||
|
||||
FEATURES:
|
||||
|
||||
* X-Forwarded-For support: `X-Forwarded-For` headers can now be used to set
|
||||
the client IP seen by Vault. See the [TCP listener configuration
|
||||
page](https://www.vaultproject.io/docs/configuration/listener/tcp.html) for
|
||||
details.
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* auth/token: Add to the token lookup response, the policies inherited due to
|
||||
identity associations [GH-4366]
|
||||
* core: Add X-Forwarded-For support [GH-4380]
|
||||
* identity: Add the ability to disable an entity. Disabling an entity does not
|
||||
revoke associated tokens, but while the entity is disabled they cannot be
|
||||
used. [GH-4353]
|
||||
* auth/token: Add to the token lookup response, the policies inherited due to
|
||||
identity associations [GH-4366]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/helper/dbtxn"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
|
|
@ -90,15 +91,11 @@ func (b *backend) pathCredsCreateRead(ctx context.Context, req *logical.Request,
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.Prepare(Query(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
"password": password,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.Exec(); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/vault/helper/dbtxn"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
|
@ -130,16 +131,11 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, d
|
|||
// many permissions as possible right now
|
||||
var lastStmtError error
|
||||
for _, query := range revokeStmts {
|
||||
stmt, err := db.Prepare(query)
|
||||
if err != nil {
|
||||
|
||||
if err := dbtxn.ExecuteDBQuery(ctx, db, nil, query); err != nil {
|
||||
lastStmtError = err
|
||||
continue
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec()
|
||||
if err != nil {
|
||||
lastStmtError = err
|
||||
}
|
||||
}
|
||||
|
||||
// can't drop if not all database users are dropped
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/helper/dbtxn"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
|
|
@ -103,15 +104,11 @@ func (b *backend) pathRoleCreateRead(ctx context.Context, req *logical.Request,
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.Prepare(Query(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
"password": password,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.Exec(); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/helper/dbtxn"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
|
|
@ -106,16 +107,13 @@ func (b *backend) pathRoleCreateRead(ctx context.Context, req *logical.Request,
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.Prepare(Query(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
"password": password,
|
||||
"expiration": expiration,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.Exec(); err != nil {
|
||||
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/vault/helper/dbtxn"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
|
|
@ -211,14 +212,7 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, d
|
|||
// many permissions as possible right now
|
||||
var lastStmtError error
|
||||
for _, query := range revocationStmts {
|
||||
stmt, err := db.Prepare(query)
|
||||
if err != nil {
|
||||
lastStmtError = err
|
||||
continue
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec()
|
||||
if err != nil {
|
||||
if err := dbtxn.ExecuteDBQuery(ctx, db, nil, query); err != nil {
|
||||
lastStmtError = err
|
||||
}
|
||||
}
|
||||
|
|
@ -258,15 +252,10 @@ func (b *backend) secretCredsRevoke(ctx context.Context, req *logical.Request, d
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.Prepare(Query(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.Exec(); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import (
|
|||
"github.com/hashicorp/errwrap"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
sockaddr "github.com/hashicorp/go-sockaddr"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
"github.com/hashicorp/vault/command/server"
|
||||
"github.com/hashicorp/vault/helper/gated-writer"
|
||||
|
|
@ -92,6 +93,11 @@ type ServerCommand struct {
|
|||
flagTestVerifyOnly bool
|
||||
}
|
||||
|
||||
type ServerListener struct {
|
||||
net.Listener
|
||||
config map[string]interface{}
|
||||
}
|
||||
|
||||
func (c *ServerCommand) Synopsis() string {
|
||||
return "Start a Vault server"
|
||||
}
|
||||
|
|
@ -670,8 +676,8 @@ CLUSTER_SYNTHESIS_COMPLETE:
|
|||
clusterAddrs := []*net.TCPAddr{}
|
||||
|
||||
// Initialize the listeners
|
||||
lns := make([]ServerListener, 0, len(config.Listeners))
|
||||
c.reloadFuncsLock.Lock()
|
||||
lns := make([]net.Listener, 0, len(config.Listeners))
|
||||
for i, lnConfig := range config.Listeners {
|
||||
ln, props, reloadFunc, err := server.NewListener(lnConfig.Type, lnConfig.Config, c.logGate, c.UI)
|
||||
if err != nil {
|
||||
|
|
@ -679,7 +685,10 @@ CLUSTER_SYNTHESIS_COMPLETE:
|
|||
return 1
|
||||
}
|
||||
|
||||
lns = append(lns, ln)
|
||||
lns = append(lns, ServerListener{
|
||||
Listener: ln,
|
||||
config: lnConfig.Config,
|
||||
})
|
||||
|
||||
if reloadFunc != nil {
|
||||
relSlice := (*c.reloadFuncs)["listener|"+lnConfig.Type]
|
||||
|
|
@ -738,7 +747,7 @@ CLUSTER_SYNTHESIS_COMPLETE:
|
|||
// Make sure we close all listeners from this point on
|
||||
listenerCloseFunc := func() {
|
||||
for _, ln := range lns {
|
||||
ln.Close()
|
||||
ln.Listener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -776,12 +785,10 @@ CLUSTER_SYNTHESIS_COMPLETE:
|
|||
return 0
|
||||
}
|
||||
|
||||
handler := vaulthttp.Handler(core)
|
||||
|
||||
// This needs to happen before we first unseal, so before we trigger dev
|
||||
// mode if it's set
|
||||
core.SetClusterListenerAddrs(clusterAddrs)
|
||||
core.SetClusterHandler(handler)
|
||||
core.SetClusterHandler(vaulthttp.Handler(core))
|
||||
|
||||
err = core.UnsealWithStoredKeys(context.Background())
|
||||
if err != nil {
|
||||
|
|
@ -914,10 +921,23 @@ CLUSTER_SYNTHESIS_COMPLETE:
|
|||
|
||||
// Initialize the HTTP servers
|
||||
for _, ln := range lns {
|
||||
handler := vaulthttp.Handler(core)
|
||||
|
||||
// We perform validation on the config earlier, we can just cast here
|
||||
if _, ok := ln.config["x_forwarded_for_authorized_addrs"]; ok {
|
||||
hopSkips := ln.config["x_forwarded_for_hop_skips"].(int)
|
||||
authzdAddrs := ln.config["x_forwarded_for_authorized_addrs"].([]*sockaddr.SockAddrMarshaler)
|
||||
rejectNotPresent := ln.config["x_forwarded_for_reject_not_present"].(bool)
|
||||
rejectNonAuthz := ln.config["x_forwarded_for_reject_not_authorized"].(bool)
|
||||
if len(authzdAddrs) > 0 {
|
||||
handler = vaulthttp.WrapForwardedForHandler(handler, authzdAddrs, rejectNotPresent, rejectNonAuthz, hopSkips)
|
||||
}
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Handler: handler,
|
||||
}
|
||||
go server.Serve(ln)
|
||||
go server.Serve(ln.Listener)
|
||||
}
|
||||
|
||||
if newCoreError != nil {
|
||||
|
|
|
|||
|
|
@ -761,6 +761,10 @@ func parseListeners(result *Config, list *ast.ObjectList) error {
|
|||
"address",
|
||||
"cluster_address",
|
||||
"endpoint",
|
||||
"x_forwarded_for_authorized_addrs",
|
||||
"x_forwarded_for_hop_skips",
|
||||
"x_forwarded_for_reject_not_authorized",
|
||||
"x_forwarded_for_reject_not_present",
|
||||
"infrastructure",
|
||||
"node_id",
|
||||
"proxy_protocol_behavior",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
"github.com/hashicorp/vault/helper/reload"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
|
@ -39,6 +43,57 @@ func tcpListenerFactory(config map[string]interface{}, _ io.Writer, ui cli.Ui) (
|
|||
}
|
||||
|
||||
props := map[string]string{"addr": addr}
|
||||
|
||||
ffAllowedRaw, ffAllowedOK := config["x_forwarded_for_authorized_addrs"]
|
||||
if ffAllowedOK {
|
||||
ffAllowed, err := parseutil.ParseAddrs(ffAllowedRaw)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errwrap.Wrapf("error parsing \"x_forwarded_for_authorized_addrs\": {{err}}", err)
|
||||
}
|
||||
props["x_forwarded_for_authorized_addrs"] = fmt.Sprintf("%v", ffAllowed)
|
||||
config["x_forwarded_for_authorized_addrs"] = ffAllowed
|
||||
}
|
||||
|
||||
if ffHopsRaw, ok := config["x_forwarded_for_hop_skips"]; ok {
|
||||
ffHops64, err := parseutil.ParseInt(ffHopsRaw)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errwrap.Wrapf("error parsing \"x_forwarded_for_hop_skips\": {{err}}", err)
|
||||
}
|
||||
if ffHops64 < 0 {
|
||||
return nil, nil, nil, fmt.Errorf("\"x_forwarded_for_hop_skips\" cannot be negative")
|
||||
}
|
||||
ffHops := int(ffHops64)
|
||||
props["x_forwarded_for_hop_skips"] = strconv.Itoa(ffHops)
|
||||
config["x_forwarded_for_hop_skips"] = ffHops
|
||||
} else if ffAllowedOK {
|
||||
props["x_forwarded_for_hop_skips"] = "0"
|
||||
config["x_forwarded_for_hop_skips"] = int(0)
|
||||
}
|
||||
|
||||
if ffRejectNotPresentRaw, ok := config["x_forwarded_for_reject_not_present"]; ok {
|
||||
ffRejectNotPresent, err := parseutil.ParseBool(ffRejectNotPresentRaw)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errwrap.Wrapf("error parsing \"x_forwarded_for_reject_not_present\": {{err}}", err)
|
||||
}
|
||||
props["x_forwarded_for_reject_not_present"] = strconv.FormatBool(ffRejectNotPresent)
|
||||
config["x_forwarded_for_reject_not_present"] = ffRejectNotPresent
|
||||
} else if ffAllowedOK {
|
||||
props["x_forwarded_for_reject_not_present"] = "true"
|
||||
config["x_forwarded_for_reject_not_present"] = true
|
||||
}
|
||||
|
||||
if ffRejectNonAuthorizedRaw, ok := config["x_forwarded_for_reject_not_authorized"]; ok {
|
||||
ffRejectNonAuthorized, err := parseutil.ParseBool(ffRejectNonAuthorizedRaw)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errwrap.Wrapf("error parsing \"x_forwarded_for_reject_not_authorized\": {{err}}", err)
|
||||
}
|
||||
props["x_forwarded_for_reject_not_authorized"] = strconv.FormatBool(ffRejectNonAuthorized)
|
||||
config["x_forwarded_for_reject_not_authorized"] = ffRejectNonAuthorized
|
||||
} else if ffAllowedOK {
|
||||
props["x_forwarded_for_reject_not_authorized"] = "true"
|
||||
config["x_forwarded_for_reject_not_authorized"] = true
|
||||
}
|
||||
|
||||
return listenerWrapTLS(ln, props, config, ui)
|
||||
}
|
||||
|
||||
|
|
|
|||
63
helper/dbtxn/dbtxn.go
Normal file
63
helper/dbtxn/dbtxn.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package dbtxn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExecuteDBQuery handles executing one single statement, while properly releasing its resources.
|
||||
// - ctx: Required
|
||||
// - db: Required
|
||||
// - config: Optional, may be nil
|
||||
// - query: Required
|
||||
func ExecuteDBQuery(ctx context.Context, db *sql.DB, params map[string]string, query string) error {
|
||||
|
||||
parsedQuery := parseQuery(params, query)
|
||||
|
||||
stmt, err := db.PrepareContext(ctx, parsedQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
return execute(ctx, stmt)
|
||||
}
|
||||
|
||||
// ExecuteTxQuery handles executing one single statement, while properly releasing its resources.
|
||||
// - ctx: Required
|
||||
// - tx: Required
|
||||
// - config: Optional, may be nil
|
||||
// - query: Required
|
||||
func ExecuteTxQuery(ctx context.Context, tx *sql.Tx, params map[string]string, query string) error {
|
||||
|
||||
parsedQuery := parseQuery(params, query)
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, parsedQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
return execute(ctx, stmt)
|
||||
}
|
||||
|
||||
func execute(ctx context.Context, stmt *sql.Stmt) error {
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseQuery(m map[string]string, tpl string) string {
|
||||
|
||||
if m == nil || len(m) <= 0 {
|
||||
return tpl
|
||||
}
|
||||
|
||||
for k, v := range m {
|
||||
tpl = strings.Replace(tpl, fmt.Sprintf("{{%s}}", k), v, -1)
|
||||
}
|
||||
return tpl
|
||||
}
|
||||
|
|
@ -3,10 +3,13 @@ package parseutil
|
|||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
sockaddr "github.com/hashicorp/go-sockaddr"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
|
@ -118,3 +121,43 @@ func ParseCommaStringSlice(in interface{}) ([]string, error) {
|
|||
}
|
||||
return strutil.TrimStrings(result), nil
|
||||
}
|
||||
|
||||
func ParseAddrs(addrs interface{}) ([]*sockaddr.SockAddrMarshaler, error) {
|
||||
out := make([]*sockaddr.SockAddrMarshaler, 0)
|
||||
stringAddrs := make([]string, 0)
|
||||
|
||||
switch addrs.(type) {
|
||||
case string:
|
||||
stringAddrs = strutil.ParseArbitraryStringSlice(addrs.(string), ",")
|
||||
if len(stringAddrs) == 0 {
|
||||
return nil, fmt.Errorf("unable to parse addresses from %v", addrs)
|
||||
}
|
||||
|
||||
case []string:
|
||||
stringAddrs = addrs.([]string)
|
||||
|
||||
case []interface{}:
|
||||
for _, v := range addrs.([]interface{}) {
|
||||
stringAddr, ok := v.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error parsing %v as string", v)
|
||||
}
|
||||
stringAddrs = append(stringAddrs, stringAddr)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown address input type %T", addrs)
|
||||
}
|
||||
|
||||
for _, addr := range stringAddrs {
|
||||
sa, err := sockaddr.NewSockAddr(addr)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(fmt.Sprintf("error parsing address %q: {{err}}", addr), err)
|
||||
}
|
||||
out = append(out, &sockaddr.SockAddrMarshaler{
|
||||
SockAddr: sa,
|
||||
})
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
proxyproto "github.com/armon/go-proxyproto"
|
||||
"github.com/hashicorp/errwrap"
|
||||
sockaddr "github.com/hashicorp/go-sockaddr"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
)
|
||||
|
||||
// ProxyProtoConfig contains configuration for the PROXY protocol
|
||||
|
|
@ -19,42 +19,12 @@ type ProxyProtoConfig struct {
|
|||
}
|
||||
|
||||
func (p *ProxyProtoConfig) SetAuthorizedAddrs(addrs interface{}) error {
|
||||
p.AuthorizedAddrs = make([]*sockaddr.SockAddrMarshaler, 0)
|
||||
stringAddrs := make([]string, 0)
|
||||
|
||||
switch addrs.(type) {
|
||||
case string:
|
||||
stringAddrs = strutil.ParseArbitraryStringSlice(addrs.(string), ",")
|
||||
if len(stringAddrs) == 0 {
|
||||
return fmt.Errorf("unable to parse addresses from %v", addrs)
|
||||
}
|
||||
|
||||
case []string:
|
||||
stringAddrs = addrs.([]string)
|
||||
|
||||
case []interface{}:
|
||||
for _, v := range addrs.([]interface{}) {
|
||||
stringAddr, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("error parsing %v as string", v)
|
||||
}
|
||||
stringAddrs = append(stringAddrs, stringAddr)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown address input type %T", addrs)
|
||||
}
|
||||
|
||||
for _, addr := range stringAddrs {
|
||||
sa, err := sockaddr.NewSockAddr(addr)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf("error parsing authorized address: {{err}}", err)
|
||||
}
|
||||
p.AuthorizedAddrs = append(p.AuthorizedAddrs, &sockaddr.SockAddrMarshaler{
|
||||
SockAddr: sa,
|
||||
})
|
||||
aa, err := parseutil.ParseAddrs(addrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.AuthorizedAddrs = aa
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
249
http/forwarded_for_test.go
Normal file
249
http/forwarded_for_test.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sockaddr "github.com/hashicorp/go-sockaddr"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
)
|
||||
|
||||
func TestHandler_XForwardedFor(t *testing.T) {
|
||||
goodAddr, err := sockaddr.NewIPAddr("127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
badAddr, err := sockaddr.NewIPAddr("1.2.3.4")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// First: test reject not present
|
||||
t.Run("reject_not_present", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testHandler := func(c *vault.Core) http.Handler {
|
||||
origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(r.RemoteAddr))
|
||||
})
|
||||
return WrapForwardedForHandler(origHandler, []*sockaddr.SockAddrMarshaler{
|
||||
&sockaddr.SockAddrMarshaler{
|
||||
SockAddr: goodAddr,
|
||||
},
|
||||
}, true, false, 0)
|
||||
}
|
||||
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: testHandler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
req := client.NewRequest("GET", "/")
|
||||
_, err = client.RawRequest(req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "missing x-forwarded-for") {
|
||||
t.Fatalf("bad error message: %v", err)
|
||||
}
|
||||
req = client.NewRequest("GET", "/")
|
||||
req.Headers = make(http.Header)
|
||||
req.Headers.Set("x-forwarded-for", "1.2.3.4")
|
||||
resp, err := client.RawRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.ReadFrom(resp.Body)
|
||||
if !strings.HasPrefix(buf.String(), "1.2.3.4:") {
|
||||
t.Fatalf("bad body: %s", buf.String())
|
||||
}
|
||||
})
|
||||
|
||||
// Next: test allow unauth
|
||||
t.Run("allow_unauth", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testHandler := func(c *vault.Core) http.Handler {
|
||||
origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(r.RemoteAddr))
|
||||
})
|
||||
return WrapForwardedForHandler(origHandler, []*sockaddr.SockAddrMarshaler{
|
||||
&sockaddr.SockAddrMarshaler{
|
||||
SockAddr: badAddr,
|
||||
},
|
||||
}, true, false, 0)
|
||||
}
|
||||
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: testHandler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
req := client.NewRequest("GET", "/")
|
||||
req.Headers = make(http.Header)
|
||||
req.Headers.Set("x-forwarded-for", "5.6.7.8")
|
||||
resp, err := client.RawRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.ReadFrom(resp.Body)
|
||||
if !strings.HasPrefix(buf.String(), "127.0.0.1:") {
|
||||
t.Fatalf("bad body: %s", buf.String())
|
||||
}
|
||||
})
|
||||
|
||||
// Next: test fail unauth
|
||||
t.Run("fail_unauth", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testHandler := func(c *vault.Core) http.Handler {
|
||||
origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(r.RemoteAddr))
|
||||
})
|
||||
return WrapForwardedForHandler(origHandler, []*sockaddr.SockAddrMarshaler{
|
||||
&sockaddr.SockAddrMarshaler{
|
||||
SockAddr: badAddr,
|
||||
},
|
||||
}, true, true, 0)
|
||||
}
|
||||
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: testHandler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
req := client.NewRequest("GET", "/")
|
||||
req.Headers = make(http.Header)
|
||||
req.Headers.Set("x-forwarded-for", "5.6.7.8")
|
||||
_, err = client.RawRequest(req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not authorized for x-forwarded-for") {
|
||||
t.Fatalf("bad error message: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Next: test bad hops (too many)
|
||||
t.Run("too_many_hops", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testHandler := func(c *vault.Core) http.Handler {
|
||||
origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(r.RemoteAddr))
|
||||
})
|
||||
return WrapForwardedForHandler(origHandler, []*sockaddr.SockAddrMarshaler{
|
||||
&sockaddr.SockAddrMarshaler{
|
||||
SockAddr: goodAddr,
|
||||
},
|
||||
}, true, true, 4)
|
||||
}
|
||||
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: testHandler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
req := client.NewRequest("GET", "/")
|
||||
req.Headers = make(http.Header)
|
||||
req.Headers.Set("x-forwarded-for", "2.3.4.5,3.4.5.6")
|
||||
_, err = client.RawRequest(req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "would skip before earliest") {
|
||||
t.Fatalf("bad error message: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Next: test picking correct value
|
||||
t.Run("correct_hop_skipping", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testHandler := func(c *vault.Core) http.Handler {
|
||||
origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(r.RemoteAddr))
|
||||
})
|
||||
return WrapForwardedForHandler(origHandler, []*sockaddr.SockAddrMarshaler{
|
||||
&sockaddr.SockAddrMarshaler{
|
||||
SockAddr: goodAddr,
|
||||
},
|
||||
}, true, true, 1)
|
||||
}
|
||||
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: testHandler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
req := client.NewRequest("GET", "/")
|
||||
req.Headers = make(http.Header)
|
||||
req.Headers.Set("x-forwarded-for", "2.3.4.5,3.4.5.6,4.5.6.7,5.6.7.8")
|
||||
resp, err := client.RawRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.ReadFrom(resp.Body)
|
||||
if !strings.HasPrefix(buf.String(), "4.5.6.7:") {
|
||||
t.Fatalf("bad body: %s", buf.String())
|
||||
}
|
||||
})
|
||||
|
||||
// Next: multi-header approach
|
||||
t.Run("correct_hop_skipping_multi_header", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testHandler := func(c *vault.Core) http.Handler {
|
||||
origHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(r.RemoteAddr))
|
||||
})
|
||||
return WrapForwardedForHandler(origHandler, []*sockaddr.SockAddrMarshaler{
|
||||
&sockaddr.SockAddrMarshaler{
|
||||
SockAddr: goodAddr,
|
||||
},
|
||||
}, true, true, 1)
|
||||
}
|
||||
|
||||
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
|
||||
HandlerFunc: testHandler,
|
||||
})
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
req := client.NewRequest("GET", "/")
|
||||
req.Headers = make(http.Header)
|
||||
req.Headers.Add("x-forwarded-for", "2.3.4.5")
|
||||
req.Headers.Add("x-forwarded-for", "3.4.5.6,4.5.6.7")
|
||||
req.Headers.Add("x-forwarded-for", "5.6.7.8")
|
||||
resp, err := client.RawRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.ReadFrom(resp.Body)
|
||||
if !strings.HasPrefix(buf.String(), "4.5.6.7:") {
|
||||
t.Fatalf("bad body: %s", buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
|
@ -13,6 +15,7 @@ import (
|
|||
"github.com/elazarl/go-bindata-assetfs"
|
||||
"github.com/hashicorp/errwrap"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
sockaddr "github.com/hashicorp/go-sockaddr"
|
||||
"github.com/hashicorp/vault/helper/consts"
|
||||
"github.com/hashicorp/vault/helper/jsonutil"
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
|
|
@ -124,6 +127,94 @@ func wrapGenericHandler(h http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func WrapForwardedForHandler(h http.Handler, authorizedAddrs []*sockaddr.SockAddrMarshaler, rejectNotPresent, rejectNonAuthz bool, hopSkips int) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
headers, headersOK := r.Header[textproto.CanonicalMIMEHeaderKey("X-Forwarded-For")]
|
||||
if !headersOK || len(headers) == 0 {
|
||||
if !rejectNotPresent {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusBadRequest, fmt.Errorf("missing x-forwarded-for header and configured to reject when not present"))
|
||||
return
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
// If not rejecting treat it like we just don't have a valid
|
||||
// header because we can't do a comparison against an address we
|
||||
// can't understand
|
||||
if !rejectNotPresent {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client hostport: {{err}}", err))
|
||||
return
|
||||
}
|
||||
|
||||
addr, err := sockaddr.NewIPAddr(host)
|
||||
if err != nil {
|
||||
// We treat this the same as the case above
|
||||
if !rejectNotPresent {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing client address: {{err}}", err))
|
||||
return
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, authz := range authorizedAddrs {
|
||||
if authz.Contains(addr) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
// If we didn't find it and aren't configured to reject, simply
|
||||
// don't trust it
|
||||
if !rejectNonAuthz {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
respondError(w, http.StatusBadRequest, fmt.Errorf("client address not authorized for x-forwarded-for and configured to reject connection"))
|
||||
return
|
||||
}
|
||||
|
||||
// At this point we have at least one value and it's authorized
|
||||
|
||||
// Split comma separated ones, which are common. This brings it in line
|
||||
// to the multiple-header case.
|
||||
var acc []string
|
||||
for _, header := range headers {
|
||||
vals := strings.Split(header, ",")
|
||||
for _, v := range vals {
|
||||
acc = append(acc, strings.TrimSpace(v))
|
||||
}
|
||||
}
|
||||
|
||||
indexToUse := len(acc) - 1 - hopSkips
|
||||
if indexToUse < 0 {
|
||||
// This is likely an error in either configuration or other
|
||||
// infrastructure. We could either deny the request, or we
|
||||
// could simply not trust the value. Denying the request is
|
||||
// "safer" since if this logic is configured at all there may
|
||||
// be an assumption it can always be trusted. Given that we can
|
||||
// deny accepting the request at all if it's not from an
|
||||
// authorized address, if we're at this point the address is
|
||||
// authorized (or we've turned off explicit rejection) and we
|
||||
// should assume that what comes in should be properly
|
||||
// formatted.
|
||||
respondError(w, http.StatusBadRequest, fmt.Errorf("malformed x-forwarded-for configuration or request, hops to skip (%d) would skip before earliest chain link (chain length %d)", hopSkips, len(headers)))
|
||||
return
|
||||
}
|
||||
|
||||
r.RemoteAddr = net.JoinHostPort(acc[indexToUse], port)
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// A lookup on a token that is about to expire returns nil, which means by the
|
||||
// time we can validate a wrapping token lookup will return nil since it will
|
||||
// be revoked after the call. So we have to do the validation here.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
_ "github.com/SAP/go-hdb/driver"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||
"github.com/hashicorp/vault/helper/dbtxn"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/plugins"
|
||||
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||
|
|
@ -143,16 +144,12 @@ func (h *HANA) CreateUser(ctx context.Context, statements dbplugin.Statements, u
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
"password": password,
|
||||
"expiration": expirationStr,
|
||||
}))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
|
@ -238,14 +235,10 @@ func (h *HANA) RevokeUser(ctx context.Context, statements dbplugin.Statements, u
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||
"github.com/hashicorp/vault/helper/dbtxn"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/plugins"
|
||||
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||
|
|
@ -129,16 +130,13 @@ func (m *MSSQL) CreateUser(ctx context.Context, statements dbplugin.Statements,
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
"password": password,
|
||||
"expiration": expirationStr,
|
||||
}))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
|
@ -189,14 +187,10 @@ func (m *MSSQL) RevokeUser(ctx context.Context, statements dbplugin.Statements,
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -285,14 +279,7 @@ func (m *MSSQL) revokeUserDefault(ctx context.Context, username string) error {
|
|||
// many permissions as possible right now
|
||||
var lastStmtError error
|
||||
for _, query := range revokeStmts {
|
||||
stmt, err := db.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
lastStmtError = err
|
||||
continue
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx)
|
||||
if err != nil {
|
||||
if err := dbtxn.ExecuteDBQuery(ctx, db, nil, query); err != nil {
|
||||
lastStmtError = err
|
||||
}
|
||||
}
|
||||
|
|
@ -355,16 +342,12 @@ func (m *MSSQL) RotateRootCredentials(ctx context.Context, statements []string)
|
|||
if len(query) == 0 {
|
||||
continue
|
||||
}
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
|
||||
m := map[string]string{
|
||||
"username": m.Username,
|
||||
"password": password,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
stdmysql "github.com/go-sql-driver/mysql"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||
"github.com/hashicorp/vault/helper/dbtxn"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/plugins"
|
||||
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||
|
|
@ -182,10 +183,11 @@ func (m *MySQL) CreateUser(ctx context.Context, statements dbplugin.Statements,
|
|||
|
||||
return "", "", err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
stmt.Close()
|
||||
return "", "", err
|
||||
}
|
||||
stmt.Close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -291,16 +293,12 @@ func (m *MySQL) RotateRootCredentials(ctx context.Context, statements []string)
|
|||
if len(query) == 0 {
|
||||
continue
|
||||
}
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
|
||||
m := map[string]string{
|
||||
"username": m.Username,
|
||||
"password": password,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/builtin/logical/database/dbplugin"
|
||||
"github.com/hashicorp/vault/helper/dbtxn"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/plugins"
|
||||
"github.com/hashicorp/vault/plugins/helper/database/connutil"
|
||||
|
|
@ -139,16 +140,12 @@ func (p *PostgreSQL) CreateUser(ctx context.Context, statements dbplugin.Stateme
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
"password": password,
|
||||
"expiration": expirationStr,
|
||||
}))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
|
|
@ -157,7 +154,6 @@ func (p *PostgreSQL) CreateUser(ctx context.Context, statements dbplugin.Stateme
|
|||
// Commit the transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
return "", "", err
|
||||
|
||||
}
|
||||
|
||||
return username, password, nil
|
||||
|
|
@ -198,16 +194,12 @@ func (p *PostgreSQL) RenewUser(ctx context.Context, statements dbplugin.Statemen
|
|||
if len(query) == 0 {
|
||||
continue
|
||||
}
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
"expiration": expirationStr,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -251,15 +243,10 @@ func (p *PostgreSQL) customRevokeUser(ctx context.Context, username string, revo
|
|||
continue
|
||||
}
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"name": username,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -352,14 +339,7 @@ func (p *PostgreSQL) defaultRevokeUser(ctx context.Context, username string) err
|
|||
// many permissions as possible right now
|
||||
var lastStmtError error
|
||||
for _, query := range revocationStmts {
|
||||
stmt, err := db.PrepareContext(ctx, query)
|
||||
if err != nil {
|
||||
lastStmtError = err
|
||||
continue
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx)
|
||||
if err != nil {
|
||||
if err := dbtxn.ExecuteDBQuery(ctx, db, nil, query); err != nil {
|
||||
lastStmtError = err
|
||||
}
|
||||
}
|
||||
|
|
@ -423,16 +403,11 @@ func (p *PostgreSQL) RotateRootCredentials(ctx context.Context, statements []str
|
|||
if len(query) == 0 {
|
||||
continue
|
||||
}
|
||||
stmt, err := tx.PrepareContext(ctx, dbutil.QueryHelper(query, map[string]string{
|
||||
m := map[string]string{
|
||||
"username": p.Username,
|
||||
"password": password,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.ExecContext(ctx); err != nil {
|
||||
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,21 @@ import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
|
|||
const BACKENDS = supportedAuthBackends();
|
||||
const { computed, inject } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
const attributesForSelectedAuthBackend = {
|
||||
token: ['token'],
|
||||
userpass: ['username', 'password'],
|
||||
ldap: ['username', 'password'],
|
||||
github: ['username', 'password'],
|
||||
okta: ['username', 'password'],
|
||||
};
|
||||
|
||||
const DEFAULTS = {
|
||||
token: null,
|
||||
username: null,
|
||||
password: null,
|
||||
};
|
||||
|
||||
export default Ember.Component.extend(DEFAULTS, {
|
||||
classNames: ['auth-form'],
|
||||
routing: inject.service('-routing'),
|
||||
auth: inject.service(),
|
||||
|
|
@ -14,6 +28,21 @@ export default Ember.Component.extend({
|
|||
this.$('li.is-active').get(0).scrollIntoView();
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
let newMethod = this.get('selectedAuthType');
|
||||
let oldMethod = this.get('oldSelectedAuthType');
|
||||
|
||||
if (oldMethod && oldMethod !== newMethod) {
|
||||
this.resetDefaults();
|
||||
}
|
||||
this.set('oldSelectedAuthType', newMethod);
|
||||
},
|
||||
|
||||
resetDefaults() {
|
||||
this.setProperties(DEFAULTS);
|
||||
},
|
||||
|
||||
cluster: null,
|
||||
redirectTo: null,
|
||||
|
||||
|
|
@ -22,9 +51,9 @@ export default Ember.Component.extend({
|
|||
return BACKENDS.findBy('type', this.get('selectedAuthType'));
|
||||
}),
|
||||
|
||||
providerComponentName: Ember.computed('selectedAuthBackend.type', function() {
|
||||
const type = Ember.String.dasherize(this.get('selectedAuthBackend.type'));
|
||||
return `auth-form/${type}`;
|
||||
providerPartialName: Ember.computed('selectedAuthType', function() {
|
||||
const type = Ember.String.dasherize(this.get('selectedAuthType'));
|
||||
return `partials/auth-form/${type}`;
|
||||
}),
|
||||
|
||||
hasCSPError: computed.alias('csp.connectionViolations.firstObject'),
|
||||
|
|
@ -45,15 +74,18 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
doSubmit(data) {
|
||||
doSubmit() {
|
||||
let data = {};
|
||||
this.setProperties({
|
||||
loading: true,
|
||||
error: null,
|
||||
});
|
||||
const targetRoute = this.get('redirectTo') || 'vault.cluster';
|
||||
//const {password, token, username} = data;
|
||||
const backend = this.get('selectedAuthBackend.type');
|
||||
const path = this.get('customPath');
|
||||
let targetRoute = this.get('redirectTo') || 'vault.cluster';
|
||||
let backend = this.get('selectedAuthBackend.type');
|
||||
let path = this.get('customPath');
|
||||
let attributes = attributesForSelectedAuthBackend[backend];
|
||||
|
||||
data = Ember.assign(data, this.getProperties(...attributes));
|
||||
if (this.get('useCustomPath') && path) {
|
||||
data.path = path;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export default Ember.Component.extend({
|
|||
return `${this.get('filename')}-${new Date().toISOString()}.${this.get('extension')}`;
|
||||
}),
|
||||
|
||||
fileLike: computed('data', 'mime', 'strigify', 'download', function() {
|
||||
fileLike: computed('data', 'mime', 'stringify', 'download', function() {
|
||||
let file;
|
||||
let data = this.get('data');
|
||||
let filename = this.get('download');
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
.shamir-progress {
|
||||
.shamir-progress-progress {
|
||||
display: inline-block;
|
||||
margin-top: $size-10;
|
||||
margin-right: $size-8;
|
||||
}
|
||||
.progress {
|
||||
box-shadow: 0 0 0 4px $progress-bar-background-color;
|
||||
display: inline;
|
||||
width: 150px;
|
||||
margin-top: $size-10;
|
||||
min-width: 90px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<nav class="tabs sub-nav is-marginless">
|
||||
<ul>
|
||||
{{#each (supported-auth-backends) as |backend|}}
|
||||
<li class="{{if (eq selectedAuthBackend.type backend.type) 'is-active' ''}}">
|
||||
<li class="{{if (eq selectedAuthBackend.type backend.type) 'is-active' ''}}" data-test-auth-method>
|
||||
<a href="{{href-to 'vault.cluster.auth' cluster.name (query-params with=backend.type)}}" data-test-auth-method-link={{backend.type}}>
|
||||
{{capitalize backend.type}}
|
||||
</a>
|
||||
|
|
@ -9,41 +9,46 @@
|
|||
{{/each}}
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
{{#if (and cluster.standby hasCSPError)}}
|
||||
{{message-error errorMessage=cspErrorText data-test-auth-error=true}}
|
||||
{{else}}
|
||||
{{message-error errorMessage=error data-test-auth-error=true}}
|
||||
{{/if}}
|
||||
{{component providerComponentName onSubmit=(action 'doSubmit') }}
|
||||
<div class="box has-slim-padding is-shadowless">
|
||||
{{#unless (eq selectedAuthBackend.type "token")}}
|
||||
{{toggle-button toggleTarget=this toggleAttr="useCustomPath"}}
|
||||
<div class="field">
|
||||
{{#if useCustomPath}}
|
||||
<label for="custom-path" class="is-label">
|
||||
Mount path
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
name="custom-path"
|
||||
id="custom-path"
|
||||
class="input"
|
||||
value={{customPath}}
|
||||
oninput={{action (mut customPath) value="target.value"}}
|
||||
/>
|
||||
</div>
|
||||
<p class="help has-text-grey-dark">
|
||||
If this backend was mounted using a non-default path, enter it here.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
<form
|
||||
id="auth-form"
|
||||
{{action (action "doSubmit") on="submit"}}
|
||||
>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
{{#if (and cluster.standby hasCSPError)}}
|
||||
{{message-error errorMessage=cspErrorText data-test-auth-error=true}}
|
||||
{{else}}
|
||||
{{message-error errorMessage=error data-test-auth-error=true}}
|
||||
{{/if}}
|
||||
{{partial providerPartialName}}
|
||||
<div class="box has-slim-padding is-shadowless">
|
||||
{{#unless (eq selectedAuthBackend.type "token")}}
|
||||
{{toggle-button toggleTarget=this toggleAttr="useCustomPath"}}
|
||||
<div class="field">
|
||||
{{#if useCustomPath}}
|
||||
<label for="custom-path" class="is-label">
|
||||
Mount path
|
||||
</label>
|
||||
<div class="control">
|
||||
<input
|
||||
type="text"
|
||||
name="custom-path"
|
||||
id="custom-path"
|
||||
class="input"
|
||||
value={{customPath}}
|
||||
oninput={{action (mut customPath) value="target.value"}}
|
||||
/>
|
||||
</div>
|
||||
<p class="help has-text-grey-dark">
|
||||
If this backend was mounted using a non-default path, enter it here.
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless has-background-white-bis">
|
||||
<button data-test-auth-submit=true type="submit" disabled={{loading}} form="auth-form" class="button is-primary {{if loading 'is-loading'}}" id="auth-submit">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless has-background-white-bis">
|
||||
<button data-test-auth-submit=true type="submit" disabled={{loading}} class="button is-primary {{if loading 'is-loading'}}" id="auth-submit">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
<form
|
||||
id="auth-form"
|
||||
{{action (action onSubmit (hash token=token)) on="submit"}}
|
||||
>
|
||||
<div class="field">
|
||||
<label for="token" class="is-label">GitHub Token</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
type="password"
|
||||
value=token
|
||||
name="token"
|
||||
id="token"
|
||||
class="input"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<form
|
||||
id="auth-form"
|
||||
{{action (action onSubmit (hash token=token)) on="submit"}}
|
||||
>
|
||||
<div class="field">
|
||||
<label for="token" class="is-label">Token</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
type="password"
|
||||
value=token
|
||||
name="token"
|
||||
class="input"
|
||||
data-test-token=true
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -114,9 +114,9 @@
|
|||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="box is-marginless is-shadowless">
|
||||
{{message-error errors=errors}}
|
||||
<form {{action 'onSubmit' (hash key=key) on="submit"}} id="shamir">
|
||||
<form {{action 'onSubmit' (hash key=key) on="submit"}} id="shamir">
|
||||
<div class="box is-marginless is-shadowless">
|
||||
{{message-error errors=errors}}
|
||||
<div class="box has-slim-padding is-shadowless is-marginless">
|
||||
{{#if hasBlock}}
|
||||
{{yield}}
|
||||
|
|
@ -132,29 +132,28 @@
|
|||
{{input class="input"type="password" name="key" value=key data-test-shamir-input=true}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless has-background-white-bis">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<button
|
||||
form="shamir"
|
||||
type="submit"
|
||||
class="button is-primary"
|
||||
disabled={{loading}}
|
||||
data-test-shamir-submit=true
|
||||
>
|
||||
{{if generateAction "Generate Token" buttonText}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-offset-2 is-flex-v-centered">
|
||||
{{#if (or started hasProgress)}}
|
||||
{{shamir-progress
|
||||
threshold=threshold
|
||||
progress=progress
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless has-background-white-bis">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-narrow">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary"
|
||||
disabled={{loading}}
|
||||
data-test-shamir-submit=true
|
||||
>
|
||||
{{if generateAction "Generate Token" buttonText}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-flex-v-centered is-flex-end">
|
||||
{{#if (or started hasProgress)}}
|
||||
{{shamir-progress
|
||||
threshold=threshold
|
||||
progress=progress
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{/if}}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<div class="level">
|
||||
<div class="level-item">
|
||||
<div class="level-left">
|
||||
<span class="has-text-grey is-size-8 shamir-progress-progress">
|
||||
{{progress}} / {{threshold}} keys provided
|
||||
</span>
|
||||
</div>
|
||||
<div class="level-right is-marginless">
|
||||
<progress max="100" value="{{progressPercent}}" class="progress is-success is-rounded"></progress>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
13
ui/app/templates/partials/auth-form/git-hub.hbs
Normal file
13
ui/app/templates/partials/auth-form/git-hub.hbs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<div class="field">
|
||||
<label for="token" class="is-label">GitHub Token</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
type="password"
|
||||
value=token
|
||||
name="token"
|
||||
id="token"
|
||||
class="input"
|
||||
data-test-token=true
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
12
ui/app/templates/partials/auth-form/token.hbs
Normal file
12
ui/app/templates/partials/auth-form/token.hbs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<div class="field">
|
||||
<label for="token" class="is-label">Token</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
type="password"
|
||||
value=token
|
||||
name="token"
|
||||
class="input"
|
||||
data-test-token=true
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,28 +1,25 @@
|
|||
<form
|
||||
id="auth-form"
|
||||
{{action (action onSubmit (hash username=username password=password)) on="submit"}}
|
||||
>
|
||||
<div class="field">
|
||||
<label for="username" class="is-label">Username</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
value=username
|
||||
name="username"
|
||||
id="username"
|
||||
class="input"
|
||||
}}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="username" class="is-label">Username</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
value=username
|
||||
name="username"
|
||||
id="username"
|
||||
class="input"
|
||||
data-test-username=true
|
||||
}}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password" class="is-label">Password</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
value=password
|
||||
name="password"
|
||||
id="password"
|
||||
type="password"
|
||||
class="input"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password" class="is-label">Password</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
value=password
|
||||
name="password"
|
||||
id="password"
|
||||
type="password"
|
||||
class="input"
|
||||
data-test-password=true
|
||||
}}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -71,19 +71,19 @@
|
|||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<form {{action 'initCluster' (hash
|
||||
secret_shares=secret_shares
|
||||
secret_threshold=secret_threshold
|
||||
pgp_keys=pgp_keys
|
||||
use_pgp=use_pgp
|
||||
use_pgp_for_root=use_pgp_for_root
|
||||
root_token_pgp_key=root_token_pgp_key
|
||||
)
|
||||
on="submit"
|
||||
}}
|
||||
id="init"
|
||||
>
|
||||
<form {{action 'initCluster' (hash
|
||||
secret_shares=secret_shares
|
||||
secret_threshold=secret_threshold
|
||||
pgp_keys=pgp_keys
|
||||
use_pgp=use_pgp
|
||||
use_pgp_for_root=use_pgp_for_root
|
||||
root_token_pgp_key=root_token_pgp_key
|
||||
)
|
||||
on="submit"
|
||||
}}
|
||||
id="init"
|
||||
>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-half">
|
||||
<h1 class="title is-5">
|
||||
|
|
@ -156,18 +156,17 @@
|
|||
{{pgp-list listLength=1 onDataUpdate=(action 'setRootKey')}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless has-background-white-bis">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if loading 'is-loading'}}"
|
||||
disabled={{loading}}
|
||||
form="init"
|
||||
>
|
||||
Initialize
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless has-background-white-bis">
|
||||
<button
|
||||
type="submit"
|
||||
class="button is-primary {{if loading 'is-loading'}}"
|
||||
disabled={{loading}}
|
||||
>
|
||||
Initialize
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{{/if}}
|
||||
{{/s.content}}
|
||||
{{/splash-page}}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
"ember-cli-inject-live-reload": "^1.4.1",
|
||||
"ember-cli-mirage": "^0.4.1",
|
||||
"ember-cli-moment-shim": "2.2.1",
|
||||
"ember-cli-page-object": "^1.13.0",
|
||||
"ember-cli-page-object": "1.14",
|
||||
"ember-cli-pretender": "0.7.0",
|
||||
"ember-cli-qunit": "^4.0.0",
|
||||
"ember-cli-sass": "6.0.0",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { test } from 'qunit';
|
||||
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
|
||||
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
|
||||
import authForm from '../pages/components/auth-form';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
|
||||
const component = create(authForm);
|
||||
|
||||
moduleForAcceptance('Acceptance | auth', {
|
||||
afterEach() {
|
||||
beforeEach() {
|
||||
return authLogout();
|
||||
},
|
||||
});
|
||||
|
|
@ -25,3 +29,15 @@ test('auth query params', function(assert) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('it clears token when changing selected auth method', function(assert) {
|
||||
visit('/vault/auth');
|
||||
andThen(function() {
|
||||
assert.equal(currentURL(), '/vault/auth');
|
||||
});
|
||||
component.token('token').tabs.filterBy('name', 'GitHub')[0].link();
|
||||
component.tabs.filterBy('name', 'Token')[0].link();
|
||||
andThen(function() {
|
||||
assert.equal(component.tokenValue, '', 'it clears the token value when toggling methods');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { clickable, text } from 'ember-cli-page-object';
|
||||
import { collection, clickable, fillable, text, value } from 'ember-cli-page-object';
|
||||
|
||||
export default {
|
||||
tabs: collection('[data-test-auth-method]', {
|
||||
name: text(),
|
||||
link: clickable('[data-test-auth-method-link]'),
|
||||
}),
|
||||
username: fillable('[data-test-username]'),
|
||||
token: fillable('[data-test-token]'),
|
||||
tokenValue: value('[data-test-token]'),
|
||||
password: fillable('[data-test-password]'),
|
||||
errorText: text('[data-test-auth-error]'),
|
||||
login: clickable('[data-test-auth-submit]'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2817,15 +2817,14 @@ ember-cli-normalize-entity-name@^1.0.0:
|
|||
dependencies:
|
||||
silent-error "^1.0.0"
|
||||
|
||||
ember-cli-page-object@^1.13.0:
|
||||
version "1.13.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-page-object/-/ember-cli-page-object-1.13.0.tgz#9ac9342d9f90a363c429fbb14f3ad5c0be11827a"
|
||||
ember-cli-page-object@1.14:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-page-object/-/ember-cli-page-object-1.14.1.tgz#2e3599c204c56440c6c8154fc686c603816f877a"
|
||||
dependencies:
|
||||
ceibo "~2.0.0"
|
||||
ember-cli-babel "^6.6.0"
|
||||
ember-cli-node-assets "^0.2.2"
|
||||
ember-native-dom-helpers "^0.5.3"
|
||||
ember-test-helpers "^0.6.3"
|
||||
jquery "^3.2.1"
|
||||
rsvp "^4.7.0"
|
||||
|
||||
|
|
|
|||
BIN
website/source/assets/images/vault-versioned-kv-1.png
Normal file
BIN
website/source/assets/images/vault-versioned-kv-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
website/source/assets/images/vault-versioned-kv-2.png
Normal file
BIN
website/source/assets/images/vault-versioned-kv-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
BIN
website/source/assets/images/vault-versioned-kv-3.png
Normal file
BIN
website/source/assets/images/vault-versioned-kv-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
BIN
website/source/assets/images/vault-versioned-kv-4.png
Normal file
BIN
website/source/assets/images/vault-versioned-kv-4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
BIN
website/source/assets/images/vault-versioned-kv-5.png
Normal file
BIN
website/source/assets/images/vault-versioned-kv-5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
website/source/assets/images/vault-versioned-kv-6.png
Normal file
BIN
website/source/assets/images/vault-versioned-kv-6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
|
|
@ -85,6 +85,25 @@ listener "tcp" {
|
|||
authentication for this listener. The default behavior (when this is false)
|
||||
is for Vault to request client certificates when available.
|
||||
|
||||
- `x_forwarded_for_authorized_addrs` `(string: <required-to-enable>)` –
|
||||
Specifies the list of source IP addresses for which an X-Forwarded-For header
|
||||
will be trusted. Comma-separated list or JSON array. This turns on
|
||||
X-Forwarded-For support.
|
||||
|
||||
- `x_forwarded_for_hop_skips` `(string: "0")` – The number of addresses that will be
|
||||
skipped from the *rear* of the set of hops. For instance, for a header value
|
||||
of `1.2.3.4, 2.3.4.5, 3.4.5.6`, if this value is set to `"1"`, the address that
|
||||
will be used as the originating client IP is `2.3.4.5`.
|
||||
|
||||
- `x_forwarded_for_reject_not_authorized` `(string: "true")` – If set false,
|
||||
if there is an X-Forwarded-For header in a connection from an unauthorized
|
||||
address, the header will be ignored and the client connection used as-is,
|
||||
rather than the client connection rejected.
|
||||
|
||||
- `x_forwarded_for_reject_not_present` `(string: "true")` – If set false, if
|
||||
there is no X-Forwarded-For header or it is empty, the client address will be
|
||||
used as-is, rather than the client connection rejected.
|
||||
|
||||
## `tcp` Listener Examples
|
||||
|
||||
### Configuring TLS
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ walks you through the commands to activate the Vault servers in replication mode
|
|||
Please note that [Vault Replication](/docs/vault-enterprise/replication/index.html)
|
||||
is a Vault Enterprise feature.
|
||||
|
||||
- **[Enterprise Only]** [Vault Auto-unseal using AWS Key Management Service (KMS)](/guides/operations/autounseal-aws-kms.html) guide demonstrates an example
|
||||
of how to use Terraform to provision an instance that utilizes an encryption key
|
||||
- **[Enterprise Only]** [Vault Auto-unseal using AWS Key Management Service (KMS)](/guides/operations/autounseal-aws-kms.html) guide demonstrates an example of
|
||||
how to use Terraform to provision an instance that utilizes an encryption key
|
||||
from AWS Key Management Service (KMS).
|
||||
|
||||
- [Root Token Generation](/guides/operations/generate-root.html) guide
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ secrets.
|
|||
- [Static Secrets](/guides/secret-mgmt/static-secrets.html) guide walks you
|
||||
through the steps to write secrets in Vault, and control who can access them.
|
||||
|
||||
- [Versioned KV Secret Engine](/guides/secret-mgmt/versioned-kv.html) guide
|
||||
demonstrates the secret versioning capabilities provided by KV Secret Engine v2.
|
||||
|
||||
- [Secret as a Service: Dynamic Secrets](/guides/secret-mgmt/dynamic-secrets.html)
|
||||
guide demonstrates the Vault feature to generate database credentials
|
||||
on-demand so that each application or system can obtain its own credentials,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ description: |-
|
|||
keys.
|
||||
---
|
||||
|
||||
# Static Secrets
|
||||
# Static Secrets - Key/Value Secret Engine
|
||||
|
||||
Vault can be used to store any secret in a secure manner. The secrets may be
|
||||
SSL certificates and keys for your organization's domain, credentials to connect
|
||||
|
|
@ -57,6 +57,8 @@ nonce prior to writing them to its persistent storage. The storage backend never
|
|||
sees the unencrypted value, so gaining access to the raw storage isn't enough to
|
||||
access your secrets.
|
||||
|
||||
~> **NOTE:** This guide demonstrates secret management using [v2 of the KV
|
||||
secret engine](/docs/secrets/kv/kv-v2.html).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
|
@ -116,17 +118,103 @@ at the same path.
|
|||
|
||||
You will perform the following:
|
||||
|
||||
1. [Store the Google API key](#step1)
|
||||
2. [Store the root certificate for MySQL](#step2)
|
||||
3. [Generate a token for apps](#step3)
|
||||
4. [Retrieve the secrets](#step4)
|
||||
1. [Enable KV Secret Engine v2](#step1)
|
||||
1. [Store the Google API key](#step2)
|
||||
1. [Store the root certificate for MySQL](#step3)
|
||||
1. [Generate a token for apps](#step4)
|
||||
1. [Retrieve the secrets](#step5)
|
||||
|
||||

|
||||
|
||||
Step 1 through 3 are performed by `devops` persona. Step 4 describes the
|
||||
Step 1 through 4 are performed by `devops` persona. Step 5 describes the
|
||||
commands that `apps` persona runs to read secrets from Vault.
|
||||
|
||||
### <a name="step1"></a>Step 1: Store the Google API key
|
||||
### <a name="step1"></a>Step 1: Enable KV Secret Engine v2
|
||||
(**Persona:** devops)
|
||||
|
||||
Currently, when you start the Vault server in [**dev
|
||||
mode**](/intro/getting-started/dev-server.html#starting-the-dev-server), it
|
||||
automatically enables **v2** of the KV secret engine at **`secret/`**. If you
|
||||
start the Vault server in non-dev mode, the default is v1.
|
||||
|
||||
If you are running the server in **dev** mode, skip to [Step 2](#step2).
|
||||
Otherwise, you must perform one of the following:
|
||||
|
||||
- Option 1: Upgrade the v1 of KV secret engine to v2
|
||||
- Option 2: Enable the KV secret engine v2 at a different path
|
||||
|
||||
|
||||
#### CLI command
|
||||
|
||||
Option 1: To upgrade from **v1** to **v2**:
|
||||
|
||||
```plaintext
|
||||
$ vault kv enable-versioning secret/
|
||||
```
|
||||
<br>
|
||||
Option 2: To enable the KV secret engine v2 at **`secret_v2/`**:
|
||||
|
||||
```plaintext
|
||||
$ vault secrets enable -path=secret_v2/ kv-v2
|
||||
```
|
||||
|
||||
Or
|
||||
|
||||
```plaintext
|
||||
$ vault secrets enable -path=secret_v2/ -version=2 kv
|
||||
```
|
||||
|
||||
|
||||
#### API call using cURL
|
||||
|
||||
Option 1: To upgrade from **v1** to **v2**:
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: <TOKEN>" \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
<VAULT_ADDRESS>/v1/sys/mounts/secret/tune
|
||||
```
|
||||
|
||||
Where `<TOKEN>` is your valid token, and `<VAULT_ADDRESS>` is where your vault
|
||||
server is running. The `payload.json` includes the version information.
|
||||
|
||||
|
||||
**Example:**
|
||||
|
||||
```plaintext
|
||||
$ cat payload.json
|
||||
{
|
||||
"options": {
|
||||
"version": "2"
|
||||
}
|
||||
}
|
||||
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/sys/mounts/secret/tune
|
||||
```
|
||||
|
||||
<br>
|
||||
Option 2: To enable the KV secret engine v2 at **`secret_v2/`**:
|
||||
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data '{"type":"kv-v2"}' \
|
||||
https://127.0.0.1:8200/v1/sys/mounts/secret_v2
|
||||
```
|
||||
|
||||
|
||||
<br>
|
||||
|
||||
~> **NOTE:** This guide assumes that you are working with KV secret engine
|
||||
**v2** which is mounted at **`secret/`**.
|
||||
|
||||
|
||||
### <a name="step2"></a>Step 2: Store the Google API key
|
||||
(**Persona:** devops)
|
||||
|
||||
Everything after the **`secret/`** path is a key-value pair to write to the
|
||||
|
|
@ -144,7 +232,7 @@ have an API key for New Relic owned by the DevOps team, the path would look like
|
|||
|
||||
To create key/value secrets:
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ vault kv put secret/<PATH> <KEY>=VALUE>
|
||||
```
|
||||
|
||||
|
|
@ -153,7 +241,7 @@ decide on the naming convention that makes most sense.
|
|||
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ vault kv put secret/eng/apikey/Google key=AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI
|
||||
Success! Data written to: secret/eng/apikey/Google
|
||||
```
|
||||
|
|
@ -163,23 +251,32 @@ this example.
|
|||
|
||||
#### API call using cURL
|
||||
|
||||
Use `/secret/<PATH>` endpoint to create secrets:
|
||||
Use `/secret/data/<PATH>` endpoint to create secrets:
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: <TOKEN>" \
|
||||
--request POST \
|
||||
--data <SECRETS> \
|
||||
--data @payload.json \
|
||||
<VAULT_ADDRESS>/v1/secret/data/<PATH>
|
||||
```
|
||||
|
||||
Where `<TOKEN>` is your valid token, `<SECRETS>` is the key-value pair(s) of your
|
||||
secrets, and `secret/data/<PATH>` is the path to your secrets.
|
||||
Where `<TOKEN>` is your valid token, and `secret/data/<PATH>` is the path to
|
||||
your secrets. The [`payload.json`](/api/secret/kv/kv-v2.html#parameters-2)
|
||||
contains the parameters to invoke the endpoint.
|
||||
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ tee payload.json <<EOF
|
||||
{
|
||||
"data": {
|
||||
"key": "AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
$ curl --header "X-Vault-Token: ..." --request POST \
|
||||
--data '{"key": "AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI"}' \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/secret/data/eng/apikey/Google
|
||||
```
|
||||
|
||||
|
|
@ -187,13 +284,13 @@ The secret key is "key" and its value is
|
|||
"AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI" in this example.
|
||||
|
||||
|
||||
### <a name="step2"></a>Step 2: Store the root certificate for MySQL
|
||||
### <a name="step3"></a>Step 3: Store the root certificate for MySQL
|
||||
(**Persona:** devops)
|
||||
|
||||
For the purpose of this guide, generate a new self-sign certificate using
|
||||
[OpenSSL](https://www.openssl.org/source/).
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ openssl req --request509 -sha256 -nodes -newkey rsa:2048 -keyout selfsigned.key -out cert.pem
|
||||
```
|
||||
|
||||
|
|
@ -235,7 +332,7 @@ store the root certificate for production MySQL, the path becomes
|
|||
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ vault kv put secret/prod/cert/mysql cert=@cert.pem
|
||||
```
|
||||
|
||||
|
|
@ -250,7 +347,7 @@ To perform the same task using the Vault API, pass the token in the request head
|
|||
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @cert.pem \
|
||||
|
|
@ -259,7 +356,7 @@ $ curl --header "X-Vault-Token: ..." \
|
|||
> **NOTE:** Any value begins with "@" is loaded from a file.
|
||||
|
||||
|
||||
### <a name="step3"></a>Step 3: Generate a token for apps
|
||||
### <a name="step4"></a>Step 4: Generate a token for apps
|
||||
(**Persona:** devops)
|
||||
|
||||
To read the secrets, `apps` persona needs "read" permit on those secret engine
|
||||
|
|
@ -313,10 +410,11 @@ as an `app` persona.
|
|||
|
||||
```shell
|
||||
# Payload to pass in the API call
|
||||
$ cat payload.json
|
||||
$ tee payload.json <<EOF
|
||||
{
|
||||
"policy": "path \"secret/data/eng/apikey/Google\" { capabilities = [ \"read\" ] ...}"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create "apps" policy
|
||||
$ curl --header "X-Vault-Token: ..." --request PUT \
|
||||
|
|
@ -363,10 +461,10 @@ access.
|
|||
|
||||
|
||||
|
||||
### <a name="step4"></a>Step 4: Retrieve the secrets
|
||||
### <a name="step5"></a>Step 5: Retrieve the secrets
|
||||
(**Persona:** apps)
|
||||
|
||||
Using the token from [Step 3](#step3), read the Google API key, and root certificate for
|
||||
Using the token from [Step 4](#step4), read the Google API key, and root certificate for
|
||||
MySQL.
|
||||
|
||||
|
||||
|
|
@ -374,7 +472,7 @@ MySQL.
|
|||
|
||||
The command to read secret is:
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ vault kv get secret/<PATH>
|
||||
```
|
||||
|
||||
|
|
@ -398,7 +496,7 @@ key AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI
|
|||
|
||||
To return the key value alone, pass `-field=key` as an argument.
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ vault kv get -field=key secret/eng/apikey/Google
|
||||
AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI
|
||||
```
|
||||
|
|
@ -407,7 +505,7 @@ AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI
|
|||
|
||||
The command is basically the same:
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ vault kv get -field=cert secret/prod/cert/mysql
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA6E2Uq0XqreZISgVMUu9pnoMsq+OoK1PI54rsA9vtDE6wiRk0GWhf5vD4DGf1
|
||||
|
|
@ -418,7 +516,7 @@ MIIEowIBAAKCAQEA6E2Uq0XqreZISgVMUu9pnoMsq+OoK1PI54rsA9vtDE6wiRk0GWhf5vD4DGf1
|
|||
|
||||
Use `secret/` endpoint to retrieve secrets from key/value secret engine:
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: <TOKEN_FROM_STEP3>" \
|
||||
--request Get \
|
||||
<VAULT_ADDRESS>/v1/secret/data/<PATH>
|
||||
|
|
@ -428,7 +526,7 @@ $ curl --header "X-Vault-Token: <TOKEN_FROM_STEP3>" \
|
|||
|
||||
Read the Google API key.
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: 1c97b03a-6098-31cf-9d8b-b404e52dcb4a" \
|
||||
--request GET \
|
||||
http://127.0.0.1:8200/v1/secret/data/eng/apikey/Google | jq
|
||||
|
|
@ -450,7 +548,7 @@ $ curl --header "X-Vault-Token: 1c97b03a-6098-31cf-9d8b-b404e52dcb4a" \
|
|||
|
||||
Retrieve the key value with `jq`:
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: 1c97b03a-6098-31cf-9d8b-b404e52dcb4a" \
|
||||
--request GET \
|
||||
http://127.0.0.1:8200/v1/secret/data/eng/apikey/Google | jq ".data.key"
|
||||
|
|
@ -458,7 +556,7 @@ $ curl --header "X-Vault-Token: 1c97b03a-6098-31cf-9d8b-b404e52dcb4a" \
|
|||
|
||||
#### Root certificate example:
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: 1c97b03a-6098-31cf-9d8b-b404e52dcb4a" \
|
||||
--request GET \
|
||||
http://127.0.0.1:8200/v1/secret/data/prod/cert/mysql | jq ".data.cert"
|
||||
|
|
@ -478,7 +576,7 @@ An easy technique is to use a dash "-" and then press Enter. This allows you to
|
|||
enter the secret in a new line. After entering the secret, press **`Ctrl+d`** to
|
||||
end the pipe and write the secret to the Vault.
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ vault kv put secret/eng/apikey/Google key=-
|
||||
|
||||
AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI
|
||||
|
|
@ -489,7 +587,7 @@ AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI
|
|||
|
||||
Using the Google API key example, you can create a file containing the key (apikey.txt):
|
||||
|
||||
```text
|
||||
```plaintext
|
||||
{
|
||||
"key": "AAaaBBccDDeeOTXzSMT1234BB_Z8JzG7JkSVxI"
|
||||
}
|
||||
|
|
@ -497,7 +595,7 @@ Using the Google API key example, you can create a file containing the key (apik
|
|||
|
||||
The CLI command would look like:
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ vault kv put secret/eng/apikey/Google @apikey.txt
|
||||
```
|
||||
|
||||
|
|
@ -510,7 +608,7 @@ in history.
|
|||
|
||||
In bash:
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ export HISTIGNORE="&:vault*"
|
||||
```
|
||||
|
||||
|
|
@ -522,23 +620,24 @@ $ export HISTIGNORE="&:vault*"
|
|||
The two examples introduced in this guide only had a single key-value pair. You
|
||||
can pass multiple values in the command.
|
||||
|
||||
```shell
|
||||
```plaintext
|
||||
$ vault kv put secret/dev/config/mongodb url=foo.example.com:35533 db_name=users \
|
||||
username=admin password=pa$$w0rd
|
||||
username=admin password=passw0rd
|
||||
```
|
||||
|
||||
Or, read the secret from a file:
|
||||
|
||||
```shell
|
||||
$ vault kv put secret/dev/config/mongodb @mongodb.txt
|
||||
|
||||
$ cat mongodb.txt
|
||||
```plaintext
|
||||
$ tee mongodb.txt <<EOF
|
||||
{
|
||||
"url": "foo.example.com:35533",
|
||||
"db_name": "users",
|
||||
"username": "admin",
|
||||
"password": "pa$$w0rd"
|
||||
"url": "foo.example.com:35533",
|
||||
"db_name": "users",
|
||||
"username": "admin",
|
||||
"password": "pa$$w0rd"
|
||||
}
|
||||
EOF
|
||||
|
||||
$ vault kv put secret/dev/config/mongodb @mongodb.txt
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
|
|
|||
947
website/source/guides/secret-mgmt/versioned-kv.html.md
Normal file
947
website/source/guides/secret-mgmt/versioned-kv.html.md
Normal file
|
|
@ -0,0 +1,947 @@
|
|||
---
|
||||
layout: "guides"
|
||||
page_title: "Versioned KV Secret Engine - Guides"
|
||||
sidebar_current: "guides-secret-mgmt-versioned-kv"
|
||||
description: |-
|
||||
Vault 0.10.0 introduced version 2 of key-value secret engine which supports
|
||||
versioning of your secrets so that you can undo the accidental deletion of
|
||||
secrets, or compare the different versions of the secret.
|
||||
---
|
||||
|
||||
# Versioned Key/Value Secret Engine
|
||||
|
||||
The [Static Secrets](/guides/secret-mgmt/static-secrets.html) guide introduced
|
||||
the basics of working with key-value secret engine. **Vault 0.10** introduced [_K/V
|
||||
Secrets Engine v2 with Secret
|
||||
Versioning_](https://www.hashicorp.com/blog/vault-0-10). This guide
|
||||
demonstrates the new features introduced by the key-value secret engine v2.
|
||||
|
||||
|
||||
## Reference Material
|
||||
|
||||
- [Static Secrets guide](/guides/secret-mgmt/static-secrets.html)
|
||||
- [KV Secrets Engine - Version 2](/docs/secrets/kv/kv-v2.html)
|
||||
- [KV Secrets Engine - Version 2 (API)](/api/secret/kv/kv-v2.html)
|
||||
|
||||
## Estimated Time to Complete
|
||||
|
||||
10 minutes
|
||||
|
||||
|
||||
## Challenge
|
||||
|
||||
The KV secret engine v1 does not provide a way to version or roll back secrets.
|
||||
This made it difficult to recover from unintentional data loss or overwrite when
|
||||
more than one user is writing at the same path.
|
||||
|
||||
|
||||
## Solution
|
||||
|
||||
Run the **version 2** of KV secret engine which can retain a configurable
|
||||
number of secret versions. This enables older versions' data to be retrievable
|
||||
in case of unwanted deletion or updates of the data. In addition, its
|
||||
_Check-and-Set_ operations can be used to protect the data from being overwritten
|
||||
unintentionally.
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
To perform the tasks described in this guide, you need to have a Vault
|
||||
environment. Refer to the [Getting
|
||||
Started](/intro/getting-started/install.html) guide to install Vault. Make sure
|
||||
that your Vault server has been [initialized and
|
||||
unsealed](/intro/getting-started/deploy.html).
|
||||
|
||||
### Policy requirements
|
||||
|
||||
-> **NOTE:** For the purpose of this guide, you can use **`root`** token to work
|
||||
with Vault. However, it is recommended that root tokens are only used for
|
||||
initial setup or in emergencies. As a best practice, use tokens with
|
||||
appropriate set of policies based on your role in the organization.
|
||||
|
||||
To perform all tasks demonstrated in this guide, your policy must include the
|
||||
following permissions:
|
||||
|
||||
```shell
|
||||
# To view in Web UI
|
||||
path "sys/mounts" {
|
||||
capabilities = [ "read", "update" ]
|
||||
}
|
||||
|
||||
# Write and manage secrets in key-value secret engine
|
||||
path "secret*" {
|
||||
capabilities = [ "create", "read", "update", "delete", "list" ]
|
||||
}
|
||||
|
||||
# To enable secret engines
|
||||
path "sys/mounts/*" {
|
||||
capabilities = [ "create", "read", "update", "delete" ]
|
||||
}
|
||||
```
|
||||
|
||||
If you are not familiar with policies, complete the
|
||||
[policies](/guides/identity/policies.html) guide.
|
||||
|
||||
|
||||
## Steps
|
||||
|
||||
This guide demonstrates the basic commands for working with KV secret engine v2.
|
||||
|
||||
You will perform the following:
|
||||
|
||||
1. [Check the KV secret engine version](#step1)
|
||||
2. [Write secrets](#step2)
|
||||
3. [Retrieve a specific version of secret](#step3)
|
||||
4. [Specify the number of versions to keep](#step4)
|
||||
5. [Delete versions of secret](#step5)
|
||||
6. [Permanently delete data](#step6)
|
||||
|
||||
|
||||
### <a name="step1"></a>Step 1: Check the KV secret engine version
|
||||
(**Persona:** devops)
|
||||
|
||||
Before beginning, verify that you are using the v2 of the KV secret engine.
|
||||
|
||||
#### CLI command
|
||||
|
||||
To check the KV secret engine version:
|
||||
|
||||
```plaintext
|
||||
$ vault secrets list -format=json
|
||||
...
|
||||
"secret/": {
|
||||
"type": "kv",
|
||||
"description": "key/value secret storage",
|
||||
"accessor": "kv_f05b8b9c",
|
||||
"config": {
|
||||
"default_lease_ttl": 0,
|
||||
"max_lease_ttl": 0,
|
||||
"force_no_cache": false
|
||||
},
|
||||
"options": {
|
||||
"version": "2"
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
The indicated **`version`** should be **`2`**. If the version is **`1`**,
|
||||
upgrade it to v2.
|
||||
|
||||
```plaintext
|
||||
$ vault kv enable-versioning secret/
|
||||
```
|
||||
|
||||
#### API call using cURL
|
||||
|
||||
To check the KV secret engine version:
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: <TOKEN>" \
|
||||
<VAULT_ADDRESS>/v1/sys/mounts
|
||||
```
|
||||
|
||||
Where `<TOKEN>` is your valid token, and `<VAULT_ADDRESS>` is where your vault
|
||||
server is running.
|
||||
|
||||
|
||||
**Example:**
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/sys/mounts | jq
|
||||
...
|
||||
"secret/": {
|
||||
"accessor": "kv_f05b8b9c",
|
||||
"config": {
|
||||
"default_lease_ttl": 0,
|
||||
"force_no_cache": false,
|
||||
"max_lease_ttl": 0,
|
||||
"plugin_name": ""
|
||||
},
|
||||
"description": "key/value secret storage",
|
||||
"local": false,
|
||||
"options": {
|
||||
"version": "2"
|
||||
},
|
||||
"seal_wrap": false,
|
||||
"type": "kv"
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
The indicated **`version`** should be **`2`**. If the version is **`1`**,
|
||||
upgrade it to v2.
|
||||
|
||||
```plaintext
|
||||
$ cat payload.json
|
||||
{
|
||||
"options": {
|
||||
"version": "2"
|
||||
}
|
||||
}
|
||||
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/sys/mounts/secret/tune
|
||||
```
|
||||
|
||||
#### Web UI
|
||||
|
||||
Open a web browser and launch the Vault UI (e.g. `http://127.0.0.1:8200/ui`) and
|
||||
then login.
|
||||
|
||||

|
||||
|
||||
If `secret/` does not indicates **`v2`**, you can upgrade it from `v1` to `v2`
|
||||
by executing the following CLI command:
|
||||
|
||||
```plaintext
|
||||
$ vault kv enable-versioning secret/
|
||||
```
|
||||
|
||||
Alternatively, you can enable KV secret engine v2 at a different path by
|
||||
clicking **Enable new engine**. Select **KV** from the **Secret engine type**
|
||||
drop-down list. Be sure that the **Version** is set to be **Version 2**.
|
||||
|
||||

|
||||
|
||||
Click **Enable Engine** to complete.
|
||||
|
||||
|
||||
### <a name="step2"></a>Step 2: Write Secrets
|
||||
|
||||
To understand how the versioning works, let's write some test data.
|
||||
|
||||
#### CLI commands
|
||||
|
||||
To write secrets, run `vault kv put` command instead of `vault write`:
|
||||
|
||||
```plaintext
|
||||
$ vault kv put secret/customer/acme name="ACME Inc." contact_email="jsmith@acme.com"
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-14T00:05:47.115378933Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
version 1
|
||||
```
|
||||
|
||||
To update the existing secret, run the `vault kv put` command again:
|
||||
|
||||
```plaintext
|
||||
$ vault kv put secret/customer/acme name="ACME Inc." contact_email="john.smith@acme.com"
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-14T00:13:35.296018431Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
version 2
|
||||
```
|
||||
|
||||
Now you have two versions of the `secret/customer/acme` data. Run `vault kv get`
|
||||
to read the data.
|
||||
|
||||
```plaintext
|
||||
$ vault kv get secret/customer/acme
|
||||
====== Metadata ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-14T00:13:35.296018431Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
version 2
|
||||
|
||||
======== Data ========
|
||||
Key Value
|
||||
--- -----
|
||||
contact_email john.smith@acme.com
|
||||
name ACME Inc.
|
||||
```
|
||||
|
||||
#### API call using cURL
|
||||
|
||||
Write some data at `secret/customer/acme`:
|
||||
|
||||
```plaintext
|
||||
$ tee payload.json <<EOF
|
||||
{
|
||||
"data": {
|
||||
"name": "ACME Inc.",
|
||||
"contact_email": "jsmith@acme.com"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/secret/data/customer/acme
|
||||
```
|
||||
|
||||
Notice that the endpoint for KV v2 is **`/secret/data/<path>`**; therefore, to
|
||||
write secrets at `secret/customer/acme`, the API endpoint becomes
|
||||
`/secret/data/customer/acme`.
|
||||
|
||||
Update the secret to create another version:
|
||||
|
||||
```plaintext
|
||||
$ tee payload.json <<EOF
|
||||
{
|
||||
"data": {
|
||||
"name": "ACME Inc.",
|
||||
"contact_email": "john.smith@acme.com"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/secret/data/customer/acme
|
||||
```
|
||||
|
||||
Now you have two versions of the `secret/customer/acme` data. Read back the secret.
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/secret/data/customer/acme
|
||||
{
|
||||
"request_id": "7233b69d-35d9-6c1b-ae81-9a679a03082d",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"data": {
|
||||
"contact_email": "john.smith@acme.com",
|
||||
"name": "ACME Inc."
|
||||
},
|
||||
"metadata": {
|
||||
"created_time": "2018-04-14T00:59:11.27903511Z",
|
||||
"deletion_time": "",
|
||||
"destroyed": false,
|
||||
"version": 2
|
||||
}
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### Web UI
|
||||
|
||||
In the Web UI, select `secret/` and then click **Create secret**.
|
||||
|
||||

|
||||
|
||||
Click **Save**.
|
||||
|
||||
To update the existing secret, select **Edit**, change the `contact_email`
|
||||
value, and then click **Save**.
|
||||
|
||||

|
||||
|
||||
|
||||
### <a name="step3"></a>Step 3: Retrieve a Specific Version of Secret
|
||||
|
||||
You may run into a situation where you need to view the secret before an update.
|
||||
|
||||
#### CLI commands
|
||||
|
||||
To retrieve the version 1 of the secret written at `secret/customer/acme`:
|
||||
|
||||
```plaintext
|
||||
$ vault kv get -version=1 secret/customer/acme
|
||||
====== Metadata ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-14T00:05:47.115378933Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
version 1
|
||||
|
||||
======== Data ========
|
||||
Key Value
|
||||
--- -----
|
||||
contact_email jsmith@acme.com
|
||||
name ACME Inc.
|
||||
```
|
||||
|
||||
To read the **metadata** of `secret/customer/acme`:
|
||||
|
||||
```plaintext
|
||||
$ vault kv metadata get secret/customer/acme
|
||||
======= Metadata =======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-14T00:05:47.115378933Z
|
||||
current_version 2
|
||||
max_versions 0
|
||||
oldest_version 0
|
||||
updated_time 2018-04-14T00:13:35.296018431Z
|
||||
|
||||
====== Version 1 ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-14T00:05:47.115378933Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
|
||||
====== Version 2 ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-14T00:13:35.296018431Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
```
|
||||
|
||||
|
||||
#### API call using cURL
|
||||
|
||||
To retrieve the version 1 of the secret written at `secret/customer/acme`:
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/secret/data/customer/acme?version=1 | jq
|
||||
{
|
||||
"request_id": "3bf5a2c1-d89b-9dd5-9bb5-0bc61a4a6d83",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"data": {
|
||||
"contact_email": "jsmith@acme.com",
|
||||
"name": "ACME Inc."
|
||||
},
|
||||
"metadata": {
|
||||
"created_time": "2018-04-14T00:05:47.115378933Z",
|
||||
"deletion_time": "",
|
||||
"destroyed": false,
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}
|
||||
```
|
||||
|
||||
To read the **metadata** of `secret/customer/acme`:
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/secret/metadata/customer/acme | jq
|
||||
{
|
||||
"request_id": "34708262-59cd-9a94-247f-3b1db0909050",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"created_time": "2018-04-14T00:05:47.115378933Z",
|
||||
"current_version": 2,
|
||||
"max_versions": 0,
|
||||
"oldest_version": 0,
|
||||
"updated_time": "2018-04-14T00:13:35.296018431Z",
|
||||
"versions": {
|
||||
"1": {
|
||||
"created_time": "2018-04-14T00:05:47.115378933Z",
|
||||
"deletion_time": "",
|
||||
"destroyed": false
|
||||
},
|
||||
"2": {
|
||||
"created_time": "2018-04-14T00:13:35.296018431Z",
|
||||
"deletion_time": "",
|
||||
"destroyed": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### <a name="step4"></a>Step 4: Specify the number of versions to keep
|
||||
|
||||
By default, the `kv-v2` secret engine keeps up to 10 versions. Let's limit the
|
||||
maximum number of versions to keep to be 4.
|
||||
|
||||
#### CLI command
|
||||
|
||||
To set the `secret/` to keep up to 4 versions:
|
||||
|
||||
```shell
|
||||
$ vault write secret/config max_versions=4
|
||||
Success! Data written to: secret/config
|
||||
|
||||
# View the configuration settings
|
||||
$ vault read secret/config
|
||||
Key Value
|
||||
--- -----
|
||||
cas_required false
|
||||
max_versions 4
|
||||
```
|
||||
|
||||
Alternatively, to limit the number of versions only on the
|
||||
**`secret/customer/acme`** path rather than the entire `secret/` engine:
|
||||
|
||||
```plaintext
|
||||
$ vault kv metadata put -max-versions=4 secret/customer/acme
|
||||
```
|
||||
|
||||
Overwrite the data a few more times to see what happens to the data.
|
||||
|
||||
```plaintext
|
||||
$ vault kv metadata get secret/customer/acme
|
||||
======= Metadata =======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-14T00:42:25.677078177Z
|
||||
current_version 6
|
||||
max_versions 0
|
||||
oldest_version 3
|
||||
updated_time 2018-04-16T00:17:23.930473344Z
|
||||
|
||||
====== Version 3 ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-16T00:15:59.880368849Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
|
||||
====== Version 4 ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-16T00:16:18.941331243Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
|
||||
====== Version 5 ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-16T00:16:34.407951572Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
|
||||
====== Version 6 ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-16T00:17:23.930473344Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
```
|
||||
|
||||
In this example, the current version is 6. Notice that version 1 and 2 do not
|
||||
show up in the metadata. Because the kv secret engine is configured to keep only
|
||||
4 versions, the oldest two versions are permanently deleted and you won't be
|
||||
able to read them.
|
||||
|
||||
```plaintext
|
||||
$ vault kv get -version=1 secret/customer/acme
|
||||
No value found at secret/data/customer/data
|
||||
```
|
||||
|
||||
#### API call using cURL
|
||||
|
||||
To set the `secret/` to keep up to 4 versions:
|
||||
|
||||
```plaintext
|
||||
$ tee payload.json<<EOF
|
||||
{
|
||||
"max_versions": 4,
|
||||
"cas_required": false
|
||||
}
|
||||
EOF
|
||||
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json
|
||||
http://127.0.0.1:8200/v1/secret/config
|
||||
```
|
||||
|
||||
To view the configuration:
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/secret/config | jq
|
||||
{
|
||||
"request_id": "8addfed1-41eb-6a19-8342-93f493c51538",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"cas_required": false,
|
||||
"max_versions": 4
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, to limit the number of versions only on the
|
||||
**`secret/customer/acme`** path rather than the entire `secret/` engine:
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json
|
||||
http://127.0.0.1:8200/v1/secret/metadata/customer/acme
|
||||
```
|
||||
|
||||
Invoke the `secret/metadata/customer/acme` endpoint instead.
|
||||
|
||||
|
||||
Overwrite the data a few more times to see what happens to the data.
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/secret/metadata/customer/acme | jq
|
||||
{
|
||||
"request_id": "f2dd7f69-294c-e5c3-d582-f723005ea243",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"created_time": "2018-04-14T00:42:25.677078177Z",
|
||||
"current_version": 6,
|
||||
"max_versions": 0,
|
||||
"oldest_version": 3,
|
||||
"updated_time": "2018-04-16T00:17:23.930473344Z",
|
||||
"versions": {
|
||||
"3": {
|
||||
"created_time": "2018-04-16T00:15:59.880368849Z",
|
||||
"deletion_time": "",
|
||||
"destroyed": false
|
||||
},
|
||||
"4": {
|
||||
"created_time": "2018-04-16T00:16:18.941331243Z",
|
||||
"deletion_time": "",
|
||||
"destroyed": false
|
||||
},
|
||||
"5": {
|
||||
"created_time": "2018-04-16T00:16:34.407951572Z",
|
||||
"deletion_time": "",
|
||||
"destroyed": false
|
||||
},
|
||||
"6": {
|
||||
"created_time": "2018-04-16T00:17:23.930473344Z",
|
||||
"deletion_time": "",
|
||||
"destroyed": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}
|
||||
```
|
||||
|
||||
In this example, the current version is 6. Notice that version 1 and 2 do not
|
||||
show up in the metadata. Because the kv secret engine is configured to keep only
|
||||
4 versions, the oldest two versions are permanently deleted and you won't be
|
||||
able to read them.
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/secret/data/customer/acme?version=1 | jq
|
||||
{
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### <a name="step5"></a>Step 5: Delete versions of secret
|
||||
|
||||
|
||||
#### CLI command
|
||||
|
||||
Let's delete versions 4 and 5:
|
||||
|
||||
```shell
|
||||
$ vault kv delete -versions="4,5" secret/customer/acme
|
||||
Success! Data deleted (if it existed) at: secret/customer/acme
|
||||
|
||||
# Check the metadata
|
||||
$ vault kv metadata get secret/customer/acme
|
||||
...
|
||||
====== Version 4 ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-16T00:12:25.404198622Z
|
||||
deletion_time 2018-04-16T01:04:01.160426888Z
|
||||
destroyed false
|
||||
|
||||
====== Version 5 ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-16T00:12:47.527981267Z
|
||||
deletion_time 2018-04-16T01:04:01.160427742Z
|
||||
destroyed false
|
||||
...
|
||||
```
|
||||
|
||||
The metadata on versions 4 and 5 reports its deletion timestamp
|
||||
(`deletion_time`); however, the `destroyed` parameter is set to `false`.
|
||||
|
||||
If version 5 was deleted by mistake and you wish to recover, invoke the `vault
|
||||
kv undelete` command:
|
||||
|
||||
```plaintext
|
||||
$ vault kv undelete -versions=5 secret/customer/acme
|
||||
Success! Data written to: secret/undelete/customer/acme
|
||||
```
|
||||
|
||||
|
||||
#### API call using cURL
|
||||
|
||||
Let's delete versions 4 and 5:
|
||||
|
||||
```shell
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data '{ "versions":[4,5] }'
|
||||
http://127.0.0.1:8200/v1/secret/delete/customer/acme
|
||||
|
||||
# Check the metadata
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/secret/metadata/customer/acme | jq
|
||||
...
|
||||
"4": {
|
||||
"created_time": "2018-04-16T00:16:18.941331243Z",
|
||||
"deletion_time": "2018-04-16T01:17:42.003111567Z",
|
||||
"destroyed": false
|
||||
},
|
||||
"5": {
|
||||
"created_time": "2018-04-16T00:16:34.407951572Z",
|
||||
"deletion_time": "2018-04-16T01:17:42.003111978Z",
|
||||
"destroyed": false
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
The metadata on versions 4 and 5 reports its deletion timestamp
|
||||
(`deletion_time`); however, the `destroyed` parameter is set to `false`.
|
||||
|
||||
If version 5 was deleted by mistake and you wish to recover, invoke the
|
||||
`/secret/undelete` endpoint:
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data '{ "versions":[5] }'
|
||||
http://127.0.0.1:8200/v1/secret/undelete/customer/acme
|
||||
```
|
||||
|
||||
|
||||
### <a name="step6"></a>Step 6: Permanently delete data
|
||||
|
||||
#### CLI command
|
||||
|
||||
To permanently delete a version of secret:
|
||||
|
||||
```shell
|
||||
$ vault kv destroy -versions=4 secret/customer/acme
|
||||
Success! Data written to: secret/destroy/customer/acme
|
||||
|
||||
# Check the metadata
|
||||
$ vault kv metadata get secret/customer/acme
|
||||
...
|
||||
====== Version 4 ======
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-16T00:12:25.404198622Z
|
||||
deletion_time 2018-04-16T01:04:01.160426888Z
|
||||
destroyed true
|
||||
...
|
||||
```
|
||||
|
||||
The metadata indicates that Version 4 is destroyed.
|
||||
|
||||
If you wish to destroy all the keys and versions at `secret/customer/acme`,
|
||||
invoke the `vault kv metadata delete` command:
|
||||
|
||||
```plaintext
|
||||
$ vault kv metadata delete secret/customer/acme
|
||||
Success! Data deleted (if it existed) at: secret/metadata/customer/acme
|
||||
```
|
||||
|
||||
|
||||
#### API call using cURL
|
||||
|
||||
To permanently delete a version of secret:
|
||||
|
||||
```shell
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data '{ "versions":[4] }'
|
||||
http://127.0.0.1:8200/v1/secret/destroy/customer/acme
|
||||
|
||||
# Check the metadata
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
http://127.0.0.1:8200/v1/secret/metadata/customer/acme | jq
|
||||
...
|
||||
"4": {
|
||||
"created_time": "2018-04-16T00:16:18.941331243Z",
|
||||
"deletion_time": "2018-04-16T01:17:42.003111567Z",
|
||||
"destroyed": true
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
The metadata indicates that Version 4 is destroyed.
|
||||
|
||||
If you wish to destroy all the keys and versions at `secret/customer/acme`,
|
||||
invoke the `secret/metadata` endpoint:
|
||||
|
||||
```plaintext
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request DELETE
|
||||
http://127.0.0.1:8200/v1/secret/metadata/customer/acme
|
||||
```
|
||||
|
||||
|
||||
### Additional Discussion
|
||||
|
||||
The v2 of KV secret engine supports a **_Check-And-Set_** operation to prevent
|
||||
unintentional secret overwrite. When you pass the `cas` flag to Vault, it first
|
||||
checks if the key already exists.
|
||||
|
||||
By default, _Check-And-Set_ operation is not enabled on the KV secret engine;
|
||||
therefore, write is always allowed (no checking is performed).
|
||||
|
||||
```plaintext
|
||||
$ vault read secret/config
|
||||
Key Value
|
||||
--- -----
|
||||
cas_required false
|
||||
max_versions 0
|
||||
```
|
||||
|
||||
#### CLI command
|
||||
|
||||
To enable the **_Check-And-Set_** operation:
|
||||
|
||||
```shell
|
||||
# Enable cas_requied on the secret engine mounted at secret/
|
||||
$ vault write secret/config cas-required=true
|
||||
|
||||
# Enable cas_requied only on the secret/partner path
|
||||
$ vault kv metadata put -cas-required=true secret/partner
|
||||
```
|
||||
|
||||
Once check-and-set is enabled, every write operation requires `cas` value to be
|
||||
passed. If you are sure that you want to overwrite the existing key-value, set
|
||||
`cas` to match the current version. Set `cas` to `0` if you want to write the
|
||||
secret _only if_ the key does not exists.
|
||||
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
# To write if the key does not already exists
|
||||
$ vault kv put -cas=0 secret/partner name="Example Co." partner_id="123456789"
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-16T22:58:15.798753323Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
version 1
|
||||
|
||||
# To overwrite the secret, you must specify the current version with -cas flag
|
||||
$ vault kv put -cas=1 secret/partner name="Example Co." partner_id="ABCDEFGHIJKLMN"
|
||||
Key Value
|
||||
--- -----
|
||||
created_time 2018-04-16T23:00:28.66552289Z
|
||||
deletion_time n/a
|
||||
destroyed false
|
||||
version 2
|
||||
```
|
||||
|
||||
#### API call using cURL
|
||||
|
||||
To enable the **_Check-And-Set_** operation:
|
||||
|
||||
```shell
|
||||
$ tee payload.json<<EOF
|
||||
{
|
||||
"max_versions": 10,
|
||||
"cas_required": true
|
||||
}
|
||||
EOF
|
||||
|
||||
# Enable cas_requied on the secret engine mounted at secret/
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json
|
||||
http://127.0.0.1:8200/v1/secret/config
|
||||
|
||||
# Enable cas_requied only on the secret/partner path
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json
|
||||
http://127.0.0.1:8200/v1/secret/metadata/partner
|
||||
```
|
||||
|
||||
Once check-and-set is enabled, every write operation requires `cas` value to be
|
||||
passed. If you are sure that you want to overwrite the existing key-value, set
|
||||
`cas` to match the current version. Set `cas` to `0` if you want to write the
|
||||
secret _only if_ the key does not exists.
|
||||
|
||||
**Example:**
|
||||
|
||||
```shell
|
||||
# Write if the key does not already exists
|
||||
$ tee payload.json <<EOF
|
||||
{
|
||||
"options": {
|
||||
"cas": 0
|
||||
},
|
||||
"data": {
|
||||
"name": "Example Co.",
|
||||
"partner_id": "123456789"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/secret/data/partner
|
||||
|
||||
# To overwrite the secret, you must pass the current version
|
||||
$ tee payload.json <<EOF
|
||||
{
|
||||
"options": {
|
||||
"cas": 1
|
||||
},
|
||||
"data": {
|
||||
"name": "Example Co.",
|
||||
"partner_id": "ABCDEFGHIJKLMN"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
$ curl --header "X-Vault-Token: ..." \
|
||||
--request POST \
|
||||
--data @payload.json \
|
||||
http://127.0.0.1:8200/v1/secret/data/partner
|
||||
```
|
||||
|
||||
<br>
|
||||
|
||||
~> If the **`cas`** value is missing in your write request, the
|
||||
"`check-and-set parameter required for this call`" error will be returned. If
|
||||
the `cas` does not match the current version number, you will receive the
|
||||
"`check-and-set parameter did not match the current version`" message.
|
||||
|
||||
|
||||
|
||||
## Next steps
|
||||
|
||||
This guide introduced the CLI commands and API endpoints to read and write
|
||||
static secrets in the key-value secret engine. Read [Secret as a Service: Dynamic Secrets](/guides/secret-mgmt/dynamic-secrets.html) guide to learn about the
|
||||
usage of database secret engine.
|
||||
|
|
@ -61,6 +61,9 @@
|
|||
<li<%= sidebar_current("guides-secret-mgmt-static-secrets") %>>
|
||||
<a href="/guides/secret-mgmt/static-secrets.html">Static Secrets</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("guides-secret-mgmt-versioned-kv") %>>
|
||||
<a href="/guides/secret-mgmt/versioned-kv.html">Versioned KV Secret Engine</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("guides-secret-mgmt-dynamic-secrets") %>>
|
||||
<a href="/guides/secret-mgmt/dynamic-secrets.html">Secret as a Service</a>
|
||||
</li>
|
||||
|
|
|
|||
Loading…
Reference in a new issue