Enable audit-logging of seal and step-down commands.

This pulls the logical request building code into its own function so
that it's accessible from other HTTP handlers, then uses that with some
added logic to the Seal() and StepDown() commands to have meaningful
audit log entries.
This commit is contained in:
Jeff Mitchell 2016-05-20 17:03:54 +00:00
parent c9aaabe235
commit 05b0e0a866
4 changed files with 168 additions and 96 deletions

View File

@ -14,72 +14,76 @@ import (
type PrepareRequestFunc func(req *logical.Request) error
func buildLogicalRequest(w http.ResponseWriter, r *http.Request) (*logical.Request, int, error) {
// Determine the path...
if !strings.HasPrefix(r.URL.Path, "/v1/") {
return nil, http.StatusNotFound, nil
}
path := r.URL.Path[len("/v1/"):]
if path == "" {
return nil, http.StatusNotFound, nil
}
// Determine the operation
var op logical.Operation
switch r.Method {
case "DELETE":
op = logical.DeleteOperation
case "GET":
op = logical.ReadOperation
// Need to call ParseForm to get query params loaded
queryVals := r.URL.Query()
listStr := queryVals.Get("list")
if listStr != "" {
list, err := strconv.ParseBool(listStr)
if err != nil {
return nil, http.StatusBadRequest, nil
}
if list {
op = logical.ListOperation
}
}
case "POST", "PUT":
op = logical.UpdateOperation
case "LIST":
op = logical.ListOperation
default:
return nil, http.StatusMethodNotAllowed, nil
}
// Parse the request if we can
var data map[string]interface{}
if op == logical.UpdateOperation {
err := parseRequest(r, &data)
if err == io.EOF {
data = nil
err = nil
}
if err != nil {
return nil, http.StatusBadRequest, err
}
}
var err error
req := requestAuth(r, &logical.Request{
Operation: op,
Path: path,
Data: data,
Connection: getConnection(r),
})
req, err = requestWrapTTL(r, req)
if err != nil {
return nil, http.StatusBadRequest, errwrap.Wrapf("error parsing X-Vault-Wrap-TTL header: {{err}}", err)
}
return req, 0, nil
}
func handleLogical(core *vault.Core, dataOnly bool, prepareRequestCallback PrepareRequestFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Determine the path...
if !strings.HasPrefix(r.URL.Path, "/v1/") {
respondError(w, http.StatusNotFound, nil)
return
}
path := r.URL.Path[len("/v1/"):]
if path == "" {
respondError(w, http.StatusNotFound, nil)
return
}
// Determine the operation
var op logical.Operation
switch r.Method {
case "DELETE":
op = logical.DeleteOperation
case "GET":
op = logical.ReadOperation
// Need to call ParseForm to get query params loaded
queryVals := r.URL.Query()
listStr := queryVals.Get("list")
if listStr != "" {
list, err := strconv.ParseBool(listStr)
if err != nil {
respondError(w, http.StatusBadRequest, nil)
return
}
if list {
op = logical.ListOperation
}
}
case "POST", "PUT":
op = logical.UpdateOperation
case "LIST":
op = logical.ListOperation
default:
respondError(w, http.StatusMethodNotAllowed, nil)
return
}
// Parse the request if we can
var data map[string]interface{}
if op == logical.UpdateOperation {
err := parseRequest(r, &data)
if err == io.EOF {
data = nil
err = nil
}
if err != nil {
respondError(w, http.StatusBadRequest, err)
return
}
}
var err error
req := requestAuth(r, &logical.Request{
Operation: op,
Path: path,
Data: data,
Connection: getConnection(r),
})
req, err = requestWrapTTL(r, req)
if err != nil {
respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing X-Vault-Wrap-TTL header: {{err}}", err))
req, statusCode, err := buildLogicalRequest(w, r)
if err != nil || statusCode != 0 {
respondError(w, statusCode, err)
return
}
@ -101,7 +105,7 @@ func handleLogical(core *vault.Core, dataOnly bool, prepareRequestCallback Prepa
return
}
switch {
case op == logical.ReadOperation:
case req.Operation == logical.ReadOperation:
if resp == nil {
respondError(w, http.StatusNotFound, nil)
return
@ -109,7 +113,7 @@ func handleLogical(core *vault.Core, dataOnly bool, prepareRequestCallback Prepa
// Basically: if we have empty "keys" or no keys at all, 404. This
// provides consistency with GET.
case op == logical.ListOperation:
case req.Operation == logical.ListOperation:
if resp == nil || len(resp.Data) == 0 {
respondError(w, http.StatusNotFound, nil)
return
@ -131,7 +135,7 @@ func handleLogical(core *vault.Core, dataOnly bool, prepareRequestCallback Prepa
}
// Build the proper response
respondLogical(w, r, path, dataOnly, resp)
respondLogical(w, r, req.Path, dataOnly, resp)
})
}

View File

@ -13,19 +13,21 @@ import (
func handleSysSeal(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "PUT":
case "POST":
req, statusCode, err := buildLogicalRequest(w, r)
if err != nil || statusCode != 0 {
respondError(w, statusCode, err)
return
}
switch req.Operation {
case logical.UpdateOperation:
default:
respondError(w, http.StatusMethodNotAllowed, nil)
return
}
// Get the auth for the request so we can access the token directly
req := requestAuth(r, &logical.Request{})
// Seal with the token above
if err := core.Seal(req.ClientToken); err != nil {
if err := core.SealWithRequest(req); err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
@ -36,19 +38,21 @@ func handleSysSeal(core *vault.Core) http.Handler {
func handleSysStepDown(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "PUT":
case "POST":
req, statusCode, err := buildLogicalRequest(w, r)
if err != nil || statusCode != 0 {
respondError(w, statusCode, err)
return
}
switch req.Operation {
case logical.UpdateOperation:
default:
respondError(w, http.StatusMethodNotAllowed, nil)
return
}
// Get the auth for the request so we can access the token directly
req := requestAuth(r, &logical.Request{})
// Seal with the token above
if err := core.StepDown(req.ClientToken); err != nil {
if err := core.StepDown(req); err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}

View File

@ -658,25 +658,54 @@ func (c *Core) Unseal(key []byte) (bool, error) {
return true, nil
}
// Seal is used to re-seal the Vault. This requires the Vault to
// be unsealed again to perform any further operations.
func (c *Core) Seal(token string) (retErr error) {
// SealWithRequest takes in a logical.Request, acquires the lock, and passes
// through to sealInternal
func (c *Core) SealWithRequest(req *logical.Request) error {
defer metrics.MeasureSince([]string{"core", "seal-with-request"}, time.Now())
c.stateLock.Lock()
defer c.stateLock.Unlock()
if c.sealed {
return nil
}
return c.sealInitCommon(req)
}
// Seal takes in a token and creates a logical.Request, acquires the lock, and
// passes through to sealInternal
func (c *Core) Seal(token string) error {
defer metrics.MeasureSince([]string{"core", "seal"}, time.Now())
c.stateLock.Lock()
defer c.stateLock.Unlock()
if c.sealed {
return retErr
return nil
}
// Validate the token is a root token
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "sys/seal",
ClientToken: token,
}
return c.sealInitCommon(req)
}
// sealInitCommon is common logic for Seal and SealWithRequest and is used to
// re-seal the Vault. This requires the Vault to be unsealed again to perform
// any further operations.
func (c *Core) sealInitCommon(req *logical.Request) (retErr error) {
defer metrics.MeasureSince([]string{"core", "seal-internal"}, time.Now())
if req == nil {
retErr = multierror.Append(retErr, errors.New("nil request to seal"))
return retErr
}
// Validate the token is a root token
acl, te, err := c.fetchACLandTokenEntry(req)
if err != nil {
// Since there is no token store in standby nodes, sealing cannot
@ -692,6 +721,22 @@ func (c *Core) Seal(token string) (retErr error) {
retErr = multierror.Append(retErr, err)
return retErr
}
// Audit-log the request before going any further
auth := &logical.Auth{
ClientToken: req.ClientToken,
Policies: te.Policies,
Metadata: te.Meta,
DisplayName: te.DisplayName,
}
if err := c.auditBroker.LogRequest(auth, req, nil); err != nil {
c.logger.Printf("[ERR] core: failed to audit request with path %s: %v",
req.Path, err)
retErr = multierror.Append(retErr, errors.New("failed to audit request, cannot continue"))
return retErr
}
// Attempt to use the token (decrement num_uses)
// On error bail out; if the token has been revoked, bail out too
if te != nil {
@ -741,9 +786,14 @@ func (c *Core) Seal(token string) (retErr error) {
}
// StepDown is used to step down from leadership
func (c *Core) StepDown(token string) (retErr error) {
func (c *Core) StepDown(req *logical.Request) (retErr error) {
defer metrics.MeasureSince([]string{"core", "step_down"}, time.Now())
if req == nil {
retErr = multierror.Append(retErr, errors.New("nil request to step-down"))
return retErr
}
c.stateLock.Lock()
defer c.stateLock.Unlock()
if c.sealed {
@ -753,18 +803,27 @@ func (c *Core) StepDown(token string) (retErr error) {
return nil
}
// Validate the token is a root token
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "sys/step-down",
ClientToken: token,
}
acl, te, err := c.fetchACLandTokenEntry(req)
if err != nil {
retErr = multierror.Append(retErr, err)
return retErr
}
// Audit-log the request before going any further
auth := &logical.Auth{
ClientToken: req.ClientToken,
Policies: te.Policies,
Metadata: te.Meta,
DisplayName: te.DisplayName,
}
if err := c.auditBroker.LogRequest(auth, req, nil); err != nil {
c.logger.Printf("[ERR] core: failed to audit request with path %s: %v",
req.Path, err)
retErr = multierror.Append(retErr, errors.New("failed to audit request, cannot continue"))
return retErr
}
// Attempt to use the token (decrement num_uses)
if te != nil {
te, err = c.tokenStore.UseToken(te)

View File

@ -1148,8 +1148,13 @@ func TestCore_StepDown(t *testing.T) {
t.Fatalf("Bad advertise: %v", advertise)
}
req := &logical.Request{
ClientToken: root,
Path: "sys/step-down",
}
// Step down core
err = core.StepDown(root)
err = core.StepDown(req)
if err != nil {
t.Fatal("error stepping down core 1")
}
@ -1191,7 +1196,7 @@ func TestCore_StepDown(t *testing.T) {
}
// Step down core2
err = core2.StepDown(root)
err = core2.StepDown(req)
if err != nil {
t.Fatal("error stepping down core 1")
}