From 2cbdc9ad27f14aba8857df2b189abdb9c1661713 Mon Sep 17 00:00:00 2001 From: Jaime Soriano Pastor Date: Thu, 9 May 2019 16:52:18 +0200 Subject: [PATCH] Document and improve permission checks when running socket metricset from Docker (#12039) Update instructions for system/socket metricset on Docker. And base permission checks on capabilities rather than on the effective uid. Running a process as root doesn't mean that it has all privileges, specially when run as container. --- CHANGELOG.next.asciidoc | 2 + libbeat/common/capabilities_linux.go | 62 +++++++++++++++++ metricbeat/docs/running-on-docker.asciidoc | 13 ++++ metricbeat/helper/socket/ptable.go | 69 ++++++++++++++----- metricbeat/helper/socket/ptable_linux.go | 36 ++++++++++ metricbeat/helper/socket/ptable_other.go | 30 ++++++++ .../module/system/socket/_meta/docs.asciidoc | 8 ++- metricbeat/module/system/socket/socket.go | 8 +-- 8 files changed, 203 insertions(+), 25 deletions(-) create mode 100644 libbeat/common/capabilities_linux.go create mode 100644 metricbeat/helper/socket/ptable_linux.go create mode 100644 metricbeat/helper/socket/ptable_other.go diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 2cff6a14010..3d476e2091b 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -111,6 +111,8 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d - Change diskio metrics retrieval method (only for Windows) from wmi query to DeviceIOControl function using the IOCTL_DISK_PERFORMANCE control code {pull}11635[11635] - Call GetMetricData api per region instead of per instance. {issue}11820[11820] {pull}11882[11882] - Update documentation with cloudwatch:ListMetrics permission. {pull}11987[11987] +- Check permissions in system socket metricset based on capabilities. {pull}12039[12039] +- Get process information from sockets owned by current user when system socket metricset is run without privileges. {pull}12039[12039] - Avoid generating hints-based configuration with empty hosts when no exposed port is suitable for the hosts hint. {issue}8264[8264] {pull}12086[12086] - Fixed a socket leak in the postgresql module under Windows when SSL is disabled on the server. {pull}11393[11393] diff --git a/libbeat/common/capabilities_linux.go b/libbeat/common/capabilities_linux.go new file mode 100644 index 00000000000..e05cf99fb72 --- /dev/null +++ b/libbeat/common/capabilities_linux.go @@ -0,0 +1,62 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build linux + +package common + +import ( + "github.com/pkg/errors" + + "github.com/elastic/go-sysinfo" + "github.com/elastic/go-sysinfo/types" +) + +// Capabilities contains the capability sets of a process +type Capabilities types.CapabilityInfo + +// Check performs a permission check for a given capabilities set +func (c Capabilities) Check(set []string) bool { + for _, capability := range set { + found := false + for _, effective := range c.Effective { + if capability == effective { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +// GetCapabilities gets the capabilities of this process +func GetCapabilities() (Capabilities, error) { + p, err := sysinfo.Self() + if err != nil { + return Capabilities{}, errors.Wrap(err, "failed to read self process information") + } + + if c, ok := p.(types.Capabilities); ok { + capabilities, err := c.Capabilities() + return Capabilities(*capabilities), errors.Wrap(err, "failed to read process capabilities") + } + + return Capabilities{}, errors.New("capabilities not available") +} diff --git a/metricbeat/docs/running-on-docker.asciidoc b/metricbeat/docs/running-on-docker.asciidoc index 98e5f5d7df6..97736a319c4 100644 --- a/metricbeat/docs/running-on-docker.asciidoc +++ b/metricbeat/docs/running-on-docker.asciidoc @@ -46,7 +46,20 @@ NOTE: The special filesystems +/proc+ and +/sys+ are only available if the host system is running Linux. Attempts to bind-mount these filesystems will fail on Windows and MacOS. + +If the <> +is being used on Linux, more privileges will need to be granted to Metricbeat. +This metricset reads files from `/proc` that are an interface to internal +objects owned by other users. The capabilities needed to read all these files +(`sys_ptrace` and `dac_read_search`) are disabled by default on Docker. To +grant these permissions these flags are needed too: + +["source","sh",subs="attributes"] +---- +--user root --cap-add sys_ptrace --cap-add dac_read_search +---- [float] + [[monitoring-service]] ==== Monitor a service in another container diff --git a/metricbeat/helper/socket/ptable.go b/metricbeat/helper/socket/ptable.go index 84d600fbe00..af22156849d 100644 --- a/metricbeat/helper/socket/ptable.go +++ b/metricbeat/helper/socket/ptable.go @@ -15,12 +15,15 @@ // specific language governing permissions and limitations // under the License. +// +build !windows + package socket import ( "os" "strconv" "strings" + "syscall" "github.com/joeshaw/multierror" "github.com/prometheus/procfs" @@ -39,10 +42,10 @@ type Proc struct { // ProcTable contains all of the active processes (if the current user is root). type ProcTable struct { - fs procfs.FS - procs map[int]*Proc - inodes map[uint32]*Proc - euid int + fs procfs.FS + procs map[int]*Proc + inodes map[uint32]*Proc + privileged bool } // NewProcTable returns a new ProcTable that reads data from the /proc @@ -58,29 +61,30 @@ func NewProcTable(mountpoint string) (*ProcTable, error) { return nil, err } - p := &ProcTable{fs: fs, euid: os.Geteuid()} + privileged, err := isPrivileged() + if err != nil { + return nil, err + } + + p := &ProcTable{fs: fs, privileged: privileged} p.Refresh() return p, nil } +// Privileged returns true if the process has enough permissions to read +// sockets of all users +func (t *ProcTable) Privileged() bool { + return t.privileged +} + // Refresh updates the process table with new processes and removes processes // that have exited. It collects the PID, command, and socket inode information. // If running as non-root, only information from the current process will be // collected. func (t *ProcTable) Refresh() error { - var err error - var procs []procfs.Proc - if t.euid == 0 { - procs, err = t.fs.AllProcs() - if err != nil { - return err - } - } else { - proc, err := t.fs.Self() - if err != nil { - return err - } - procs = append(procs, proc) + procs, err := t.accessibleProcs() + if err != nil { + return err } var errs multierror.Errors @@ -124,6 +128,35 @@ func (t *ProcTable) Refresh() error { return errs.Err() } +func (t *ProcTable) accessibleProcs() ([]procfs.Proc, error) { + procs, err := t.fs.AllProcs() + if err != nil { + return nil, err + } + if t.privileged { + return procs, nil + } + + // Filter out not owned processes + k := 0 + euid := uint32(os.Geteuid()) + for i := 0; i < len(procs); i++ { + p := t.fs.Path(strconv.Itoa(procs[i].PID)) + info, err := os.Stat(p) + if err != nil { + continue + } + stat, ok := info.Sys().(*syscall.Stat_t) + if !ok || stat.Uid != euid { + continue + } + procs[k] = procs[i] + k++ + } + + return procs[:k], nil +} + func socketInodes(p *procfs.Proc) ([]uint32, error) { fds, err := p.FileDescriptorTargets() if err != nil { diff --git a/metricbeat/helper/socket/ptable_linux.go b/metricbeat/helper/socket/ptable_linux.go new file mode 100644 index 00000000000..091efe60bfe --- /dev/null +++ b/metricbeat/helper/socket/ptable_linux.go @@ -0,0 +1,36 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build linux + +package socket + +import ( + "github.com/elastic/beats/libbeat/common" +) + +var requiredCapabilities = []string{"sys_ptrace", "dac_read_search"} + +// isPrivileged checks if this process has privileges to read sockets +// of all users +func isPrivileged() (bool, error) { + capabilities, err := common.GetCapabilities() + if err != nil { + return false, err + } + return capabilities.Check(requiredCapabilities), nil +} diff --git a/metricbeat/helper/socket/ptable_other.go b/metricbeat/helper/socket/ptable_other.go new file mode 100644 index 00000000000..447ba24448c --- /dev/null +++ b/metricbeat/helper/socket/ptable_other.go @@ -0,0 +1,30 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build !linux,!windows + +package socket + +import ( + "os" +) + +// isPrivileged checks if this process has privileges to read sockets +// of all users +func isPrivileged() (bool, error) { + return os.Geteuid() == 0, nil +} diff --git a/metricbeat/module/system/socket/_meta/docs.asciidoc b/metricbeat/module/system/socket/_meta/docs.asciidoc index 7dd1697ba41..e705b8ea1b8 100644 --- a/metricbeat/module/system/socket/_meta/docs.asciidoc +++ b/metricbeat/module/system/socket/_meta/docs.asciidoc @@ -19,9 +19,11 @@ metricbeat.modules: <1> You can configure the `socket` metricset separately to specify a different `period` value than the other metricsets. -The metricset reports the process that has the socket open. In order to provide -this information, Metricbeat must be running as root. Root access is also -required to read the file descriptor information of other processes. +The metricset reports the process that has the socket open. To provide this +information on Linux for all processes, Metricbeat must be run with +`sys_ptrace` and `dac_read_search` capabilities. These permissions are usually +granted when running as root, but they can and may need to be explictly added +when running Metricbeat inside a container. [float] === Configuration diff --git a/metricbeat/module/system/socket/socket.go b/metricbeat/module/system/socket/socket.go index 9bb25f7f675..fe90231683c 100644 --- a/metricbeat/module/system/socket/socket.go +++ b/metricbeat/module/system/socket/socket.go @@ -77,9 +77,9 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { if err != nil { return nil, err } - if os.Geteuid() != 0 { - logp.Info("socket process info will only be available for " + - "metricbeat because the process is running as a non-root user") + if !ptable.Privileged() { + logp.Info("socket process info will only be available for processes owned by the %v user "+ + "because this Beat is not running with enough privileges", os.Geteuid()) } m := &MetricSet{ @@ -212,7 +212,7 @@ func (m *MetricSet) enrichConnectionData(c *connection) { c.Command = proc.Command c.CmdLine = proc.CmdLine c.Args = proc.Args - } else if m.euid == 0 { + } else if m.ptable.Privileged() { if c.Inode == 0 { c.ProcessError = fmt.Errorf("process has exited. inode=%v, tcp_state=%v", c.Inode, c.State)