Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add plugin metadata to audit logging #19814

Merged
merged 10 commits into from
Apr 6, 2023
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_running_sha256"])
}
}

// 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
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