1c7157e632
with a new endpoint '/sys/audit-hash', which returns the given input string hashed with the given audit backend's hash function and salt (currently, always HMAC-SHA256 and a backend-specific salt). In the process of adding the HTTP handler, this also removes the custom HTTP handlers for the other audit endpoints, which were simply forwarding to the logical system backend. This means that the various audit functions will now redirect correctly from a standby to master. (Tests all pass.) Fixes #784
364 lines
9.4 KiB
Go
364 lines
9.4 KiB
Go
package vault
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/armon/go-metrics"
|
|
"github.com/hashicorp/uuid"
|
|
"github.com/hashicorp/vault/audit"
|
|
"github.com/hashicorp/vault/helper/salt"
|
|
"github.com/hashicorp/vault/logical"
|
|
)
|
|
|
|
const (
|
|
// coreAuditConfigPath is used to store the audit configuration.
|
|
// 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 (
|
|
// loadAuditFailed if loading audit tables encounters an error
|
|
errLoadAuditFailed = errors.New("failed to setup audit table")
|
|
)
|
|
|
|
// enableAudit is used to enable a new audit backend
|
|
func (c *Core) enableAudit(entry *MountEntry) error {
|
|
// Ensure we end the path in a slash
|
|
if !strings.HasSuffix(entry.Path, "/") {
|
|
entry.Path += "/"
|
|
}
|
|
|
|
// Ensure there is a name
|
|
if entry.Path == "/" {
|
|
return fmt.Errorf("backend path must be specified")
|
|
}
|
|
|
|
// Update the audit table
|
|
c.auditLock.Lock()
|
|
defer c.auditLock.Unlock()
|
|
|
|
// Look for matching name
|
|
for _, ent := range c.audit.Entries {
|
|
switch {
|
|
// Existing is sql/mysql/ new is sql/ or
|
|
// existing is sql/ and new is sql/mysql/
|
|
case strings.HasPrefix(ent.Path, entry.Path):
|
|
fallthrough
|
|
case strings.HasPrefix(entry.Path, ent.Path):
|
|
return fmt.Errorf("path already in use")
|
|
}
|
|
}
|
|
|
|
// Generate a new UUID and view
|
|
entry.UUID = uuid.GenerateUUID()
|
|
view := NewBarrierView(c.barrier, auditBarrierPrefix+entry.UUID+"/")
|
|
|
|
// Lookup the new backend
|
|
backend, err := c.newAuditBackend(entry.Type, view, entry.Options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newTable := c.audit.ShallowClone()
|
|
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 {
|
|
// Ensure we end the path in a slash
|
|
if !strings.HasSuffix(path, "/") {
|
|
path += "/"
|
|
}
|
|
|
|
// Remove the entry from the mount table
|
|
c.auditLock.Lock()
|
|
defer c.auditLock.Unlock()
|
|
|
|
newTable := c.audit.ShallowClone()
|
|
found := newTable.Remove(path)
|
|
|
|
// 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 {
|
|
auditTable := &MountTable{}
|
|
|
|
// Load the existing audit table
|
|
raw, err := c.barrier.Get(coreAuditConfigPath)
|
|
if err != nil {
|
|
c.logger.Printf("[ERR] core: failed to read audit table: %v", err)
|
|
return errLoadAuditFailed
|
|
}
|
|
|
|
c.auditLock.Lock()
|
|
defer c.auditLock.Unlock()
|
|
|
|
if raw != nil {
|
|
if err := json.Unmarshal(raw.Value, auditTable); err != nil {
|
|
c.logger.Printf("[ERR] core: failed to decode audit table: %v", err)
|
|
return errLoadAuditFailed
|
|
}
|
|
c.audit = auditTable
|
|
}
|
|
|
|
// Done if we have restored the audit table
|
|
if c.audit != nil {
|
|
return nil
|
|
}
|
|
|
|
// Create and persist the default audit table
|
|
c.audit = defaultAuditTable()
|
|
if err := c.persistAudit(c.audit); err != nil {
|
|
return errLoadAuditFailed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// persistAudit is used to persist the audit table after modification
|
|
func (c *Core) persistAudit(table *MountTable) error {
|
|
// Marshal the table
|
|
raw, err := json.Marshal(table)
|
|
if err != nil {
|
|
c.logger.Printf("[ERR] core: failed to encode audit table: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Create an entry
|
|
entry := &Entry{
|
|
Key: coreAuditConfigPath,
|
|
Value: raw,
|
|
}
|
|
|
|
// Write to the physical backend
|
|
if err := c.barrier.Put(entry); err != nil {
|
|
c.logger.Printf("[ERR] core: failed to persist audit table: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setupAudit is invoked after we've loaded the audit able to
|
|
// initialize the audit backends
|
|
func (c *Core) setupAudits() error {
|
|
broker := NewAuditBroker(c.logger)
|
|
|
|
c.auditLock.Lock()
|
|
defer c.auditLock.Unlock()
|
|
|
|
for _, entry := range c.audit.Entries {
|
|
// Create a barrier view using the UUID
|
|
view := NewBarrierView(c.barrier, auditBarrierPrefix+entry.UUID+"/")
|
|
|
|
// Initialize the backend
|
|
audit, err := c.newAuditBackend(entry.Type, view, entry.Options)
|
|
if err != nil {
|
|
c.logger.Printf(
|
|
"[ERR] core: failed to create audit entry %s: %v",
|
|
entry.Path, err)
|
|
return errLoadAuditFailed
|
|
}
|
|
|
|
// Mount the backend
|
|
broker.Register(entry.Path, audit, view)
|
|
}
|
|
c.auditBroker = broker
|
|
return nil
|
|
}
|
|
|
|
// teardownAudit is used before we seal the vault to reset the audit
|
|
// backends to their unloaded state. This is reversed by loadAudits.
|
|
func (c *Core) teardownAudits() error {
|
|
c.auditLock.Lock()
|
|
defer c.auditLock.Unlock()
|
|
|
|
c.audit = nil
|
|
c.auditBroker = nil
|
|
return nil
|
|
}
|
|
|
|
// newAuditBackend is used to create and configure a new audit backend by name
|
|
func (c *Core) newAuditBackend(t string, view logical.Storage, conf map[string]string) (audit.Backend, error) {
|
|
f, ok := c.auditBackends[t]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown backend type: %s", t)
|
|
}
|
|
salter, err := salt.NewSalt(view, &salt.Config{
|
|
HMAC: sha256.New,
|
|
HMACType: "hmac-sha256",
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("[ERR] core: unable to generate salt: %v", err)
|
|
}
|
|
return f(&audit.BackendConfig{
|
|
Salt: salter,
|
|
Config: conf,
|
|
})
|
|
}
|
|
|
|
// defaultAuditTable creates a default audit table
|
|
func defaultAuditTable() *MountTable {
|
|
table := &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 {
|
|
l sync.RWMutex
|
|
backends map[string]backendEntry
|
|
logger *log.Logger
|
|
}
|
|
|
|
// NewAuditBroker creates a new audit broker
|
|
func NewAuditBroker(log *log.Logger) *AuditBroker {
|
|
b := &AuditBroker{
|
|
backends: make(map[string]backendEntry),
|
|
logger: log,
|
|
}
|
|
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
|
|
}
|
|
|
|
// GetHash returns a hash using the salt of the given backend
|
|
func (a *AuditBroker) GetHash(name string, input string) (string, error) {
|
|
a.l.RLock()
|
|
defer a.l.RUnlock()
|
|
be, ok := a.backends[name]
|
|
if !ok {
|
|
return "", fmt.Errorf("unknown audit backend %s", name)
|
|
}
|
|
|
|
return be.backend.GetHash(input), nil
|
|
}
|
|
|
|
// LogRequest is used to ensure all the audit backends have an opportunity to
|
|
// log the given request and that *at least one* succeeds.
|
|
func (a *AuditBroker) LogRequest(auth *logical.Auth, req *logical.Request, outerErr error) (reterr error) {
|
|
defer metrics.MeasureSince([]string{"audit", "log_request"}, time.Now())
|
|
a.l.RLock()
|
|
defer a.l.RUnlock()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
a.logger.Printf("[ERR] audit: panic logging: req path: %s", req.Path)
|
|
reterr = fmt.Errorf("panic generating audit log")
|
|
}
|
|
}()
|
|
|
|
// Ensure at least one backend logs
|
|
anyLogged := false
|
|
for name, be := range a.backends {
|
|
start := time.Now()
|
|
err := be.backend.LogRequest(auth, req, outerErr)
|
|
metrics.MeasureSince([]string{"audit", name, "log_request"}, start)
|
|
if err != nil {
|
|
a.logger.Printf("[ERR] audit: backend '%s' failed to log request: %v", name, err)
|
|
} else {
|
|
anyLogged = true
|
|
}
|
|
}
|
|
if !anyLogged && len(a.backends) > 0 {
|
|
return fmt.Errorf("no audit backend succeeded in logging the request")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LogResponse is used to ensure all the audit backends have an opportunity to
|
|
// log the given response and that *at least one* succeeds.
|
|
func (a *AuditBroker) LogResponse(auth *logical.Auth, req *logical.Request,
|
|
resp *logical.Response, err error) (reterr error) {
|
|
defer metrics.MeasureSince([]string{"audit", "log_response"}, time.Now())
|
|
a.l.RLock()
|
|
defer a.l.RUnlock()
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
a.logger.Printf("[ERR] audit: panic logging: req path: %s: %v", req.Path, r)
|
|
reterr = fmt.Errorf("panic generating audit log")
|
|
}
|
|
}()
|
|
|
|
// Ensure at least one backend logs
|
|
anyLogged := false
|
|
for name, be := range a.backends {
|
|
start := time.Now()
|
|
err := be.backend.LogResponse(auth, req, resp, err)
|
|
metrics.MeasureSince([]string{"audit", name, "log_response"}, start)
|
|
if err != nil {
|
|
a.logger.Printf("[ERR] audit: backend '%s' failed to log response: %v", name, err)
|
|
} else {
|
|
anyLogged = true
|
|
}
|
|
}
|
|
if !anyLogged && len(a.backends) > 0 {
|
|
return fmt.Errorf("no audit backend succeeded in logging the response")
|
|
}
|
|
return nil
|
|
}
|