feat: add plugin metadata to audit logging (#19814)

This commit is contained in:
Thy Ton 2023-04-06 00:41:07 -07:00 committed by GitHub
parent e26aa0aff2
commit fcf06d5874
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 514 additions and 32 deletions

View File

@ -112,13 +112,17 @@ func (f *AuditFormatter) FormatRequest(ctx context.Context, w io.Writer, config
},
Request: &AuditRequest{
ID: req.ID,
ClientID: req.ClientID,
ClientToken: req.ClientToken,
ClientTokenAccessor: req.ClientTokenAccessor,
Operation: req.Operation,
MountType: req.MountType,
MountAccessor: req.MountAccessor,
ID: req.ID,
ClientID: req.ClientID,
ClientToken: req.ClientToken,
ClientTokenAccessor: req.ClientTokenAccessor,
Operation: req.Operation,
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,
@ -311,13 +315,17 @@ func (f *AuditFormatter) FormatResponse(ctx context.Context, w io.Writer, config
},
Request: &AuditRequest{
ID: req.ID,
ClientToken: req.ClientToken,
ClientTokenAccessor: req.ClientTokenAccessor,
ClientID: req.ClientID,
Operation: req.Operation,
MountType: req.MountType,
MountAccessor: req.MountAccessor,
ID: req.ID,
ClientToken: req.ClientToken,
ClientTokenAccessor: req.ClientTokenAccessor,
ClientID: req.ClientID,
Operation: req.Operation,
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,
@ -333,15 +341,19 @@ func (f *AuditFormatter) FormatResponse(ctx context.Context, w io.Writer, config
},
Response: &AuditResponse{
MountType: req.MountType,
MountAccessor: req.MountAccessor,
Auth: respAuth,
Secret: respSecret,
Data: respData,
Warnings: resp.Warnings,
Redirect: resp.Redirect,
WrapInfo: respWrapInfo,
Headers: resp.Headers,
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,
},
}
@ -399,6 +411,10 @@ type AuditRequest struct {
Operation logical.Operation `json:"operation,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"`
@ -413,15 +429,19 @@ type AuditRequest struct {
}
type AuditResponse struct {
Auth *AuditAuth `json:"auth,omitempty"`
MountType string `json:"mount_type,omitempty"`
MountAccessor string `json:"mount_accessor,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"`
Auth *AuditAuth `json:"auth,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 {

3
changelog/19814.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
audit: add plugin metadata, including plugin name, type, version, sha256, and whether plugin is external, to audit logging
```

View File

@ -11,6 +11,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strconv"
"strings"
@ -760,3 +761,180 @@ func TestLogical_ErrRelativePath(t *testing.T) {
t.Errorf("expected response for write to include %q", logical.ErrRelativePath.Error())
}
}
func testBuiltinPluginMetadataAuditLog(t *testing.T, log map[string]interface{}, expectedMountClass string) {
if mountClass, ok := log["mount_class"].(string); !ok {
t.Fatalf("mount_class should be a string, not %T", log["mount_class"])
} else if mountClass != expectedMountClass {
t.Fatalf("bad: mount_class should be %s, not %s", expectedMountClass, mountClass)
}
if _, ok := log["mount_running_version"].(string); !ok {
t.Fatalf("mount_running_version should be a string, not %T", log["mount_running_version"])
}
if _, ok := log["mount_running_sha256"].(string); ok {
t.Fatalf("mount_running_sha256 should be nil, not %T", log["mount_running_sha256"])
}
if mountIsExternalPlugin, ok := log["mount_is_external_plugin"].(bool); ok && mountIsExternalPlugin {
t.Fatalf("mount_is_external_plugin should be nil or false, not %T", log["mount_is_external_plugin"])
}
}
// TestLogical_AuditEnabled_ShouldLogPluginMetadata_Auth tests that we have plugin metadata of a builtin auth plugin
// in audit log when it is enabled
func TestLogical_AuditEnabled_ShouldLogPluginMetadata_Auth(t *testing.T) {
coreConfig := &vault.CoreConfig{
AuditBackends: map[string]audit.Factory{
"file": auditFile.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
core := cores[0].Core
c := cluster.Cores[0].Client
vault.TestWaitActive(t, core)
// Enable the audit backend
tempDir := t.TempDir()
auditLogFile, err := os.CreateTemp(tempDir, "")
if err != nil {
t.Fatal(err)
}
err = c.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{
Type: "file",
Options: map[string]string{
"file_path": auditLogFile.Name(),
},
})
if err != nil {
t.Fatal(err)
}
_, err = c.Logical().Write("auth/token/create", map[string]interface{}{
"ttl": "10s",
})
if err != nil {
t.Fatal(err)
}
// Check the audit trail on request and response
decoder := json.NewDecoder(auditLogFile)
var auditRecord map[string]interface{}
for decoder.Decode(&auditRecord) == nil {
auditRequest := map[string]interface{}{}
if req, ok := auditRecord["request"]; ok {
auditRequest = req.(map[string]interface{})
if auditRequest["path"] != "auth/token/create" {
continue
}
}
testBuiltinPluginMetadataAuditLog(t, auditRequest, consts.PluginTypeCredential.String())
auditResponse := map[string]interface{}{}
if req, ok := auditRecord["response"]; ok {
auditRequest = req.(map[string]interface{})
if auditResponse["path"] != "auth/token/create" {
continue
}
}
testBuiltinPluginMetadataAuditLog(t, auditResponse, consts.PluginTypeCredential.String())
}
}
// TestLogical_AuditEnabled_ShouldLogPluginMetadata_Secret tests that we have plugin metadata of a builtin secret plugin
// in audit log when it is enabled
func TestLogical_AuditEnabled_ShouldLogPluginMetadata_Secret(t *testing.T) {
coreConfig := &vault.CoreConfig{
LogicalBackends: map[string]logical.Factory{
"kv": kv.VersionedKVFactory,
},
AuditBackends: map[string]audit.Factory{
"file": auditFile.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
core := cores[0].Core
c := cluster.Cores[0].Client
vault.TestWaitActive(t, core)
if err := c.Sys().Mount("kv/", &api.MountInput{
Type: "kv-v2",
}); err != nil {
t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err)
}
// Enable the audit backend
tempDir := t.TempDir()
auditLogFile, err := os.CreateTemp(tempDir, "")
if err != nil {
t.Fatal(err)
}
err = c.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{
Type: "file",
Options: map[string]string{
"file_path": auditLogFile.Name(),
},
})
if err != nil {
t.Fatal(err)
}
{
writeData := map[string]interface{}{
"data": map[string]interface{}{
"bar": "a",
},
}
corehelpers.RetryUntil(t, 10*time.Second, func() error {
resp, err := c.Logical().Write("kv/data/foo", writeData)
if err != nil {
t.Fatalf("write request failed, err: %#v, resp: %#v\n", err, resp)
}
return nil
})
}
// Check the audit trail on request and response
decoder := json.NewDecoder(auditLogFile)
var auditRecord map[string]interface{}
for decoder.Decode(&auditRecord) == nil {
auditRequest := map[string]interface{}{}
if req, ok := auditRecord["request"]; ok {
auditRequest = req.(map[string]interface{})
if auditRequest["path"] != "kv/data/foo" {
continue
}
}
testBuiltinPluginMetadataAuditLog(t, auditRequest, consts.PluginTypeSecrets.String())
auditResponse := map[string]interface{}{}
if req, ok := auditRecord["response"]; ok {
auditRequest = req.(map[string]interface{})
if auditResponse["path"] != "kv/data/foo" {
continue
}
}
testBuiltinPluginMetadataAuditLog(t, auditResponse, consts.PluginTypeSecrets.String())
}
}

View File

@ -156,6 +156,22 @@ type Request struct {
// backends can be tied to the mount it belongs to.
MountAccessor string `json:"mount_accessor" structs:"mount_accessor" mapstructure:"mount_accessor" sentinel:""`
// mountRunningVersion is used internally to propagate the semantic version
// of the mounted plugin as reported by its vault.MountEntry to audit logging
mountRunningVersion string
// mountRunningSha256 is used internally to propagate the encoded sha256
// of the mounted plugin as reported its vault.MountEntry to audit logging
mountRunningSha256 string
// mountIsExternalPlugin is used internally to propagate whether
// the backend of the mounted plugin is running externally (i.e., over GRPC)
// to audit logging
mountIsExternalPlugin bool
// mountClass is used internally to propagate the mount class of the mounted plugin to audit logging
mountClass string
// WrapInfo contains requested response wrapping parameters
WrapInfo *RequestWrapInfo `json:"wrap_info" structs:"wrap_info" mapstructure:"wrap_info" sentinel:""`
@ -283,6 +299,38 @@ func (r *Request) SentinelKeys() []string {
}
}
func (r *Request) MountRunningVersion() string {
return r.mountRunningVersion
}
func (r *Request) SetMountRunningVersion(mountRunningVersion string) {
r.mountRunningVersion = mountRunningVersion
}
func (r *Request) MountRunningSha256() string {
return r.mountRunningSha256
}
func (r *Request) SetMountRunningSha256(mountRunningSha256 string) {
r.mountRunningSha256 = mountRunningSha256
}
func (r *Request) MountIsExternalPlugin() bool {
return r.mountIsExternalPlugin
}
func (r *Request) SetMountIsExternalPlugin(mountIsExternalPlugin bool) {
r.mountIsExternalPlugin = mountIsExternalPlugin
}
func (r *Request) MountClass() string {
return r.mountClass
}
func (r *Request) SetMountClass(mountClass string) {
r.mountClass = mountClass
}
func (r *Request) LastRemoteWAL() uint64 {
return r.lastRemoteWAL
}

View File

@ -5,12 +5,16 @@ package plugin_test
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/hashicorp/vault/audit"
auditFile "github.com/hashicorp/vault/builtin/audit/file"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/api/auth/approle"
"github.com/hashicorp/vault/builtin/logical/database"
@ -27,6 +31,35 @@ import (
_ "github.com/jackc/pgx/v4/stdlib"
)
func getClusterWithFileAuditBackend(t *testing.T, typ consts.PluginType, numCores int) *vault.TestCluster {
pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
t.Cleanup(func() { cleanup(t) })
coreConfig := &vault.CoreConfig{
PluginDirectory: pluginDir,
LogicalBackends: map[string]logical.Factory{
"database": database.Factory,
},
AuditBackends: map[string]audit.Factory{
"file": auditFile.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
TempDir: pluginDir,
NumCores: numCores,
Plugins: &vault.TestPluginConfig{
Typ: typ,
Versions: []string{""},
},
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
vault.TestWaitActive(t, cluster.Cores[0].Core)
return cluster
}
func getCluster(t *testing.T, typ consts.PluginType, numCores int) *vault.TestCluster {
pluginDir, cleanup := corehelpers.MakeTestPluginDir(t)
t.Cleanup(func() { cleanup(t) })
@ -826,3 +859,164 @@ CREATE ROLE "{{name}}" WITH
VALID UNTIL '{{expiration}}';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
`
func testExternalPluginMetadataAuditLog(t *testing.T, log map[string]interface{}, expectedMountClass string) {
if mountClass, ok := log["mount_class"].(string); !ok {
t.Fatalf("mount_class should be a string, not %T", log["mount_class"])
} else if mountClass != expectedMountClass {
t.Fatalf("bad: mount_class should be %s, not %s", expectedMountClass, mountClass)
}
if mountIsExternalPlugin, ok := log["mount_is_external_plugin"].(bool); !ok {
t.Fatalf("mount_is_external_plugin should be a bool, not %T", log["mount_is_external_plugin"])
} else if !mountIsExternalPlugin {
t.Fatalf("bad: mount_is_external_plugin should be true, not %t", mountIsExternalPlugin)
}
if _, ok := log["mount_running_sha256"].(string); !ok {
t.Fatalf("mount_running_sha256 should be a string, not %T", log["mount_running_sha256"])
}
}
// TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Auth tests that we have plugin metadata of an auth plugin
// in audit log when it is enabled
func TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Auth(t *testing.T) {
cluster := getClusterWithFileAuditBackend(t, consts.PluginTypeCredential, 1)
defer cluster.Cleanup()
plugin := cluster.Plugins[0]
client := cluster.Cores[0].Client
client.SetToken(cluster.RootToken)
testRegisterAndEnable(t, client, plugin)
// Enable the audit backend
tempDir := t.TempDir()
auditLogFile, err := os.CreateTemp(tempDir, "")
if err != nil {
t.Fatal(err)
}
err = client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{
Type: "file",
Options: map[string]string{
"file_path": auditLogFile.Name(),
},
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write("auth/"+plugin.Name+"/role/role1", map[string]interface{}{
"bind_secret_id": "true",
"period": "300",
})
if err != nil {
t.Fatal(err)
}
// Check the audit trail on request and response
decoder := json.NewDecoder(auditLogFile)
var auditRecord map[string]interface{}
for decoder.Decode(&auditRecord) == nil {
auditRequest := map[string]interface{}{}
if req, ok := auditRecord["request"]; ok {
auditRequest = req.(map[string]interface{})
if auditRequest["path"] != "auth/"+plugin.Name+"/role/role1" {
continue
}
}
testExternalPluginMetadataAuditLog(t, auditRequest, consts.PluginTypeCredential.String())
auditResponse := map[string]interface{}{}
if req, ok := auditRecord["response"]; ok {
auditRequest = req.(map[string]interface{})
if auditResponse["path"] != "auth/"+plugin.Name+"/role/role1" {
continue
}
}
testExternalPluginMetadataAuditLog(t, auditResponse, consts.PluginTypeCredential.String())
}
// Deregister
if err := client.Sys().DeregisterPlugin(&api.DeregisterPluginInput{
Name: plugin.Name,
Type: api.PluginType(plugin.Typ),
Version: plugin.Version,
}); err != nil {
t.Fatal(err)
}
}
// TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Secret tests that we have plugin metadata of a secret plugin
// in audit log when it is enabled
func TestExternalPlugin_AuditEnabled_ShouldLogPluginMetadata_Secret(t *testing.T) {
cluster := getClusterWithFileAuditBackend(t, consts.PluginTypeSecrets, 1)
defer cluster.Cleanup()
plugin := cluster.Plugins[0]
client := cluster.Cores[0].Client
client.SetToken(cluster.RootToken)
testRegisterAndEnable(t, client, plugin)
// Enable the audit backend
tempDir := t.TempDir()
auditLogFile, err := os.CreateTemp(tempDir, "")
if err != nil {
t.Fatal(err)
}
err = client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{
Type: "file",
Options: map[string]string{
"file_path": auditLogFile.Name(),
},
})
if err != nil {
t.Fatal(err)
}
// Configure
cleanupConsul, consulConfig := consul.PrepareTestContainer(t, "", false, true)
defer cleanupConsul()
_, err = client.Logical().Write(plugin.Name+"/config/access", map[string]interface{}{
"address": consulConfig.Address(),
"token": consulConfig.Token,
})
if err != nil {
t.Fatal(err)
}
// Check the audit trail on request and response
decoder := json.NewDecoder(auditLogFile)
var auditRecord map[string]interface{}
for decoder.Decode(&auditRecord) == nil {
auditRequest := map[string]interface{}{}
if req, ok := auditRecord["request"]; ok {
auditRequest = req.(map[string]interface{})
if auditRequest["path"] != plugin.Name+"/config/access" {
continue
}
}
testExternalPluginMetadataAuditLog(t, auditRequest, consts.PluginTypeSecrets.String())
auditResponse := map[string]interface{}{}
if req, ok := auditRecord["response"]; ok {
auditRequest = req.(map[string]interface{})
if auditResponse["path"] != plugin.Name+"/config/access" {
continue
}
}
testExternalPluginMetadataAuditLog(t, auditResponse, consts.PluginTypeSecrets.String())
}
// Deregister
if err := client.Sys().DeregisterPlugin(&api.DeregisterPluginInput{
Name: plugin.Name,
Type: api.PluginType(plugin.Typ),
Version: plugin.Version,
}); err != nil {
t.Fatal(err)
}
}

View File

@ -422,6 +422,25 @@ func (e *MountEntry) Clone() (*MountEntry, error) {
return cp.(*MountEntry), nil
}
// IsExternalPlugin returns whether the plugin is running externally
// if the RunningSha256 is non-empty, the builtin is external. Otherwise, it's builtin
func (e *MountEntry) IsExternalPlugin() bool {
return e.RunningSha256 != ""
}
// MountClass returns the mount class based on Accessor and Path
func (e *MountEntry) MountClass() string {
if e.Accessor == "" || strings.HasPrefix(e.Path, fmt.Sprintf("%s/", systemMountPath)) {
return ""
}
if e.Table == credentialTableType {
return consts.PluginTypeCredential.String()
}
return consts.PluginTypeSecrets.String()
}
// Namespace returns the namespace for the mount entry
func (e *MountEntry) Namespace() *namespace.Namespace {
return e.namespace

View File

@ -850,6 +850,11 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp
if entry != nil {
// Set here so the audit log has it even if authorization fails
req.MountType = entry.Type
req.SetMountRunningSha256(entry.RunningSha256)
req.SetMountRunningVersion(entry.RunningVersion)
req.SetMountIsExternalPlugin(entry.IsExternalPlugin())
req.SetMountClass(entry.MountClass())
// Get and set ignored HMAC'd value.
if rawVals, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok {
nonHMACReqDataKeys = rawVals.([]string)
@ -1270,6 +1275,11 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
if entry != nil {
// Set here so the audit log has it even if authorization fails
req.MountType = entry.Type
req.SetMountRunningSha256(entry.RunningSha256)
req.SetMountRunningVersion(entry.RunningVersion)
req.SetMountIsExternalPlugin(entry.IsExternalPlugin())
req.SetMountClass(entry.MountClass())
// Get and set ignored HMAC'd value.
if rawVals, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok {
nonHMACReqDataKeys = rawVals.([]string)

View File

@ -618,6 +618,11 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc
req.Path = strings.TrimPrefix(ns.Path+req.Path, mount)
req.MountPoint = mount
req.MountType = re.mountEntry.Type
req.SetMountRunningSha256(re.mountEntry.RunningSha256)
req.SetMountRunningVersion(re.mountEntry.RunningVersion)
req.SetMountIsExternalPlugin(re.mountEntry.IsExternalPlugin())
req.SetMountClass(re.mountEntry.MountClass())
if req.Path == "/" {
req.Path = ""
}
@ -733,6 +738,11 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc
req.Path = originalPath
req.MountPoint = mount
req.MountType = re.mountEntry.Type
req.SetMountRunningSha256(re.mountEntry.RunningSha256)
req.SetMountRunningVersion(re.mountEntry.RunningVersion)
req.SetMountIsExternalPlugin(re.mountEntry.IsExternalPlugin())
req.SetMountClass(re.mountEntry.MountClass())
req.Connection = originalConn
req.ID = originalReqID
req.Storage = nil