diff --git a/CHANGELOG.md b/CHANGELOG.md index 902a01c12f..f58f5985fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/api/secret.go b/api/secret.go index bc52f78b72..7e10f1ff0c 100644 --- a/api/secret.go +++ b/api/secret.go @@ -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"` diff --git a/api/sys_capabilities.go b/api/sys_capabilities.go new file mode 100644 index 0000000000..1640a2fc9b --- /dev/null +++ b/api/sys_capabilities.go @@ -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"` +} diff --git a/api/sys_lease.go b/api/sys_lease.go index 4c29fdcaf6..e103990d4f 100644 --- a/api/sys_lease.go +++ b/api/sys_lease.go @@ -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 +} diff --git a/audit/format_json.go b/audit/format_json.go index 131aa6a3a0..ae181e24af 100644 --- a/audit/format_json.go +++ b/audit/format_json.go @@ -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"` diff --git a/builtin/credential/cert/path_login.go b/builtin/credential/cert/path_login.go index 6ad76c6f86..ce336fe629 100644 --- a/builtin/credential/cert/path_login.go +++ b/builtin/credential/cert/path_login.go @@ -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, diff --git a/builtin/credential/github/backend_test.go b/builtin/credential/github/backend_test.go index f748c9c8f1..f728f9c878 100644 --- a/builtin/credential/github/backend_test.go +++ b/builtin/credential/github/backend_test.go @@ -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", } diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go index 1bd6e25047..feda8a5539 100644 --- a/builtin/logical/consul/backend_test.go +++ b/builtin/logical/consul/backend_test.go @@ -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") diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 34e20f1d4c..d030bd340f 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -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) diff --git a/builtin/logical/pki/cert_util.go b/builtin/logical/pki/cert_util.go index 262f36340b..f435e42354 100644 --- a/builtin/logical/pki/cert_util.go +++ b/builtin/logical/pki/cert_util.go @@ -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) } diff --git a/builtin/logical/pki/path_fetch.go b/builtin/logical/pki/path_fetch.go index ebde73da8e..ed63682e2b 100644 --- a/builtin/logical/pki/path_fetch.go +++ b/builtin/logical/pki/path_fetch.go @@ -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 diff --git a/builtin/logical/pki/path_issue_sign.go b/builtin/logical/pki/path_issue_sign.go index 9d523ffcc0..9d7a81984f 100644 --- a/builtin/logical/pki/path_issue_sign.go +++ b/builtin/logical/pki/path_issue_sign.go @@ -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, diff --git a/builtin/logical/pki/path_revoke.go b/builtin/logical/pki/path_revoke.go index b6a7e2e72f..2bd205fe14 100644 --- a/builtin/logical/pki/path_revoke.go +++ b/builtin/logical/pki/path_revoke.go @@ -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() diff --git a/cli/commands.go b/cli/commands.go index 1f5b89f911..ec6cb27359 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -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() diff --git a/command/capabilities.go b/command/capabilities.go new file mode 100644 index 0000000000..b33a6914a2 --- /dev/null +++ b/command/capabilities.go @@ -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) +} diff --git a/command/capabilities_test.go b/command/capabilities_test.go new file mode 100644 index 0000000000..d300ff6b67 --- /dev/null +++ b/command/capabilities_test.go @@ -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") + } +} diff --git a/command/format.go b/command/format.go index 0acac4b70a..93b812312e 100644 --- a/command/format.go +++ b/command/format.go @@ -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)) diff --git a/command/revoke.go b/command/revoke.go index 149cbad84e..6cd7296797 100644 --- a/command/revoke.go +++ b/command/revoke.go @@ -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) } diff --git a/http/handler.go b/http/handler.go index a9be257e35..67cff6af4c 100644 --- a/http/handler.go +++ b/http/handler.go @@ -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 { diff --git a/http/logical.go b/http/logical.go index eb07a8f640..816df95c0c 100644 --- a/http/logical.go +++ b/http/logical.go @@ -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"` diff --git a/http/logical_test.go b/http/logical_test.go index 51f34faae3..f7c0924459 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -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) } diff --git a/http/sys_capabilities.go b/http/sys_capabilities.go new file mode 100644 index 0000000000..93d135ed3f --- /dev/null +++ b/http/sys_capabilities.go @@ -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"` +} diff --git a/http/sys_capabilities_test.go b/http/sys_capabilities_test.go new file mode 100644 index 0000000000..726621ec7f --- /dev/null +++ b/http/sys_capabilities_test.go @@ -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) + } +} diff --git a/http/sys_generate_root_test.go b/http/sys_generate_root_test.go index dac075ca78..70cc3fed3b 100644 --- a/http/sys_generate_root_test.go +++ b/http/sys_generate_root_test.go @@ -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"]) diff --git a/logical/auth.go b/logical/auth.go index e297e10eb3..1636fb5bdf 100644 --- a/logical/auth.go +++ b/logical/auth.go @@ -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 { diff --git a/vault/acl.go b/vault/acl.go index ac4922407b..ead38089d4 100644 --- a/vault/acl.go +++ b/vault/acl.go @@ -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. diff --git a/vault/acl_test.go b/vault/acl_test.go index 41df6a2071..ac1ab4f7f9 100644 --- a/vault/acl_test.go +++ b/vault/acl_test.go @@ -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"}} diff --git a/vault/capabilities.go b/vault/capabilities.go new file mode 100644 index 0000000000..9c0164ed82 --- /dev/null +++ b/vault/capabilities.go @@ -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 +} diff --git a/vault/capabilities_test.go b/vault/capabilities_test.go new file mode 100644 index 0000000000..b560787a28 --- /dev/null +++ b/vault/capabilities_test.go @@ -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) + } +} diff --git a/vault/core.go b/vault/core.go index ab1b6a3877..8725945b9e 100644 --- a/vault/core.go +++ b/vault/core.go @@ -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 { diff --git a/vault/core_test.go b/vault/core_test.go index ec54585736..0d28eab0c0 100644 --- a/vault/core_test.go +++ b/vault/core_test.go @@ -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", diff --git a/vault/expiration.go b/vault/expiration.go index a13bbfcc27..3e3104c14a 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -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) { diff --git a/vault/expiration_test.go b/vault/expiration_test.go index c086cd05e9..4521cec573 100644 --- a/vault/expiration_test.go +++ b/vault/expiration_test.go @@ -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) +} diff --git a/vault/logical_system.go b/vault/logical_system.go index 5217c97e7a..2cf7dfb741 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -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.+)", + + 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.+)", @@ -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.", ` diff --git a/vault/policy.go b/vault/policy.go index 6f856e7b72..7e459f59a9 100644 --- a/vault/policy.go +++ b/vault/policy.go @@ -15,6 +15,7 @@ const ( DeleteCapability = "delete" ListCapability = "list" SudoCapability = "sudo" + RootCapability = "root" // Backwards compatibility OldDenyPathPolicy = "deny" diff --git a/vault/token_store.go b/vault/token_store.go index 67f44f7193..c0618e6934 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -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.+)", + + 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.+)", + + 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.` diff --git a/vault/token_store_test.go b/vault/token_store_test.go index b006c44f49..9613810f60 100644 --- a/vault/token_store_test.go +++ b/vault/token_store_test.go @@ -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), diff --git a/website/source/docs/auth/token.html.md b/website/source/docs/auth/token.html.md index bfd09f99e6..a650bc2777 100644 --- a/website/source/docs/auth/token.html.md +++ b/website/source/docs/auth/token.html.md @@ -584,3 +584,85 @@ of the header should be "X-Vault-Token" and the value should be the token. +### /auth/token/lookup-accessor +#### POST + +
+
Description
+
+ 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. +
+ +
Method
+
POST
+ +
URL
+
`/auth/token/lookup-accessor`
+ +
Parameters
+
+
    +
  • + accessor + required + Accessor of the token to lookup. +
  • +
+
+ +
Returns
+
+ + ```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 + } + ``` +
+
+ +### /auth/token/revoke-accessor/ +#### POST + +
+
Description
+
+ 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. +
+ +
Method
+
POST
+ +
URL
+
`/auth/token/revoke-accessor/`
+ +
Parameters
+
+ None +
+ +
Returns
+
`204` response code. +
+
+ diff --git a/website/source/docs/http/sys-capabilities-accessor.html.md b/website/source/docs/http/sys-capabilities-accessor.html.md new file mode 100644 index 0000000000..4549b203fb --- /dev/null +++ b/website/source/docs/http/sys-capabilities-accessor.html.md @@ -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 + +
+
Description
+
+ Returns the capabilities of the token associated with an accessor, on the given path. +
+ +
Method
+
POST
+ +
Parameters
+
+
    +
  • + accessor + required + Accessor of the token. +
  • +
  • + path + required + Path on which the token's capabilities will be checked. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "capabilities": ["read", "list"] + } + ``` + +
+
diff --git a/website/source/docs/http/sys-capabilities-self.html.md b/website/source/docs/http/sys-capabilities-self.html.md new file mode 100644 index 0000000000..f4fdbffba0 --- /dev/null +++ b/website/source/docs/http/sys-capabilities-self.html.md @@ -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 + +
+
Description
+
+ Returns the capabilities of client token on the given path. + Client token is the Vault token with which this API call is made. +
+ +
Method
+
POST
+ +
Parameters
+
+
    +
  • + path + required + Path on which the client token's capabilities will be checked. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "capabilities": ["read", "list"] + } + ``` + +
+
diff --git a/website/source/docs/http/sys-capabilities.html.md b/website/source/docs/http/sys-capabilities.html.md new file mode 100644 index 0000000000..4776bfc06a --- /dev/null +++ b/website/source/docs/http/sys-capabilities.html.md @@ -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 + +
+
Description
+
+ Returns the capabilities of the token on the given path. +
+ +
Method
+
POST
+ +
Parameters
+
+
    +
  • + token + required + Token for which capabilities are being queried. +
  • +
  • + path + required + Path on which the token's capabilities will be checked. +
  • +
+
+ +
Returns
+
+ + ```javascript + { + "capabilities": ["read", "list"] + } + ``` + +
+
diff --git a/website/source/docs/http/sys-revoke-force.html.md b/website/source/docs/http/sys-revoke-force.html.md new file mode 100644 index 0000000000..c8115ff277 --- /dev/null +++ b/website/source/docs/http/sys-revoke-force.html.md @@ -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 + +
+
Description
+
+ Revoke all secrets generated under a given prefix immediately. Unlike + `/sys/revoke-prefix`, this path ignores backend errors encountered during + revocation. This is potentially very dangerous and should only be + used in specific emergency situations where errors in the backend or the + connected backend service prevent normal revocation. 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. +
+ +
Method
+
PUT
+ +
URL
+
`/sys/revoke-force/`
+ +
Parameters
+
None
+ +
Returns
+
A `204` response code. +
+
diff --git a/website/source/docs/secrets/aws/index.html.md b/website/source/docs/secrets/aws/index.html.md index 43eedcccaa..3b82fca597 100644 --- a/website/source/docs/secrets/aws/index.html.md +++ b/website/source/docs/secrets/aws/index.html.md @@ -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-*" diff --git a/website/source/docs/secrets/consul/index.html.md b/website/source/docs/secrets/consul/index.html.md index 7c6f52d030..bb1329e93f 100644 --- a/website/source/docs/secrets/consul/index.html.md +++ b/website/source/docs/secrets/consul/index.html.md @@ -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: ``` diff --git a/website/source/layouts/http.erb b/website/source/layouts/http.erb index ee75b37f9c..3659e8e501 100644 --- a/website/source/layouts/http.erb +++ b/website/source/layouts/http.erb @@ -69,6 +69,18 @@ > /sys/policy + + > + /sys/capabilities + + + > + /sys/capabilities-self + + + > + /sys/capabilities-accessor + @@ -98,6 +110,10 @@ > /sys/revoke-prefix + + > + /sys/revoke-force +