mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 10:25:42 +03:00
Audit config, seams for enterprise audit features
allow oss to parse sink duration clean up audit sink parsing ent eventer config reload fix typo SetEnabled to eventer interface client acl test rm dead code fix failing test
This commit is contained in:
@@ -74,6 +74,11 @@ func (c *Client) ResolveToken(secretID string) (*acl.ACL, error) {
|
||||
return a, err
|
||||
}
|
||||
|
||||
func (c *Client) ResolveSecretToken(secretID string) (*structs.ACLToken, error) {
|
||||
_, t, err := c.resolveTokenAndACL(secretID)
|
||||
return t, err
|
||||
}
|
||||
|
||||
func (c *Client) resolveTokenAndACL(secretID string) (*acl.ACL, *structs.ACLToken, error) {
|
||||
// Fast-path if ACLs are disabled
|
||||
if !c.config.ACLEnabled {
|
||||
|
||||
@@ -165,3 +165,29 @@ func TestClient_ACL_ResolveToken(t *testing.T) {
|
||||
assert.Equal(t, structs.ErrTokenNotFound, err)
|
||||
assert.Nil(t, out4)
|
||||
}
|
||||
|
||||
func TestClient_ACL_ResolveSecretToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s1, _, _, cleanupS1 := testACLServer(t, nil)
|
||||
defer cleanupS1()
|
||||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
c1, cleanup := TestClient(t, func(c *config.Config) {
|
||||
c.RPCHandler = s1
|
||||
c.ACLEnabled = true
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
token := mock.ACLToken()
|
||||
|
||||
err := s1.State().UpsertACLTokens(110, []*structs.ACLToken{token})
|
||||
assert.Nil(t, err)
|
||||
|
||||
respToken, err := c1.ResolveSecretToken(token.SecretID)
|
||||
assert.Nil(t, err)
|
||||
if assert.NotNil(t, respToken) {
|
||||
assert.NotEmpty(t, respToken.AccessorID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
clientconfig "github.com/hashicorp/nomad/client/config"
|
||||
"github.com/hashicorp/nomad/client/state"
|
||||
"github.com/hashicorp/nomad/command/agent/consul"
|
||||
"github.com/hashicorp/nomad/command/agent/event"
|
||||
"github.com/hashicorp/nomad/helper/pluginutils/loader"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad"
|
||||
@@ -53,6 +54,7 @@ type Agent struct {
|
||||
configLock sync.Mutex
|
||||
|
||||
logger log.InterceptLogger
|
||||
eventer event.Eventer
|
||||
httpLogger log.Logger
|
||||
logOutput io.Writer
|
||||
|
||||
@@ -118,6 +120,9 @@ func NewAgent(config *Config, logger log.InterceptLogger, logOutput io.Writer, i
|
||||
if err := a.setupClient(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.setupEnterpriseAgent(logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if a.client == nil && a.server == nil {
|
||||
return nil, fmt.Errorf("must have at least client or server mode enabled")
|
||||
}
|
||||
@@ -998,6 +1003,13 @@ func (a *Agent) Reload(newConfig *Config) error {
|
||||
a.logger.SetLevel(log.LevelFromString(newConfig.LogLevel))
|
||||
}
|
||||
|
||||
// Update eventer config
|
||||
if newConfig.Audit != nil {
|
||||
if err := a.entReloadEventer(a.config.Audit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fullUpdateTLSConfig := func() {
|
||||
// Completely reload the agent's TLS configuration (moving from non-TLS to
|
||||
// TLS, or vice versa)
|
||||
|
||||
41
command/agent/agent_oss.go
Normal file
41
command/agent/agent_oss.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// +build !ent
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/nomad/command/agent/event"
|
||||
"github.com/hashicorp/nomad/nomad/structs/config"
|
||||
)
|
||||
|
||||
type noOpEventer struct{}
|
||||
|
||||
// Ensure noOpEventer is an Eventer
|
||||
var _ event.Eventer = &noOpEventer{}
|
||||
|
||||
func (e *noOpEventer) Event(ctx context.Context, eventType string, payload interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *noOpEventer) Enabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *noOpEventer) Reopen() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *noOpEventer) SetEnabled(enabled bool) {}
|
||||
|
||||
func (a *Agent) setupEnterpriseAgent(log hclog.Logger) error {
|
||||
// configure eventer
|
||||
a.eventer = &noOpEventer{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) entReloadEventer(cfg *config.AuditConfig) error {
|
||||
return nil
|
||||
}
|
||||
@@ -72,6 +72,7 @@ func (c *Command) readConfig() *Config {
|
||||
},
|
||||
Vault: &config.VaultConfig{},
|
||||
ACL: &ACLConfig{},
|
||||
Audit: &config.AuditConfig{},
|
||||
}
|
||||
|
||||
flags := flag.NewFlagSet("agent", flag.ContinueOnError)
|
||||
|
||||
@@ -169,6 +169,9 @@ type Config struct {
|
||||
// Limits contains the configuration for timeouts.
|
||||
Limits config.Limits `hcl:"limits"`
|
||||
|
||||
// Audit contains the configuration for audit logging.
|
||||
Audit *config.AuditConfig `hcl:"audit"`
|
||||
|
||||
// ExtraKeysHCL is used by hcl to surface unexpected keys
|
||||
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
|
||||
}
|
||||
@@ -865,6 +868,7 @@ func DefaultConfig() *Config {
|
||||
Sentinel: &config.SentinelConfig{},
|
||||
Version: version.GetVersion(),
|
||||
Autopilot: config.DefaultAutopilotConfig(),
|
||||
Audit: &config.AuditConfig{},
|
||||
DisableUpdateCheck: helper.BoolToPtr(false),
|
||||
Limits: config.DefaultLimits(),
|
||||
}
|
||||
@@ -996,6 +1000,14 @@ func (c *Config) Merge(b *Config) *Config {
|
||||
result.ACL = result.ACL.Merge(b.ACL)
|
||||
}
|
||||
|
||||
// Apply the Audit config
|
||||
if result.Audit == nil && b.Audit != nil {
|
||||
audit := *b.Audit
|
||||
result.Audit = &audit
|
||||
} else if b.ACL != nil {
|
||||
result.Audit = result.Audit.Merge(b.Audit)
|
||||
}
|
||||
|
||||
// Apply the ports config
|
||||
if result.Ports == nil && b.Ports != nil {
|
||||
ports := *b.Ports
|
||||
|
||||
@@ -35,6 +35,7 @@ func ParseConfigFile(path string) (*Config, error) {
|
||||
c := &Config{
|
||||
Client: &ClientConfig{ServerJoin: &ServerJoin{}},
|
||||
ACL: &ACLConfig{},
|
||||
Audit: &config.AuditConfig{},
|
||||
Server: &ServerConfig{ServerJoin: &ServerJoin{}},
|
||||
Consul: &config.ConsulConfig{},
|
||||
Autopilot: &config.AutopilotConfig{},
|
||||
@@ -48,7 +49,7 @@ func ParseConfigFile(path string) (*Config, error) {
|
||||
}
|
||||
|
||||
// convert strings to time.Durations
|
||||
err = durations([]td{
|
||||
tds := []td{
|
||||
{"gc_interval", &c.Client.GCInterval, &c.Client.GCIntervalHCL},
|
||||
{"acl.token_ttl", &c.ACL.TokenTTL, &c.ACL.TokenTTLHCL},
|
||||
{"acl.policy_ttl", &c.ACL.PolicyTTL, &c.ACL.PolicyTTLHCL},
|
||||
@@ -61,7 +62,17 @@ func ParseConfigFile(path string) (*Config, error) {
|
||||
{"autopilot.server_stabilization_time", &c.Autopilot.ServerStabilizationTime, &c.Autopilot.ServerStabilizationTimeHCL},
|
||||
{"autopilot.last_contact_threshold", &c.Autopilot.LastContactThreshold, &c.Autopilot.LastContactThresholdHCL},
|
||||
{"telemetry.collection_interval", &c.Telemetry.collectionInterval, &c.Telemetry.CollectionInterval},
|
||||
})
|
||||
}
|
||||
|
||||
// Add enterprise audit sinks for time.Duration parsing
|
||||
for i, sink := range c.Audit.Sinks {
|
||||
tds = append(tds, td{
|
||||
fmt.Sprintf("audit.sink.%d", i), &sink.RotateDuration, &sink.RotateDurationHCL,
|
||||
})
|
||||
}
|
||||
|
||||
// convert strings to time.Durations
|
||||
err = durations(tds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -144,6 +155,17 @@ func extraKeys(c *Config) error {
|
||||
removeEqualFold(&c.Client.ExtraKeysHCL, "host_volume")
|
||||
}
|
||||
|
||||
// Remove AuditConfig extra keys
|
||||
for _, f := range c.Audit.Filters {
|
||||
removeEqualFold(&c.Audit.ExtraKeysHCL, f.Name)
|
||||
removeEqualFold(&c.Audit.ExtraKeysHCL, "filter")
|
||||
}
|
||||
|
||||
for _, s := range c.Audit.Sinks {
|
||||
removeEqualFold(&c.Audit.ExtraKeysHCL, s.Name)
|
||||
removeEqualFold(&c.Audit.ExtraKeysHCL, "sink")
|
||||
}
|
||||
|
||||
for _, k := range []string{"enabled_schedulers", "start_join", "retry_join", "server_join"} {
|
||||
removeEqualFold(&c.ExtraKeysHCL, k)
|
||||
removeEqualFold(&c.ExtraKeysHCL, "server")
|
||||
|
||||
@@ -134,6 +134,31 @@ var basicConfig = &Config{
|
||||
PolicyTTLHCL: "60s",
|
||||
ReplicationToken: "foobar",
|
||||
},
|
||||
Audit: &config.AuditConfig{
|
||||
Enabled: helper.BoolToPtr(true),
|
||||
Sinks: []*config.AuditSink{
|
||||
{
|
||||
DeliveryGuarantee: "enforced",
|
||||
Name: "file",
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Path: "/opt/nomad/audit.log",
|
||||
RotateDuration: 24 * time.Hour,
|
||||
RotateDurationHCL: "24h",
|
||||
RotateBytes: 100,
|
||||
RotateMaxFiles: 10,
|
||||
},
|
||||
},
|
||||
Filters: []*config.AuditFilter{
|
||||
{
|
||||
Name: "default",
|
||||
Type: "HTTPEvent",
|
||||
Endpoints: []string{"/ui/", "/v1/agent/health"},
|
||||
Stages: []string{"*"},
|
||||
Operations: []string{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Telemetry: &Telemetry{
|
||||
StatsiteAddr: "127.0.0.1:1234",
|
||||
StatsdAddr: "127.0.0.1:2345",
|
||||
@@ -389,6 +414,7 @@ func TestConfig_ParseMerge(t *testing.T) {
|
||||
Autopilot: config.DefaultAutopilotConfig(),
|
||||
Client: &ClientConfig{},
|
||||
Server: &ServerConfig{},
|
||||
Audit: &config.AuditConfig{},
|
||||
}
|
||||
merged := oldDefault.Merge(actual)
|
||||
require.Equal(t, basicConfig.Client, merged.Client)
|
||||
@@ -480,6 +506,9 @@ func (c *Config) addDefaults() {
|
||||
if c.ACL == nil {
|
||||
c.ACL = &ACLConfig{}
|
||||
}
|
||||
if c.Audit == nil {
|
||||
c.Audit = &config.AuditConfig{}
|
||||
}
|
||||
if c.Consul == nil {
|
||||
c.Consul = config.DefaultConsulConfig()
|
||||
}
|
||||
@@ -575,6 +604,31 @@ var sample0 = &Config{
|
||||
ACL: &ACLConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
Audit: &config.AuditConfig{
|
||||
Enabled: helper.BoolToPtr(true),
|
||||
Sinks: []*config.AuditSink{
|
||||
{
|
||||
DeliveryGuarantee: "enforced",
|
||||
Name: "file",
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Path: "/opt/nomad/audit.log",
|
||||
RotateDuration: 24 * time.Hour,
|
||||
RotateDurationHCL: "24h",
|
||||
RotateBytes: 100,
|
||||
RotateMaxFiles: 10,
|
||||
},
|
||||
},
|
||||
Filters: []*config.AuditFilter{
|
||||
{
|
||||
Name: "default",
|
||||
Type: "HTTPEvent",
|
||||
Endpoints: []string{"/ui/", "/v1/agent/health"},
|
||||
Stages: []string{"*"},
|
||||
Operations: []string{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Telemetry: &Telemetry{
|
||||
PrometheusMetrics: true,
|
||||
DisableHostname: true,
|
||||
@@ -638,6 +692,31 @@ var sample1 = &Config{
|
||||
ACL: &ACLConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
Audit: &config.AuditConfig{
|
||||
Enabled: helper.BoolToPtr(true),
|
||||
Sinks: []*config.AuditSink{
|
||||
{
|
||||
Name: "file",
|
||||
Type: "file",
|
||||
DeliveryGuarantee: "enforced",
|
||||
Format: "json",
|
||||
Path: "/opt/nomad/audit.log",
|
||||
RotateDuration: 24 * time.Hour,
|
||||
RotateDurationHCL: "24h",
|
||||
RotateBytes: 100,
|
||||
RotateMaxFiles: 10,
|
||||
},
|
||||
},
|
||||
Filters: []*config.AuditFilter{
|
||||
{
|
||||
Name: "default",
|
||||
Type: "HTTPEvent",
|
||||
Endpoints: []string{"/ui/", "/v1/agent/health"},
|
||||
Stages: []string{"*"},
|
||||
Operations: []string{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Telemetry: &Telemetry{
|
||||
PrometheusMetrics: true,
|
||||
DisableHostname: true,
|
||||
|
||||
@@ -34,6 +34,7 @@ func TestConfig_Merge(t *testing.T) {
|
||||
Client: &ClientConfig{},
|
||||
Server: &ServerConfig{},
|
||||
ACL: &ACLConfig{},
|
||||
Audit: &config.AuditConfig{},
|
||||
Ports: &Ports{},
|
||||
Addresses: &Addresses{},
|
||||
AdvertiseAddrs: &AdvertiseAddrs{},
|
||||
@@ -83,6 +84,22 @@ func TestConfig_Merge(t *testing.T) {
|
||||
CirconusBrokerSelectTag: "dc:dc1",
|
||||
PrefixFilter: []string{"filter1", "filter2"},
|
||||
},
|
||||
Audit: &config.AuditConfig{
|
||||
Enabled: helper.BoolToPtr(true),
|
||||
Sinks: []*config.AuditSink{
|
||||
{
|
||||
DeliveryGuarantee: "enforced",
|
||||
Name: "file",
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Path: "/opt/nomad/audit.log",
|
||||
RotateDuration: 24 * time.Hour,
|
||||
RotateDurationHCL: "24h",
|
||||
RotateBytes: 100,
|
||||
RotateMaxFiles: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
Client: &ClientConfig{
|
||||
Enabled: false,
|
||||
StateDir: "/tmp/state1",
|
||||
@@ -213,6 +230,22 @@ func TestConfig_Merge(t *testing.T) {
|
||||
DisableUpdateCheck: helper.BoolToPtr(true),
|
||||
DisableAnonymousSignature: true,
|
||||
BindAddr: "127.0.0.2",
|
||||
Audit: &config.AuditConfig{
|
||||
Enabled: helper.BoolToPtr(true),
|
||||
Sinks: []*config.AuditSink{
|
||||
{
|
||||
DeliveryGuarantee: "enforced",
|
||||
Name: "file",
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Path: "/opt/nomad/audit.log",
|
||||
RotateDuration: 24 * time.Hour,
|
||||
RotateDurationHCL: "24h",
|
||||
RotateBytes: 100,
|
||||
RotateMaxFiles: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
Telemetry: &Telemetry{
|
||||
StatsiteAddr: "127.0.0.2:8125",
|
||||
StatsdAddr: "127.0.0.2:8125",
|
||||
|
||||
19
command/agent/event/event.go
Normal file
19
command/agent/event/event.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Eventer describes the interface that must be implemented by an eventer.
|
||||
type Eventer interface {
|
||||
// Emit and event
|
||||
Event(ctx context.Context, eventType string, payload interface{}) error
|
||||
// Specifies if the eventer is enabled or not
|
||||
Enabled() bool
|
||||
|
||||
// Reopen signals to eventer to reopen any files they have open.
|
||||
Reopen() error
|
||||
|
||||
// SetEnabled sets the eventer to enabled or disabled.
|
||||
SetEnabled(enabled bool)
|
||||
}
|
||||
@@ -31,6 +31,13 @@ const (
|
||||
// ErrEntOnly is the error returned if accessing an enterprise only
|
||||
// endpoint
|
||||
ErrEntOnly = "Nomad Enterprise only endpoint"
|
||||
|
||||
// ContextKeyReqID is a unique ID for a given request
|
||||
ContextKeyReqID = "requestID"
|
||||
|
||||
// MissingRequestID is a placeholder if we cannot retrieve a request
|
||||
// UUID from context
|
||||
MissingRequestID = "<missing request id>"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -49,6 +56,9 @@ var (
|
||||
})
|
||||
)
|
||||
|
||||
type handlerFn func(resp http.ResponseWriter, req *http.Request) (interface{}, error)
|
||||
type handlerByteFn func(resp http.ResponseWriter, req *http.Request) ([]byte, error)
|
||||
|
||||
// HTTPServer is used to wrap an Agent and expose it over an HTTP interface
|
||||
type HTTPServer struct {
|
||||
agent *Agent
|
||||
@@ -380,6 +390,32 @@ func handleRootFallthrough() http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func errCodeFromHandler(err error) (int, string) {
|
||||
if err == nil {
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
code := 500
|
||||
errMsg := err.Error()
|
||||
if http, ok := err.(HTTPCodedError); ok {
|
||||
code = http.Code()
|
||||
} else if ecode, emsg, ok := structs.CodeFromRPCCodedErr(err); ok {
|
||||
code = ecode
|
||||
errMsg = emsg
|
||||
} else {
|
||||
// RPC errors get wrapped, so manually unwrap by only looking at their suffix
|
||||
if strings.HasSuffix(errMsg, structs.ErrPermissionDenied.Error()) {
|
||||
errMsg = structs.ErrPermissionDenied.Error()
|
||||
code = 403
|
||||
} else if strings.HasSuffix(errMsg, structs.ErrTokenNotFound.Error()) {
|
||||
errMsg = structs.ErrTokenNotFound.Error()
|
||||
code = 403
|
||||
}
|
||||
}
|
||||
|
||||
return code, errMsg
|
||||
}
|
||||
|
||||
// wrap is used to wrap functions to make them more convenient
|
||||
func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) func(resp http.ResponseWriter, req *http.Request) {
|
||||
f := func(resp http.ResponseWriter, req *http.Request) {
|
||||
@@ -390,7 +426,7 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque
|
||||
defer func() {
|
||||
s.logger.Debug("request complete", "method", req.Method, "path", reqURL, "duration", time.Now().Sub(start))
|
||||
}()
|
||||
obj, err := handler(resp, req)
|
||||
obj, err := s.auditHandler(handler)(resp, req)
|
||||
|
||||
// Check for an error
|
||||
HAS_ERR:
|
||||
@@ -462,28 +498,11 @@ func (s *HTTPServer) wrapNonJSON(handler func(resp http.ResponseWriter, req *htt
|
||||
defer func() {
|
||||
s.logger.Debug("request complete", "method", req.Method, "path", reqURL, "duration", time.Now().Sub(start))
|
||||
}()
|
||||
obj, err := handler(resp, req)
|
||||
obj, err := s.auditByteHandler(handler)(resp, req)
|
||||
|
||||
// Check for an error
|
||||
if err != nil {
|
||||
code := 500
|
||||
errMsg := err.Error()
|
||||
if http, ok := err.(HTTPCodedError); ok {
|
||||
code = http.Code()
|
||||
} else if ecode, emsg, ok := structs.CodeFromRPCCodedErr(err); ok {
|
||||
code = ecode
|
||||
errMsg = emsg
|
||||
} else {
|
||||
// RPC errors get wrapped, so manually unwrap by only looking at their suffix
|
||||
if strings.HasSuffix(errMsg, structs.ErrPermissionDenied.Error()) {
|
||||
errMsg = structs.ErrPermissionDenied.Error()
|
||||
code = 403
|
||||
} else if strings.HasSuffix(errMsg, structs.ErrTokenNotFound.Error()) {
|
||||
errMsg = structs.ErrTokenNotFound.Error()
|
||||
code = 403
|
||||
}
|
||||
}
|
||||
|
||||
code, errMsg := errCodeFromHandler(err)
|
||||
resp.WriteHeader(code)
|
||||
resp.Write([]byte(errMsg))
|
||||
s.logger.Error("request failed", "method", req.Method, "path", reqURL, "error", err, "code", code)
|
||||
|
||||
@@ -22,3 +22,15 @@ func (s *HTTPServer) registerEnterpriseHandlers() {
|
||||
func (s *HTTPServer) entOnly(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
return nil, CodedError(501, ErrEntOnly)
|
||||
}
|
||||
|
||||
func (s HTTPServer) auditHandler(h handlerFn) handlerFn {
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *HTTPServer) auditByteHandler(h handlerByteFn) handlerByteFn {
|
||||
return h
|
||||
}
|
||||
|
||||
func (s *HTTPServer) auditHTTPHandler(h http.Handler) http.Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
21
command/agent/testdata/basic.hcl
vendored
21
command/agent/testdata/basic.hcl
vendored
@@ -142,6 +142,27 @@ acl {
|
||||
replication_token = "foobar"
|
||||
}
|
||||
|
||||
audit {
|
||||
enabled = true
|
||||
|
||||
sink "file" {
|
||||
type = "file"
|
||||
delivery_guarantee = "enforced"
|
||||
format = "json"
|
||||
path = "/opt/nomad/audit.log"
|
||||
rotate_bytes = 100
|
||||
rotate_duration = "24h"
|
||||
rotate_max_files = 10
|
||||
}
|
||||
|
||||
filter "default" {
|
||||
type = "HTTPEvent"
|
||||
endpoints = ["/ui/", "/v1/agent/health"]
|
||||
stages = ["*"]
|
||||
operations = ["*"]
|
||||
}
|
||||
}
|
||||
|
||||
telemetry {
|
||||
statsite_address = "127.0.0.1:1234"
|
||||
statsd_address = "127.0.0.1:2345"
|
||||
|
||||
28
command/agent/testdata/basic.json
vendored
28
command/agent/testdata/basic.json
vendored
@@ -7,6 +7,34 @@
|
||||
"token_ttl": "60s"
|
||||
}
|
||||
],
|
||||
"audit": {
|
||||
"enabled": true,
|
||||
"sink": [
|
||||
{
|
||||
"file": {
|
||||
"type": "file",
|
||||
"format": "json",
|
||||
"delivery_guarantee": "enforced",
|
||||
"path": "/opt/nomad/audit.log",
|
||||
"rotate_bytes": 100,
|
||||
"rotate_duration": "24h",
|
||||
"rotate_max_files": 10
|
||||
}
|
||||
}
|
||||
],
|
||||
"filter": [
|
||||
{
|
||||
"default": [
|
||||
{
|
||||
"endpoints": ["/ui/", "/v1/agent/health"],
|
||||
"operations": ["*"],
|
||||
"stages": ["*"],
|
||||
"type": "HTTPEvent"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"addresses": [
|
||||
{
|
||||
"http": "127.0.0.1",
|
||||
|
||||
28
command/agent/testdata/sample0.json
vendored
28
command/agent/testdata/sample0.json
vendored
@@ -5,6 +5,34 @@
|
||||
"acl": {
|
||||
"enabled": true
|
||||
},
|
||||
"audit": {
|
||||
"enabled": true,
|
||||
"sink": [
|
||||
{
|
||||
"file": {
|
||||
"type": "file",
|
||||
"format": "json",
|
||||
"delivery_guarantee": "enforced",
|
||||
"path": "/opt/nomad/audit.log",
|
||||
"rotate_bytes": 100,
|
||||
"rotate_duration": "24h",
|
||||
"rotate_max_files": 10
|
||||
}
|
||||
}
|
||||
],
|
||||
"filter": [
|
||||
{
|
||||
"default": [
|
||||
{
|
||||
"endpoints": ["/ui/", "/v1/agent/health"],
|
||||
"operations": ["*"],
|
||||
"stages": ["*"],
|
||||
"type": "HTTPEvent"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"advertise": {
|
||||
"http": "host.example.com",
|
||||
"rpc": "host.example.com",
|
||||
|
||||
21
command/agent/testdata/sample1/sample2.hcl
vendored
21
command/agent/testdata/sample1/sample2.hcl
vendored
@@ -17,3 +17,24 @@
|
||||
vault = {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
audit {
|
||||
enabled = true
|
||||
|
||||
sink "file" {
|
||||
type = "file"
|
||||
format = "json"
|
||||
delivery_guarantee = "enforced"
|
||||
path = "/opt/nomad/audit.log"
|
||||
rotate_bytes = 100
|
||||
rotate_duration = "24h"
|
||||
rotate_max_files = 10
|
||||
}
|
||||
|
||||
filter "default" {
|
||||
type = "HTTPEvent"
|
||||
endpoints = ["/ui/", "/v1/agent/health"]
|
||||
stages = ["*"]
|
||||
operations = ["*"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ job "binstore-storagelocker" {
|
||||
driver = "docker"
|
||||
|
||||
lifecycle {
|
||||
hook = "prestart"
|
||||
hook = "prestart"
|
||||
sidecar = true
|
||||
}
|
||||
|
||||
|
||||
34
nomad/acl.go
34
nomad/acl.go
@@ -84,3 +84,37 @@ func resolveTokenFromSnapshotCache(snap *state.StateSnapshot, cache *lru.TwoQueu
|
||||
}
|
||||
return aclObj, nil
|
||||
}
|
||||
|
||||
// ResolveSecretToken is used to translate an ACL Token Secret ID into
|
||||
// an ACLToken object, nil if ACLs are disabled, or an error.
|
||||
func (s *Server) ResolveSecretToken(secretID string) (*structs.ACLToken, error) {
|
||||
// TODO(Drew) Look into using ACLObject cache or create a separate cache
|
||||
|
||||
// Fast-path if ACLs are disabled
|
||||
if !s.config.ACLEnabled {
|
||||
return nil, nil
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "resolveSecretToken"}, time.Now())
|
||||
|
||||
snap, err := s.fsm.State().Snapshot()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Lookup the ACL Token
|
||||
var token *structs.ACLToken
|
||||
// Handle anonymous requests
|
||||
if secretID == "" {
|
||||
token = structs.AnonymousACLToken
|
||||
} else {
|
||||
token, err = snap.ACLTokenBySecretID(nil, secretID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token == nil {
|
||||
return nil, structs.ErrTokenNotFound
|
||||
}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
@@ -107,3 +107,27 @@ func TestResolveACLToken_LeaderToken(t *testing.T) {
|
||||
assert.True(token.IsManagement())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSecretToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s1, _, cleanupS1 := TestACLServer(t, nil)
|
||||
defer cleanupS1()
|
||||
testutil.WaitForLeader(t, s1.RPC)
|
||||
|
||||
state := s1.State()
|
||||
leaderToken := s1.getLeaderAcl()
|
||||
assert.NotEmpty(t, leaderToken)
|
||||
|
||||
token := mock.ACLToken()
|
||||
|
||||
err := state.UpsertACLTokens(110, []*structs.ACLToken{token})
|
||||
assert.Nil(t, err)
|
||||
|
||||
respToken, err := s1.ResolveSecretToken(token.SecretID)
|
||||
assert.Nil(t, err)
|
||||
if assert.NotNil(t, respToken) {
|
||||
assert.NotEmpty(t, respToken.AccessorID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
213
nomad/structs/config/audit.go
Normal file
213
nomad/structs/config/audit.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
)
|
||||
|
||||
// AuditConfig is the configuration specific to Audit Logging
|
||||
type AuditConfig struct {
|
||||
// Enabled controls the Audit Logging mode
|
||||
Enabled *bool `hcl:"enabled"`
|
||||
|
||||
// Sinks configure output sinks for audit logs
|
||||
Sinks []*AuditSink `hcl:"sink"`
|
||||
|
||||
// Filters configure audit event filters to filter out certain eevents
|
||||
// from being written to a sink.
|
||||
Filters []*AuditFilter `hcl:"filter"`
|
||||
|
||||
// ExtraKeysHCL is used by hcl to surface unexpected keys
|
||||
ExtraKeysHCL []string `hcl:",unusedKeys" json:"-"`
|
||||
}
|
||||
|
||||
type AuditSink struct {
|
||||
// Name is a unique name given to the filter
|
||||
Name string `hcl:",key"`
|
||||
|
||||
// DeliveryGuarantee is the level at which delivery of logs must
|
||||
// be met in order to successfully make requests
|
||||
DeliveryGuarantee string `hcl:"delivery_guarantee"`
|
||||
|
||||
// Type is the sink type to configure. (file)
|
||||
Type string `hcl:"type"`
|
||||
|
||||
// Format is the sink output format. (json)
|
||||
Format string `hcl:"format"`
|
||||
|
||||
// FileName is the name that the audit log should follow.
|
||||
// If rotation is enabled the pattern will be name-timestamp.log
|
||||
Path string `hcl:"path"`
|
||||
|
||||
// RotateDuration is the time period that logs should be rotated in
|
||||
RotateDuration time.Duration
|
||||
RotateDurationHCL string `hcl:"rotate_duration" json:"-"`
|
||||
|
||||
// RotateBytes is the max number of bytes that should be written to a file
|
||||
RotateBytes int `hcl:"rotate_bytes"`
|
||||
|
||||
// RotateMaxFiles is the max number of log files to keep
|
||||
RotateMaxFiles int `hcl:"rotate_max_files"`
|
||||
}
|
||||
|
||||
// AuditFilter is the configuration for a Audit Log Filter
|
||||
type AuditFilter struct {
|
||||
// Name is a unique name given to the filter
|
||||
Name string `hcl:",key"`
|
||||
|
||||
// Type of auditing event to filter, such as HTTPEvent
|
||||
Type string `hcl:"type"`
|
||||
|
||||
// Endpoints is the list of endpoints to include in the filter
|
||||
Endpoints []string `hcl:"endpoints"`
|
||||
|
||||
// State is the auditing request lifecycle stage to filter
|
||||
Stages []string `hcl:"stages"`
|
||||
|
||||
// Operations is the type of operation to filter, such as GET, DELETE
|
||||
Operations []string `hcl:"operations"`
|
||||
}
|
||||
|
||||
// Copy returns a new copy of an AuditConfig
|
||||
func (a *AuditConfig) Copy() *AuditConfig {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nc := new(AuditConfig)
|
||||
*nc = *a
|
||||
|
||||
// Copy bool pointers
|
||||
if a.Enabled != nil {
|
||||
nc.Enabled = helper.BoolToPtr(*a.Enabled)
|
||||
}
|
||||
|
||||
// Copy Sinks and Filters
|
||||
nc.Sinks = copySliceAuditSink(nc.Sinks)
|
||||
nc.Filters = copySliceAuditFilter(nc.Filters)
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
// Merge is used to merge two Audit Configs together. Settings from the input take precedence.
|
||||
func (a *AuditConfig) Merge(b *AuditConfig) *AuditConfig {
|
||||
result := a.Copy()
|
||||
|
||||
if b.Enabled != nil {
|
||||
result.Enabled = helper.BoolToPtr(*b.Enabled)
|
||||
}
|
||||
|
||||
// Merge Sinks
|
||||
if len(a.Sinks) == 0 && len(b.Sinks) != 0 {
|
||||
result.Sinks = copySliceAuditSink(b.Sinks)
|
||||
} else if len(b.Sinks) != 0 {
|
||||
result.Sinks = auditSinkSliceMerge(a.Sinks, b.Sinks)
|
||||
}
|
||||
|
||||
// Merge Filters
|
||||
if len(a.Filters) == 0 && len(b.Filters) != 0 {
|
||||
result.Filters = copySliceAuditFilter(b.Filters)
|
||||
} else if len(b.Filters) != 0 {
|
||||
result.Filters = auditFilterSliceMerge(a.Filters, b.Filters)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (a *AuditSink) Copy() *AuditSink {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nc := new(AuditSink)
|
||||
*nc = *a
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
func (a *AuditFilter) Copy() *AuditFilter {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
nc := new(AuditFilter)
|
||||
*nc = *a
|
||||
|
||||
// Copy slices
|
||||
nc.Endpoints = helper.CopySliceString(nc.Endpoints)
|
||||
nc.Stages = helper.CopySliceString(nc.Stages)
|
||||
nc.Operations = helper.CopySliceString(nc.Operations)
|
||||
|
||||
return nc
|
||||
}
|
||||
|
||||
func copySliceAuditFilter(a []*AuditFilter) []*AuditFilter {
|
||||
l := len(a)
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ns := make([]*AuditFilter, l)
|
||||
for idx, cfg := range a {
|
||||
ns[idx] = cfg.Copy()
|
||||
}
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
func auditFilterSliceMerge(a, b []*AuditFilter) []*AuditFilter {
|
||||
n := make([]*AuditFilter, len(a))
|
||||
seenKeys := make(map[string]int, len(a))
|
||||
|
||||
for i, config := range a {
|
||||
n[i] = config.Copy()
|
||||
seenKeys[config.Name] = i
|
||||
}
|
||||
|
||||
for _, config := range b {
|
||||
if fIndex, ok := seenKeys[config.Name]; ok {
|
||||
n[fIndex] = config.Copy()
|
||||
continue
|
||||
}
|
||||
|
||||
n = append(n, config.Copy())
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func copySliceAuditSink(a []*AuditSink) []*AuditSink {
|
||||
l := len(a)
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ns := make([]*AuditSink, l)
|
||||
for idx, cfg := range a {
|
||||
ns[idx] = cfg.Copy()
|
||||
}
|
||||
|
||||
return ns
|
||||
}
|
||||
|
||||
func auditSinkSliceMerge(a, b []*AuditSink) []*AuditSink {
|
||||
n := make([]*AuditSink, len(a))
|
||||
seenKeys := make(map[string]int, len(a))
|
||||
|
||||
for i, config := range a {
|
||||
n[i] = config.Copy()
|
||||
seenKeys[config.Name] = i
|
||||
}
|
||||
|
||||
for _, config := range b {
|
||||
if fIndex, ok := seenKeys[config.Name]; ok {
|
||||
n[fIndex] = config.Copy()
|
||||
continue
|
||||
}
|
||||
|
||||
n = append(n, config.Copy())
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
106
nomad/structs/config/audit_test.go
Normal file
106
nomad/structs/config/audit_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/nomad/helper"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAuditConfig_Merge(t *testing.T) {
|
||||
c1 := &AuditConfig{
|
||||
Enabled: helper.BoolToPtr(true),
|
||||
Sinks: []*AuditSink{
|
||||
{
|
||||
DeliveryGuarantee: "enforced",
|
||||
Name: "file",
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Path: "/opt/nomad/audit.log",
|
||||
RotateDuration: 24 * time.Hour,
|
||||
RotateDurationHCL: "24h",
|
||||
RotateBytes: 100,
|
||||
RotateMaxFiles: 10,
|
||||
},
|
||||
},
|
||||
Filters: []*AuditFilter{
|
||||
{
|
||||
Name: "one",
|
||||
Type: "HTTPEvent",
|
||||
Endpoints: []string{"/ui/", "/v1/agent/health"},
|
||||
Stages: []string{"*"},
|
||||
Operations: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c2 := &AuditConfig{
|
||||
Sinks: []*AuditSink{
|
||||
{
|
||||
DeliveryGuarantee: "best-effort",
|
||||
Name: "file",
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Path: "/opt/nomad/audit.log",
|
||||
RotateDuration: 48 * time.Hour,
|
||||
RotateDurationHCL: "48h",
|
||||
RotateBytes: 20,
|
||||
RotateMaxFiles: 2,
|
||||
},
|
||||
},
|
||||
Filters: []*AuditFilter{
|
||||
{
|
||||
Name: "one",
|
||||
Type: "HTTPEvent",
|
||||
Endpoints: []string{"/ui/", "/v1/agent/health"},
|
||||
Stages: []string{"OperationReceived"},
|
||||
Operations: []string{"GET"},
|
||||
},
|
||||
{
|
||||
Name: "two",
|
||||
Type: "HTTPEvent",
|
||||
Endpoints: []string{"*"},
|
||||
Stages: []string{"OperationReceived"},
|
||||
Operations: []string{"OPTIONS"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
e := &AuditConfig{
|
||||
Enabled: helper.BoolToPtr(true),
|
||||
Sinks: []*AuditSink{
|
||||
{
|
||||
DeliveryGuarantee: "best-effort",
|
||||
Name: "file",
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
Path: "/opt/nomad/audit.log",
|
||||
RotateDuration: 48 * time.Hour,
|
||||
RotateDurationHCL: "48h",
|
||||
RotateBytes: 20,
|
||||
RotateMaxFiles: 2,
|
||||
},
|
||||
},
|
||||
Filters: []*AuditFilter{
|
||||
{
|
||||
Name: "one",
|
||||
Type: "HTTPEvent",
|
||||
Endpoints: []string{"/ui/", "/v1/agent/health"},
|
||||
Stages: []string{"OperationReceived"},
|
||||
Operations: []string{"GET"},
|
||||
},
|
||||
{
|
||||
Name: "two",
|
||||
Type: "HTTPEvent",
|
||||
Endpoints: []string{"*"},
|
||||
Stages: []string{"OperationReceived"},
|
||||
Operations: []string{"OPTIONS"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := c1.Merge(c2)
|
||||
|
||||
require.Equal(t, e, result)
|
||||
}
|
||||
Reference in New Issue
Block a user