Skip to content

Commit

Permalink
[Auditbeat] Make detecting password changes optional (elastic#9461)
Browse files Browse the repository at this point in the history
Introduces a `user.detect_password_changes` config parameter that defaults to true in the config, but false in the code. Only if it is set to true will the code read the password field in /etc/passwd and /etc/shadow to detect password changes.

The read password field values are put through 10 round of SHA-512 hashing before being locally stored.
  • Loading branch information
Christoph Wurm committed Dec 15, 2018
1 parent 8b3d5a8 commit 9d68625
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 93 deletions.
5 changes: 5 additions & 0 deletions x-pack/auditbeat/auditbeat.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ auditbeat.modules:

state.period: 12h

# Enabled by default. Auditbeat will read password fields in
# /etc/passwd and /etc/shadow and store a hash locally to
# detect any changes.
user.detect_password_changes: true

report_changes: true


Expand Down
5 changes: 5 additions & 0 deletions x-pack/auditbeat/auditbeat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ auditbeat.modules:

state.period: 12h

# Enabled by default. Auditbeat will read password fields in
# /etc/passwd and /etc/shadow and store a hash locally to
# detect any changes.
user.detect_password_changes: true

report_changes: true


Expand Down
7 changes: 7 additions & 0 deletions x-pack/auditbeat/module/system/_meta/config.yml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@

state.period: 12h

{{ if eq .GOOS "linux" -}}
# Enabled by default. Auditbeat will read password fields in
# /etc/passwd and /etc/shadow and store a hash locally to
# detect any changes.
user.detect_password_changes: true
{{- end }}

report_changes: true
{{- end }}
{{ if .Reference }}
Expand Down
23 changes: 11 additions & 12 deletions x-pack/auditbeat/module/system/user/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,23 @@ import (
"time"
)

// Config defines the metricset's configuration options.
type Config struct {
StatePeriod time.Duration `config:"state.period"`
UserStatePeriod time.Duration `config:"user.state.period"`
// config defines the metricset's configuration options.
type config struct {
StatePeriod time.Duration `config:"state.period"`
UserStatePeriod time.Duration `config:"user.state.period"`
DetectPasswordChanges bool `config:"user.detect_password_changes"`
}

// Validate validates the host metricset config.
func (c *Config) Validate() error {
return nil
}

func (c *Config) effectiveStatePeriod() time.Duration {
func (c *config) effectiveStatePeriod() time.Duration {
if c.UserStatePeriod != 0 {
return c.UserStatePeriod
}
return c.StatePeriod
}

var defaultConfig = Config{
StatePeriod: 12 * time.Hour,
func defaultConfig() config {
return config{
StatePeriod: 12 * time.Hour,
DetectPasswordChanges: false,
}
}
148 changes: 92 additions & 56 deletions x-pack/auditbeat/module/system/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package user

import (
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"io"
Expand All @@ -32,6 +33,10 @@ const (
moduleName = "system"
metricsetName = "user"

passwdFile = "/etc/passwd"
groupFile = "/etc/group"
shadowFile = "/etc/shadow"

bucketName = "user.v1"
bucketKeyUsers = "users"
bucketKeyStateTimestamp = "state_timestamp"
Expand All @@ -46,10 +51,35 @@ const (
eventActionPasswordChanged = "password_changed"
)

type passwordType uint8

const (
detectionDisabled passwordType = iota
shadowPassword
passwordDisabled
noPassword
cryptPassword
)

func (t passwordType) String() string {
switch t {
case shadowPassword:
return "shadow_password"
case passwordDisabled:
return "password_disabled"
case noPassword:
return "no_password"
case cryptPassword:
return "crypt_password"
default:
return ""
}
}

// User represents a user. Fields according to getpwent(3).
type User struct {
Name string
PasswordType string
PasswordType passwordType
PasswordChanged time.Time
PasswordHashHash []byte
UID uint32
Expand All @@ -72,7 +102,7 @@ func (user User) Hash() uint64 {
h := xxhash.New64()
// Use everything except userInfo
h.WriteString(user.Name)
h.WriteString(user.PasswordType)
binary.Write(h, binary.BigEndian, uint8(user.PasswordType))
h.WriteString(user.PasswordChanged.String())
h.Write(user.PasswordHashHash)
h.WriteString(strconv.Itoa(int(user.UID)))
Expand All @@ -90,10 +120,7 @@ func (user User) Hash() uint64 {

func (user User) toMapStr() common.MapStr {
evt := common.MapStr{
"name": user.Name,
"password": common.MapStr{
"type": user.PasswordType,
},
"name": user.Name,
"uid": user.UID,
"gid": user.GID,
"dir": user.Dir,
Expand All @@ -104,6 +131,10 @@ func (user User) toMapStr() common.MapStr {
evt.Put("user_information", user.UserInfo)
}

if user.PasswordType != detectionDisabled {
evt.Put("password.type", user.PasswordType.String())
}

if !user.PasswordChanged.IsZero() {
evt.Put("password.last_changed", user.PasswordChanged)
}
Expand Down Expand Up @@ -131,12 +162,13 @@ func init() {
// MetricSet collects data about a system's users.
type MetricSet struct {
mb.BaseMetricSet
config Config
log *logp.Logger
cache *cache.Cache
bucket datastore.Bucket
lastState time.Time
lastChange time.Time
config config
log *logp.Logger
cache *cache.Cache
bucket datastore.Bucket
lastState time.Time
userFiles []string
lastRead time.Time
}

// New constructs a new MetricSet.
Expand All @@ -146,7 +178,7 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) {
return nil, fmt.Errorf("the %v/%v dataset is only supported on Linux", moduleName, metricsetName)
}

config := defaultConfig
config := defaultConfig()
if err := base.Module().UnpackConfig(&config); err != nil {
return nil, errors.Wrapf(err, "failed to unpack the %v/%v config", moduleName, metricsetName)
}
Expand All @@ -164,6 +196,12 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) {
bucket: bucket,
}

if ms.config.DetectPasswordChanges {
ms.userFiles = []string{passwdFile, groupFile, shadowFile}
} else {
ms.userFiles = []string{passwdFile, groupFile}
}

// Load from disk: Time when state was last sent
err = bucket.Load(bucketKeyStateTimestamp, func(blob []byte) error {
if len(blob) > 0 {
Expand Down Expand Up @@ -224,7 +262,7 @@ func (ms *MetricSet) Fetch(report mb.ReporterV2) {
func (ms *MetricSet) reportState(report mb.ReporterV2) error {
ms.lastState = time.Now()

users, err := GetUsers()
users, err := GetUsers(ms.config.DetectPasswordChanges)
if err != nil {
return errors.Wrap(err, "failed to get users")
}
Expand Down Expand Up @@ -261,16 +299,21 @@ func (ms *MetricSet) reportState(report mb.ReporterV2) error {
// reportChanges detects and reports any changes to users on this system since the last call.
func (ms *MetricSet) reportChanges(report mb.ReporterV2) error {
currentTime := time.Now()
changed, err := haveFilesChanged(ms.lastChange)
if err != nil {
return err
}
if !changed {
return nil

// If this is not the first call to Fetch/reportChanges,
// check if files have changed since the last time before going any further.
if !ms.lastRead.IsZero() {
changed, err := ms.haveFilesChanged()
if err != nil {
return err
}
if !changed {
return nil
}
}
ms.lastChange = currentTime
ms.lastRead = currentTime

users, err := GetUsers()
users, err := GetUsers(ms.config.DetectPasswordChanges)
if err != nil {
return errors.Wrap(err, "failed to get users")
}
Expand All @@ -287,25 +330,31 @@ func (ms *MetricSet) reportChanges(report mb.ReporterV2) error {

for _, userFromCache := range newInCache {
newUser := userFromCache.(*User)
matchingMissingUser, found := missingUserMap[newUser.UID]
oldUser, found := missingUserMap[newUser.UID]

if found {
// Report password change separately
if newUser.PasswordChanged.Before(matchingMissingUser.PasswordChanged) ||
!bytes.Equal(newUser.PasswordHashHash, matchingMissingUser.PasswordHashHash) ||
newUser.PasswordType != matchingMissingUser.PasswordType {
report.Event(userEvent(newUser, eventTypeEvent, eventActionPasswordChanged))
if ms.config.DetectPasswordChanges && newUser.PasswordType != detectionDisabled &&
oldUser.PasswordType != detectionDisabled {

passwordChanged := newUser.PasswordChanged.Before(oldUser.PasswordChanged) ||
!bytes.Equal(newUser.PasswordHashHash, oldUser.PasswordHashHash) ||
newUser.PasswordType != oldUser.PasswordType

if passwordChanged {
report.Event(userEvent(newUser, eventTypeEvent, eventActionPasswordChanged))
}
}

// Hack to check if only the password changed
matchingMissingUser.PasswordChanged = newUser.PasswordChanged
matchingMissingUser.PasswordHashHash = newUser.PasswordHashHash
matchingMissingUser.PasswordType = newUser.PasswordType
if newUser.Hash() != matchingMissingUser.Hash() {
oldUser.PasswordChanged = newUser.PasswordChanged
oldUser.PasswordHashHash = newUser.PasswordHashHash
oldUser.PasswordType = newUser.PasswordType
if newUser.Hash() != oldUser.Hash() {
report.Event(userEvent(newUser, eventTypeEvent, eventActionUserChanged))
}

delete(missingUserMap, matchingMissingUser.UID)
delete(missingUserMap, oldUser.UID)
} else {
report.Event(userEvent(newUser, eventTypeEvent, eventActionUserAdded))
}
Expand Down Expand Up @@ -409,33 +458,20 @@ func (ms *MetricSet) saveUsersToDisk(users []*User) error {
return nil
}

// haveFilesChanged checks if any of the relevant files (/etc/passwd, /etc/shadow, /etc/group)
// have changed.
func haveFilesChanged(since time.Time) (bool, error) {
const passwdFile = "/etc/passwd"
const shadowFile = "/etc/shadow"
const groupFile = "/etc/group"

// haveFilesChanged checks if the ctime of any of the user files has changed.
func (ms *MetricSet) haveFilesChanged() (bool, error) {
var stats syscall.Stat_t
if err := syscall.Stat(passwdFile, &stats); err != nil {
return true, errors.Wrapf(err, "failed to stat %v", passwdFile)
}
if since.Before(time.Unix(stats.Ctim.Sec, stats.Ctim.Nsec)) {
return true, nil
}
for _, path := range ms.userFiles {
if err := syscall.Stat(path, &stats); err != nil {
return true, errors.Wrapf(err, "failed to stat %v", path)
}

if err := syscall.Stat(shadowFile, &stats); err != nil {
return true, errors.Wrapf(err, "failed to stat %v", shadowFile)
}
if since.Before(time.Unix(stats.Ctim.Sec, stats.Ctim.Nsec)) {
return true, nil
}
ctime := time.Unix(stats.Ctim.Sec, stats.Ctim.Nsec)
if ms.lastRead.Before(ctime) {
ms.log.Debugf("File changed: %v (lastRead=%v, ctime=%v)", path, ms.lastRead, ctime)

if err := syscall.Stat(groupFile, &stats); err != nil {
return true, errors.Wrapf(err, "failed to stat %v", groupFile)
}
if since.Before(time.Unix(stats.Ctim.Sec, stats.Ctim.Nsec)) {
return true, nil
return true, nil
}
}

return false, nil
Expand Down
Loading

0 comments on commit 9d68625

Please sign in to comment.