diff --git a/vault/audit.go b/vault/audit.go index d23e969e91..20556a3f73 100644 --- a/vault/audit.go +++ b/vault/audit.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "fmt" + "strings" + "sync" "github.com/hashicorp/vault/audit" ) @@ -13,6 +15,10 @@ const ( // Audit configuration is protected within the Vault itself, which means it // can only be viewed or modified after an unseal. coreAuditConfigPath = "core/audit" + + // auditBarrierPrefix is the prefix to the UUID used in the + // barrier view for the audit backends. + auditBarrierPrefix = "audit/" ) var ( @@ -20,6 +26,86 @@ var ( loadAuditFailed = errors.New("failed to setup audit table") ) +// enableAudit is used to enable a new audit backend +func (c *Core) enableAudit(entry *MountEntry) error { + c.audit.Lock() + defer c.audit.Unlock() + + // Ensure there is a name + if entry.Path == "" { + return fmt.Errorf("backend path must be specified") + } + if strings.Contains(entry.Path, "/") { + return fmt.Errorf("backend path cannot have a forward slash") + } + + // Look for matching name + for _, ent := range c.audit.Entries { + if ent.Path == entry.Path { + return fmt.Errorf("path already in use") + } + } + + // Lookup the new backend + backend, err := c.newAuditBackend(entry.Type, entry.Options) + if err != nil { + return err + } + + // Generate a new UUID and view + entry.UUID = generateUUID() + view := NewBarrierView(c.barrier, auditBarrierPrefix+entry.UUID+"/") + + // Update the audit table + newTable := c.audit.Clone() + newTable.Entries = append(newTable.Entries, entry) + if err := c.persistAudit(newTable); err != nil { + return errors.New("failed to update audit table") + } + c.audit = newTable + + // Register the backend + c.auditBroker.Register(entry.Path, backend, view) + c.logger.Printf("[INFO] core: enabled audit backend '%s' type: %s", + entry.Path, entry.Type) + return nil +} + +// disableAudit is used to disable an existing audit backend +func (c *Core) disableAudit(path string) error { + c.audit.Lock() + defer c.audit.Unlock() + + // Remove the entry from the mount table + found := false + newTable := c.audit.Clone() + n := len(newTable.Entries) + for i := 0; i < n; i++ { + if newTable.Entries[i].Path == path { + newTable.Entries[i], newTable.Entries[n-1] = newTable.Entries[n-1], nil + newTable.Entries = newTable.Entries[:n-1] + found = true + break + } + } + + // Ensure there was a match + if !found { + return fmt.Errorf("no matching backend") + } + + // Update the audit table + if err := c.persistAudit(newTable); err != nil { + return errors.New("failed to update audit table") + } + c.audit = newTable + + // Unmount the backend + c.auditBroker.Deregister(path) + c.logger.Printf("[INFO] core: disabled audit backend '%s'", path) + return nil +} + // loadAudits is invoked as part of postUnseal to load the audit table func (c *Core) loadAudits() error { // Load the existing audit table @@ -75,7 +161,7 @@ func (c *Core) persistAudit(table *MountTable) error { // setupAudit is invoked after we've loaded the audit able to // initialize the audit backends func (c *Core) setupAudits() error { - var backends []audit.Backend + broker := NewAuditBroker() for _, entry := range c.audit.Entries { // Initialize the backend audit, err := c.newAuditBackend(entry.Type, entry.Options) @@ -86,12 +172,13 @@ func (c *Core) setupAudits() error { return loadAuditFailed } - // Append to the audit entry to the list of backends - backends = append(backends, audit) - } + // Create a barrier view using the UUID + view := NewBarrierView(c.barrier, auditBarrierPrefix+entry.UUID+"/") - // Setup the audit broker - c.auditBroker = NewAuditBroker(backends) + // Mount the backend + broker.Register(entry.Path, audit, view) + } + c.auditBroker = broker return nil } @@ -118,16 +205,47 @@ func defaultAuditTable() *MountTable { return table } +type backendEntry struct { + backend audit.Backend + view *BarrierView +} + // AuditBroker is used to provide a single ingest interface to auditable // events given that multiple backends may be configured. type AuditBroker struct { - backends []audit.Backend + l sync.RWMutex + backends map[string]backendEntry } -// NewAuditBroker creates a new broker given the list of backends -func NewAuditBroker(backends []audit.Backend) *AuditBroker { +// NewAuditBroker creates a new audit broker +func NewAuditBroker() *AuditBroker { b := &AuditBroker{ - backends: backends, + backends: make(map[string]backendEntry), } return b } + +// Register is used to add new audit backend to the broker +func (a *AuditBroker) Register(name string, b audit.Backend, v *BarrierView) { + a.l.Lock() + defer a.l.Unlock() + a.backends[name] = backendEntry{ + backend: b, + view: v, + } +} + +// Deregister is used to remove an audit backend from the broker +func (a *AuditBroker) Deregister(name string) { + a.l.Lock() + defer a.l.Unlock() + delete(a.backends, name) +} + +// IsRegistered is used to check if a given audit backend is registered +func (a *AuditBroker) IsRegistered(name string) bool { + a.l.RLock() + defer a.l.RUnlock() + _, ok := a.backends[name] + return ok +} diff --git a/vault/audit_test.go b/vault/audit_test.go index 2d63ad2e49..fedfc3b2a8 100644 --- a/vault/audit_test.go +++ b/vault/audit_test.go @@ -3,8 +3,110 @@ package vault import ( "reflect" "testing" + + "github.com/hashicorp/vault/audit" ) +type NoopAudit struct{} + +func TestCore_EnableAudit(t *testing.T) { + c, key, _ := TestCoreUnsealed(t) + c.auditBackends["noop"] = func(map[string]string) (audit.Backend, error) { + return &NoopAudit{}, nil + } + + me := &MountEntry{ + Path: "foo", + Type: "noop", + } + err := c.enableAudit(me) + if err != nil { + t.Fatalf("err: %v", err) + } + + if !c.auditBroker.IsRegistered("foo") { + t.Fatalf("missing audit backend") + } + + conf := &CoreConfig{ + Physical: c.physical, + AuditBackends: make(map[string]audit.Factory), + } + conf.AuditBackends["noop"] = func(map[string]string) (audit.Backend, error) { + return &NoopAudit{}, nil + } + c2, err := NewCore(conf) + if err != nil { + t.Fatalf("err: %v", err) + } + unseal, err := c2.Unseal(key) + if err != nil { + t.Fatalf("err: %v", err) + } + if !unseal { + t.Fatalf("should be unsealed") + } + + // Verify matching audit tables + if !reflect.DeepEqual(c.audit, c2.audit) { + t.Fatalf("mismatch: %v %v", c.audit, c2.audit) + } + + // Check for registration + if !c2.auditBroker.IsRegistered("foo") { + t.Fatalf("missing audit backend") + } +} + +func TestCore_DisableAudit(t *testing.T) { + c, key, _ := TestCoreUnsealed(t) + c.auditBackends["noop"] = func(map[string]string) (audit.Backend, error) { + return &NoopAudit{}, nil + } + + err := c.disableAudit("foo") + if err.Error() != "no matching backend" { + t.Fatalf("err: %v", err) + } + + me := &MountEntry{ + Path: "foo", + Type: "noop", + } + err = c.enableAudit(me) + if err != nil { + t.Fatalf("err: %v", err) + } + + err = c.disableAudit("foo") + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check for registration + if c.auditBroker.IsRegistered("foo") { + t.Fatalf("audit backend present") + } + + conf := &CoreConfig{Physical: c.physical} + c2, err := NewCore(conf) + if err != nil { + t.Fatalf("err: %v", err) + } + unseal, err := c2.Unseal(key) + if err != nil { + t.Fatalf("err: %v", err) + } + if !unseal { + t.Fatalf("should be unsealed") + } + + // Verify matching mount tables + if !reflect.DeepEqual(c.audit, c2.audit) { + t.Fatalf("mismatch: %v %v", c.audit, c2.audit) + } +} + func TestCore_DefaultAuditTable(t *testing.T) { c, key, _ := TestCoreUnsealed(t) verifyDefaultAuditTable(t, c.audit)