VAULT-17078: Implement Register and Deregister Audit Devices for EventLogger Framework (#21898)

* begin refactoring of event package into audit package

* audit options additions

* rename option structs

* Trying to remove 'audit' from the start of names.

* typo

* typo

* typo

* newEvent required params

* typo

* comments on noop sink

* more refactoring - merge json/jsonx formatters

* fix file backend and tests

* Moved unexported funcs to formatter, fixed file tests

* typos, comments, moved func

* fix corehelpers

* fix backends (syslog, socket)

* Moved some sinks back to generic event package.

* return of the file sink

* remove unneeded sink params/return vars

* Implement Register and Deregister Audit Devices for EventLogger Framework (#21940)

* add function to create StdoutSinkNode

* add boolean argument to audit Factory function

* create eventlogger nodes in backend factory functions

* simplify NewNoopSink function and remove DiscardSinkNode

* make the sanity test in the file backend mutually exclusive based on useEventLogger value

* remove test cases that no longer made sense and were failing

* NewFileSink attempts to open file for sanity check

* fix FileSink tests and update FileSink to remove discard, stdout but add /dev/null

* Moved WithPrefix from FileSink to EventFormatter

* move prefix in backend

* NewFormatterConfig and Options (tests fixed)

* Little tidy up

* add test where audit file is created with useEventLogger set to true

* only create eventlogger.Node instances when useEventLogger is true
fix failing test due to invalid string conversion of FileMode value

* moved variable definition to more appropriate scope

---------

Co-authored-by: Marc Boudreau <marc.boudreau@hashicorp.com>
This commit is contained in:
Peter Wilson
2023-07-24 14:27:09 +01:00
committed by GitHub
parent 4811ef9cc3
commit 050759f661
10 changed files with 319 additions and 65 deletions

View File

@@ -8,6 +8,7 @@ import (
"io"
"time"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
@@ -282,6 +283,12 @@ type Backend interface {
// Invalidate is called for path invalidation
Invalidate(context.Context)
// RegisterNodesAndPipeline provides an eventlogger.Broker pointer so that
// the Backend can call its RegisterNode and RegisterPipeline methods with
// the nodes and the pipeline that were created in the corresponding
// Factory function.
RegisterNodesAndPipeline(*eventlogger.Broker, string) error
}
// BackendConfig contains configuration parameters used in the factory func to

View File

@@ -15,7 +15,9 @@ import (
"sync"
"sync/atomic"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/internal/observability/event"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
)
@@ -46,10 +48,10 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
format, ok := conf.Config["format"]
if !ok {
format = "json"
format = audit.JSONFormat.String()
}
switch format {
case "json", "jsonx":
case audit.JSONFormat.String(), audit.JSONxFormat.String():
default:
return nil, fmt.Errorf("unknown format type %q", format)
}
@@ -102,9 +104,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
}
default:
mode = os.FileMode(m)
}
}
cfg, err := audit.NewFormatterConfig(
@@ -149,15 +149,54 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
}
b.formatter = fw
switch path {
case "stdout", "discard":
// no need to test opening file if outputting to stdout or discarding
default:
// Ensure that the file can be successfully opened for writing;
// otherwise it will be too late to catch later without problems
// (ref: https://github.com/hashicorp/vault/issues/550)
if err := b.open(); err != nil {
return nil, fmt.Errorf("sanity check failed; unable to open %q for writing: %w", path, err)
if useEventLogger {
b.nodeIDList = make([]eventlogger.NodeID, 2)
b.nodeMap = make(map[eventlogger.NodeID]eventlogger.Node)
formatterNodeID, err := event.GenerateNodeID()
if err != nil {
return nil, fmt.Errorf("error generating random NodeID for formatter node: %w", err)
}
b.nodeIDList[0] = formatterNodeID
b.nodeMap[formatterNodeID] = f
var sinkNode eventlogger.Node
switch path {
case "stdout":
sinkNode = event.NewStdoutSinkNode(format)
case "discard":
sinkNode = event.NewNoopSink()
default:
var err error
// The NewFileSink function attempts to open the file and will
// return an error if it can't.
sinkNode, err = event.NewFileSink(b.path, format, event.WithFileMode(strconv.FormatUint(uint64(mode), 8)))
if err != nil {
return nil, fmt.Errorf("file sink creation failed for path %q: %w", path, err)
}
}
sinkNodeID, err := event.GenerateNodeID()
if err != nil {
return nil, fmt.Errorf("error generating random NodeID for sink node: %w", err)
}
b.nodeIDList[1] = sinkNodeID
b.nodeMap[sinkNodeID] = sinkNode
} else {
switch path {
case "stdout":
case "discard":
default:
// Ensure that the file can be successfully opened for writing;
// otherwise it will be too late to catch later without problems
// (ref: https://github.com/hashicorp/vault/issues/550)
if err := b.open(); err != nil {
return nil, fmt.Errorf("sanity check failed; unable to open %q for writing: %w", path, err)
}
}
}
@@ -183,6 +222,9 @@ type Backend struct {
salt *atomic.Value
saltConfig *salt.Config
saltView logical.Storage
nodeIDList []eventlogger.NodeID
nodeMap map[eventlogger.NodeID]eventlogger.Node
}
var _ audit.Backend = (*Backend)(nil)
@@ -238,7 +280,7 @@ func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error {
return b.log(ctx, buf, writer)
}
func (b *Backend) log(ctx context.Context, buf *bytes.Buffer, writer io.Writer) error {
func (b *Backend) log(_ context.Context, buf *bytes.Buffer, writer io.Writer) error {
reader := bytes.NewReader(buf.Bytes())
b.fileLock.Lock()
@@ -304,6 +346,7 @@ func (b *Backend) LogTestMessage(ctx context.Context, in *logical.LogInput, conf
}
var buf bytes.Buffer
temporaryFormatter, err := audit.NewTemporaryFormatter(config["format"], config["prefix"])
if err != nil {
return err
@@ -376,3 +419,21 @@ func (b *Backend) Invalidate(_ context.Context) {
defer b.saltMutex.Unlock()
b.salt.Store((*salt.Salt)(nil))
}
// RegisterNodesAndPipeline registers the nodes and a pipeline as required by
// the audit.Backend interface.
func (b *Backend) RegisterNodesAndPipeline(broker *eventlogger.Broker, name string) error {
for id, node := range b.nodeMap {
if err := broker.RegisterNode(id, node); err != nil {
return err
}
}
pipeline := eventlogger.Pipeline{
PipelineID: eventlogger.PipelineID(name),
EventType: eventlogger.EventType("audit"),
NodeIDs: b.nodeIDList,
}
return broker.RegisterPipeline(pipeline)
}

View File

@@ -25,15 +25,7 @@ func TestAuditFile_fileModeNew(t *testing.T) {
t.Fatal(err)
}
path, err := ioutil.TempDir("", "vault-test_audit_file-file_mode_new")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(path)
file := filepath.Join(path, "auditTest.txt")
file := filepath.Join(t.TempDir(), "auditTest.txt")
config := map[string]string{
"path": file,
"mode": modeStr,
@@ -136,6 +128,40 @@ func TestAuditFile_fileMode0000(t *testing.T) {
}
}
// TestAuditFile_EventLogger_fileModeNew verifies that the Factory function
// correctly sets the file mode when the useEventLogger argument is set to
// true.
func TestAuditFile_EventLogger_fileModeNew(t *testing.T) {
modeStr := "0777"
mode, err := strconv.ParseUint(modeStr, 8, 32)
if err != nil {
t.Fatal(err)
}
file := filepath.Join(t.TempDir(), "auditTest.txt")
config := map[string]string{
"path": file,
"mode": modeStr,
}
_, err = Factory(context.Background(), &audit.BackendConfig{
SaltConfig: &salt.Config{},
SaltView: &logical.InmemStorage{},
Config: config,
}, true)
if err != nil {
t.Fatal(err)
}
info, err := os.Stat(file)
if err != nil {
t.Fatalf("Cannot retrieve file mode from `Stat`")
}
if info.Mode() != os.FileMode(mode) {
t.Fatalf("File mode does not match.")
}
}
func BenchmarkAuditFile_request(b *testing.B) {
config := map[string]string{
"path": "/dev/null",
@@ -174,7 +200,7 @@ func BenchmarkAuditFile_request(b *testing.B) {
},
}
ctx := namespace.RootContext(nil)
ctx := namespace.RootContext(context.Background())
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {

View File

@@ -12,9 +12,11 @@ import (
"sync"
"time"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/internal/observability/event"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
)
@@ -48,10 +50,10 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
format, ok := conf.Config["format"]
if !ok {
format = "json"
format = audit.JSONFormat.String()
}
switch format {
case "json", "jsonx":
case audit.JSONFormat.String(), audit.JSONxFormat.String():
default:
return nil, fmt.Errorf("unknown format type %q", format)
}
@@ -112,9 +114,9 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
}
var w audit.Writer
switch format {
case "json":
case audit.JSONFormat.String():
w = &audit.JSONWriter{Prefix: conf.Config["prefix"]}
case "jsonx":
case audit.JSONxFormat.String():
w = &audit.JSONxWriter{Prefix: conf.Config["prefix"]}
}
@@ -125,6 +127,29 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
b.formatter = fw
if useEventLogger {
b.nodeIDList = make([]eventlogger.NodeID, 2)
b.nodeMap = make(map[eventlogger.NodeID]eventlogger.Node)
formatterNodeID, err := event.GenerateNodeID()
if err != nil {
return nil, fmt.Errorf("error generating random NodeID for formatter node: %w", err)
}
b.nodeIDList[0] = formatterNodeID
b.nodeMap[formatterNodeID] = f
sinkNode, err := event.NewSocketSink(format, address, event.WithSocketType(socketType), event.WithMaxDuration(writeDuration.String()))
if err != nil {
return nil, fmt.Errorf("error creating socket sink node: %w", err)
}
sinkNodeID, err := event.GenerateNodeID()
if err != nil {
return nil, fmt.Errorf("error generating random NodeID for sink node: %w", err)
}
b.nodeIDList[1] = sinkNodeID
b.nodeMap[sinkNodeID] = sinkNode
}
return b, nil
}
@@ -145,6 +170,9 @@ type Backend struct {
salt *salt.Salt
saltConfig *salt.Config
saltView logical.Storage
nodeIDList []eventlogger.NodeID
nodeMap map[eventlogger.NodeID]eventlogger.Node
}
var _ audit.Backend = (*Backend)(nil)
@@ -306,3 +334,21 @@ func (b *Backend) Invalidate(_ context.Context) {
defer b.saltMutex.Unlock()
b.salt = nil
}
// RegisterNodesAndPipeline registers the nodes and a pipeline as required by
// the audit.Backend interface.
func (b *Backend) RegisterNodesAndPipeline(broker *eventlogger.Broker, name string) error {
for id, node := range b.nodeMap {
if err := broker.RegisterNode(id, node); err != nil {
return err
}
}
pipeline := eventlogger.Pipeline{
PipelineID: eventlogger.PipelineID(name),
EventType: eventlogger.EventType("audit"),
NodeIDs: b.nodeIDList,
}
return broker.RegisterPipeline(pipeline)
}

View File

@@ -10,8 +10,10 @@ import (
"strconv"
"sync"
"github.com/hashicorp/eventlogger"
gsyslog "github.com/hashicorp/go-syslog"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/internal/observability/event"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
)
@@ -20,6 +22,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
if conf.SaltConfig == nil {
return nil, fmt.Errorf("nil salt config")
}
if conf.SaltView == nil {
return nil, fmt.Errorf("nil salt view")
}
@@ -38,10 +41,10 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
format, ok := conf.Config["format"]
if !ok {
format = "json"
format = audit.JSONFormat.String()
}
switch format {
case "json", "jsonx":
case audit.JSONFormat.String(), audit.JSONxFormat.String():
default:
return nil, fmt.Errorf("unknown format type %q", format)
}
@@ -106,9 +109,9 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
var w audit.Writer
switch format {
case "json":
case audit.JSONFormat.String():
w = &audit.JSONWriter{Prefix: conf.Config["prefix"]}
case "jsonx":
case audit.JSONxFormat.String():
w = &audit.JSONxWriter{Prefix: conf.Config["prefix"]}
}
@@ -119,6 +122,29 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
b.formatter = fw
if useEventLogger {
b.nodeIDList = make([]eventlogger.NodeID, 2)
b.nodeMap = make(map[eventlogger.NodeID]eventlogger.Node)
formatterNodeID, err := event.GenerateNodeID()
if err != nil {
return nil, fmt.Errorf("error generating random NodeID for formatter node: %w", err)
}
b.nodeIDList[0] = formatterNodeID
b.nodeMap[formatterNodeID] = f
sinkNode, err := event.NewSyslogSink(format, event.WithFacility(facility), event.WithTag(tag))
if err != nil {
return nil, fmt.Errorf("error creating syslog sink node: %w", err)
}
sinkNodeID, err := event.GenerateNodeID()
if err != nil {
return nil, fmt.Errorf("error generating random NodeID for sink node: %w", err)
}
b.nodeIDList[1] = sinkNodeID
b.nodeMap[sinkNodeID] = sinkNode
}
return b, nil
}
@@ -133,6 +159,9 @@ type Backend struct {
salt *salt.Salt
saltConfig *salt.Config
saltView logical.Storage
nodeIDList []eventlogger.NodeID
nodeMap map[eventlogger.NodeID]eventlogger.Node
}
var _ audit.Backend = (*Backend)(nil)
@@ -213,3 +242,21 @@ func (b *Backend) Invalidate(_ context.Context) {
defer b.saltMutex.Unlock()
b.salt = nil
}
// RegisterNodesAndPipeline registers the nodes and a pipeline as required by
// the audit.Backend interface.
func (b *Backend) RegisterNodesAndPipeline(broker *eventlogger.Broker, name string) error {
for id, node := range b.nodeMap {
if err := broker.RegisterNode(id, node); err != nil {
return err
}
}
pipeline := eventlogger.Pipeline{
PipelineID: eventlogger.PipelineID(name),
EventType: eventlogger.EventType("audit"),
NodeIDs: b.nodeIDList,
}
return broker.RegisterPipeline(pipeline)
}

View File

@@ -5,6 +5,9 @@ package event
import (
"fmt"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-uuid"
)
// EventType represents the event's type
@@ -24,3 +27,11 @@ func (et EventType) Validate() error {
return fmt.Errorf("%s: '%s' is not a valid event type: %w", op, et, ErrInvalidParameter)
}
}
// GenerateNodeID generates a new UUID that it casts to the eventlogger.NodeID
// type.
func GenerateNodeID() (eventlogger.NodeID, error) {
id, err := uuid.GenerateUUID()
return eventlogger.NodeID(id), err
}

View File

@@ -156,7 +156,7 @@ func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage
c.audit = newTable
// Register the backend
c.auditBroker.Register(entry.Path, backend, entry.Local, c.IsExperimentEnabled(experiments.VaultExperimentCoreAuditEventsAlpha1))
c.auditBroker.Register(entry.Path, backend, entry.Local)
if c.logger.IsInfo() {
c.logger.Info("enabled audit backend", "path", entry.Path, "type", entry.Type)
}
@@ -208,8 +208,9 @@ func (c *Core) disableAudit(ctx context.Context, path string, updateStorage bool
c.audit = newTable
// Unmount the backend
c.auditBroker.Deregister(path, c.IsExperimentEnabled(experiments.VaultExperimentCoreAuditEventsAlpha1))
// Unmount the backend, any returned error can be ignored since the
// Backend will already have been removed from the AuditBroker's map.
c.auditBroker.Deregister(ctx, path)
if c.logger.IsInfo() {
c.logger.Info("disabled audit backend", "path", path)
}
@@ -383,7 +384,10 @@ func (c *Core) persistAudit(ctx context.Context, table *MountTable, localOnly bo
func (c *Core) setupAudits(ctx context.Context) error {
brokerLogger := c.baseLogger.Named("audit")
c.AddLogger(brokerLogger)
broker := NewAuditBroker(brokerLogger)
broker, err := NewAuditBroker(brokerLogger, c.IsExperimentEnabled(experiments.VaultExperimentCoreAuditEventsAlpha1))
if err != nil {
return err
}
c.auditLock.Lock()
defer c.auditLock.Unlock()
@@ -417,7 +421,7 @@ func (c *Core) setupAudits(ctx context.Context) error {
}
// Mount the backend
broker.Register(entry.Path, backend, entry.Local, c.IsExperimentEnabled(experiments.VaultExperimentCoreAuditEventsAlpha1))
broker.Register(entry.Path, backend, entry.Local)
successCount++
}

View File

@@ -11,6 +11,7 @@ import (
"time"
metrics "github.com/armon/go-metrics"
"github.com/hashicorp/eventlogger"
log "github.com/hashicorp/go-hclog"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/audit"
@@ -28,46 +29,82 @@ type AuditBroker struct {
sync.RWMutex
backends map[string]backendEntry
logger log.Logger
broker *eventlogger.Broker
}
// NewAuditBroker creates a new audit broker
func NewAuditBroker(log log.Logger) *AuditBroker {
func NewAuditBroker(log log.Logger, useEventLogger bool) (*AuditBroker, error) {
var eventBroker *eventlogger.Broker
var err error
if useEventLogger {
// Ignoring the second error return value since an error will only occur
// if an unrecognized eventlogger.RegistrationPolicy is provided to an
// eventlogger.Option function.
eventBroker, err = eventlogger.NewBroker(eventlogger.WithNodeRegistrationPolicy(eventlogger.DenyOverwrite), eventlogger.WithPipelineRegistrationPolicy(eventlogger.DenyOverwrite))
if err != nil {
return nil, fmt.Errorf("error creating event broker for audit events: %w", err)
}
}
b := &AuditBroker{
backends: make(map[string]backendEntry),
logger: log,
broker: eventBroker,
}
return b
return b, nil
}
// Register is used to add new audit backend to the broker
func (a *AuditBroker) Register(name string, b audit.Backend, local bool, useEventLogger bool) {
if useEventLogger {
// TODO: Coming soon
} else {
a.Lock()
defer a.Unlock()
a.backends[name] = backendEntry{
backend: b,
local: local,
func (a *AuditBroker) Register(name string, b audit.Backend, local bool) error {
a.Lock()
defer a.Unlock()
if a.broker != nil {
err := b.RegisterNodesAndPipeline(a.broker, name)
if err != nil {
return err
}
}
a.backends[name] = backendEntry{
backend: b,
local: local,
}
return nil
}
// Deregister is used to remove an audit backend from the broker
func (a *AuditBroker) Deregister(name string, useEventLogger bool) {
if useEventLogger {
// TODO: Coming soon
} else {
a.Lock()
defer a.Unlock()
delete(a.backends, name)
func (a *AuditBroker) Deregister(ctx context.Context, name string) error {
a.Lock()
defer a.Unlock()
// Remove the Backend from the map first, so that if an error occurs while
// removing the pipeline and nodes, we can quickly exit this method with
// the error.
delete(a.backends, name)
if a.broker != nil {
// The first return value, a bool, indicates whether
// RemovePipelineAndNodes encountered the error while evaluating
// pre-conditions (false) or once it started removing the pipeline and
// the nodes (true). This code doesn't care either way.
_, err := a.broker.RemovePipelineAndNodes(ctx, eventlogger.EventType("audit"), eventlogger.PipelineID(name))
if err != nil {
return err
}
}
return nil
}
// IsRegistered is used to check if a given audit backend is registered
func (a *AuditBroker) IsRegistered(name string) bool {
a.RLock()
defer a.RUnlock()
_, ok := a.backends[name]
return ok
}
@@ -99,8 +136,10 @@ func (a *AuditBroker) GetHash(ctx context.Context, name string, input string) (s
// log the given request and that *at least one* succeeds.
func (a *AuditBroker) LogRequest(ctx context.Context, in *logical.LogInput, headersConfig *AuditedHeadersConfig) (ret error) {
defer metrics.MeasureSince([]string{"audit", "log_request"}, time.Now())
a.RLock()
defer a.RUnlock()
if in.Request.InboundSSCToken != "" {
if in.Auth != nil {
reqAuthToken := in.Auth.ClientToken

View File

@@ -340,11 +340,14 @@ func verifyDefaultAuditTable(t *testing.T, table *MountTable) {
func TestAuditBroker_LogRequest(t *testing.T) {
l := logging.NewVaultLogger(log.Trace)
b := NewAuditBroker(l)
b, err := NewAuditBroker(l, false)
if err != nil {
t.Fatal(err)
}
a1 := corehelpers.TestNoopAudit(t, nil)
a2 := corehelpers.TestNoopAudit(t, nil)
b.Register("foo", a1, false, false)
b.Register("bar", a2, false, false)
b.Register("foo", a1, false)
b.Register("bar", a2, false)
auth := &logical.Auth{
ClientToken: "foo",
@@ -427,11 +430,14 @@ func TestAuditBroker_LogRequest(t *testing.T) {
func TestAuditBroker_LogResponse(t *testing.T) {
l := logging.NewVaultLogger(log.Trace)
b := NewAuditBroker(l)
b, err := NewAuditBroker(l, false)
if err != nil {
t.Fatal(err)
}
a1 := corehelpers.TestNoopAudit(t, nil)
a2 := corehelpers.TestNoopAudit(t, nil)
b.Register("foo", a1, false, false)
b.Register("bar", a2, false, false)
b.Register("foo", a1, false)
b.Register("bar", a2, false)
auth := &logical.Auth{
NumUses: 10,
@@ -532,13 +538,16 @@ func TestAuditBroker_LogResponse(t *testing.T) {
func TestAuditBroker_AuditHeaders(t *testing.T) {
logger := logging.NewVaultLogger(log.Trace)
b := NewAuditBroker(logger)
b, err := NewAuditBroker(logger, false)
if err != nil {
t.Fatal(err)
}
_, barrier, _ := mockBarrier(t)
view := NewBarrierView(barrier, "headers/")
a1 := corehelpers.TestNoopAudit(t, nil)
a2 := corehelpers.TestNoopAudit(t, nil)
b.Register("foo", a1, false, false)
b.Register("bar", a2, false, false)
b.Register("foo", a1, false)
b.Register("bar", a2, false)
auth := &logical.Auth{
ClientToken: "foo",

View File

@@ -2365,7 +2365,11 @@ func (s standardUnsealStrategy) unseal(ctx context.Context, logger log.Logger, c
return err
}
} else {
c.auditBroker = NewAuditBroker(c.logger)
var err error
c.auditBroker, err = NewAuditBroker(c.logger, c.IsExperimentEnabled(experiments.VaultExperimentCoreAuditEventsAlpha1))
if err != nil {
return err
}
}
if !c.ReplicationState().HasState(consts.ReplicationPerformanceSecondary | consts.ReplicationDRSecondary) {