open-vault/audit/format.go
Jeff Mitchell 5d44c54947
Changes the way policies are reported in audit logs (#4747)
* This changes the way policies are reported in audit logs.

Previously, only policies tied to tokens would be reported. This could
make it difficult to perform after-the-fact analysis based on both the
initial response entry and further requests. Now, the full set of
applicable policies from both the token and any derived policies from
Identity are reported.

To keep things consistent, token authentications now also return the
full set of policies in api.Secret.Auth responses, so this both makes it
easier for users to understand their actual full set, and it matches
what the audit logs now report.
2018-06-14 09:49:33 -04:00

455 lines
12 KiB
Go

package audit
import (
"context"
"fmt"
"io"
"strings"
"time"
"github.com/SermoDigital/jose/jws"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/helper/salt"
"github.com/hashicorp/vault/logical"
"github.com/mitchellh/copystructure"
)
type AuditFormatWriter interface {
WriteRequest(io.Writer, *AuditRequestEntry) error
WriteResponse(io.Writer, *AuditResponseEntry) error
Salt(context.Context) (*salt.Salt, error)
}
// AuditFormatter implements the Formatter interface, and allows the underlying
// marshaller to be swapped out
type AuditFormatter struct {
AuditFormatWriter
}
var _ Formatter = (*AuditFormatter)(nil)
func (f *AuditFormatter) FormatRequest(ctx context.Context, w io.Writer, config FormatterConfig, in *LogInput) error {
if in == nil || in.Request == nil {
return fmt.Errorf("request to request-audit a nil request")
}
if w == nil {
return fmt.Errorf("writer for audit request is nil")
}
if f.AuditFormatWriter == nil {
return fmt.Errorf("no format writer specified")
}
salt, err := f.Salt(ctx)
if err != nil {
return errwrap.Wrapf("error fetching salt: {{err}}", err)
}
// Set these to the input values at first
auth := in.Auth
req := in.Request
if !config.Raw {
// Before we copy the structure we must nil out some data
// otherwise we will cause reflection to panic and die
if in.Request.Connection != nil && in.Request.Connection.ConnState != nil {
origState := in.Request.Connection.ConnState
in.Request.Connection.ConnState = nil
defer func() {
in.Request.Connection.ConnState = origState
}()
}
// Copy the auth structure
if in.Auth != nil {
cp, err := copystructure.Copy(in.Auth)
if err != nil {
return err
}
auth = cp.(*logical.Auth)
}
cp, err := copystructure.Copy(in.Request)
if err != nil {
return err
}
req = cp.(*logical.Request)
// Hash any sensitive information
if auth != nil {
// Cache and restore accessor in the auth
var authAccessor string
if !config.HMACAccessor && auth.Accessor != "" {
authAccessor = auth.Accessor
}
if err := Hash(salt, auth, nil); err != nil {
return err
}
if authAccessor != "" {
auth.Accessor = authAccessor
}
}
// Cache and restore accessor in the request
var clientTokenAccessor string
if !config.HMACAccessor && req != nil && req.ClientTokenAccessor != "" {
clientTokenAccessor = req.ClientTokenAccessor
}
if err := Hash(salt, req, in.NonHMACReqDataKeys); err != nil {
return err
}
if clientTokenAccessor != "" {
req.ClientTokenAccessor = clientTokenAccessor
}
}
// If auth is nil, make an empty one
if auth == nil {
auth = new(logical.Auth)
}
var errString string
if in.OuterErr != nil {
errString = in.OuterErr.Error()
}
reqEntry := &AuditRequestEntry{
Type: "request",
Error: errString,
Auth: AuditAuth{
ClientToken: auth.ClientToken,
Accessor: auth.Accessor,
DisplayName: auth.DisplayName,
Policies: auth.Policies,
TokenPolicies: auth.TokenPolicies,
IdentityPolicies: auth.IdentityPolicies,
Metadata: auth.Metadata,
EntityID: auth.EntityID,
RemainingUses: req.ClientTokenRemainingUses,
},
Request: AuditRequest{
ID: req.ID,
ClientToken: req.ClientToken,
ClientTokenAccessor: req.ClientTokenAccessor,
Operation: req.Operation,
Path: req.Path,
Data: req.Data,
PolicyOverride: req.PolicyOverride,
RemoteAddr: getRemoteAddr(req),
ReplicationCluster: req.ReplicationCluster,
Headers: req.Headers,
},
}
if req.WrapInfo != nil {
reqEntry.Request.WrapTTL = int(req.WrapInfo.TTL / time.Second)
}
if !config.OmitTime {
reqEntry.Time = time.Now().UTC().Format(time.RFC3339Nano)
}
return f.AuditFormatWriter.WriteRequest(w, reqEntry)
}
func (f *AuditFormatter) FormatResponse(ctx context.Context, w io.Writer, config FormatterConfig, in *LogInput) error {
if in == nil || in.Request == nil {
return fmt.Errorf("request to response-audit a nil request")
}
if w == nil {
return fmt.Errorf("writer for audit request is nil")
}
if f.AuditFormatWriter == nil {
return fmt.Errorf("no format writer specified")
}
salt, err := f.Salt(ctx)
if err != nil {
return errwrap.Wrapf("error fetching salt: {{err}}", err)
}
// Set these to the input values at first
auth := in.Auth
req := in.Request
resp := in.Response
if !config.Raw {
// Before we copy the structure we must nil out some data
// otherwise we will cause reflection to panic and die
if in.Request.Connection != nil && in.Request.Connection.ConnState != nil {
origState := in.Request.Connection.ConnState
in.Request.Connection.ConnState = nil
defer func() {
in.Request.Connection.ConnState = origState
}()
}
// Copy the auth structure
if in.Auth != nil {
cp, err := copystructure.Copy(in.Auth)
if err != nil {
return err
}
auth = cp.(*logical.Auth)
}
cp, err := copystructure.Copy(in.Request)
if err != nil {
return err
}
req = cp.(*logical.Request)
if in.Response != nil {
cp, err := copystructure.Copy(in.Response)
if err != nil {
return err
}
resp = cp.(*logical.Response)
}
// Hash any sensitive information
// Cache and restore accessor in the auth
if auth != nil {
var accessor string
if !config.HMACAccessor && auth.Accessor != "" {
accessor = auth.Accessor
}
if err := Hash(salt, auth, nil); err != nil {
return err
}
if accessor != "" {
auth.Accessor = accessor
}
}
// Cache and restore accessor in the request
var clientTokenAccessor string
if !config.HMACAccessor && req != nil && req.ClientTokenAccessor != "" {
clientTokenAccessor = req.ClientTokenAccessor
}
if err := Hash(salt, req, in.NonHMACReqDataKeys); err != nil {
return err
}
if clientTokenAccessor != "" {
req.ClientTokenAccessor = clientTokenAccessor
}
// Cache and restore accessor in the response
if resp != nil {
var accessor, wrappedAccessor, wrappingAccessor string
if !config.HMACAccessor && resp != nil && resp.Auth != nil && resp.Auth.Accessor != "" {
accessor = resp.Auth.Accessor
}
if !config.HMACAccessor && resp != nil && resp.WrapInfo != nil && resp.WrapInfo.WrappedAccessor != "" {
wrappedAccessor = resp.WrapInfo.WrappedAccessor
wrappingAccessor = resp.WrapInfo.Accessor
}
if err := Hash(salt, resp, in.NonHMACRespDataKeys); err != nil {
return err
}
if accessor != "" {
resp.Auth.Accessor = accessor
}
if wrappedAccessor != "" {
resp.WrapInfo.WrappedAccessor = wrappedAccessor
}
if wrappingAccessor != "" {
resp.WrapInfo.Accessor = wrappingAccessor
}
}
}
// If things are nil, make empty to avoid panics
if auth == nil {
auth = new(logical.Auth)
}
if resp == nil {
resp = new(logical.Response)
}
var errString string
if in.OuterErr != nil {
errString = in.OuterErr.Error()
}
var respAuth *AuditAuth
if resp.Auth != nil {
respAuth = &AuditAuth{
ClientToken: resp.Auth.ClientToken,
Accessor: resp.Auth.Accessor,
DisplayName: resp.Auth.DisplayName,
Policies: resp.Auth.Policies,
TokenPolicies: resp.Auth.TokenPolicies,
IdentityPolicies: resp.Auth.IdentityPolicies,
Metadata: resp.Auth.Metadata,
NumUses: resp.Auth.NumUses,
}
}
var respSecret *AuditSecret
if resp.Secret != nil {
respSecret = &AuditSecret{
LeaseID: resp.Secret.LeaseID,
}
}
var respWrapInfo *AuditResponseWrapInfo
if resp.WrapInfo != nil {
token := resp.WrapInfo.Token
if jwtToken := parseVaultTokenFromJWT(token); jwtToken != nil {
token = *jwtToken
}
respWrapInfo = &AuditResponseWrapInfo{
TTL: int(resp.WrapInfo.TTL / time.Second),
Token: token,
Accessor: resp.WrapInfo.Accessor,
CreationTime: resp.WrapInfo.CreationTime.UTC().Format(time.RFC3339Nano),
CreationPath: resp.WrapInfo.CreationPath,
WrappedAccessor: resp.WrapInfo.WrappedAccessor,
}
}
respEntry := &AuditResponseEntry{
Type: "response",
Error: errString,
Auth: AuditAuth{
DisplayName: auth.DisplayName,
Policies: auth.Policies,
TokenPolicies: auth.TokenPolicies,
IdentityPolicies: auth.IdentityPolicies,
Metadata: auth.Metadata,
ClientToken: auth.ClientToken,
Accessor: auth.Accessor,
RemainingUses: req.ClientTokenRemainingUses,
EntityID: auth.EntityID,
},
Request: AuditRequest{
ID: req.ID,
ClientToken: req.ClientToken,
ClientTokenAccessor: req.ClientTokenAccessor,
Operation: req.Operation,
Path: req.Path,
Data: req.Data,
PolicyOverride: req.PolicyOverride,
RemoteAddr: getRemoteAddr(req),
ReplicationCluster: req.ReplicationCluster,
Headers: req.Headers,
},
Response: AuditResponse{
Auth: respAuth,
Secret: respSecret,
Data: resp.Data,
Redirect: resp.Redirect,
WrapInfo: respWrapInfo,
},
}
if req.WrapInfo != nil {
respEntry.Request.WrapTTL = int(req.WrapInfo.TTL / time.Second)
}
if !config.OmitTime {
respEntry.Time = time.Now().UTC().Format(time.RFC3339Nano)
}
return f.AuditFormatWriter.WriteResponse(w, respEntry)
}
// AuditRequestEntry is the structure of a request audit log entry in Audit.
type AuditRequestEntry struct {
Time string `json:"time,omitempty"`
Type string `json:"type"`
Auth AuditAuth `json:"auth"`
Request AuditRequest `json:"request"`
Error string `json:"error"`
}
// AuditResponseEntry is the structure of a response audit log entry in Audit.
type AuditResponseEntry struct {
Time string `json:"time,omitempty"`
Type string `json:"type"`
Auth AuditAuth `json:"auth"`
Request AuditRequest `json:"request"`
Response AuditResponse `json:"response"`
Error string `json:"error"`
}
type AuditRequest struct {
ID string `json:"id"`
ReplicationCluster string `json:"replication_cluster,omitempty"`
Operation logical.Operation `json:"operation"`
ClientToken string `json:"client_token"`
ClientTokenAccessor string `json:"client_token_accessor"`
Path string `json:"path"`
Data map[string]interface{} `json:"data"`
PolicyOverride bool `json:"policy_override"`
RemoteAddr string `json:"remote_address"`
WrapTTL int `json:"wrap_ttl"`
Headers map[string][]string `json:"headers"`
}
type AuditResponse struct {
Auth *AuditAuth `json:"auth,omitempty"`
Secret *AuditSecret `json:"secret,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Redirect string `json:"redirect,omitempty"`
WrapInfo *AuditResponseWrapInfo `json:"wrap_info,omitempty"`
}
type AuditAuth struct {
ClientToken string `json:"client_token"`
Accessor string `json:"accessor"`
DisplayName string `json:"display_name"`
Policies []string `json:"policies"`
TokenPolicies []string `json:"token_policies,omitempty"`
IdentityPolicies []string `json:"identity_policies,omitempty"`
Metadata map[string]string `json:"metadata"`
NumUses int `json:"num_uses,omitempty"`
RemainingUses int `json:"remaining_uses,omitempty"`
EntityID string `json:"entity_id"`
}
type AuditSecret struct {
LeaseID string `json:"lease_id"`
}
type AuditResponseWrapInfo struct {
TTL int `json:"ttl"`
Token string `json:"token"`
Accessor string `json:"accessor"`
CreationTime string `json:"creation_time"`
CreationPath string `json:"creation_path"`
WrappedAccessor string `json:"wrapped_accessor,omitempty"`
}
// getRemoteAddr safely gets the remote address avoiding a nil pointer
func getRemoteAddr(req *logical.Request) string {
if req != nil && req.Connection != nil {
return req.Connection.RemoteAddr
}
return ""
}
// parseVaultTokenFromJWT returns a string iff the token was a JWT and we could
// extract the original token ID from inside
func parseVaultTokenFromJWT(token string) *string {
if strings.Count(token, ".") != 2 {
return nil
}
wt, err := jws.ParseJWT([]byte(token))
if err != nil || wt == nil {
return nil
}
result, _ := wt.Claims().JWTID()
return &result
}