Merge branch 'master' into jo-ie11-fixes

This commit is contained in:
Joshua Ogle 2018-04-17 18:44:59 -06:00 committed by GitHub
commit 981427cbff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1932 additions and 383 deletions

View file

@ -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:

View file

@ -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
}
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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",

View file

@ -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
View 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
}

View file

@ -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
}

View file

@ -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
View 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())
}
})
}

View file

@ -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.

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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;
}

View file

@ -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');

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}}

View file

@ -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>

View 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>

View 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>

View file

@ -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>

View file

@ -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}}

View file

@ -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",

View file

@ -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');
});
});

View file

@ -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]'),
};

View file

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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)
![Personas Introduction](/assets/images/vault-static-secrets.png)
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

View 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.
![Versioned KV](/assets/images/vault-versioned-kv-1.png)
## 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.
![Web UI](/assets/images/vault-versioned-kv-2.png)
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**.
![Enabling kv-v2](/assets/images/vault-versioned-kv-3.png)
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**.
![Write Secret](/assets/images/vault-versioned-kv-5.png)
Click **Save**.
To update the existing secret, select **Edit**, change the `contact_email`
value, and then click **Save**.
![Write Secret](/assets/images/vault-versioned-kv-6.png)
### <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.

View file

@ -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>