Skip to content

Commit

Permalink
Document and improve permission checks when running socket metricset …
Browse files Browse the repository at this point in the history
…from Docker (elastic#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.
  • Loading branch information
jsoriano authored and ph committed May 21, 2019
1 parent 7cdda44 commit cffe6fe
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 25 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
62 changes: 62 additions & 0 deletions libbeat/common/capabilities_linux.go
Original file line number Diff line number Diff line change
@@ -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")
}
13 changes: 13 additions & 0 deletions metricbeat/docs/running-on-docker.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<metricbeat-metricset-system-socket,system socket metricset>>
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

Expand Down
69 changes: 51 additions & 18 deletions metricbeat/helper/socket/ptable.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
36 changes: 36 additions & 0 deletions metricbeat/helper/socket/ptable_linux.go
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 30 additions & 0 deletions metricbeat/helper/socket/ptable_other.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 5 additions & 3 deletions metricbeat/module/system/socket/_meta/docs.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions metricbeat/module/system/socket/socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit cffe6fe

Please sign in to comment.