559 lines
16 KiB
Go
559 lines
16 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
squarejwt "gopkg.in/square/go-jose.v2/jwt"
|
|
|
|
"github.com/hashicorp/errwrap"
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/sdk/helper/salt"
|
|
"github.com/hashicorp/vault/sdk/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 *logical.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
|
|
var connState *tls.ConnectionState
|
|
|
|
if in.Request.Connection != nil && in.Request.Connection.ConnState != nil {
|
|
connState = in.Request.Connection.ConnState
|
|
}
|
|
|
|
if !config.Raw {
|
|
// Before we copy the structure we must nil out some data
|
|
// otherwise we will cause reflection to panic and die
|
|
if connState != nil {
|
|
in.Request.Connection.ConnState = nil
|
|
defer func() {
|
|
in.Request.Connection.ConnState = connState
|
|
}()
|
|
}
|
|
|
|
// 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)
|
|
for k, v := range req.Data {
|
|
if o, ok := v.(logical.OptMarshaler); ok {
|
|
marshaled, err := o.MarshalJSONWithOptions(&logical.MarshalOptions{
|
|
ValueHasher: salt.GetIdentifiedHMAC,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Data[k] = json.RawMessage(marshaled)
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
ns, err := namespace.FromContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reqType := in.Type
|
|
if reqType == "" {
|
|
reqType = "request"
|
|
}
|
|
reqEntry := &AuditRequestEntry{
|
|
Type: reqType,
|
|
Error: errString,
|
|
|
|
Auth: &AuditAuth{
|
|
ClientToken: auth.ClientToken,
|
|
Accessor: auth.Accessor,
|
|
DisplayName: auth.DisplayName,
|
|
Policies: auth.Policies,
|
|
TokenPolicies: auth.TokenPolicies,
|
|
IdentityPolicies: auth.IdentityPolicies,
|
|
ExternalNamespacePolicies: auth.ExternalNamespacePolicies,
|
|
Metadata: auth.Metadata,
|
|
EntityID: auth.EntityID,
|
|
RemainingUses: req.ClientTokenRemainingUses,
|
|
TokenType: auth.TokenType.String(),
|
|
},
|
|
|
|
Request: &AuditRequest{
|
|
ID: req.ID,
|
|
ClientToken: req.ClientToken,
|
|
ClientTokenAccessor: req.ClientTokenAccessor,
|
|
Operation: req.Operation,
|
|
Namespace: &AuditNamespace{
|
|
ID: ns.ID,
|
|
Path: ns.Path,
|
|
},
|
|
Path: req.Path,
|
|
Data: req.Data,
|
|
PolicyOverride: req.PolicyOverride,
|
|
RemoteAddr: getRemoteAddr(req),
|
|
ReplicationCluster: req.ReplicationCluster,
|
|
Headers: req.Headers,
|
|
ClientCertificateSerialNumber: getClientCertificateSerialNumber(connState),
|
|
},
|
|
}
|
|
|
|
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 *logical.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
|
|
var connState *tls.ConnectionState
|
|
|
|
if in.Request.Connection != nil && in.Request.Connection.ConnState != nil {
|
|
connState = in.Request.Connection.ConnState
|
|
}
|
|
|
|
if !config.Raw {
|
|
// Before we copy the structure we must nil out some data
|
|
// otherwise we will cause reflection to panic and die
|
|
if connState != nil {
|
|
in.Request.Connection.ConnState = nil
|
|
defer func() {
|
|
in.Request.Connection.ConnState = connState
|
|
}()
|
|
}
|
|
|
|
// 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)
|
|
for k, v := range req.Data {
|
|
if o, ok := v.(logical.OptMarshaler); ok {
|
|
marshaled, err := o.MarshalJSONWithOptions(&logical.MarshalOptions{
|
|
ValueHasher: salt.GetIdentifiedHMAC,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Data[k] = json.RawMessage(marshaled)
|
|
}
|
|
}
|
|
|
|
if in.Response != nil {
|
|
cp, err := copystructure.Copy(in.Response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp = cp.(*logical.Response)
|
|
for k, v := range resp.Data {
|
|
if o, ok := v.(logical.OptMarshaler); ok {
|
|
marshaled, err := o.MarshalJSONWithOptions(&logical.MarshalOptions{
|
|
ValueHasher: salt.GetIdentifiedHMAC,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Data[k] = json.RawMessage(marshaled)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
ns, err := namespace.FromContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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,
|
|
ExternalNamespacePolicies: resp.Auth.ExternalNamespacePolicies,
|
|
Metadata: resp.Auth.Metadata,
|
|
NumUses: resp.Auth.NumUses,
|
|
EntityID: resp.Auth.EntityID,
|
|
TokenType: resp.Auth.TokenType.String(),
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
|
|
respType := in.Type
|
|
if respType == "" {
|
|
respType = "response"
|
|
}
|
|
respEntry := &AuditResponseEntry{
|
|
Type: respType,
|
|
Error: errString,
|
|
Auth: &AuditAuth{
|
|
ClientToken: auth.ClientToken,
|
|
Accessor: auth.Accessor,
|
|
DisplayName: auth.DisplayName,
|
|
Policies: auth.Policies,
|
|
TokenPolicies: auth.TokenPolicies,
|
|
IdentityPolicies: auth.IdentityPolicies,
|
|
ExternalNamespacePolicies: auth.ExternalNamespacePolicies,
|
|
Metadata: auth.Metadata,
|
|
RemainingUses: req.ClientTokenRemainingUses,
|
|
EntityID: auth.EntityID,
|
|
TokenType: auth.TokenType.String(),
|
|
},
|
|
|
|
Request: &AuditRequest{
|
|
ID: req.ID,
|
|
ClientToken: req.ClientToken,
|
|
ClientTokenAccessor: req.ClientTokenAccessor,
|
|
Operation: req.Operation,
|
|
Namespace: &AuditNamespace{
|
|
ID: ns.ID,
|
|
Path: ns.Path,
|
|
},
|
|
Path: req.Path,
|
|
Data: req.Data,
|
|
PolicyOverride: req.PolicyOverride,
|
|
RemoteAddr: getRemoteAddr(req),
|
|
ClientCertificateSerialNumber: getClientCertificateSerialNumber(connState),
|
|
ReplicationCluster: req.ReplicationCluster,
|
|
Headers: req.Headers,
|
|
},
|
|
|
|
Response: &AuditResponse{
|
|
Auth: respAuth,
|
|
Secret: respSecret,
|
|
Data: resp.Data,
|
|
Warnings: resp.Warnings,
|
|
Redirect: resp.Redirect,
|
|
WrapInfo: respWrapInfo,
|
|
Headers: resp.Headers,
|
|
},
|
|
}
|
|
|
|
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,omitempty"`
|
|
Auth *AuditAuth `json:"auth,omitempty"`
|
|
Request *AuditRequest `json:"request,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// AuditResponseEntry is the structure of a response audit log entry in Audit.
|
|
type AuditResponseEntry struct {
|
|
Time string `json:"time,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
Auth *AuditAuth `json:"auth,omitempty"`
|
|
Request *AuditRequest `json:"request,omitempty"`
|
|
Response *AuditResponse `json:"response,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type AuditRequest struct {
|
|
ID string `json:"id,omitempty"`
|
|
ReplicationCluster string `json:"replication_cluster,omitempty"`
|
|
Operation logical.Operation `json:"operation,omitempty"`
|
|
ClientToken string `json:"client_token,omitempty"`
|
|
ClientTokenAccessor string `json:"client_token_accessor,omitempty"`
|
|
Namespace *AuditNamespace `json:"namespace,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
PolicyOverride bool `json:"policy_override,omitempty"`
|
|
RemoteAddr string `json:"remote_address,omitempty"`
|
|
WrapTTL int `json:"wrap_ttl,omitempty"`
|
|
Headers map[string][]string `json:"headers,omitempty"`
|
|
ClientCertificateSerialNumber string `json:"client_certificate_serial_number,omitempty"`
|
|
}
|
|
|
|
type AuditResponse struct {
|
|
Auth *AuditAuth `json:"auth,omitempty"`
|
|
Secret *AuditSecret `json:"secret,omitempty"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
Warnings []string `json:"warnings,omitempty"`
|
|
Redirect string `json:"redirect,omitempty"`
|
|
WrapInfo *AuditResponseWrapInfo `json:"wrap_info,omitempty"`
|
|
Headers map[string][]string `json:"headers,omitempty"`
|
|
}
|
|
|
|
type AuditAuth struct {
|
|
ClientToken string `json:"client_token,omitempty"`
|
|
Accessor string `json:"accessor,omitempty"`
|
|
DisplayName string `json:"display_name,omitempty"`
|
|
Policies []string `json:"policies,omitempty"`
|
|
TokenPolicies []string `json:"token_policies,omitempty"`
|
|
IdentityPolicies []string `json:"identity_policies,omitempty"`
|
|
ExternalNamespacePolicies map[string][]string `json:"external_namespace_policies,omitempty"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
NumUses int `json:"num_uses,omitempty"`
|
|
RemainingUses int `json:"remaining_uses,omitempty"`
|
|
EntityID string `json:"entity_id,omitempty"`
|
|
TokenType string `json:"token_type,omitempty"`
|
|
}
|
|
|
|
type AuditSecret struct {
|
|
LeaseID string `json:"lease_id,omitempty"`
|
|
}
|
|
|
|
type AuditResponseWrapInfo struct {
|
|
TTL int `json:"ttl,omitempty"`
|
|
Token string `json:"token,omitempty"`
|
|
Accessor string `json:"accessor,omitempty"`
|
|
CreationTime string `json:"creation_time,omitempty"`
|
|
CreationPath string `json:"creation_path,omitempty"`
|
|
WrappedAccessor string `json:"wrapped_accessor,omitempty"`
|
|
}
|
|
|
|
type AuditNamespace struct {
|
|
ID string `json:"id,omitempty"`
|
|
Path string `json:"path,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 ""
|
|
}
|
|
|
|
func getClientCertificateSerialNumber(connState *tls.ConnectionState) string {
|
|
if connState == nil || len(connState.VerifiedChains) == 0 || len(connState.VerifiedChains[0]) == 0 {
|
|
return ""
|
|
}
|
|
|
|
return connState.VerifiedChains[0][0].SerialNumber.String()
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
parsedJWT, err := squarejwt.ParseSigned(token)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var claims squarejwt.Claims
|
|
if err = parsedJWT.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
|
return nil
|
|
}
|
|
|
|
return &claims.ID
|
|
}
|