Skip to content

Commit

Permalink
feat: add plugin metadata to audit logging (hashicorp#19814)
Browse files Browse the repository at this point in the history
  • Loading branch information
thyton authored Apr 6, 2023
1 parent ecf3f44 commit 0e9b3b0
Show file tree
Hide file tree
Showing 8 changed files with 514 additions and 32 deletions.
84 changes: 52 additions & 32 deletions audit/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
},
}

Expand Down Expand Up @@ -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"`
Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions changelog/19814.txt
Original file line number Diff line number Diff line change
@@ -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
```
178 changes: 178 additions & 0 deletions http/logical_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strconv"
"strings"
Expand Down Expand Up @@ -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())
}
}
48 changes: 48 additions & 0 deletions sdk/logical/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:""`

Expand Down Expand Up @@ -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
}
Expand Down
Loading

0 comments on commit 0e9b3b0

Please sign in to comment.