Merge branch 'master' into token-roles

This commit is contained in:
Jeff Mitchell 2016-03-09 17:23:34 -05:00
commit 8b6df2a1a4
45 changed files with 1600 additions and 53 deletions

View file

@ -1,6 +1,20 @@
## 0.5.2 (Unreleased)
FEATURES:
* **Token Accessors**: Vault now provides an accessor with each issued token.
This accessor is an identifier that can be used for a limited set of
actions, notably for token revocation. This value is logged in plaintext to
audit logs, and in combination with the plaintext metadata logged to audit
logs, provides a searchable and straightforward way to revoke particular
users' or services' tokens in many cases.
IMPROVEMENTS:
* auth/token,sys/capabilities: Added new endpoints
`auth/token/lookup-accessor`, `auth/token/revoke-accessor` and
`sys/capabilities-accessor`, which enables performing the respective actions
with just the accessor of the tokens, without having access to the actual
token [GH-1188]
* 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
@ -19,7 +33,19 @@ IMPROVEMENTS:
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]
* credential/cert: Subject and Authority key IDs are output in metadata; this
allows more flexible searching/revocation in the audit logs [GH-1183]
* secret/pki: Add revocation time (zero or Unix epoch) to `pki/cert/SERIAL`
endpoint [GH-1180]
* secret/pki: Sanitize serial number in `pki/revoke` endpoint to allow some
other formats [GH-1187]
* secret/ssh: Added documentation for `ssh/config/zeroaddress` endpoint.
[GH-1154]
* sys: Added new endpoints `sys/capabilities` and `sys/capabilities-self` to
fetch the capabilities of a token on a given path [GH-1171]
* sys: Added `sys/revoke-force`, which enables a user to ignore backend errors
when revoking a lease, necessary in some emergency/failure scenarios
[GH-1168]
BUG FIXES:

View file

@ -28,6 +28,7 @@ type Secret struct {
// SecretAuth is the structure containing auth information if we have it.
type SecretAuth struct {
ClientToken string `json:"client_token"`
Accessor string `json:"accessor"`
Policies []string `json:"policies"`
Metadata map[string]string `json:"metadata"`

48
api/sys_capabilities.go Normal file
View file

@ -0,0 +1,48 @@
package api
func (c *Sys) CapabilitiesSelf(path string) ([]string, error) {
body := map[string]string{
"path": path,
}
r := c.c.NewRequest("POST", "/v1/sys/capabilities-self")
if err := r.SetJSONBody(body); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result CapabilitiesResponse
err = resp.DecodeJSON(&result)
return result.Capabilities, err
}
func (c *Sys) Capabilities(token, path string) ([]string, error) {
body := map[string]string{
"token": token,
"path": path,
}
r := c.c.NewRequest("POST", "/v1/sys/capabilities")
if err := r.SetJSONBody(body); err != nil {
return nil, err
}
resp, err := c.c.RawRequest(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result CapabilitiesResponse
err = resp.DecodeJSON(&result)
return result.Capabilities, err
}
type CapabilitiesResponse struct {
Capabilities []string `json:"capabilities"`
}

View file

@ -34,3 +34,12 @@ func (c *Sys) RevokePrefix(id string) error {
}
return err
}
func (c *Sys) RevokeForce(id string) error {
r := c.c.NewRequest("PUT", "/v1/sys/revoke-force/"+id)
resp, err := c.c.RawRequest(r)
if err == nil {
defer resp.Body.Close()
}
return err
}

View file

@ -72,6 +72,7 @@ func (f *FormatJSON) FormatResponse(
if resp.Auth != nil {
respAuth = &JSONAuth{
ClientToken: resp.Auth.ClientToken,
Accessor: resp.Auth.Accessor,
DisplayName: resp.Auth.DisplayName,
Policies: resp.Auth.Policies,
Metadata: resp.Auth.Metadata,
@ -149,6 +150,7 @@ type JSONResponse struct {
type JSONAuth struct {
ClientToken string `json:"client_token,omitempty"`
Accessor string `json:"accessor,omitempty"`
DisplayName string `json:"display_name"`
Policies []string `json:"policies"`
Metadata map[string]string `json:"metadata"`

View file

@ -9,6 +9,7 @@ import (
"errors"
"strings"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
@ -67,8 +68,10 @@ func (b *backend) pathLogin(
Policies: matched.Entry.Policies,
DisplayName: matched.Entry.DisplayName,
Metadata: map[string]string{
"cert_name": matched.Entry.Name,
"common_name": clientCerts[0].Subject.CommonName,
"cert_name": matched.Entry.Name,
"common_name": clientCerts[0].Subject.CommonName,
"subject_key_id": certutil.GetOctalFormatted(clientCerts[0].SubjectKeyId, ":"),
"authority_key_id": certutil.GetOctalFormatted(clientCerts[0].AuthorityKeyId, ":"),
},
LeaseOptions: logical.LeaseOptions{
Renewable: true,

View file

@ -29,19 +29,19 @@ func TestBackend_Config(t *testing.T) {
"token": os.Getenv("GITHUB_TOKEN"),
}
config_data1 := map[string]interface{}{
"organization": "hashicorp",
"organization": os.Getenv("GITHUB_ORG"),
"ttl": "",
"max_ttl": "",
}
expectedTTL1, _ := time.ParseDuration("24h0m0s")
config_data2 := map[string]interface{}{
"organization": "hashicorp",
"organization": os.Getenv("GITHUB_ORG"),
"ttl": "1h",
"max_ttl": "2h",
}
expectedTTL2, _ := time.ParseDuration("1h0m0s")
config_data3 := map[string]interface{}{
"organization": "hashicorp",
"organization": os.Getenv("GITHUB_ORG"),
"ttl": "50h",
"max_ttl": "50h",
}

View file

@ -100,7 +100,7 @@ func TestBackend_role_lease(t *testing.T) {
func testStartConsulServer(t *testing.T) (map[string]interface{}, *os.Process) {
if _, err := exec.LookPath("consul"); err != nil {
t.Skipf("consul not found: %s", err)
t.Errorf("consul not found: %s", err)
}
td, err := ioutil.TempDir("", "vault")

View file

@ -914,6 +914,26 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
Data: reqdata,
Check: func(resp *logical.Response) error {
delete(reqdata, "certificate")
serialUnderTest = "cert/" + reqdata["rsa_int_serial_number"].(string)
return nil
},
},
// We expect to find a zero revocation time
logicaltest.TestStep{
Operation: logical.ReadOperation,
PreFlight: setSerialUnderTest,
Check: func(resp *logical.Response) error {
if resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
return fmt.Errorf("got an error: %s", resp.Data["error"].(string))
}
if resp.Data["revocation_time"].(int64) != 0 {
return fmt.Errorf("expected a zero revocation time")
}
return nil
},
},
@ -1051,10 +1071,29 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
Data: reqdata,
Check: func(resp *logical.Response) error {
delete(reqdata, "certificate")
serialUnderTest = "cert/" + reqdata["ec_int_serial_number"].(string)
return nil
},
},
// We expect to find a zero revocation time
logicaltest.TestStep{
Operation: logical.ReadOperation,
PreFlight: setSerialUnderTest,
Check: func(resp *logical.Response) error {
if resp.Data["error"] != nil && resp.Data["error"].(string) != "" {
return fmt.Errorf("got an error: %s", resp.Data["error"].(string))
}
if resp.Data["revocation_time"].(int64) != 0 {
return fmt.Errorf("expected a zero revocation time")
}
return nil
},
},
logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "revoke",
@ -1102,6 +1141,10 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
return fmt.Errorf("got an error: %s", resp.Data["error"].(string))
}
if resp.Data["revocation_time"].(int64) == 0 {
return fmt.Errorf("expected a non-zero revocation time")
}
serialUnderTest = "cert/" + reqdata["ec_int_serial_number"].(string)
return nil
@ -1116,6 +1159,10 @@ func generateCATestingSteps(t *testing.T, caCert, caKey, otherCaCert string, int
return fmt.Errorf("got an error: %s", resp.Data["error"].(string))
}
if resp.Data["revocation_time"].(int64) == 0 {
return fmt.Errorf("expected a non-zero revocation time")
}
// Give time for the certificates to pass the safety buffer
t.Logf("Sleeping for 15 seconds to allow safety buffer time to pass before testing tidying")
time.Sleep(15 * time.Second)

View file

@ -162,12 +162,14 @@ func fetchCertBySerial(req *logical.Request, prefix, serial string) (*logical.St
var path string
switch {
// Revoked goes first as otherwise ca/crl get hardcoded paths which fail if
// we actually want revocation info
case strings.HasPrefix(prefix, "revoked/"):
path = "revoked/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
case serial == "ca":
path = "ca"
case serial == "crl":
path = "crl"
case strings.HasPrefix(prefix, "revoked/"):
path = "revoked/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
default:
path = "certs/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
}

View file

@ -74,12 +74,11 @@ func pathFetchCRLViaCertPath(b *backend) *framework.Path {
}
func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) {
var serial string
var pemType string
var contentType string
var certEntry *logical.StorageEntry
var serial, pemType, contentType string
var certEntry, revokedEntry *logical.StorageEntry
var funcErr error
var certificate []byte
var revocationTime int64
response = &logical.Response{
Data: map[string]interface{}{},
}
@ -140,6 +139,26 @@ func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData)
certificate = pem.EncodeToMemory(&block)
}
revokedEntry, funcErr = fetchCertBySerial(req, "revoked/", serial)
if funcErr != nil {
switch funcErr.(type) {
case certutil.UserError:
response = logical.ErrorResponse(funcErr.Error())
goto reply
case certutil.InternalError:
retErr = funcErr
goto reply
}
}
if revokedEntry != nil {
var revInfo revocationInfo
err := revokedEntry.DecodeJSON(&revInfo)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error decoding revocation entry for serial %s: %s", serial, err)), nil
}
revocationTime = revInfo.RevocationTime
}
reply:
switch {
case len(contentType) != 0:
@ -157,6 +176,7 @@ reply:
response = nil
default:
response.Data["certificate"] = string(certificate)
response.Data["revocation_time"] = revocationTime
}
return

View file

@ -171,8 +171,9 @@ func (b *backend) pathIssueSignCert(
resp := b.Secret(SecretCertsType).Response(
map[string]interface{}{
"certificate": cb.Certificate,
"issuing_ca": cb.IssuingCA,
"certificate": cb.Certificate,
"issuing_ca": cb.IssuingCA,
"serial_number": cb.SerialNumber,
},
map[string]interface{}{
"serial_number": cb.SerialNumber,

View file

@ -2,6 +2,7 @@ package pki
import (
"fmt"
"strings"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
@ -47,6 +48,10 @@ func (b *backend) pathRevokeWrite(req *logical.Request, data *framework.FieldDat
return logical.ErrorResponse("The serial number must be provided"), nil
}
// We store and identify by lowercase colon-separated hex, but other
// utilities use dashes and/or uppercase, so normalize
serial = strings.Replace(strings.ToLower(serial), "-", ":", -1)
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()

View file

@ -290,6 +290,12 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
}, nil
},
"capabilities": func() (cli.Command, error) {
return &command.CapabilitiesCommand{
Meta: meta,
}, nil
},
"version": func() (cli.Command, error) {
versionInfo := version.GetVersion()

86
command/capabilities.go Normal file
View file

@ -0,0 +1,86 @@
package command
import (
"fmt"
"strings"
)
// CapabilitiesCommand is a Command that enables a new endpoint.
type CapabilitiesCommand struct {
Meta
}
func (c *CapabilitiesCommand) Run(args []string) int {
flags := c.Meta.FlagSet("capabilities", FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) > 2 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\ncapabilities expects at most two arguments"))
return 1
}
var token string
var path string
switch {
case len(args) == 1:
path = args[0]
case len(args) == 2:
token = args[0]
path = args[1]
default:
flags.Usage()
c.Ui.Error(fmt.Sprintf("\ncapabilities expects at least one argument"))
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
var capabilities []string
if token == "" {
capabilities, err = client.Sys().CapabilitiesSelf(path)
} else {
capabilities, err = client.Sys().Capabilities(token, path)
}
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error retrieving capabilities: %s", err))
return 1
}
c.Ui.Output(fmt.Sprintf("Capabilities: %s", capabilities))
return 0
}
func (c *CapabilitiesCommand) Synopsis() string {
return "Fetch the capabilities of a token on a given path"
}
func (c *CapabilitiesCommand) Help() string {
helpText := `
Usage: vault capabilities [options] [token] path
Fetch the capabilities of a token on a given path.
If a token is provided as an argument, the '/sys/capabilities' endpoint will be invoked
with the given token; otherwise the '/sys/capabilities-self' endpoint will be invoked
with the client token.
If a token does not have any capability on a given path, or if any of the policies
belonging to the token explicitly have ["deny"] capability, or if the argument path
is invalid, this command will respond with a ["deny"].
General Options:
` + generalOptionsUsage()
return strings.TrimSpace(helpText)
}

View file

@ -0,0 +1,44 @@
package command
import (
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestCapabilities_Basic(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &CapabilitiesCommand{
Meta: Meta{
ClientToken: token,
Ui: ui,
},
}
var args []string
args = []string{"-address", addr}
if code := c.Run(args); code == 0 {
t.Fatalf("expected failure due to no args")
}
args = []string{"-address", addr, "testpath"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
args = []string{"-address", addr, token, "test"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
args = []string{"-address", addr, "invalidtoken", "test"}
if code := c.Run(args); code == 0 {
t.Fatalf("expected failure due to invalid token")
}
}

View file

@ -143,6 +143,7 @@ func (t TableFormatter) OutputSecret(ui cli.Ui, secret, s *api.Secret) error {
if s.Auth != nil {
input = append(input, fmt.Sprintf("token %s %s", config.Delim, s.Auth.ClientToken))
input = append(input, fmt.Sprintf("token_accessor %s %s", config.Delim, s.Auth.Accessor))
input = append(input, fmt.Sprintf("token_duration %s %d", config.Delim, s.Auth.LeaseDuration))
input = append(input, fmt.Sprintf("token_renewable %s %v", config.Delim, s.Auth.Renewable))
input = append(input, fmt.Sprintf("token_policies %s %v", config.Delim, s.Auth.Policies))

View file

@ -11,9 +11,10 @@ type RevokeCommand struct {
}
func (c *RevokeCommand) Run(args []string) int {
var prefix bool
var prefix, force bool
flags := c.Meta.FlagSet("revoke", FlagSetDefault)
flags.BoolVar(&prefix, "prefix", false, "")
flags.BoolVar(&force, "force", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
@ -35,9 +36,16 @@ func (c *RevokeCommand) Run(args []string) int {
return 2
}
if prefix {
switch {
case force && !prefix:
c.Ui.Error(fmt.Sprintf(
"-force requires -prefix"))
return 1
case force && prefix:
err = client.Sys().RevokeForce(leaseId)
case prefix:
err = client.Sys().RevokePrefix(leaseId)
} else {
default:
err = client.Sys().Revoke(leaseId)
}
if err != nil {
@ -60,12 +68,16 @@ Usage: vault revoke [options] id
Revoke a secret by its lease ID.
This command revokes a secret by its lease ID that was returned
with it. Once the key is revoked, it is no longer valid.
This command revokes a secret by its lease ID that was returned with it. Once
the key is revoked, it is no longer valid.
With the -prefix flag, the revoke is done by prefix: any secret prefixed
with the given partial ID is revoked. Lease IDs are structured in such
a way to make revocation of prefixes useful.
With the -prefix flag, the revoke is done by prefix: any secret prefixed with
the given partial ID is revoked. Lease IDs are structured in such a way to
make revocation of prefixes useful.
With the -force flag, the lease is removed from Vault even if the revocation
fails. This is meant for certain recovery scenarios and should not be used
lightly. This option requires -prefix.
General Options:
@ -76,6 +88,8 @@ Revoke Options:
-prefix=true Revoke all secrets with the matching prefix. This
defaults to false: an exact revocation.
-force=true Delete the lease even if the actual revocation
operation fails.
`
return strings.TrimSpace(helpText)
}

View file

@ -8,6 +8,7 @@ import (
"net/url"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
)
@ -32,6 +33,9 @@ func Handler(core *vault.Core) http.Handler {
mux.Handle("/v1/sys/generate-root/update", handleSysGenerateRootUpdate(core))
mux.Handle("/v1/sys/rekey/init", handleSysRekeyInit(core))
mux.Handle("/v1/sys/rekey/update", handleSysRekeyUpdate(core))
mux.Handle("/v1/sys/capabilities", handleSysCapabilities(core))
mux.Handle("/v1/sys/capabilities-self", handleSysCapabilities(core))
mux.Handle("/v1/sys/capabilities-accessor", handleSysCapabilitiesAccessor(core))
mux.Handle("/v1/sys/", handleLogical(core, true))
mux.Handle("/v1/", handleLogical(core, false))
@ -77,7 +81,7 @@ func request(core *vault.Core, w http.ResponseWriter, rawReq *http.Request, r *l
return resp, false
}
if err != nil {
respondError(w, http.StatusInternalServerError, err)
respondErrorStatus(w, err)
return resp, false
}
@ -137,6 +141,18 @@ func requestAuth(r *http.Request, req *logical.Request) *logical.Request {
return req
}
// Determines the type of the error being returned and sets the HTTP
// status code appropriately
func respondErrorStatus(w http.ResponseWriter, err error) {
status := http.StatusInternalServerError
switch {
// Keep adding more error types here to appropriate the status codes
case errwrap.ContainsType(err, new(vault.StatusBadRequest)):
status = http.StatusBadRequest
}
respondError(w, status, err)
}
func respondError(w http.ResponseWriter, status int, err error) {
// Adjust status code when sealed
if err == vault.ErrSealed {

View file

@ -124,6 +124,7 @@ func respondLogical(w http.ResponseWriter, r *http.Request, path string, dataOnl
if resp.Auth != nil {
logicalResp.Auth = &Auth{
ClientToken: resp.Auth.ClientToken,
Accessor: resp.Auth.Accessor,
Policies: resp.Auth.Policies,
Metadata: resp.Auth.Metadata,
LeaseDuration: int(resp.Auth.TTL.Seconds()),
@ -218,6 +219,7 @@ type LogicalResponse struct {
type Auth struct {
ClientToken string `json:"client_token"`
Accessor string `json:"accessor"`
Policies []string `json:"policies"`
Metadata map[string]string `json:"metadata"`
LeaseDuration int `json:"lease_duration"`

View file

@ -141,6 +141,7 @@ func TestLogical_StandbyRedirect(t *testing.T) {
testResponseBody(t, resp, &actual)
actualDataMap := actual["data"].(map[string]interface{})
delete(actualDataMap, "creation_time")
delete(actualDataMap, "accessor")
actual["data"] = actualDataMap
delete(actual, "lease_id")
if !reflect.DeepEqual(actual, expected) {
@ -181,6 +182,7 @@ func TestLogical_CreateToken(t *testing.T) {
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
delete(actual["auth"].(map[string]interface{}), "client_token")
delete(actual["auth"].(map[string]interface{}), "accessor")
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad:\nexpected:\n%#v\nactual:\n%#v", expected, actual)
}

89
http/sys_capabilities.go Normal file
View file

@ -0,0 +1,89 @@
package http
import (
"net/http"
"strings"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
)
func handleSysCapabilitiesAccessor(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
}
// Parse the request if we can
var data capabilitiesAccessorRequest
if err := parseRequest(r, &data); err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
capabilities, err := core.CapabilitiesAccessor(data.Accessor, data.Path)
if err != nil {
respondErrorStatus(w, err)
return
}
respondOk(w, &capabilitiesResponse{
Capabilities: capabilities,
})
})
}
func handleSysCapabilities(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
}
// Parse the request if we can
var data capabilitiesRequest
if err := parseRequest(r, &data); err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
if strings.HasPrefix(r.URL.Path, "/v1/sys/capabilities-self") {
// Get the auth for the request so we can access the token directly
req := requestAuth(r, &logical.Request{})
data.Token = req.ClientToken
}
capabilities, err := core.Capabilities(data.Token, data.Path)
if err != nil {
respondErrorStatus(w, err)
return
}
respondOk(w, &capabilitiesResponse{
Capabilities: capabilities,
})
})
}
type capabilitiesResponse struct {
Capabilities []string `json:"capabilities"`
}
type capabilitiesRequest struct {
Token string `json:"token"`
Path string `json:"path"`
}
type capabilitiesAccessorRequest struct {
Accessor string `json:"accessor"`
Path string `json:"path"`
}

View file

@ -0,0 +1,154 @@
package http
import (
"reflect"
"testing"
"github.com/hashicorp/vault/vault"
)
func TestSysCapabilitiesAccessor(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
// Lookup the token properties
resp := testHttpGet(t, token, addr+"/v1/auth/token/lookup/"+token)
var lookupResp map[string]interface{}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &lookupResp)
// Retrieve the accessor from the token properties
lookupData := lookupResp["data"].(map[string]interface{})
accessor := lookupData["accessor"].(string)
resp = testHttpPost(t, token, addr+"/v1/sys/capabilities-accessor", map[string]interface{}{
"accessor": accessor,
"path": "testpath",
})
var actual map[string][]string
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected := map[string][]string{
"capabilities": []string{"root"},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
// Testing for non-root token's accessor
// Create a policy first
resp = testHttpPost(t, token, addr+"/v1/sys/policy/foo", map[string]interface{}{
"rules": `path "testpath" {capabilities = ["read","sudo"]}`,
})
testResponseStatus(t, resp, 204)
// Create a token against the test policy
resp = testHttpPost(t, token, addr+"/v1/auth/token/create", map[string]interface{}{
"policies": []string{"foo"},
})
var tokenResp map[string]interface{}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &tokenResp)
// Check if desired policies are present in the token
auth := tokenResp["auth"].(map[string]interface{})
actualPolicies := auth["policies"]
expectedPolicies := []interface{}{"default", "foo"}
if !reflect.DeepEqual(actualPolicies, expectedPolicies) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actualPolicies, expectedPolicies)
}
// Check the capabilities of non-root token using the accessor
resp = testHttpPost(t, token, addr+"/v1/sys/capabilities-accessor", map[string]interface{}{
"accessor": auth["accessor"],
"path": "testpath",
})
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected = map[string][]string{
"capabilities": []string{"sudo", "read"},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
}
func TestSysCapabilities(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
// Send both token and path
resp := testHttpPost(t, token, addr+"/v1/sys/capabilities", map[string]interface{}{
"token": token,
"path": "testpath",
})
var actual map[string][]string
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected := map[string][]string{
"capabilities": []string{"root"},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
// Send only path to capabilities-self
resp = testHttpPost(t, token, addr+"/v1/sys/capabilities-self", map[string]interface{}{
"path": "testpath",
})
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
// Testing for non-root tokens
// Create a policy first
resp = testHttpPost(t, token, addr+"/v1/sys/policy/foo", map[string]interface{}{
"rules": `path "testpath" {capabilities = ["read","sudo"]}`,
})
testResponseStatus(t, resp, 204)
// Create a token against the test policy
resp = testHttpPost(t, token, addr+"/v1/auth/token/create", map[string]interface{}{
"policies": []string{"foo"},
})
var tokenResp map[string]interface{}
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &tokenResp)
// Check if desired policies are present in the token
auth := tokenResp["auth"].(map[string]interface{})
actualPolicies := auth["policies"]
expectedPolicies := []interface{}{"default", "foo"}
if !reflect.DeepEqual(actualPolicies, expectedPolicies) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actualPolicies, expectedPolicies)
}
// Check the capabilities with the created non-root token
resp = testHttpPost(t, token, addr+"/v1/sys/capabilities", map[string]interface{}{
"token": auth["client_token"],
"path": "testpath",
})
testResponseStatus(t, resp, 200)
testResponseBody(t, resp, &actual)
expected = map[string][]string{
"capabilities": []string{"sudo", "read"},
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
}

View file

@ -310,6 +310,7 @@ func TestSysGenerateRoot_Update_OTP(t *testing.T) {
testResponseBody(t, resp, &actual)
expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"]
expected["accessor"] = actual["data"].(map[string]interface{})["accessor"]
if !reflect.DeepEqual(actual["data"], expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"])
@ -391,6 +392,7 @@ func TestSysGenerateRoot_Update_PGP(t *testing.T) {
testResponseBody(t, resp, &actual)
expected["creation_time"] = actual["data"].(map[string]interface{})["creation_time"]
expected["accessor"] = actual["data"].(map[string]interface{})["accessor"]
if !reflect.DeepEqual(actual["data"], expected) {
t.Fatalf("\nexpected: %#v\nactual: %#v", expected, actual["data"])

View file

@ -33,6 +33,13 @@ type Auth struct {
// This will be filled in by Vault core when an auth structure is
// returned. Setting this manually will have no effect.
ClientToken string
// Accessor is the identifier for the ClientToken. This can be used
// to perform management functionalities (especially revocation) when
// ClientToken in the audit logs are obfuscated. Accessor can be used
// to revoke a ClientToken and to lookup the capabilities of the ClientToken,
// both without actually knowing the ClientToken.
Accessor string
}
func (a *Auth) GoString() string {

View file

@ -71,6 +71,56 @@ func NewACL(policies []*Policy) (*ACL, error) {
return a, nil
}
func (a *ACL) Capabilities(path string) (pathCapabilities []string) {
// Fast-path root
if a.root {
return []string{RootCapability}
}
// Find an exact matching rule, look for glob if no match
var capabilities uint32
raw, ok := a.exactRules.Get(path)
if ok {
capabilities = raw.(uint32)
goto CHECK
}
// Find a glob rule, default deny if no match
_, raw, ok = a.globRules.LongestPrefix(path)
if !ok {
return []string{DenyCapability}
} else {
capabilities = raw.(uint32)
}
CHECK:
if capabilities&SudoCapabilityInt > 0 {
pathCapabilities = append(pathCapabilities, SudoCapability)
}
if capabilities&ReadCapabilityInt > 0 {
pathCapabilities = append(pathCapabilities, ReadCapability)
}
if capabilities&ListCapabilityInt > 0 {
pathCapabilities = append(pathCapabilities, ListCapability)
}
if capabilities&UpdateCapabilityInt > 0 {
pathCapabilities = append(pathCapabilities, UpdateCapability)
}
if capabilities&DeleteCapabilityInt > 0 {
pathCapabilities = append(pathCapabilities, DeleteCapability)
}
if capabilities&CreateCapabilityInt > 0 {
pathCapabilities = append(pathCapabilities, CreateCapability)
}
// If "deny" is explicitly set or if the path has no capabilities at all,
// set the path capabilities to "deny"
if capabilities&DenyCapabilityInt > 0 || len(pathCapabilities) == 0 {
pathCapabilities = []string{DenyCapability}
}
return
}
// AllowOperation is used to check if the given operation is permitted. The
// first bool indicates if an op is allowed, the second whether sudo priviliges
// exist for that op and path.

View file

@ -1,11 +1,56 @@
package vault
import (
"reflect"
"testing"
"github.com/hashicorp/vault/logical"
)
func TestACL_Capabilities(t *testing.T) {
// Create the root policy ACL
policy := []*Policy{&Policy{Name: "root"}}
acl, err := NewACL(policy)
if err != nil {
t.Fatalf("err: %v", err)
}
actual := acl.Capabilities("any/path")
expected := []string{"root"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
policies, err := Parse(aclPolicy)
if err != nil {
t.Fatalf("err: %v", err)
}
acl, err = NewACL([]*Policy{policies})
if err != nil {
t.Fatalf("err: %v", err)
}
actual = acl.Capabilities("dev")
expected = []string{"deny"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: path:%s\ngot\n%#v\nexpected\n%#v\n", "deny", actual, expected)
}
actual = acl.Capabilities("dev/")
expected = []string{"sudo", "read", "list", "update", "delete", "create"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: path:%s\ngot\n%#v\nexpected\n%#v\n", "dev/", actual, expected)
}
actual = acl.Capabilities("stage/aws/test")
expected = []string{"sudo", "read", "list", "update"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: path:%s\ngot\n%#v\nexpected\n%#v\n", "stage/aws/test", actual, expected)
}
}
func TestACL_Root(t *testing.T) {
// Create the root policy ACL
policy := []*Policy{&Policy{Name: "root"}}

75
vault/capabilities.go Normal file
View file

@ -0,0 +1,75 @@
package vault
// Struct to identify user input errors.
// This is helpful in responding the appropriate status codes to clients
// from the HTTP endpoints.
type StatusBadRequest struct {
Err string
}
// Implementing error interface
func (s *StatusBadRequest) Error() string {
return s.Err
}
// CapabilitiesAccessor is used to fetch the capabilities of the token
// which associated with the given accessor on the given path
func (c *Core) CapabilitiesAccessor(accessor, path string) ([]string, error) {
if path == "" {
return nil, &StatusBadRequest{Err: "missing path"}
}
if accessor == "" {
return nil, &StatusBadRequest{Err: "missing accessor"}
}
token, err := c.tokenStore.lookupByAccessor(accessor)
if err != nil {
return nil, err
}
return c.Capabilities(token, path)
}
// Capabilities is used to fetch the capabilities of the given token on the given path
func (c *Core) Capabilities(token, path string) ([]string, error) {
if path == "" {
return nil, &StatusBadRequest{Err: "missing path"}
}
if token == "" {
return nil, &StatusBadRequest{Err: "missing token"}
}
te, err := c.tokenStore.Lookup(token)
if err != nil {
return nil, err
}
if te == nil {
return nil, &StatusBadRequest{Err: "invalid token"}
}
if te.Policies == nil {
return []string{DenyCapability}, nil
}
var policies []*Policy
for _, tePolicy := range te.Policies {
policy, err := c.policyStore.GetPolicy(tePolicy)
if err != nil {
return nil, err
}
policies = append(policies, policy)
}
if len(policies) == 0 {
return []string{DenyCapability}, nil
}
acl, err := NewACL(policies)
if err != nil {
return nil, err
}
return acl.Capabilities(path), nil
}

100
vault/capabilities_test.go Normal file
View file

@ -0,0 +1,100 @@
package vault
import (
"reflect"
"testing"
)
func TestCapabilitiesAccessor(t *testing.T) {
c, _, token := TestCoreUnsealed(t)
// Lookup the token in the store to get root token's accessor
tokenEntry, err := c.tokenStore.Lookup(token)
if err != nil {
t.Fatalf("err: %s", err)
}
accessor := tokenEntry.Accessor
// Use the accessor to fetch the capabilities
actual, err := c.CapabilitiesAccessor(accessor, "path")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := []string{"root"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
// Create a policy
policy, _ := Parse(aclPolicy)
err = c.policyStore.SetPolicy(policy)
if err != nil {
t.Fatalf("err: %v", err)
}
// Create a token for the policy
ent := &TokenEntry{
ID: "capabilitiestoken",
Path: "testpath",
Policies: []string{"dev"},
}
if err := c.tokenStore.create(ent); err != nil {
t.Fatalf("err: %v", err)
}
// Lookup the token in the store to get token's accessor
tokenEntry, err = c.tokenStore.Lookup("capabilitiestoken")
if err != nil {
t.Fatalf("err: %s", err)
}
accessor = tokenEntry.Accessor
// Use the accessor to fetch the capabilities
actual, err = c.CapabilitiesAccessor(accessor, "foo/bar")
if err != nil {
t.Fatalf("err: %s", err)
}
expected = []string{"sudo", "read", "create"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
}
func TestCapabilities(t *testing.T) {
c, _, token := TestCoreUnsealed(t)
actual, err := c.Capabilities(token, "path")
if err != nil {
t.Fatalf("err: %s", err)
}
expected := []string{"root"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
// Create a policy
policy, _ := Parse(aclPolicy)
err = c.policyStore.SetPolicy(policy)
if err != nil {
t.Fatalf("err: %v", err)
}
// Create a token for the policy
ent := &TokenEntry{
ID: "capabilitiestoken",
Path: "testpath",
Policies: []string{"dev"},
}
if err := c.tokenStore.create(ent); err != nil {
t.Fatalf("err: %v", err)
}
actual, err = c.Capabilities("capabilitiestoken", "foo/bar")
if err != nil {
t.Fatalf("err: %s", err)
}
expected = []string{"sudo", "read", "create"}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected)
}
}

View file

@ -680,8 +680,9 @@ func (c *Core) handleLoginRequest(req *logical.Request) (*logical.Response, *log
return nil, auth, ErrInternalError
}
// Populate the client token
// Populate the client token and accessor
auth.ClientToken = te.ID
auth.Accessor = te.Accessor
// Register with the expiration manager
if err := c.expiration.RegisterAuth(req.Path, auth); err != nil {

View file

@ -787,6 +787,7 @@ func TestCore_HandleLogin_Token(t *testing.T) {
}
expect := &TokenEntry{
ID: clientToken,
Accessor: te.Accessor,
Parent: "",
Policies: []string{"foo", "bar", "default"},
Path: "auth/foo/login",
@ -986,6 +987,7 @@ func TestCore_HandleRequest_CreateToken_Lease(t *testing.T) {
}
expect := &TokenEntry{
ID: clientToken,
Accessor: te.Accessor,
Parent: root,
Policies: []string{"default", "foo"},
Path: "auth/token/create",
@ -1030,6 +1032,7 @@ func TestCore_HandleRequest_CreateToken_NoDefaultPolicy(t *testing.T) {
}
expect := &TokenEntry{
ID: clientToken,
Accessor: te.Accessor,
Parent: root,
Policies: []string{"foo"},
Path: "auth/token/create",

View file

@ -173,6 +173,14 @@ func (m *ExpirationManager) Stop() error {
// Revoke is used to revoke a secret named by the given LeaseID
func (m *ExpirationManager) Revoke(leaseID string) error {
defer metrics.MeasureSince([]string{"expire", "revoke"}, time.Now())
return m.revokeCommon(leaseID, false)
}
// revokeCommon does the heavy lifting. If force is true, we ignore a problem
// during revocation and still remove entries/index/lease timers
func (m *ExpirationManager) revokeCommon(leaseID string, force bool) error {
defer metrics.MeasureSince([]string{"expire", "revoke-common"}, time.Now())
// Load the entry
le, err := m.loadEntry(leaseID)
if err != nil {
@ -186,7 +194,11 @@ func (m *ExpirationManager) Revoke(leaseID string) error {
// Revoke the entry
if err := m.revokeEntry(le); err != nil {
return err
if !force {
return err
} else {
m.logger.Printf("[WARN]: revocation from the backend failed, but in force mode so ignoring; error was: %s", err)
}
}
// Delete the entry
@ -209,32 +221,21 @@ func (m *ExpirationManager) Revoke(leaseID string) error {
return nil
}
// RevokeForce works similarly to RevokePrefix but continues in the case of a
// revocation error; this is mostly meant for recovery operations
func (m *ExpirationManager) RevokeForce(prefix string) error {
defer metrics.MeasureSince([]string{"expire", "revoke-force"}, time.Now())
return m.revokePrefixCommon(prefix, true)
}
// RevokePrefix is used to revoke all secrets with a given prefix.
// The prefix maps to that of the mount table to make this simpler
// to reason about.
func (m *ExpirationManager) RevokePrefix(prefix string) error {
defer metrics.MeasureSince([]string{"expire", "revoke-prefix"}, time.Now())
// Ensure there is a trailing slash
if !strings.HasSuffix(prefix, "/") {
prefix = prefix + "/"
}
// Accumulate existing leases
sub := m.idView.SubView(prefix)
existing, err := CollectKeys(sub)
if err != nil {
return fmt.Errorf("failed to scan for leases: %v", err)
}
// Revoke all the keys
for idx, suffix := range existing {
leaseID := prefix + suffix
if err := m.Revoke(leaseID); err != nil {
return fmt.Errorf("failed to revoke '%s' (%d / %d): %v",
leaseID, idx+1, len(existing), err)
}
}
return nil
return m.revokePrefixCommon(prefix, false)
}
// RevokeByToken is used to revoke all the secrets issued with
@ -257,6 +258,30 @@ func (m *ExpirationManager) RevokeByToken(token string) error {
return nil
}
func (m *ExpirationManager) revokePrefixCommon(prefix string, force bool) error {
// Ensure there is a trailing slash
if !strings.HasSuffix(prefix, "/") {
prefix = prefix + "/"
}
// Accumulate existing leases
sub := m.idView.SubView(prefix)
existing, err := CollectKeys(sub)
if err != nil {
return fmt.Errorf("failed to scan for leases: %v", err)
}
// Revoke all the keys
for idx, suffix := range existing {
leaseID := prefix + suffix
if err := m.revokeCommon(leaseID, force); err != nil {
return fmt.Errorf("failed to revoke '%s' (%d / %d): %v",
leaseID, idx+1, len(existing), err)
}
}
return nil
}
// Renew is used to renew a secret using the given leaseID
// and a renew interval. The increment may be ignored.
func (m *ExpirationManager) Renew(leaseID string, increment time.Duration) (*logical.Response, error) {

View file

@ -1,6 +1,7 @@
package vault
import (
"fmt"
"reflect"
"sort"
"strings"
@ -9,6 +10,7 @@ import (
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// mockExpiration returns a mock expiration manager
@ -954,3 +956,83 @@ func TestLeaseEntry(t *testing.T) {
t.Fatalf("got: %#v, expect %#v", out, le)
}
}
func TestExpiration_RevokeForce(t *testing.T) {
core, _, _, root := TestCoreWithTokenStore(t)
core.logicalBackends["badrenew"] = badRenewFactory
me := &MountEntry{
Path: "badrenew/",
Type: "badrenew",
}
err := core.mount(me)
if err != nil {
t.Fatal(err)
}
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "badrenew/creds",
ClientToken: root,
}
resp, err := core.HandleRequest(req)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("response was nil")
}
if resp.Secret == nil {
t.Fatalf("response secret was nil, response was %#v", *resp)
}
req.Operation = logical.UpdateOperation
req.Path = "sys/revoke-prefix/badrenew/creds"
resp, err = core.HandleRequest(req)
if err == nil {
t.Fatal("expected error")
}
req.Path = "sys/revoke-force/badrenew/creds"
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("got error: %s", err)
}
}
func badRenewFactory(conf *logical.BackendConfig) (logical.Backend, error) {
be := &framework.Backend{
Paths: []*framework.Path{
&framework.Path{
Pattern: "creds",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: func(*logical.Request, *framework.FieldData) (*logical.Response, error) {
resp := &logical.Response{
Secret: &logical.Secret{
InternalData: map[string]interface{}{
"secret_type": "badRenewBackend",
},
},
}
resp.Secret.TTL = time.Second * 30
return resp, nil
},
},
},
},
Secrets: []*framework.Secret{
&framework.Secret{
Type: "badRenewBackend",
Revoke: func(*logical.Request, *framework.FieldData) (*logical.Response, error) {
return nil, fmt.Errorf("always errors")
},
},
},
}
return be.Setup(conf)
}

View file

@ -185,6 +185,24 @@ func NewSystemBackend(core *Core, config *logical.BackendConfig) logical.Backend
HelpDescription: strings.TrimSpace(sysHelp["revoke"][1]),
},
&framework.Path{
Pattern: "revoke-force/(?P<prefix>.+)",
Fields: map[string]*framework.FieldSchema{
"prefix": &framework.FieldSchema{
Type: framework.TypeString,
Description: strings.TrimSpace(sysHelp["revoke-force-path"][0]),
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.handleRevokeForce,
},
HelpSynopsis: strings.TrimSpace(sysHelp["revoke-force"][0]),
HelpDescription: strings.TrimSpace(sysHelp["revoke-force"][1]),
},
&framework.Path{
Pattern: "revoke-prefix/(?P<prefix>.+)",
@ -736,11 +754,29 @@ func (b *SystemBackend) handleRevoke(
// handleRevokePrefix is used to revoke a prefix with many LeaseIDs
func (b *SystemBackend) handleRevokePrefix(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return b.handleRevokePrefixCommon(req, data, false)
}
// handleRevokeForce is used to revoke a prefix with many LeaseIDs, ignoring errors
func (b *SystemBackend) handleRevokeForce(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return b.handleRevokePrefixCommon(req, data, true)
}
// handleRevokePrefixCommon is used to revoke a prefix with many LeaseIDs
func (b *SystemBackend) handleRevokePrefixCommon(
req *logical.Request, data *framework.FieldData, force bool) (*logical.Response, error) {
// Get all the options
prefix := data.Get("prefix").(string)
// Invoke the expiration manager directly
if err := b.Core.expiration.RevokePrefix(prefix); err != nil {
var err error
if force {
err = b.Core.expiration.RevokeForce(prefix)
} else {
err = b.Core.expiration.RevokePrefix(prefix)
}
if err != nil {
b.Backend.Logger().Printf("[ERR] sys: revoke prefix '%s' failed: %v", prefix, err)
return handleError(err)
}
@ -1228,6 +1264,23 @@ all matching leases.
"",
},
"revoke-force": {
"Revoke all secrets generated in a given prefix, ignoring errors.",
`
See the path help for 'revoke-prefix'; this behaves the same, except that it
ignores errors encountered during revocation. This can be used in certain
recovery situations; for instance, when you want to unmount a backend, but it
is impossible to fix revocation errors and these errors prevent the unmount
from proceeding. This is a DANGEROUS operation as it removes Vault's oversight
of external secrets. Access to this prefix should be tightly controlled.
`,
},
"revoke-force-path": {
`The path to revoke keys under. Example: "prod/aws/ops"`,
"",
},
"auth-table": {
"List the currently enabled credential backends.",
`

View file

@ -15,6 +15,7 @@ const (
DeleteCapability = "delete"
ListCapability = "list"
SudoCapability = "sudo"
RootCapability = "root"
// Backwards compatibility
OldDenyPathPolicy = "deny"

View file

@ -22,6 +22,10 @@ const (
// primary ID based index
lookupPrefix = "id/"
// accessorPrefix is the prefix used to store the index from
// Accessor to Token ID
accessorPrefix = "accessor/"
// parentPrefix is the prefix used to store tokens for their
// secondar parent based index
parentPrefix = "parent/"
@ -209,6 +213,24 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error)
HelpDescription: strings.TrimSpace(tokenLookupHelp),
},
&framework.Path{
Pattern: "lookup-accessor/(?P<accessor>.+)",
Fields: map[string]*framework.FieldSchema{
"accessor": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Accessor of the token to lookup",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleUpdateLookupAccessor,
},
HelpSynopsis: strings.TrimSpace(tokenLookupAccessorHelp),
HelpDescription: strings.TrimSpace(tokenLookupAccessorHelp),
},
&framework.Path{
Pattern: "lookup-self$",
@ -227,6 +249,24 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error)
HelpDescription: strings.TrimSpace(tokenLookupHelp),
},
&framework.Path{
Pattern: "revoke-accessor/(?P<accessor>.+)",
Fields: map[string]*framework.FieldSchema{
"accessor": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Accessor of the token",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: t.handleUpdateRevokeAccessor,
},
HelpSynopsis: strings.TrimSpace(tokenRevokeAccessorHelp),
HelpDescription: strings.TrimSpace(tokenRevokeAccessorHelp),
},
&framework.Path{
Pattern: "revoke-self$",
@ -348,6 +388,7 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error)
// TokenEntry is used to represent a given token
type TokenEntry struct {
ID string // ID of this entry, generally a random UUID
Accessor string // Accessor for this token, a random UUID
Parent string // Parent token, used for revocation trees
Policies []string // Which named policies should be used
Path string // Used for audit trails, this is something like "auth/user/login"
@ -406,6 +447,27 @@ func (ts *TokenStore) rootToken() (*TokenEntry, error) {
return te, nil
}
// createAccessor is used to create an identifier for the token ID.
// A storage index, mapping the accessor to the token ID is also created.
func (ts *TokenStore) createAccessor(entry *TokenEntry) error {
defer metrics.MeasureSince([]string{"token", "createAccessor"}, time.Now())
// Create a random accessor
accessorUUID, err := uuid.GenerateUUID()
if err != nil {
return err
}
entry.Accessor = accessorUUID
// Create index entry, mapping the accessor to the token ID
path := accessorPrefix + ts.SaltID(entry.Accessor)
le := &logical.StorageEntry{Key: path, Value: []byte(entry.ID)}
if err := ts.view.Put(le); err != nil {
return fmt.Errorf("failed to persist accessor index entry: %v", err)
}
return nil
}
// Create is used to create a new token entry. The entry is assigned
// a newly generated ID if not provided.
func (ts *TokenStore) create(entry *TokenEntry) error {
@ -419,6 +481,11 @@ func (ts *TokenStore) create(entry *TokenEntry) error {
entry.ID = entryUUID
}
err := ts.createAccessor(entry)
if err != nil {
return err
}
return ts.storeCommon(entry, true)
}
@ -575,6 +642,14 @@ func (ts *TokenStore) revokeSalted(saltedId string) error {
}
}
// Clear the accessor index if any
if entry != nil && entry.Accessor != "" {
path := accessorPrefix + ts.SaltID(entry.Accessor)
if ts.view.Delete(path); err != nil {
return fmt.Errorf("failed to delete entry: %v", err)
}
}
// Revoke all secrets under this token
if entry != nil {
if err := ts.expiration.RevokeByToken(entry.ID); err != nil {
@ -651,6 +726,83 @@ func (ts *TokenStore) handleCreateAgainstRole(
return ts.handleCreateCommon(req, d, false, roleEntry)
}
func (ts *TokenStore) lookupByAccessor(accessor string) (string, error) {
entry, err := ts.view.Get(accessorPrefix + ts.SaltID(accessor))
if err != nil {
return "", fmt.Errorf("failed to read index using accessor: %s", err)
}
if entry == nil {
return "", &StatusBadRequest{Err: "invalid accessor"}
}
return string(entry.Value), nil
}
// handleUpdateLookupAccessor handles the auth/token/lookup-accessor path for returning
// the properties of the token associated with the accessor
func (ts *TokenStore) handleUpdateLookupAccessor(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
accessor := data.Get("accessor").(string)
if accessor == "" {
return nil, &StatusBadRequest{Err: "missing accessor"}
}
tokenID, err := ts.lookupByAccessor(accessor)
if err != nil {
return nil, err
}
// Prepare the field data required for a lookup call
d := &framework.FieldData{
Raw: map[string]interface{}{
"token": tokenID,
},
Schema: map[string]*framework.FieldSchema{
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token to lookup",
},
},
}
resp, err := ts.handleLookup(req, d)
if err != nil {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("failed to lookup the token")
}
if resp.IsError() {
return resp, nil
}
// Remove the token ID from the response
if resp.Data != nil {
resp.Data["id"] = ""
}
return resp, nil
}
// handleUpdateRevokeAccessor handles the auth/token/revoke-accessor path for revoking
// the token associated with the accessor
func (ts *TokenStore) handleUpdateRevokeAccessor(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
accessor := data.Get("accessor").(string)
if accessor == "" {
return nil, &StatusBadRequest{Err: "missing accessor"}
}
tokenID, err := ts.lookupByAccessor(accessor)
if err != nil {
return nil, err
}
// Revoke the token and its children
if err := ts.RevokeTree(tokenID); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
return nil, nil
}
// handleCreate handles the auth/token/create path for creation of new orphan
// tokens
func (ts *TokenStore) handleCreateOrphan(
@ -864,6 +1016,7 @@ func (ts *TokenStore) handleCreateCommon(
Renewable: true,
},
ClientToken: te.ID,
Accessor: te.Accessor,
},
}
@ -990,6 +1143,7 @@ func (ts *TokenStore) handleLookup(
resp := &logical.Response{
Data: map[string]interface{}{
"id": out.ID,
"accessor": out.Accessor,
"policies": out.Policies,
"path": out.Path,
"meta": out.Meta,
@ -1262,8 +1416,10 @@ as revocation of tokens. The tokens are renewable if associated with a lease.`
tokenCreateOrphanHelp = `The token create path is used to create new orphan tokens.`
tokenCreateRoleHelp = `This token create path is used to create new tokens adhering to the given role.`
tokenListRolesHelp = `This endpoint lists configured roles.`
tokenLookupAccessorHelp = `This endpoint will lookup a token associated with the given accessor and its properties. Response will not contain the token ID.`
tokenLookupHelp = `This endpoint will lookup a token and its properties.`
tokenPathRolesHelp = `This endpoint allows creating, reading, and deleting roles.`
tokenRevokeAccessorHelp = `This endpoint will delete the token associated with the accessor and all of its child tokens.`
tokenRevokeHelp = `This endpoint will delete the given token and all of its child tokens.`
tokenRevokeSelfHelp = `This endpoint will delete the token used to call it and all of its child tokens.`
tokenRevokeOrphanHelp = `This endpoint will delete the token and orphan its child tokens.`

View file

@ -53,6 +53,94 @@ func testCoreMakeToken(t *testing.T, c *Core, root, client, ttl string, policy [
}
}
func TestTokenStore_AccessorIndex(t *testing.T) {
_, ts, _, _ := TestCoreWithTokenStore(t)
ent := &TokenEntry{Path: "test", Policies: []string{"dev", "ops"}}
if err := ts.create(ent); err != nil {
t.Fatalf("err: %s", err)
}
out, err := ts.Lookup(ent.ID)
if err != nil {
t.Fatalf("err: %s", err)
}
// Ensure that accessor is created
if out == nil || out.Accessor == "" {
t.Fatalf("bad: %#v", out)
}
token, err := ts.lookupByAccessor(out.Accessor)
if err != nil {
t.Fatalf("err: %s", err)
}
// Verify that the value returned from the index matches the token ID
if token != ent.ID {
t.Fatalf("bad: got\n%s\nexpected\n%s\n", token, ent.ID)
}
}
func TestTokenStore_HandleRequest_LookupAccessor(t *testing.T) {
_, ts, _, root := TestCoreWithTokenStore(t)
testMakeToken(t, ts, root, "tokenid", "", []string{"foo"})
out, err := ts.Lookup("tokenid")
if err != nil {
t.Fatalf("err: %s", err)
}
if out == nil {
t.Fatalf("err: %s", err)
}
req := logical.TestRequest(t, logical.UpdateOperation, "lookup-accessor/"+out.Accessor)
resp, err := ts.HandleRequest(req)
if err != nil {
t.Fatalf("err: %s", err)
}
if resp.Data == nil {
t.Fatalf("response should contain data")
}
if resp.Data["accessor"].(string) == "" {
t.Fatalf("accessor should not be empty")
}
// Verify that the lookup-accessor operation does not return the token ID
if resp.Data["id"].(string) != "" {
t.Fatalf("token ID should not be returned")
}
}
func TestTokenStore_HandleRequest_RevokeAccessor(t *testing.T) {
_, ts, _, root := TestCoreWithTokenStore(t)
testMakeToken(t, ts, root, "tokenid", "", []string{"foo"})
out, err := ts.Lookup("tokenid")
if err != nil {
t.Fatalf("err: %s", err)
}
if out == nil {
t.Fatalf("err: %s", err)
}
req := logical.TestRequest(t, logical.UpdateOperation, "revoke-accessor/"+out.Accessor)
_, err = ts.HandleRequest(req)
if err != nil {
t.Fatalf("err: %s", err)
}
out, err = ts.Lookup("tokenid")
if err != nil {
t.Fatalf("err: %s", err)
}
if out != nil {
t.Fatalf("bad:\ngot %#v\nexpected: nil\n", out)
}
}
func TestTokenStore_RootToken(t *testing.T) {
_, ts, _, _ := TestCoreWithTokenStore(t)
@ -417,6 +505,7 @@ func TestTokenStore_HandleRequest_CreateToken_DisplayName(t *testing.T) {
expected := &TokenEntry{
ID: resp.Auth.ClientToken,
Accessor: resp.Auth.Accessor,
Parent: root,
Policies: []string{"root"},
Path: "auth/token/create",
@ -447,6 +536,7 @@ func TestTokenStore_HandleRequest_CreateToken_NumUses(t *testing.T) {
expected := &TokenEntry{
ID: resp.Auth.ClientToken,
Accessor: resp.Auth.Accessor,
Parent: root,
Policies: []string{"root"},
Path: "auth/token/create",
@ -510,6 +600,7 @@ func TestTokenStore_HandleRequest_CreateToken_NoPolicy(t *testing.T) {
expected := &TokenEntry{
ID: resp.Auth.ClientToken,
Accessor: resp.Auth.Accessor,
Parent: root,
Policies: []string{"root"},
Path: "auth/token/create",
@ -866,6 +957,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) {
exp := map[string]interface{}{
"id": root,
"accessor": resp.Data["accessor"].(string),
"policies": []string{"root"},
"path": "auth/token/root",
"meta": map[string]string(nil),
@ -899,6 +991,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) {
exp = map[string]interface{}{
"id": "client",
"accessor": resp.Data["accessor"],
"policies": []string{"default", "foo"},
"path": "auth/token/create",
"meta": map[string]string(nil),
@ -1002,6 +1095,7 @@ func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) {
exp := map[string]interface{}{
"id": root,
"accessor": resp.Data["accessor"],
"policies": []string{"root"},
"path": "auth/token/root",
"meta": map[string]string(nil),

View file

@ -584,3 +584,85 @@ of the header should be "X-Vault-Token" and the value should be the token.
</dd>
</dl>
### /auth/token/lookup-accessor
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Fetch the properties of the token associated with the accessor, except the token ID.
This is meant for purposes where there is no access to token ID but there is need
to fetch the properties of a token.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/auth/token/lookup-accessor`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">accessor</span>
<span class="param-flags">required</span>
Accessor of the token to lookup.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": {
"creation_time": 1457533232,
"creation_ttl": 2592000,
"display_name": "token",
"id": "",
"meta": null,
"num_uses": 0,
"orphan": false,
"path": "auth/token/create",
"policies": ["default", "web"],
"ttl": 2591976
},
"warnings": null,
"auth": null
}
```
</dd>
</dl>
### /auth/token/revoke-accessor/
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Revoke the token associated with the accessor and all the child tokens.
This is meant for purposes where there is no access to token ID but
there is need to revoke a token and its children.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/auth/token/revoke-accessor/<accessor>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>`204` response code.
</dd>
</dl>

View file

@ -0,0 +1,48 @@
---
layout: "http"
page_title: "HTTP API: /sys/capabilities-accessor"
sidebar_current: "docs-http-auth-capabilities-accessor"
description: |-
The `/sys/capabilities-accessor` endpoint is used to fetch the capabilities of the token associated with an accessor, on the given path.
---
# /sys/capabilities-accessor
## POST
<dl>
<dt>Description</dt>
<dd>
Returns the capabilities of the token associated with an accessor, on the given path.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">accessor</span>
<span class="param-flags">required</span>
Accessor of the token.
</li>
<li>
<span class="param">path</span>
<span class="param-flags">required</span>
Path on which the token's capabilities will be checked.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"capabilities": ["read", "list"]
}
```
</dd>
</dl>

View file

@ -0,0 +1,44 @@
---
layout: "http"
page_title: "HTTP API: /sys/capabilities-self"
sidebar_current: "docs-http-auth-capabilities-self"
description: |-
The `/sys/capabilities-self` endpoint is used to fetch the capabilities of client token on a given path.
---
# /sys/capabilities-self
## POST
<dl>
<dt>Description</dt>
<dd>
Returns the capabilities of client token on the given path.
Client token is the Vault token with which this API call is made.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">path</span>
<span class="param-flags">required</span>
Path on which the client token's capabilities will be checked.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"capabilities": ["read", "list"]
}
```
</dd>
</dl>

View file

@ -0,0 +1,48 @@
---
layout: "http"
page_title: "HTTP API: /sys/capabilities"
sidebar_current: "docs-http-auth-capabilities"
description: |-
The `/sys/capabilities` endpoint is used to fetch the capabilities of a token on a given path.
---
# /sys/capabilities
## POST
<dl>
<dt>Description</dt>
<dd>
Returns the capabilities of the token on the given path.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">token</span>
<span class="param-flags">required</span>
Token for which capabilities are being queried.
</li>
<li>
<span class="param">path</span>
<span class="param-flags">required</span>
Path on which the token's capabilities will be checked.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"capabilities": ["read", "list"]
}
```
</dd>
</dl>

View file

@ -0,0 +1,36 @@
---
layout: "http"
page_title: "HTTP API: /sys/revoke-force"
sidebar_current: "docs-http-lease-revoke-force"
description: |-
The `/sys/revoke-force` endpoint is used to revoke secrets based on prefix while ignoring backend errors.
---
# /sys/revoke-force
<dl>
<dt>Description</dt>
<dd>
Revoke all secrets generated under a given prefix immediately. Unlike
`/sys/revoke-prefix`, this path ignores backend errors encountered during
revocation. This is <i>potentially very dangerous</i> and should only be
used in specific emergency situations where errors in the backend or the
connected backend service prevent normal revocation. <i>By ignoring these
errors, Vault abdicates responsibility for ensuring that the issued
credentials or secrets are properly revoked and/or cleaned up. Access to
this endpoint should be tightly controlled.</i>
</dd>
<dt>Method</dt>
<dd>PUT</dd>
<dt>URL</dt>
<dd>`/sys/revoke-force/<path prefix>`</dd>
<dt>Parameters</dt>
<dd>None</dd>
<dt>Returns</dt>
<dd>A `204` response code.
</dd>
</dl>

View file

@ -156,14 +156,15 @@ The root credentials need permission to perform various IAM actions. These are t
"Action": [
"iam:CreateAccessKey",
"iam:CreateUser",
"iam:PutUserPolicy",
"iam:DeleteAccessKey",
"iam:DeleteUser"
"iam:DeleteUserPolicy",
"iam:ListAccessKeys",
"iam:ListAttachedUserPolicies",
"iam:ListGroupsForUser",
"iam:ListUserPolicies",
"iam:ListAccessKeys",
"iam:DeleteAccessKey",
"iam:DeleteUserPolicy",
"iam:PutUserPolicy",
"iam:RemoveUserFromGroup",
"iam:DeleteUser"
],
"Resource": [
"arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/vault-*"

View file

@ -61,7 +61,7 @@ an ACL token to use with the `token` parameter. Vault must have a management
type token so that it can create and revoke ACL tokens.
The next step is to configure a role. A role is a logical name that maps
to a role used to generated those credentials. For example, lets create
to a role used to generate those credentials. For example, lets create
a "readonly" role:
```

View file

@ -69,6 +69,18 @@
<li<%= sidebar_current("docs-http-auth-policy") %>>
<a href="/docs/http/sys-policy.html">/sys/policy</a>
</li>
<li<%= sidebar_current("docs-http-auth-capabilities") %>>
<a href="/docs/http/sys-capabilities.html">/sys/capabilities</a>
</li>
<li<%= sidebar_current("docs-http-auth-capabilities-self") %>>
<a href="/docs/http/sys-capabilities-self.html">/sys/capabilities-self</a>
</li>
<li<%= sidebar_current("docs-http-auth-capabilities-accessor") %>>
<a href="/docs/http/sys-capabilities-accessor.html">/sys/capabilities-accessor</a>
</li>
</ul>
</li>
@ -98,6 +110,10 @@
<li<%= sidebar_current("docs-http-lease-revoke-prefix") %>>
<a href="/docs/http/sys-revoke-prefix.html">/sys/revoke-prefix</a>
</li>
<li<%= sidebar_current("docs-http-lease-revoke-force") %>>
<a href="/docs/http/sys-revoke-force.html">/sys/revoke-force</a>
</li>
</ul>
</li>