-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Configuration files must not be writeable by other users #3544
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,15 +5,29 @@ import ( | |
"errors" | ||
"flag" | ||
"fmt" | ||
"os" | ||
"runtime" | ||
"strings" | ||
|
||
"github.com/elastic/beats/libbeat/common/file" | ||
"github.com/elastic/beats/libbeat/logp" | ||
"github.com/elastic/go-ucfg" | ||
"github.com/elastic/go-ucfg/cfgutil" | ||
cfgflag "github.com/elastic/go-ucfg/flag" | ||
"github.com/elastic/go-ucfg/yaml" | ||
) | ||
|
||
var flagStrictPerms = flag.Bool("strict.perms", true, "Strict permission checking on config files") | ||
|
||
// IsStrictPerms returns true if strict permission checking on config files is | ||
// enabled. | ||
func IsStrictPerms() bool { | ||
if !*flagStrictPerms || os.Getenv("BEAT_STRICT_PERMS") == "false" { | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
// Config object to store hierarchical configurations into. | ||
// See https://godoc.org/github.com/elastic/go-ucfg#Config | ||
type Config ucfg.Config | ||
|
@@ -143,6 +157,12 @@ func NewFlagOverwrite( | |
} | ||
|
||
func LoadFile(path string) (*Config, error) { | ||
if IsStrictPerms() { | ||
if err := ownerHasExclusiveWritePerms(path); err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
c, err := yaml.NewConfigWithFile(path, configOpts...) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -390,3 +410,35 @@ func filterDebugObject(c interface{}) { | |
} | ||
} | ||
} | ||
|
||
// ownerHasExclusiveWritePerms asserts that the current user is the | ||
// owner of the config file and that the config file is (at most) writable by | ||
// the owner (e.g. group and other cannot have write access). | ||
func ownerHasExclusiveWritePerms(name string) error { | ||
if runtime.GOOS == "windows" { | ||
return nil | ||
} | ||
|
||
info, err := file.Stat(name) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
euid := os.Geteuid() | ||
fileUID, _ := info.UID() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is ignoring the error here ok? I'm mainly interested in the behaviour on windows. If I understand correctly, fileUID will be -1 in that case, right? Is euid also -1 on Windows? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should never get this far because of the first check to see if this is running on Windows. On Windows |
||
perm := info.Mode().Perm() | ||
|
||
if euid != fileUID { | ||
return fmt.Errorf(`config file ("%v") must be owned by the beat user `+ | ||
`(uid=%v)`, name, euid) | ||
} | ||
|
||
// Test if group or other have write permissions. | ||
if perm&0022 > 0 { | ||
return fmt.Errorf(`config file ("%v") can only be writable by the `+ | ||
`owner but the permissions are "%v"`, | ||
name, perm) | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package file | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
) | ||
|
||
// A FileInfo describes a file and is returned by Stat and Lstat. | ||
type FileInfo interface { | ||
os.FileInfo | ||
UID() (int, error) // UID of the file owner. Returns an error on non-POSIX file systems. | ||
GID() (int, error) // GID of the file owner. Returns an error on non-POSIX file systems. | ||
} | ||
|
||
// Stat returns a FileInfo describing the named file. | ||
// If there is an error, it will be of type *PathError. | ||
func Stat(name string) (FileInfo, error) { | ||
return stat(name, os.Stat) | ||
} | ||
|
||
// Lstat returns a FileInfo describing the named file. | ||
// If the file is a symbolic link, the returned FileInfo | ||
// describes the symbolic link. Lstat makes no attempt to follow the link. | ||
// If there is an error, it will be of type *PathError. | ||
func Lstat(name string) (FileInfo, error) { | ||
return stat(name, os.Lstat) | ||
} | ||
|
||
type fileInfo struct { | ||
os.FileInfo | ||
uid *int | ||
gid *int | ||
} | ||
|
||
func (f fileInfo) UID() (int, error) { | ||
if f.uid == nil { | ||
return -1, errors.New("uid not implemented") | ||
} | ||
|
||
return *f.uid, nil | ||
} | ||
|
||
func (f fileInfo) GID() (int, error) { | ||
if f.gid == nil { | ||
return -1, errors.New("gid not implemented") | ||
} | ||
|
||
return *f.gid, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
// +build !windows | ||
|
||
package file_test | ||
|
||
import ( | ||
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/elastic/beats/libbeat/common/file" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestStat(t *testing.T) { | ||
f, err := ioutil.TempFile("", "teststat") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
defer os.Remove(f.Name()) | ||
|
||
link := filepath.Join(os.TempDir(), "teststat-link") | ||
if err := os.Symlink(f.Name(), link); err != nil { | ||
t.Fatal(err) | ||
} | ||
defer os.Remove(link) | ||
|
||
info, err := file.Stat(link) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
assert.True(t, info.Mode().IsRegular()) | ||
|
||
uid, err := info.UID() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
assert.EqualValues(t, os.Geteuid(), uid) | ||
|
||
gid, err := info.GID() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
assert.EqualValues(t, os.Getegid(), gid) | ||
} | ||
|
||
func TestLstat(t *testing.T) { | ||
link := filepath.Join(os.TempDir(), "link") | ||
if err := os.Symlink("dummy", link); err != nil { | ||
t.Fatal(err) | ||
} | ||
defer os.Remove(link) | ||
|
||
info, err := file.Lstat(link) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
assert.True(t, info.Mode()&os.ModeSymlink > 0) | ||
|
||
uid, err := info.UID() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
assert.EqualValues(t, os.Geteuid(), uid) | ||
|
||
gid, err := info.GID() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
assert.EqualValues(t, os.Getegid(), gid) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// +build !windows | ||
|
||
package file | ||
|
||
import ( | ||
"errors" | ||
"os" | ||
"syscall" | ||
) | ||
|
||
func stat(name string, statFunc func(name string) (os.FileInfo, error)) (FileInfo, error) { | ||
info, err := statFunc(name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
stat, ok := info.Sys().(*syscall.Stat_t) | ||
if !ok { | ||
return nil, errors.New("failed to get uid/gid") | ||
} | ||
|
||
uid := int(stat.Uid) | ||
gid := int(stat.Gid) | ||
return fileInfo{FileInfo: info, uid: &uid, gid: &gid}, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package file | ||
|
||
import ( | ||
"os" | ||
) | ||
|
||
func stat(name string, statFunc func(name string) (os.FileInfo, error)) (FileInfo, error) { | ||
info, err := statFunc(name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return fileInfo{FileInfo: info}, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably document that there is also an environment variable available for this setting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we will need to update the documentation to explain the ownership and permission requirements. This info can go in there.