// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package audit import ( "context" "crypto/tls" "fmt" "io" "strings" "time" "github.com/go-jose/go-jose/v3/jwt" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/helper/salt" "github.com/hashicorp/vault/sdk/logical" ) type AuditFormatWriter interface { // WriteRequest writes the request entry to the writer or returns an error. WriteRequest(io.Writer, *AuditRequestEntry) error // WriteResponse writes the response entry to the writer or returns an error. WriteResponse(io.Writer, *AuditResponseEntry) error // Salt returns a non-nil salt or an 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 fmt.Errorf("error fetching salt: %w", err) } // Set these to the input values at first auth := in.Auth req := in.Request var connState *tls.ConnectionState if auth == nil { auth = new(logical.Auth) } if in.Request.Connection != nil && in.Request.Connection.ConnState != nil { connState = in.Request.Connection.ConnState } if !config.Raw { auth, err = HashAuth(salt, auth, config.HMACAccessor) if err != nil { return err } req, err = HashRequest(salt, req, config.HMACAccessor, in.NonHMACReqDataKeys) if err != nil { return err } } 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, ForwardedFrom: req.ForwardedFrom, Auth: &AuditAuth{ ClientToken: auth.ClientToken, Accessor: auth.Accessor, DisplayName: auth.DisplayName, Policies: auth.Policies, TokenPolicies: auth.TokenPolicies, IdentityPolicies: auth.IdentityPolicies, ExternalNamespacePolicies: auth.ExternalNamespacePolicies, NoDefaultPolicy: auth.NoDefaultPolicy, Metadata: auth.Metadata, EntityID: auth.EntityID, RemainingUses: req.ClientTokenRemainingUses, TokenType: auth.TokenType.String(), TokenTTL: int64(auth.TTL.Seconds()), }, Request: &AuditRequest{ ID: req.ID, ClientID: req.ClientID, ClientToken: req.ClientToken, ClientTokenAccessor: req.ClientTokenAccessor, Operation: req.Operation, MountPoint: req.MountPoint, MountType: req.MountType, MountAccessor: req.MountAccessor, MountRunningVersion: req.MountRunningVersion(), MountRunningSha256: req.MountRunningSha256(), MountIsExternalPlugin: req.MountIsExternalPlugin(), MountClass: req.MountClass(), Namespace: &AuditNamespace{ ID: ns.ID, Path: ns.Path, }, Path: req.Path, Data: req.Data, PolicyOverride: req.PolicyOverride, RemoteAddr: getRemoteAddr(req), RemotePort: getRemotePort(req), ReplicationCluster: req.ReplicationCluster, Headers: req.Headers, ClientCertificateSerialNumber: getClientCertificateSerialNumber(connState), }, } if !auth.IssueTime.IsZero() { reqEntry.Auth.TokenIssueTime = auth.IssueTime.Format(time.RFC3339) } if auth.PolicyResults != nil { reqEntry.Auth.PolicyResults = &AuditPolicyResults{ Allowed: auth.PolicyResults.Allowed, } for _, p := range auth.PolicyResults.GrantingPolicies { reqEntry.Auth.PolicyResults.GrantingPolicies = append(reqEntry.Auth.PolicyResults.GrantingPolicies, PolicyInfo{ Name: p.Name, NamespaceId: p.NamespaceId, NamespacePath: p.NamespacePath, Type: p.Type, }) } } 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 fmt.Errorf("error fetching salt: %w", err) } // Set these to the input values at first auth, req, resp := in.Auth, in.Request, in.Response if auth == nil { auth = new(logical.Auth) } if resp == nil { resp = new(logical.Response) } var connState *tls.ConnectionState if in.Request.Connection != nil && in.Request.Connection.ConnState != nil { connState = in.Request.Connection.ConnState } elideListResponseData := config.ElideListResponses && req.Operation == logical.ListOperation var respData map[string]interface{} if config.Raw { // In the non-raw case, elision of list response data occurs inside HashResponse, to avoid redundant deep // copies and hashing of data only to elide it later. In the raw case, we need to do it here. if elideListResponseData && resp.Data != nil { // Copy the data map before making changes, but we only need to go one level deep in this case respData = make(map[string]interface{}, len(resp.Data)) for k, v := range resp.Data { respData[k] = v } doElideListResponseData(respData) } else { respData = resp.Data } } else { auth, err = HashAuth(salt, auth, config.HMACAccessor) if err != nil { return err } req, err = HashRequest(salt, req, config.HMACAccessor, in.NonHMACReqDataKeys) if err != nil { return err } resp, err = HashResponse(salt, resp, config.HMACAccessor, in.NonHMACRespDataKeys, elideListResponseData) if err != nil { return err } respData = resp.Data } 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, NoDefaultPolicy: resp.Auth.NoDefaultPolicy, Metadata: resp.Auth.Metadata, NumUses: resp.Auth.NumUses, EntityID: resp.Auth.EntityID, TokenType: resp.Auth.TokenType.String(), TokenTTL: int64(resp.Auth.TTL.Seconds()), } if !resp.Auth.IssueTime.IsZero() { respAuth.TokenIssueTime = resp.Auth.IssueTime.Format(time.RFC3339) } } 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, Forwarded: req.ForwardedFrom != "", Auth: &AuditAuth{ ClientToken: auth.ClientToken, Accessor: auth.Accessor, DisplayName: auth.DisplayName, Policies: auth.Policies, TokenPolicies: auth.TokenPolicies, IdentityPolicies: auth.IdentityPolicies, ExternalNamespacePolicies: auth.ExternalNamespacePolicies, NoDefaultPolicy: auth.NoDefaultPolicy, Metadata: auth.Metadata, RemainingUses: req.ClientTokenRemainingUses, EntityID: auth.EntityID, EntityCreated: auth.EntityCreated, TokenType: auth.TokenType.String(), TokenTTL: int64(auth.TTL.Seconds()), }, Request: &AuditRequest{ ID: req.ID, ClientToken: req.ClientToken, ClientTokenAccessor: req.ClientTokenAccessor, ClientID: req.ClientID, Operation: req.Operation, MountPoint: req.MountPoint, MountType: req.MountType, MountAccessor: req.MountAccessor, MountRunningVersion: req.MountRunningVersion(), MountRunningSha256: req.MountRunningSha256(), MountIsExternalPlugin: req.MountIsExternalPlugin(), MountClass: req.MountClass(), Namespace: &AuditNamespace{ ID: ns.ID, Path: ns.Path, }, Path: req.Path, Data: req.Data, PolicyOverride: req.PolicyOverride, RemoteAddr: getRemoteAddr(req), RemotePort: getRemotePort(req), ClientCertificateSerialNumber: getClientCertificateSerialNumber(connState), ReplicationCluster: req.ReplicationCluster, Headers: req.Headers, }, Response: &AuditResponse{ MountPoint: req.MountPoint, MountType: req.MountType, MountAccessor: req.MountAccessor, MountRunningVersion: req.MountRunningVersion(), MountRunningSha256: req.MountRunningSha256(), MountIsExternalPlugin: req.MountIsExternalPlugin(), MountClass: req.MountClass(), Auth: respAuth, Secret: respSecret, Data: respData, Warnings: resp.Warnings, Redirect: resp.Redirect, WrapInfo: respWrapInfo, Headers: resp.Headers, }, } if auth.PolicyResults != nil { respEntry.Auth.PolicyResults = &AuditPolicyResults{ Allowed: auth.PolicyResults.Allowed, } for _, p := range auth.PolicyResults.GrantingPolicies { respEntry.Auth.PolicyResults.GrantingPolicies = append(respEntry.Auth.PolicyResults.GrantingPolicies, PolicyInfo{ Name: p.Name, NamespaceId: p.NamespaceId, NamespacePath: p.NamespacePath, Type: p.Type, }) } } if !auth.IssueTime.IsZero() { respEntry.Auth.TokenIssueTime = auth.IssueTime.Format(time.RFC3339) } 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"` ForwardedFrom string `json:"forwarded_from,omitempty"` // Populated in Enterprise when a request is forwarded } // 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"` Forwarded bool `json:"forwarded,omitempty"` } type AuditRequest struct { ID string `json:"id,omitempty"` ClientID string `json:"client_id,omitempty"` ReplicationCluster string `json:"replication_cluster,omitempty"` Operation logical.Operation `json:"operation,omitempty"` MountPoint string `json:"mount_point,omitempty"` MountType string `json:"mount_type,omitempty"` MountAccessor string `json:"mount_accessor,omitempty"` MountRunningVersion string `json:"mount_running_version,omitempty"` MountRunningSha256 string `json:"mount_running_sha256,omitempty"` MountClass string `json:"mount_class,omitempty"` MountIsExternalPlugin bool `json:"mount_is_external_plugin,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"` RemotePort int `json:"remote_port,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"` MountPoint string `json:"mount_point,omitempty"` MountType string `json:"mount_type,omitempty"` MountAccessor string `json:"mount_accessor,omitempty"` MountRunningVersion string `json:"mount_running_plugin_version,omitempty"` MountRunningSha256 string `json:"mount_running_sha256,omitempty"` MountClass string `json:"mount_class,omitempty"` MountIsExternalPlugin bool `json:"mount_is_external_plugin,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"` NoDefaultPolicy bool `json:"no_default_policy,omitempty"` PolicyResults *AuditPolicyResults `json:"policy_results,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"` EntityCreated bool `json:"entity_created,omitempty"` TokenType string `json:"token_type,omitempty"` TokenTTL int64 `json:"token_ttl,omitempty"` TokenIssueTime string `json:"token_issue_time,omitempty"` } type AuditPolicyResults struct { Allowed bool `json:"allowed"` GrantingPolicies []PolicyInfo `json:"granting_policies,omitempty"` } type PolicyInfo struct { Name string `json:"name,omitempty"` NamespaceId string `json:"namespace_id,omitempty"` NamespacePath string `json:"namespace_path,omitempty"` Type string `json:"type"` } 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 "" } // getRemotePort safely gets the remote port avoiding a nil pointer func getRemotePort(req *logical.Request) int { if req != nil && req.Connection != nil { return req.Connection.RemotePort } return 0 } 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 := jwt.ParseSigned(token) if err != nil { return nil } var claims jwt.Claims if err = parsedJWT.UnsafeClaimsWithoutVerification(&claims); err != nil { return nil } return &claims.ID } // NewTemporaryFormatter creates a formatter not backed by a persistent salt func NewTemporaryFormatter(format, prefix string) *AuditFormatter { temporarySalt := func(ctx context.Context) (*salt.Salt, error) { return salt.NewNonpersistentSalt(), nil } ret := &AuditFormatter{} switch format { case "jsonx": ret.AuditFormatWriter = &JSONxFormatWriter{ Prefix: prefix, SaltFunc: temporarySalt, } default: ret.AuditFormatWriter = &JSONFormatWriter{ Prefix: prefix, SaltFunc: temporarySalt, } } return ret } // doElideListResponseData performs the actual elision of list operation response data, once surrounding code has // determined it should apply to a particular request. The data map that is passed in must be a copy that is safe to // modify in place, but need not be a full recursive deep copy, as only top-level keys are changed. // // See the documentation of the controlling option in FormatterConfig for more information on the purpose. func doElideListResponseData(data map[string]interface{}) { for k, v := range data { if k == "keys" { if vSlice, ok := v.([]string); ok { data[k] = len(vSlice) } } else if k == "key_info" { if vMap, ok := v.(map[string]interface{}); ok { data[k] = len(vMap) } } } }