Merge branch 'master' into token-roles

This commit is contained in:
Jeff Mitchell
2016-03-07 10:03:54 -05:00
37 changed files with 982 additions and 344 deletions

View File

@@ -1,11 +1,34 @@
## 0.5.2 (Unreleased)
IMPROVEMENTS:
* core: Ignore leading `/` in policy paths [GH-1170]
* core: Ignore leading `/` in mount paths [GH-1172]
* command/server: The initial root token ID when running in `-dev` mode can
now be specified via `-dev-root-token-id` or the environment variable
`VAULT_DEV_ROOT_TOKEN_ID` [GH-1162]
* command/server: The listen address when running in `-dev` mode can now be
specified via `-dev-listen-address` or the environment variable
`VAULT_DEV_LISTEN_ADDRESS` [GH-1169]
* command/step-down: New `vault step-down` command and API endpoint to force
the targeted node to give up active status, but without sealing. The node
will wait ten seconds before attempting too grab the lock again. [GH-1146]
* command/token-renew: Allow no token to be passed in; use `renew-self` in
this case. Change the behavior for any token being passed in to use `renew`.
[GH-1150]
* credential/cert: Non-CA certificates can be used for authentication. They
must be matched exactly (issuer and serial number) for authentication, and
the certificate must carry the client authentication or 'any' extended usage
attributes. [GH-1153]
* secret/ssh: Added documentation for `ssh/config/zeroaddress` endpoint. [GH-1154]
BUG FIXES:
* logical/cassandra: Apply hyphen/underscore replacement to the entire
generated username, not just the UUID, in order to handle token display name
hyphens [GH-1140]
* physical/etcd: Output actual error when cluster sync fails [GH-1141]
* logical/cassandra: Apply hyphen/underscore replacement to the entire
generated username, not just the UUID, in order to handle token display name
hyphens [GH-1140]
* physical/etcd: Output actual error when cluster sync fails [GH-1141]
* vault/expiration: Not letting the error responses from the backends to skip
during renewals [GH-1176]
## 0.5.1 (February 25th, 2016)

View File

@@ -18,10 +18,6 @@ func (c *Sys) ListAuth() (map[string]*AuthMount, error) {
}
func (c *Sys) EnableAuth(path, authType, desc string) error {
if err := c.checkAuthPath(path); err != nil {
return err
}
body := map[string]string{
"type": authType,
"description": desc,
@@ -42,10 +38,6 @@ func (c *Sys) EnableAuth(path, authType, desc string) error {
}
func (c *Sys) DisableAuth(path string) error {
if err := c.checkAuthPath(path); err != nil {
return err
}
r := c.c.NewRequest("DELETE", fmt.Sprintf("/v1/sys/auth/%s", path))
resp, err := c.c.RawRequest(r)
if err == nil {
@@ -54,14 +46,6 @@ func (c *Sys) DisableAuth(path string) error {
return err
}
func (c *Sys) checkAuthPath(path string) error {
if path[0] == '/' {
return fmt.Errorf("path must not start with /: %s", path)
}
return nil
}
// Structures for the requests/resposne are all down here. They aren't
// individually documentd because the map almost directly to the raw HTTP API
// documentation. Please refer to that documentation for more details.

View File

@@ -20,10 +20,6 @@ func (c *Sys) ListMounts() (map[string]*MountOutput, error) {
}
func (c *Sys) Mount(path string, mountInfo *MountInput) error {
if err := c.checkMountPath(path); err != nil {
return err
}
body := structs.Map(mountInfo)
r := c.c.NewRequest("POST", fmt.Sprintf("/v1/sys/mounts/%s", path))
@@ -41,10 +37,6 @@ func (c *Sys) Mount(path string, mountInfo *MountInput) error {
}
func (c *Sys) Unmount(path string) error {
if err := c.checkMountPath(path); err != nil {
return err
}
r := c.c.NewRequest("DELETE", fmt.Sprintf("/v1/sys/mounts/%s", path))
resp, err := c.c.RawRequest(r)
if err == nil {
@@ -54,13 +46,6 @@ func (c *Sys) Unmount(path string) error {
}
func (c *Sys) Remount(from, to string) error {
if err := c.checkMountPath(from); err != nil {
return err
}
if err := c.checkMountPath(to); err != nil {
return err
}
body := map[string]interface{}{
"from": from,
"to": to,
@@ -79,10 +64,6 @@ func (c *Sys) Remount(from, to string) error {
}
func (c *Sys) TuneMount(path string, config MountConfigInput) error {
if err := c.checkMountPath(path); err != nil {
return err
}
body := structs.Map(config)
r := c.c.NewRequest("POST", fmt.Sprintf("/v1/sys/mounts/%s/tune", path))
if err := r.SetJSONBody(body); err != nil {
@@ -97,10 +78,6 @@ func (c *Sys) TuneMount(path string, config MountConfigInput) error {
}
func (c *Sys) MountConfig(path string) (*MountConfigOutput, error) {
if err := c.checkMountPath(path); err != nil {
return nil, err
}
r := c.c.NewRequest("GET", fmt.Sprintf("/v1/sys/mounts/%s/tune", path))
resp, err := c.c.RawRequest(r)
@@ -113,14 +90,6 @@ func (c *Sys) MountConfig(path string) (*MountConfigOutput, error) {
return &result, err
}
func (c *Sys) checkMountPath(path string) error {
if path[0] == '/' {
return fmt.Errorf("path must not start with /: %s", path)
}
return nil
}
type MountInput struct {
Type string `json:"type" structs:"type"`
Description string `json:"description" structs:"description"`

10
api/sys_stepdown.go Normal file
View File

@@ -0,0 +1,10 @@
package api
func (c *Sys) StepDown() error {
r := c.c.NewRequest("PUT", "/v1/sys/step-down")
resp, err := c.c.RawRequest(r)
if err == nil {
defer resp.Body.Close()
}
return err
}

View File

@@ -27,6 +27,33 @@ func testFactory(t *testing.T) logical.Backend {
return b
}
// Test the certificates being registered to the backend
func TestBackend_CertWrites(t *testing.T) {
// CA cert
ca1, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
// Non CA Cert
ca2, err := ioutil.ReadFile("test-fixtures/keys/cert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
// Non CA cert without TLS web client authentication
ca3, err := ioutil.ReadFile("test-fixtures/noclientauthcert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
Backend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca1, "foo", false),
testAccStepCert(t, "web", ca2, "foo", false),
testAccStepCert(t, "web", ca3, "foo", true),
},
})
}
// Test a client trusted by a CA
func TestBackend_basic_CA(t *testing.T) {
connState := testConnState(t, "test-fixtures/keys/cert.pem",
@@ -38,7 +65,7 @@ func TestBackend_basic_CA(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{
Backend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo"),
testAccStepCert(t, "web", ca, "foo", false),
testAccStepLogin(t, connState),
testAccStepCertLease(t, "web", ca, "foo"),
testAccStepCertTTL(t, "web", ca, "foo"),
@@ -86,7 +113,7 @@ func TestBackend_basic_singleCert(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{
Backend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo"),
testAccStepCert(t, "web", ca, "foo", false),
testAccStepLogin(t, connState),
},
})
@@ -196,16 +223,23 @@ func testAccStepLoginInvalid(t *testing.T, connState tls.ConnectionState) logica
}
func testAccStepCert(
t *testing.T, name string, cert []byte, policies string) logicaltest.TestStep {
t *testing.T, name string, cert []byte, policies string, expectError bool) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "certs/" + name,
ErrorOk: expectError,
Data: map[string]interface{}{
"certificate": string(cert),
"policies": policies,
"display_name": name,
"lease": 1000,
},
Check: func(resp *logical.Response) error {
if resp == nil && expectError {
return fmt.Errorf("expected error but received nil")
}
return nil
},
}
}

View File

@@ -1,6 +1,7 @@
package cert
import (
"crypto/x509"
"fmt"
"strings"
"time"
@@ -51,7 +52,7 @@ Defaults to system/backend default TTL time.`,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.DeleteOperation: b.pathCertDelete,
logical.ReadOperation: b.pathCertRead,
logical.UpdateOperation: b.pathCertWrite,
logical.UpdateOperation: b.pathCertWrite,
},
HelpSynopsis: pathCertHelpSyn,
@@ -132,6 +133,20 @@ func (b *backend) pathCertWrite(
return logical.ErrorResponse("failed to parse certificate"), nil
}
// If the certificate is not a CA cert, then ensure that x509.ExtKeyUsageClientAuth is set
if !parsed[0].IsCA && parsed[0].ExtKeyUsage != nil {
var clientAuth bool
for _, usage := range parsed[0].ExtKeyUsage {
if usage == x509.ExtKeyUsageClientAuth || usage == x509.ExtKeyUsageAny {
clientAuth = true
break
}
}
if !clientAuth {
return logical.ErrorResponse("non-CA certificates should have TLS client authentication set as an extended key usage"), nil
}
}
certEntry := &CertEntry{
Name: name,
Certificate: certificate,
@@ -140,7 +155,6 @@ func (b *backend) pathCertWrite(
}
// Parse the lease duration or default to backend/system default
var err error
maxTTL := b.System().MaxLeaseTTL()
ttl := time.Duration(d.Get("ttl").(int)) * time.Second
if ttl == time.Duration(0) {

View File

@@ -1,6 +1,7 @@
package cert
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
@@ -134,7 +135,16 @@ func (b *backend) verifyCredentials(req *logical.Request) (*ParsedCert, *logical
connState := req.Connection.ConnState
// Load the trusted certificates
roots, trusted := b.loadTrustedCerts(req.Storage)
roots, trusted, trustedNonCAs := b.loadTrustedCerts(req.Storage)
// If trustedNonCAs is not empty it means that client had registered a non-CA cert
// with the backend.
if len(trustedNonCAs) != 0 {
policy := b.matchNonCAPolicy(connState.PeerCertificates[0], trustedNonCAs)
if policy != nil {
return policy, nil, nil
}
}
// Validate the connection state is trusted
trustedChains, err := validateConnState(roots, connState)
@@ -158,6 +168,18 @@ func (b *backend) verifyCredentials(req *logical.Request) (*ParsedCert, *logical
return b.matchPolicy(trustedChains, trusted), nil, nil
}
// matchNonCAPolicy is used to match the client cert with the registered non-CA
// policies to establish client identity.
func (b *backend) matchNonCAPolicy(clientCert *x509.Certificate, trustedNonCAs []*ParsedCert) *ParsedCert {
for _, trustedNonCA := range trustedNonCAs {
tCert := trustedNonCA.Certificates[0]
if tCert.SerialNumber.Cmp(clientCert.SerialNumber) == 0 && bytes.Equal(tCert.AuthorityKeyId, clientCert.AuthorityKeyId) {
return trustedNonCA
}
}
return nil
}
// matchPolicy is used to match the associated policy with the certificate that
// was used to establish the client identity.
func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*ParsedCert) *ParsedCert {
@@ -177,7 +199,7 @@ func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*ParsedCer
}
// loadTrustedCerts is used to load all the trusted certificates from the backend
func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, trusted []*ParsedCert) {
func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert) {
pool = x509.NewCertPool()
names, err := store.List("cert/")
if err != nil {
@@ -195,15 +217,22 @@ func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool,
b.Logger().Printf("[ERR] cert: failed to parse certificate for '%s'", name)
continue
}
for _, p := range parsed {
pool.AddCert(p)
}
if !parsed[0].IsCA {
trustedNonCAs = append(trustedNonCAs, &ParsedCert{
Entry: entry,
Certificates: parsed,
})
} else {
for _, p := range parsed {
pool.AddCert(p)
}
// Create a ParsedCert entry
trusted = append(trusted, &ParsedCert{
Entry: entry,
Certificates: parsed,
})
// Create a ParsedCert entry
trusted = append(trusted, &ParsedCert{
Entry: entry,
Certificates: parsed,
})
}
}
return
}
@@ -257,6 +286,7 @@ func validateConnState(roots *x509.CertPool, cs *tls.ConnectionState) ([][]*x509
Intermediates: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
certs := cs.PeerCertificates
if len(certs) == 0 {
return nil, nil

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDGTCCAgGgAwIBAgIBBDANBgkqhkiG9w0BAQUFADBxMQowCAYDVQQDFAEqMQsw
CQYDVQQIEwJHQTELMAkGA1UEBhMCVVMxJTAjBgkqhkiG9w0BCQEWFnZpc2hhbG5h
eWFrdkBnbWFpbC5jb20xEjAQBgNVBAoTCUhhc2hpQ29ycDEOMAwGA1UECxMFVmF1
bHQwHhcNMTYwMjI5MjE0NjE2WhcNMjEwMjI3MjE0NjE2WjBxMQowCAYDVQQDFAEq
MQswCQYDVQQIEwJHQTELMAkGA1UEBhMCVVMxJTAjBgkqhkiG9w0BCQEWFnZpc2hh
bG5heWFrdkBnbWFpbC5jb20xEjAQBgNVBAoTCUhhc2hpQ29ycDEOMAwGA1UECxMF
VmF1bHQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMfRkLfIGHt1r2jjnV0N
LqRCu3oB+J1dqpM03vQt3qzIiqtuQuIA2ba7TJm2HwU3W3+rtfFcS+hkBR/LZM+u
cBPB+9b9+7i08vHjgy2P3QH/Ebxa8j1v7JtRMT2qyxWK8NlT/+wZSH82Cr812aS/
zNT56FbBo2UAtzpqeC4eiv6NAgMBAAGjQDA+MAkGA1UdEwQCMAAwCwYDVR0PBAQD
AgXgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZI
hvcNAQEFBQADggEBAG2mUwsZ6+R8qqyNjzMk7mgpsRZv9TEl6c1IiQdyjaCOPaYH
vtZpLX20um36cxrLuOUtZLllG/VJEhRZW5mXWxuOk4QunWMBXQioCDJG1ktcZAcQ
QqYv9Dzy2G9lZHjLztEac37T75RXW7OEeQREgwP11c8sQYiS9jf+7ITYL7nXjoKq
gEuH0h86BOH2O/BxgMelt9O0YCkvkLLHnE27xuNelRRZcBLSuE1GxdUi32MDJ+ff
25GUNM0zzOEaJAFE/USUBEdQqN1gvJidNXkAiMtIK7T8omQZONRaD2ZnSW8y2krh
eUg+rKis9RinqFlahLPfI5BlyQsNMEnsD07Q85E=
-----END CERTIFICATE-----

View File

@@ -76,7 +76,7 @@ func (b *backend) pathLoginRenew(
sort.Strings(policies)
if strings.Join(policies, ",") != prevpolicies {
return logical.ErrorResponse("policies have changed, revoking login"), nil
return logical.ErrorResponse("policies have changed, not renewing"), nil
}
return framework.LeaseExtend(0, 0, b.System())(req, d)

View File

@@ -224,6 +224,12 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
}, nil
},
"step-down": func() (cli.Command, error) {
return &command.StepDownCommand{
Meta: meta,
}, nil
},
"mount": func() (cli.Command, error) {
return &command.MountCommand{
Meta: meta,

View File

@@ -53,7 +53,7 @@ Usage: vault policy-delete [options] name
Delete a policy with the given name.
One the policy is deleted, all users associated with the policy will
Once the policy is deleted, all users associated with the policy will
be affected immediately. When a user is associated with a policy that
doesn't exist, it is identical to not being associated with that policy.

View File

@@ -41,9 +41,11 @@ type ServerCommand struct {
func (c *ServerCommand) Run(args []string) int {
var dev, verifyOnly bool
var configPath []string
var logLevel string
var logLevel, devRootTokenID, devListenAddress string
flags := c.Meta.FlagSet("server", FlagSetDefault)
flags.BoolVar(&dev, "dev", false, "")
flags.StringVar(&devRootTokenID, "dev-root-token-id", "", "")
flags.StringVar(&devListenAddress, "dev-listen-address", "", "")
flags.StringVar(&logLevel, "log-level", "info", "")
flags.BoolVar(&verifyOnly, "verify-only", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
@@ -52,17 +54,39 @@ func (c *ServerCommand) Run(args []string) int {
return 1
}
if os.Getenv("VAULT_DEV_ROOT_TOKEN_ID") != "" {
devRootTokenID = os.Getenv("VAULT_DEV_ROOT_TOKEN_ID")
}
if os.Getenv("VAULT_DEV_LISTEN_ADDRESS") != "" {
devListenAddress = os.Getenv("VAULT_DEV_LISTEN_ADDRESS")
}
// Validation
if !dev && len(configPath) == 0 {
c.Ui.Error("At least one config path must be specified with -config")
flags.Usage()
return 1
if !dev {
switch {
case len(configPath) == 0:
c.Ui.Error("At least one config path must be specified with -config")
flags.Usage()
return 1
case devRootTokenID != "":
c.Ui.Error("Root token ID can only be specified with -dev")
flags.Usage()
return 1
case devListenAddress != "":
c.Ui.Error("Development address can only be specified with -dev")
flags.Usage()
return 1
}
}
// Load the configuration
var config *server.Config
if dev {
config = server.DevConfig()
if devListenAddress != "" {
config.Listeners[0].Config["address"] = devListenAddress
}
}
for _, path := range configPath {
current, err := server.LoadConfig(path)
@@ -193,7 +217,7 @@ func (c *ServerCommand) Run(args []string) int {
// If we're in dev mode, then initialize the core
if dev {
init, err := c.enableDev(core)
init, err := c.enableDev(core, devRootTokenID)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing dev mode: %s", err))
@@ -215,7 +239,7 @@ func (c *ServerCommand) Run(args []string) int {
"immediately begin using the Vault CLI.\n\n"+
"The only step you need to take is to set the following\n"+
"environment variables:\n\n"+
" "+export+" VAULT_ADDR="+quote+"http://127.0.0.1:8200"+quote+"\n\n"+
" "+export+" VAULT_ADDR="+quote+"http://"+config.Listeners[0].Config["address"]+quote+"\n\n"+
"The unseal key and root token are reproduced below in case you\n"+
"want to seal/unseal the Vault or play with authentication.\n\n"+
"Unseal Key: %s\nRoot Token: %s\n",
@@ -319,7 +343,7 @@ func (c *ServerCommand) Run(args []string) int {
return 0
}
func (c *ServerCommand) enableDev(core *vault.Core) (*vault.InitResult, error) {
func (c *ServerCommand) enableDev(core *vault.Core, rootTokenID string) (*vault.InitResult, error) {
// Initialize it with a basic single key
init, err := core.Initialize(&vault.SealConfig{
SecretShares: 1,
@@ -342,6 +366,39 @@ func (c *ServerCommand) enableDev(core *vault.Core) (*vault.InitResult, error) {
return nil, fmt.Errorf("failed to unseal Vault for dev mode")
}
if rootTokenID != "" {
req := &logical.Request{
Operation: logical.UpdateOperation,
ClientToken: init.RootToken,
Path: "auth/token/create",
Data: map[string]interface{}{
"id": rootTokenID,
"policies": []string{"root"},
"no_parent": true,
"no_default_policy": true,
},
}
resp, err := core.HandleRequest(req)
if err != nil {
return nil, fmt.Errorf("failed to create root token with ID %s: %s", rootTokenID, err)
}
if resp == nil {
return nil, fmt.Errorf("nil response when creating root token with ID %s", rootTokenID)
}
if resp.Auth == nil {
return nil, fmt.Errorf("nil auth when creating root token with ID %s", rootTokenID)
}
init.RootToken = resp.Auth.ClientToken
req.Path = "auth/token/revoke-self"
req.Data = nil
resp, err = core.HandleRequest(req)
if err != nil {
return nil, fmt.Errorf("failed to revoke initial root token: %s", err)
}
}
// Set the token
tokenHelper, err := c.TokenHelper()
if err != nil {
@@ -495,18 +552,28 @@ Usage: vault server [options]
General Options:
-config=<path> Path to the configuration file or directory. This can be
specified multiple times. If it is a directory, all
files with a ".hcl" or ".json" suffix will be loaded.
-config=<path> Path to the configuration file or directory. This can
be specified multiple times. If it is a directory,
all files with a ".hcl" or ".json" suffix will be
loaded.
-dev Enables Dev mode. In this mode, Vault is completely
in-memory and unsealed. Do not run the Dev server in
production!
-dev Enables Dev mode. In this mode, Vault is completely
in-memory and unsealed. Do not run the Dev server in
production!
-log-level=info Log verbosity. Defaults to "info", will be outputted
to stderr. Supported values: "trace", "debug", "info",
"warn", "err"
-dev-root-token-id="" If set, the root token returned in Dev mode will have
the given ID. This *only* has an effect when running
in Dev mode. Can also be specified with the
VAULT_DEV_ROOT_TOKEN_ID environment variable.
-dev-listen-address="" If set, this overrides the normal Dev mode listen
address of "127.0.0.1:8200". Can also be specified
with the VAULT_DEV_LISTEN_ADDRESS environment
variable.
-log-level=info Log verbosity. Defaults to "info", will be output to
stderr. Supported values: "trace", "debug", "info",
"warn", "err"
`
return strings.TrimSpace(helpText)
}

View File

@@ -44,6 +44,7 @@ func DevConfig() *Config {
&Listener{
Type: "tcp",
Config: map[string]string{
"address": "127.0.0.1:8200",
"tls_disable": "1",
},
},

54
command/step-down.go Normal file
View File

@@ -0,0 +1,54 @@
package command
import (
"fmt"
"strings"
)
// StepDownCommand is a Command that seals the vault.
type StepDownCommand struct {
Meta
}
func (c *StepDownCommand) Run(args []string) int {
flags := c.Meta.FlagSet("step-down", FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
if err := client.Sys().StepDown(); err != nil {
c.Ui.Error(fmt.Sprintf("Error stepping down: %s", err))
return 1
}
return 0
}
func (c *StepDownCommand) Synopsis() string {
return "Force the Vault node to give up active duty"
}
func (c *StepDownCommand) Help() string {
helpText := `
Usage: vault step-down [options]
Force the Vault node to step down from active duty.
This causes the indicated node to give up active status. Note that while the
affected node will have a short delay before attempting to grab the lock
again, if no other node grabs the lock beforehand, it is possible for the
same node to re-grab the lock and become active again.
General Options:
` + generalOptionsUsage()
return strings.TrimSpace(helpText)
}

View File

@@ -2,8 +2,8 @@ package command
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/hashicorp/vault/api"
)
@@ -14,32 +14,41 @@ type TokenRenewCommand struct {
}
func (c *TokenRenewCommand) Run(args []string) int {
var format string
var format, increment string
flags := c.Meta.FlagSet("token-renew", FlagSetDefault)
flags.StringVar(&format, "format", "table", "")
flags.StringVar(&increment, "increment", "", "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) < 1 {
if len(args) > 2 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\ntoken-renew expects at least one argument"))
"\ntoken-renew expects at most two arguments"))
return 1
}
var increment int
token := args[0]
if len(args) > 1 {
value, err := strconv.ParseInt(args[1], 10, 0)
var token string
if len(args) > 0 {
token = args[0]
}
var inc int
// If both are specified prefer the argument
if len(args) == 2 {
increment = args[1]
}
if increment != "" {
dur, err := time.ParseDuration(increment)
if err != nil {
c.Ui.Error(fmt.Sprintf("Invalid increment: %s", err))
return 1
}
increment = int(value)
inc = int(dur / time.Second)
}
client, err := c.Client()
@@ -52,10 +61,10 @@ func (c *TokenRenewCommand) Run(args []string) int {
// If the given token is the same as the client's, use renew-self instead
// as this is far more likely to be allowed via policy
var secret *api.Secret
if client.Token() == token {
secret, err = client.Auth().Token().RenewSelf(increment)
if token == "" {
secret, err = client.Auth().Token().RenewSelf(inc)
} else {
secret, err = client.Auth().Token().Renew(token, increment)
secret, err = client.Auth().Token().Renew(token, inc)
}
if err != nil {
c.Ui.Error(fmt.Sprintf(
@@ -72,17 +81,20 @@ func (c *TokenRenewCommand) Synopsis() string {
func (c *TokenRenewCommand) Help() string {
helpText := `
Usage: vault token-renew [options] token [increment]
Usage: vault token-renew [options] [token] [increment]
Renew an auth token, extending the amount of time it can be used.
Token is renewable only if there is a lease associated with it.
Renew an auth token, extending the amount of time it can be used. If a token
is given to the command, '/auth/token/renew' will be called with the given
token; otherwise, '/auth/token/renew-self' will be called with the client
token.
This command is similar to "renew", but "renew" is only for lease IDs.
This command is only for tokens.
This command is similar to "renew", but "renew" is only for leases; this
command is only for tokens.
An optional increment can be given to request a certain number of
seconds to increment the lease. This request is advisory; Vault may not
adhere to it at all.
An optional increment can be given to request a certain number of seconds to
increment the lease. This request is advisory; Vault may not adhere to it at
all. If a token is being passed in on the command line, the increment can as
well; otherwise it must be passed in via the '-increment' flag.
General Options:
@@ -90,6 +102,11 @@ General Options:
Token Renew Options:
-increment=3600 The desired increment. If not supplied, Vault will
use the default TTL. If supplied, it may still be
ignored. This can be submitted as an integer number
of seconds or a string duration (e.g. "72h").
-format=table The format for output. By default it is a whitespace-
delimited table. This can also be json or yaml.

View File

@@ -41,9 +41,136 @@ func TestTokenRenew(t *testing.T) {
t.Fatalf("err: %s", err)
}
// Verify it worked
// Renew, passing in the token
args = append(args, resp.Auth.ClientToken)
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestTokenRenewWithIncrement(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &TokenRenewCommand{
Meta: Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
}
// Run it once for client
c.Run(args)
// Create a token
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{
Lease: "1h",
})
if err != nil {
t.Fatalf("err: %s", err)
}
// Renew, passing in the token
args = append(args, resp.Auth.ClientToken)
args = append(args, "72h")
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestTokenRenewSelf(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &TokenRenewCommand{
Meta: Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
}
// Run it once for client
c.Run(args)
// Create a token
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{
Lease: "1h",
})
if err != nil {
t.Fatalf("err: %s", err)
}
if resp.Auth.ClientToken == "" {
t.Fatal("returned client token is empty")
}
c.Meta.ClientToken = resp.Auth.ClientToken
// Renew using the self endpoint
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestTokenRenewSelfWithIncrement(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &TokenRenewCommand{
Meta: Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
}
// Run it once for client
c.Run(args)
// Create a token
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{
Lease: "1h",
})
if err != nil {
t.Fatalf("err: %s", err)
}
if resp.Auth.ClientToken == "" {
t.Fatal("returned client token is empty")
}
c.Meta.ClientToken = resp.Auth.ClientToken
args = append(args, "-increment=72h")
// Renew using the self endpoint
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}

View File

@@ -23,29 +23,16 @@ func Handler(core *vault.Core) http.Handler {
mux.Handle("/v1/sys/init", handleSysInit(core))
mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core))
mux.Handle("/v1/sys/seal", handleSysSeal(core))
mux.Handle("/v1/sys/step-down", handleSysStepDown(core))
mux.Handle("/v1/sys/unseal", handleSysUnseal(core))
mux.Handle("/v1/sys/mounts", proxySysRequest(core))
mux.Handle("/v1/sys/mounts/", proxySysRequest(core))
mux.Handle("/v1/sys/remount", proxySysRequest(core))
mux.Handle("/v1/sys/policy", handleSysListPolicies(core))
mux.Handle("/v1/sys/policy/", handleSysPolicy(core))
mux.Handle("/v1/sys/renew/", handleLogical(core, false))
mux.Handle("/v1/sys/revoke/", proxySysRequest(core))
mux.Handle("/v1/sys/revoke-prefix/", proxySysRequest(core))
mux.Handle("/v1/sys/auth", proxySysRequest(core))
mux.Handle("/v1/sys/auth/", proxySysRequest(core))
mux.Handle("/v1/sys/audit-hash/", proxySysRequest(core))
mux.Handle("/v1/sys/audit", proxySysRequest(core))
mux.Handle("/v1/sys/audit/", proxySysRequest(core))
mux.Handle("/v1/sys/leader", handleSysLeader(core))
mux.Handle("/v1/sys/health", handleSysHealth(core))
mux.Handle("/v1/sys/rotate", proxySysRequest(core))
mux.Handle("/v1/sys/key-status", proxySysRequest(core))
mux.Handle("/v1/sys/generate-root/attempt", handleSysGenerateRootAttempt(core))
mux.Handle("/v1/sys/generate-root/update", handleSysGenerateRootUpdate(core))
mux.Handle("/v1/sys/rekey/init", handleSysRekeyInit(core))
mux.Handle("/v1/sys/rekey/backup", proxySysRequest(core))
mux.Handle("/v1/sys/rekey/update", handleSysRekeyUpdate(core))
mux.Handle("/v1/sys/", handleLogical(core, true))
mux.Handle("/v1/", handleLogical(core, false))
// Wrap the handler in another handler to trigger all help paths.
@@ -214,10 +201,6 @@ func respondOk(w http.ResponseWriter, body interface{}) {
}
}
func proxySysRequest(core *vault.Core) http.Handler {
return handleLogical(core, true)
}
type ErrorResponse struct {
Errors []string `json:"errors"`
}

View File

@@ -85,6 +85,10 @@ func TestLogical_StandbyRedirect(t *testing.T) {
t.Fatalf("unseal err: %s", err)
}
// Attempt to fix raciness in this test by giving the first core a chance
// to grab the lock
time.Sleep(time.Second)
// Create a second HA Vault
conf2 := &vault.CoreConfig{
Physical: inmha,

View File

@@ -1,150 +0,0 @@
package http
import (
"net/http"
"strings"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
)
func handleSysListPolicies(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
respondError(w, http.StatusMethodNotAllowed, nil)
return
}
resp, ok := request(core, w, r, requestAuth(r, &logical.Request{
Operation: logical.ReadOperation,
Path: "sys/policy",
Connection: getConnection(r),
}))
if !ok {
return
}
var policies []string
policiesRaw, ok := resp.Data["keys"]
if ok {
policies = policiesRaw.([]string)
}
respondOk(w, &listPolicyResponse{Policies: policies})
})
}
func handleSysPolicy(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
handleSysReadPolicy(core, w, r)
case "PUT":
fallthrough
case "POST":
handleSysWritePolicy(core, w, r)
case "DELETE":
handleSysDeletePolicy(core, w, r)
default:
respondError(w, http.StatusMethodNotAllowed, nil)
return
}
})
}
func handleSysDeletePolicy(core *vault.Core, w http.ResponseWriter, r *http.Request) {
// Determine the path...
prefix := "/v1/sys/policy/"
if !strings.HasPrefix(r.URL.Path, prefix) {
respondError(w, http.StatusNotFound, nil)
return
}
path := r.URL.Path[len(prefix):]
if path == "" {
respondError(w, http.StatusNotFound, nil)
return
}
_, ok := request(core, w, r, requestAuth(r, &logical.Request{
Operation: logical.DeleteOperation,
Path: "sys/policy/" + path,
Connection: getConnection(r),
}))
if !ok {
return
}
respondOk(w, nil)
}
func handleSysReadPolicy(core *vault.Core, w http.ResponseWriter, r *http.Request) {
// Determine the path...
prefix := "/v1/sys/policy/"
if !strings.HasPrefix(r.URL.Path, prefix) {
respondError(w, http.StatusNotFound, nil)
return
}
path := r.URL.Path[len(prefix):]
if path == "" {
respondError(w, http.StatusNotFound, nil)
return
}
resp, ok := request(core, w, r, requestAuth(r, &logical.Request{
Operation: logical.ReadOperation,
Path: "sys/policy/" + path,
Connection: getConnection(r),
}))
if !ok {
return
}
if resp == nil {
respondError(w, http.StatusNotFound, nil)
return
}
respondOk(w, resp.Data)
}
func handleSysWritePolicy(core *vault.Core, w http.ResponseWriter, r *http.Request) {
// Determine the path...
prefix := "/v1/sys/policy/"
if !strings.HasPrefix(r.URL.Path, prefix) {
respondError(w, http.StatusNotFound, nil)
return
}
path := r.URL.Path[len(prefix):]
if path == "" {
respondError(w, http.StatusNotFound, nil)
return
}
// Parse the request if we can
var req writePolicyRequest
if err := parseRequest(r, &req); err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
_, ok := request(core, w, r, requestAuth(r, &logical.Request{
Operation: logical.UpdateOperation,
Path: "sys/policy/" + path,
Connection: getConnection(r),
Data: map[string]interface{}{
"rules": req.Rules,
},
}))
if !ok {
return
}
respondOk(w, nil)
}
type listPolicyResponse struct {
Policies []string `json:"policies"`
}
type writePolicyRequest struct {
Rules string `json:"rules"`
}

View File

@@ -18,11 +18,12 @@ func TestSysPolicies(t *testing.T) {
var actual map[string]interface{}
expected := map[string]interface{}{
"policies": []interface{}{"default", "root"},
"keys": []interface{}{"default", "root"},
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
}
@@ -42,7 +43,7 @@ func TestSysReadPolicy(t *testing.T) {
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
}
@@ -62,11 +63,12 @@ func TestSysWritePolicy(t *testing.T) {
var actual map[string]interface{}
expected := map[string]interface{}{
"policies": []interface{}{"default", "foo", "root"},
"keys": []interface{}{"default", "foo", "root"},
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
}
@@ -89,10 +91,11 @@ func TestSysDeletePolicy(t *testing.T) {
var actual map[string]interface{}
expected := map[string]interface{}{
"policies": []interface{}{"default", "root"},
"keys": []interface{}{"default", "root"},
}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
}

View File

@@ -34,6 +34,29 @@ func handleSysSeal(core *vault.Core) http.Handler {
})
}
func handleSysStepDown(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "PUT":
case "POST":
default:
respondError(w, http.StatusMethodNotAllowed, nil)
return
}
// Get the auth for the request so we can access the token directly
req := requestAuth(r, &logical.Request{})
// Seal with the token above
if err := core.StepDown(req.ClientToken); err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
respondOk(w, nil)
})
}
func handleSysUnseal(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {

View File

@@ -304,3 +304,13 @@ func TestSysSeal_Permissions(t *testing.T) {
httpResp = testHttpPut(t, "child", addr+"/v1/sys/seal", nil)
testResponseStatus(t, httpResp, 204)
}
func TestSysStepDown(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
resp := testHttpPut(t, token, addr+"/v1/sys/step-down", nil)
testResponseStatus(t, resp, 204)
}

View File

@@ -63,8 +63,7 @@ func NewACL(policies []*Policy) (*ACL, error) {
default:
// Insert the capabilities in this new policy into the existing
// value; since it's a pointer we can just modify the
// underlying data
// value
tree.Insert(pc.Prefix, existing|pc.CapabilitiesBitmap)
}
}

View File

@@ -64,6 +64,10 @@ const (
// leaderPrefixCleanDelay is how long to wait between deletions
// of orphaned leader keys, to prevent slamming the backend.
leaderPrefixCleanDelay = 200 * time.Millisecond
// manualStepDownSleepPeriod is how long to sleep after a user-initiated
// step down of the active node, to prevent instantly regrabbing the lock
manualStepDownSleepPeriod = 10 * time.Second
)
var (
@@ -206,9 +210,10 @@ type Core struct {
stateLock sync.RWMutex
sealed bool
standby bool
standbyDoneCh chan struct{}
standbyStopCh chan struct{}
standby bool
standbyDoneCh chan struct{}
standbyStopCh chan struct{}
manualStepDownCh chan struct{}
// unlockParts has the keys provided to Unseal until
// the threshold number of parts is available.
@@ -1132,7 +1137,8 @@ func (c *Core) Unseal(key []byte) (bool, error) {
// Go to standby mode, wait until we are active to unseal
c.standbyDoneCh = make(chan struct{})
c.standbyStopCh = make(chan struct{})
go c.runStandby(c.standbyDoneCh, c.standbyStopCh)
c.manualStepDownCh = make(chan struct{})
go c.runStandby(c.standbyDoneCh, c.standbyStopCh, c.manualStepDownCh)
}
// Success!
@@ -1144,6 +1150,7 @@ func (c *Core) Unseal(key []byte) (bool, error) {
// be unsealed again to perform any further operations.
func (c *Core) Seal(token string) (retErr error) {
defer metrics.MeasureSince([]string{"core", "seal"}, time.Now())
c.stateLock.Lock()
defer c.stateLock.Unlock()
if c.sealed {
@@ -1156,15 +1163,8 @@ func (c *Core) Seal(token string) (retErr error) {
Path: "sys/seal",
ClientToken: token,
}
acl, te, err := c.fetchACLandTokenEntry(req)
// Attempt to use the token (decrement num_uses)
if te != nil {
if err := c.tokenStore.UseToken(te); err != nil {
c.logger.Printf("[ERR] core: failed to use token: %v", err)
retErr = ErrInternalError
}
}
acl, te, err := c.fetchACLandTokenEntry(req)
if err != nil {
// Since there is no token store in standby nodes, sealing cannot
// be done. Ideally, the request has to be forwarded to leader node
@@ -1172,11 +1172,20 @@ func (c *Core) Seal(token string) (retErr error) {
// just returning with an error and recommending a vault restart, which
// essentially does the same thing.
if c.standby {
c.logger.Printf("[ERR] core: vault cannot be sealed when in standby mode; please restart instead")
return errors.New("vault cannot be sealed when in standby mode; please restart instead")
c.logger.Printf("[ERR] core: vault cannot seal when in standby mode; please restart instead")
return errors.New("vault cannot seal when in standby mode; please restart instead")
}
return err
}
// Attempt to use the token (decrement num_uses)
// If we can't, we still continue attempting the seal, so long as the token
// has appropriate permissions
if te != nil {
if err := c.tokenStore.UseToken(te); err != nil {
c.logger.Printf("[ERR] core: failed to use token: %v", err)
retErr = ErrInternalError
}
}
// Verify that this operation is allowed
allowed, rootPrivs := acl.AllowOperation(req.Operation, req.Path)
@@ -1189,7 +1198,7 @@ func (c *Core) Seal(token string) (retErr error) {
return logical.ErrPermissionDenied
}
// Seal the Vault
//Seal the Vault
err = c.sealInternal()
if err == nil && retErr == ErrInternalError {
c.logger.Printf("[ERR] core: core is successfully sealed but another error occurred during the operation")
@@ -1200,9 +1209,60 @@ func (c *Core) Seal(token string) (retErr error) {
return
}
// sealInternal is an internal method used to seal the vault.
// It does not do any authorization checking. The stateLock must
// be held prior to calling.
// StepDown is used to step down from leadership
func (c *Core) StepDown(token string) error {
defer metrics.MeasureSince([]string{"core", "step_down"}, time.Now())
c.stateLock.Lock()
defer c.stateLock.Unlock()
if c.sealed {
return nil
}
if c.ha == nil || c.standby {
return nil
}
// Validate the token is a root token
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "sys/step-down",
ClientToken: token,
}
acl, te, err := c.fetchACLandTokenEntry(req)
if err != nil {
return err
}
// Attempt to use the token (decrement num_uses)
if te != nil {
if err := c.tokenStore.UseToken(te); err != nil {
c.logger.Printf("[ERR] core: failed to use token: %v", err)
return err
}
}
// Verify that this operation is allowed
allowed, rootPrivs := acl.AllowOperation(req.Operation, req.Path)
if !allowed {
return logical.ErrPermissionDenied
}
// We always require root privileges for this operation
if !rootPrivs {
return logical.ErrPermissionDenied
}
select {
case c.manualStepDownCh <- struct{}{}:
default:
c.logger.Printf("[WARN] core: manual step-down operation already queued")
}
return nil
}
// sealInternal is an internal method used to seal the vault. It does not do
// any authorization checking. The stateLock must be held prior to calling.
func (c *Core) sealInternal() error {
// Enable that we are sealed to prevent furthur transactions
c.sealed = true
@@ -1227,6 +1287,7 @@ func (c *Core) sealInternal() error {
return err
}
c.logger.Printf("[INFO] core: vault is sealed")
return nil
}
@@ -1336,8 +1397,9 @@ func (c *Core) preSeal() error {
// runStandby is a long running routine that is used when an HA backend
// is enabled. It waits until we are leader and switches this Vault to
// active.
func (c *Core) runStandby(doneCh, stopCh chan struct{}) {
func (c *Core) runStandby(doneCh, stopCh, manualStepDownCh chan struct{}) {
defer close(doneCh)
defer close(manualStepDownCh)
c.logger.Printf("[INFO] core: entering standby mode")
// Monitor for key rotation
@@ -1401,11 +1463,15 @@ func (c *Core) runStandby(doneCh, stopCh chan struct{}) {
}
// Monitor a loss of leadership
var manualStepDown bool
select {
case <-leaderLostCh:
c.logger.Printf("[WARN] core: leadership lost, stopping active operation")
case <-stopCh:
c.logger.Printf("[WARN] core: stopping active operation")
case <-manualStepDownCh:
c.logger.Printf("[WARN] core: stepping down from active operation to standby")
manualStepDown = true
}
// Clear ourself as leader
@@ -1426,6 +1492,12 @@ func (c *Core) runStandby(doneCh, stopCh chan struct{}) {
if preSealErr != nil {
c.logger.Printf("[ERR] core: pre-seal teardown failed: %v", err)
}
// If we've merely stepped down, we could instantly grab the lock
// again. Give the other nodes a chance.
if manualStepDown {
time.Sleep(manualStepDownSleepPeriod)
}
}
}

View File

@@ -1106,9 +1106,6 @@ func TestCore_Standby_Seal(t *testing.T) {
// Wait for core to become active
testWaitActive(t, core)
// Ensure that the original clean function has stopped running
time.Sleep(2 * time.Second)
// Check the leader is local
isLeader, advertise, err := core.Leader()
if err != nil {
@@ -1183,6 +1180,180 @@ func TestCore_Standby_Seal(t *testing.T) {
}
}
func TestCore_StepDown(t *testing.T) {
// Create the first core and initialize it
inm := physical.NewInmem()
inmha := physical.NewInmemHA()
advertiseOriginal := "http://127.0.0.1:8200"
core, err := NewCore(&CoreConfig{
Physical: inm,
HAPhysical: inmha,
AdvertiseAddr: advertiseOriginal,
DisableMlock: true,
})
if err != nil {
t.Fatalf("err: %v", err)
}
key, root := TestCoreInit(t, core)
if _, err := core.Unseal(TestKeyCopy(key)); err != nil {
t.Fatalf("unseal err: %s", err)
}
// Verify unsealed
sealed, err := core.Sealed()
if err != nil {
t.Fatalf("err checking seal status: %s", err)
}
if sealed {
t.Fatal("should not be sealed")
}
// Wait for core to become active
testWaitActive(t, core)
// Check the leader is local
isLeader, advertise, err := core.Leader()
if err != nil {
t.Fatalf("err: %v", err)
}
if !isLeader {
t.Fatalf("should be leader")
}
if advertise != advertiseOriginal {
t.Fatalf("Bad advertise: %v", advertise)
}
// Create the second core and initialize it
advertiseOriginal2 := "http://127.0.0.1:8500"
core2, err := NewCore(&CoreConfig{
Physical: inm,
HAPhysical: inmha,
AdvertiseAddr: advertiseOriginal2,
DisableMlock: true,
})
if err != nil {
t.Fatalf("err: %v", err)
}
if _, err := core2.Unseal(TestKeyCopy(key)); err != nil {
t.Fatalf("unseal err: %s", err)
}
// Verify unsealed
sealed, err = core2.Sealed()
if err != nil {
t.Fatalf("err checking seal status: %s", err)
}
if sealed {
t.Fatal("should not be sealed")
}
// Core2 should be in standby
standby, err := core2.Standby()
if err != nil {
t.Fatalf("err: %v", err)
}
if !standby {
t.Fatalf("should be standby")
}
// Check the leader is not local
isLeader, advertise, err = core2.Leader()
if err != nil {
t.Fatalf("err: %v", err)
}
if isLeader {
t.Fatalf("should not be leader")
}
if advertise != advertiseOriginal {
t.Fatalf("Bad advertise: %v", advertise)
}
// Step down core
err = core.StepDown(root)
if err != nil {
t.Fatal("error stepping down core 1")
}
// Give time to switch leaders
time.Sleep(2 * time.Second)
// Core1 should be in standby
standby, err = core.Standby()
if err != nil {
t.Fatalf("err: %v", err)
}
if !standby {
t.Fatalf("should be standby")
}
// Check the leader is core2
isLeader, advertise, err = core2.Leader()
if err != nil {
t.Fatalf("err: %v", err)
}
if !isLeader {
t.Fatalf("should be leader")
}
if advertise != advertiseOriginal2 {
t.Fatalf("Bad advertise: %v", advertise)
}
// Check the leader is not local
isLeader, advertise, err = core.Leader()
if err != nil {
t.Fatalf("err: %v", err)
}
if isLeader {
t.Fatalf("should not be leader")
}
if advertise != advertiseOriginal2 {
t.Fatalf("Bad advertise: %v", advertise)
}
// Step down core2
err = core2.StepDown(root)
if err != nil {
t.Fatal("error stepping down core 1")
}
// Give time to switch leaders -- core 1 will still be waiting on its
// cooling off period so give it a full 10 seconds to recover
time.Sleep(10 * time.Second)
// Core2 should be in standby
standby, err = core2.Standby()
if err != nil {
t.Fatalf("err: %v", err)
}
if !standby {
t.Fatalf("should be standby")
}
// Check the leader is core1
isLeader, advertise, err = core.Leader()
if err != nil {
t.Fatalf("err: %v", err)
}
if !isLeader {
t.Fatalf("should be leader")
}
if advertise != advertiseOriginal {
t.Fatalf("Bad advertise: %v", advertise)
}
// Check the leader is not local
isLeader, advertise, err = core2.Leader()
if err != nil {
t.Fatalf("err: %v", err)
}
if isLeader {
t.Fatalf("should not be leader")
}
if advertise != advertiseOriginal {
t.Fatalf("Bad advertise: %v", advertise)
}
}
func TestCore_CleanLeaderPrefix(t *testing.T) {
// Create the first core and initialize it
inm := physical.NewInmem()

View File

@@ -310,7 +310,7 @@ func (m *ExpirationManager) Renew(leaseID string, increment time.Duration) (*log
// RenewToken is used to renew a token which does not need to
// invoke a logical backend.
func (m *ExpirationManager) RenewToken(req *logical.Request, source string, token string,
increment time.Duration) (*logical.Auth, error) {
increment time.Duration) (*logical.Response, error) {
defer metrics.MeasureSince([]string{"expire", "renew-token"}, time.Now())
// Compute the Lease ID
leaseID := path.Join(source, m.tokenStore.SaltID(token))
@@ -333,12 +333,20 @@ func (m *ExpirationManager) RenewToken(req *logical.Request, source string, toke
return nil, err
}
// Fast-path if there is no renewal
if resp == nil {
return nil, nil
}
if resp.IsError() {
return &logical.Response{
Data: resp.Data,
}, nil
}
if resp.Auth == nil || !resp.Auth.LeaseEnabled() {
return resp.Auth, nil
return &logical.Response{
Auth: resp.Auth,
}, nil
}
// Attach the ClientToken
@@ -355,7 +363,9 @@ func (m *ExpirationManager) RenewToken(req *logical.Request, source string, toke
// Update the expiration time
m.updatePending(le, resp.Auth.LeaseTotal())
return resp.Auth, nil
return &logical.Response{
Auth: resp.Auth,
}, nil
}
// Register is used to take a request and response with an associated

View File

@@ -424,7 +424,7 @@ func TestExpiration_RenewToken(t *testing.T) {
t.Fatalf("err: %v", err)
}
if auth.ClientToken != out.ClientToken {
if auth.ClientToken != out.Auth.ClientToken {
t.Fatalf("Bad: %#v", out)
}
}

View File

@@ -246,6 +246,7 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) logical.Backend
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.handlePolicyList,
logical.ListOperation: b.handlePolicyList,
},
HelpSynopsis: strings.TrimSpace(sysHelp["policy-list"][0]),
@@ -465,6 +466,8 @@ func (b *SystemBackend) handleMount(
logicalType := data.Get("type").(string)
description := data.Get("description").(string)
path = sanitizeMountPath(path)
var config MountConfig
var apiConfig struct {
@@ -561,6 +564,8 @@ func (b *SystemBackend) handleUnmount(
return logical.ErrorResponse("path cannot be blank"), logical.ErrInvalidRequest
}
suffix = sanitizeMountPath(suffix)
// Attempt unmount
if err := b.Core.unmount(suffix); err != nil {
b.Backend.Logger().Printf("[ERR] sys: unmount '%s' failed: %v", suffix, err)
@@ -582,6 +587,9 @@ func (b *SystemBackend) handleRemount(
logical.ErrInvalidRequest
}
fromPath = sanitizeMountPath(fromPath)
toPath = sanitizeMountPath(toPath)
// Attempt remount
if err := b.Core.remount(fromPath, toPath); err != nil {
b.Backend.Logger().Printf("[ERR] sys: remount '%s' to '%s' failed: %v", fromPath, toPath, err)
@@ -601,9 +609,7 @@ func (b *SystemBackend) handleMountTuneRead(
logical.ErrInvalidRequest
}
if !strings.HasSuffix(path, "/") {
path += "/"
}
path = sanitizeMountPath(path)
sysView := b.Core.router.MatchingSystemView(path)
if sysView == nil {
@@ -632,9 +638,7 @@ func (b *SystemBackend) handleMountTuneWrite(
logical.ErrInvalidRequest
}
if !strings.HasSuffix(path, "/") {
path += "/"
}
path = sanitizeMountPath(path)
// Prevent protected paths from being changed
for _, p := range untunableMounts {
@@ -776,6 +780,8 @@ func (b *SystemBackend) handleEnableAuth(
logical.ErrInvalidRequest
}
path = sanitizeMountPath(path)
// Create the mount entry
me := &MountEntry{
Path: path,
@@ -799,6 +805,8 @@ func (b *SystemBackend) handleDisableAuth(
return logical.ErrorResponse("path cannot be blank"), logical.ErrInvalidRequest
}
suffix = sanitizeMountPath(suffix)
// Attempt disable
if err := b.Core.disableCredential(suffix); err != nil {
b.Backend.Logger().Printf("[ERR] sys: disable auth '%s' failed: %v", suffix, err)
@@ -815,7 +823,12 @@ func (b *SystemBackend) handlePolicyList(
// Add the special "root" policy
policies = append(policies, "root")
return logical.ListResponse(policies), err
resp := logical.ListResponse(policies)
// Backwords compatibility
resp.Data["policies"] = resp.Data["keys"]
return resp, err
}
// handlePolicyRead handles the "policy/<name>" endpoint to read a policy
@@ -902,9 +915,7 @@ func (b *SystemBackend) handleAuditHash(
return logical.ErrorResponse("the \"input\" parameter is empty"), nil
}
if !strings.HasSuffix(path, "/") {
path += "/"
}
path = sanitizeMountPath(path)
hash, err := b.Core.auditBroker.GetHash(path, input)
if err != nil {
@@ -1083,6 +1094,18 @@ func (b *SystemBackend) handleRotate(
return nil, nil
}
func sanitizeMountPath(path string) string {
if !strings.HasSuffix(path, "/") {
path += "/"
}
if strings.HasPrefix(path, "/") {
path = path[1:]
}
return path
}
const sysHelpRoot = `
The system backend is built-in to Vault and cannot be remounted or
unmounted. It contains the paths that are used to configure Vault itself

View File

@@ -431,7 +431,8 @@ func TestSystemBackend_policyList(t *testing.T) {
}
exp := map[string]interface{}{
"keys": []string{"default", "root"},
"keys": []string{"default", "root"},
"policies": []string{"default", "root"},
}
if !reflect.DeepEqual(resp.Data, exp) {
t.Fatalf("got: %#v expect: %#v", resp.Data, exp)
@@ -483,7 +484,8 @@ func TestSystemBackend_policyCRUD(t *testing.T) {
}
exp = map[string]interface{}{
"keys": []string{"default", "foo", "root"},
"keys": []string{"default", "foo", "root"},
"policies": []string{"default", "foo", "root"},
}
if !reflect.DeepEqual(resp.Data, exp) {
t.Fatalf("got: %#v expect: %#v", resp.Data, exp)
@@ -517,7 +519,8 @@ func TestSystemBackend_policyCRUD(t *testing.T) {
}
exp = map[string]interface{}{
"keys": []string{"default", "root"},
"keys": []string{"default", "root"},
"policies": []string{"default", "root"},
}
if !reflect.DeepEqual(resp.Data, exp) {
t.Fatalf("got: %#v expect: %#v", resp.Data, exp)

View File

@@ -74,6 +74,12 @@ func Parse(rules string) (*Policy, error) {
// Validate the path policy
for _, pc := range p.Paths {
// Strip a leading '/' as paths in Vault start after the / in the API
// path
if len(pc.Prefix) > 0 && pc.Prefix[0] == '/' {
pc.Prefix = pc.Prefix[1:]
}
// Strip the glob character if found
if strings.HasSuffix(pc.Prefix, "*") {
pc.Prefix = strings.TrimSuffix(pc.Prefix, "*")

View File

@@ -80,7 +80,8 @@ path "prod/version" {
}
# Read access to foobar
path "foo/bar" {
# Also tests stripping of leading slash
path "/foo/bar" {
policy = "read"
}

View File

@@ -1047,16 +1047,7 @@ func (ts *TokenStore) handleRenew(
}
// Renew the token and its children
auth, err := ts.expiration.RenewToken(req, te.Path, te.ID, increment)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
// Generate the response
resp := &logical.Response{
Auth: auth,
}
return resp, nil
return ts.expiration.RenewToken(req, te.Path, te.ID, increment)
}
func (ts *TokenStore) destroyCubbyhole(saltedID string) error {

View File

@@ -11,7 +11,9 @@ description: |-
<dl>
<dt>Description</dt>
<dd>
Seals the Vault. In HA mode, only an active node can be sealed. Standby nodes should be restarted to get the same effect.
Seals the Vault. In HA mode, only an active node can be sealed. Standby
nodes should be restarted to get the same effect. Requires a token with
`root` policy or `sudo` capability on the path.
</dd>
<dt>Method</dt>

View File

@@ -0,0 +1,33 @@
---
layout: "http"
page_title: "HTTP API: /sys/step-down"
sidebar_current: "docs-http-ha-step-down"
description: |-
The '/sys/step-down' endpoint causes the node to give up active status.
---
# /sys/seal
<dl>
<dt>Description</dt>
<dd>
Forces the node to give up active status. If the node does not have active
status, this endpoint does nothing. Note that the node will sleep for ten
seconds before attempting to grab the active lock again, but if no standby
nodes grab the active lock in the interim, the same node may become the
active node again. Requires a token with `root` policy or `sudo` capability
on the path.
</dd>
<dt>Method</dt>
<dd>PUT</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>A `204` response code.
</dd>
</dl>

View File

@@ -15,13 +15,14 @@ the configured physical storage for Vault. It is mounted at the `cubbyhole/`
prefix by default and cannot be mounted elsewhere or removed.
This backend differs from the `generic` backend in that the `generic` backend's
values are accessible to any token with read privileges on that path. In this
backend, paths are scoped per token; no token can read secrets placed in
another token's cubbyhole. When the token expires, its cubbyhole is destroyed.
values are accessible to any token with read privileges on that path. In
`cubbyhole`, paths are scoped per token; no token can access another token's
cubbyhole, whether to read, write, list, or for any other operation. When the
token expires, its cubbyhole is destroyed.
Also unlike the `generic` backend, because the cubbyhole's lifetime is linked
to an authentication token, there is no concept of a lease or lease TTL for
values contained in the token's cubbyhole.
to that of an authentication token, there is no concept of a TTL for values
contained in the token's cubbyhole.
Writing to a key in the `cubbyhole` backend will replace the old value;
the sub-fields are not merged together.
@@ -96,9 +97,7 @@ As expected, the value previously set is returned to us.
<dd>
Returns a list of secret entries at the specified location. Folders are
suffixed with `/`. The input must be a folder; list on a file will not
return a value. Note that no policy-based filtering is performed on
returned keys; it is not recommended to put sensitive or secret values as
key names. The values themselves are not accessible via this command.
return a value. The values themselves are not accessible via this command.
</dd>
<dt>Method</dt>

View File

@@ -206,8 +206,12 @@ $ vault write ssh/roles/dynamic_key_role \
Success! Data written to: ssh/roles/dynamic_key_role
```
`cidr_list` is optional and defaults to the zero address (0.0.0.0/0), e.g. all
hosts.
`cidr_list` is a comma separated list of CIDR blocks for which a role can generate
credentials. If this is empty, the role can only generate credentials if it belongs
to the set of zero-address roles.
Zero-address roles, configured via `/ssh/config/zeroaddress` endpoint, takes comma separated list
of role names that can generate credentials for any IP address.
Use the `install_script` option to provide an install script if the remote
hosts do not resemble a typical Linux machine. The default script is compiled
@@ -388,7 +392,6 @@ username@ip:~$
(String)
Comma separated list of CIDR blocks for which the role is
applicable for. CIDR blocks can belong to more than one role.
Defaults to the zero address (0.0.0.0/0).
</li>
<li>
<span class="param">exclude_cidr_list</span>
@@ -559,6 +562,100 @@ username@ip:~$
<dd>
A `204` response code.
</dd>
### /ssh/config/zeroaddress
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Returns the list of configured zero-address roles.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/ssh/config/zeroaddress`</dd>
<dt>Parameters</dt>
<dd>None</dd>
<dt>Returns</dt>
<dd>
```json
{
"lease_id":"",
"renewable":false,
"lease_duration":0,
"data":{
"roles":[
"otp_key_role"
]
},
"warnings":null,
"auth":null
}
```
</dd>
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Configures zero-address roles.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/ssh/config/zeroaddress`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">roles</span>
<span class="param-flags">required</span>
A string containing comma separated list of role names which allows credentials to be requested
for any IP address. CIDR blocks previously registered under these roles will be ignored.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
#### DELETE
<dl class="api">
<dt>Description</dt>
<dd>
Deletes the zero-address roles configuration.
</dd>
<dt>Method</dt>
<dd>DELETE</dd>
<dt>URL</dt>
<dd>`/ssh/config/zeroaddress`</dd>
<dt>Parameters</dt>
<dd>None</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
### /ssh/creds/
#### POST

View File

@@ -107,6 +107,9 @@
<li<%= sidebar_current("docs-http-ha-leader") %>>
<a href="/docs/http/sys-leader.html">/sys/leader</a>
</li>
<li<%= sidebar_current("docs-http-ha-step-down") %>>
<a href="/docs/http/sys-step-down.html">/sys/step-down</a>
</li>
</ul>
</li>