Skip to content
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

Document and improve permission checks when running socket metricset from Docker #12039

Merged
merged 19 commits into from
May 9, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions libbeat/common/capabilities_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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 (
"unsafe"

"golang.org/x/sys/unix"
)

// CapabilitiesData contains the capability sets of a process
type CapabilitiesData struct {
// Effective is the capability set used for permission checks
Effective uint64

// Permitted is the superset of effective capabilities that the thread may assume
Permitted uint64

// Inheritable is the set of capabilities inherited to child processes
Inheritable uint64
}

// Check performs a permission check for a given capabilities set
func (d CapabilitiesData) Check(set uint64) bool {
return (d.Effective & set) > 0
}

type capData32 [2]struct {
effective uint32
permitted uint32
inheritable uint32
}

func (d capData32) to64() CapabilitiesData {
return CapabilitiesData{
Effective: uint32to64(d[1].effective, d[0].effective),
Permitted: uint32to64(d[1].permitted, d[0].permitted),
Inheritable: uint32to64(d[1].inheritable, d[0].inheritable),
}
}

func uint32to64(a, b uint32) uint64 {
return uint64(a)<<32 | uint64(b)
}

const (
capabilityVersion1 = 0x19980330 // Version 1, 32-bit capabilities
capabilityVersion3 = 0x20080522 // Version 3, 64-bit capabilities (replaced version 2)
)

// GetCapabilities gets the capabilities of this process using system calls to avoid
// depending on procfs or library functions for permission checks
func GetCapabilities() CapabilitiesData {
jsoriano marked this conversation as resolved.
Show resolved Hide resolved
header := struct {
version uint32
pid int32
}{
version: capabilityVersion3,
pid: 0, // Self
}

// Check compatibility with version 3
_, _, e := unix.Syscall(unix.SYS_CAPGET, uintptr(unsafe.Pointer(&header)), 0, 0)
if e != 0 {
header.version = capabilityVersion1
}

var data capData32
_, _, e = unix.Syscall(unix.SYS_CAPGET, uintptr(unsafe.Pointer(&header)), uintptr(unsafe.Pointer(&data)), 0)
if e != 0 {
// If this fails, there are invalid arguments, and all arguments are
// being created here.
panic(unix.ErrnoName(e))
}

return data.to64()
}
1 change: 1 addition & 0 deletions libbeat/common/seccomp/policy_linux_386.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func init() {
"_llseek",
"access",
"brk",
"capget",
"clock_gettime",
"clone",
"close",
Expand Down
1 change: 1 addition & 0 deletions libbeat/common/seccomp/policy_linux_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func init() {
"arch_prctl",
"bind",
"brk",
"capget",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewkroh I didn't manage to update these files with make seccomp, it created new include files under metricbeat.
I have added this by hand here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine this file was created by merging all of the syscalls from each Beat, and then sanity checking the list.

"clock_gettime",
"clone",
"close",
Expand Down
5 changes: 5 additions & 0 deletions metricbeat/docs/running-on-docker.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ docker run \
--mount type=bind,source=/sys/fs/cgroup,target=/hostfs/sys/fs/cgroup,readonly \ <2>
--mount type=bind,source=/,target=/hostfs,readonly \ <3>
--net=host \ <4>
--user root --privileged \ <5>
jsoriano marked this conversation as resolved.
Show resolved Hide resolved
{dockerimage} -e -system.hostfs=/hostfs
----

Expand All @@ -41,6 +42,10 @@ of the container. They can be mounted at any location.
to make this file contain the host's network devices is to use the `--net=host`
flag. This is due to Linux namespacing; simply bind mounting the host's `/proc`
to `/hostfs/proc` is not sufficient.
<5> The <<metricbeat-metricset-system-socket,system socket metricset>> needs to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps you can say this module is not enabled by default?, to avoid any doubt

read files from `/proc` that belong to multiple users. In order to access these
files Metricbeat must be run as root in a privileged container. These options
are not needed if this metricset is not used.

NOTE: The special filesystems +/proc+ and +/sys+ are only available if the
host system is running Linux. Attempts to bind-mount these filesystems will
Expand Down
32 changes: 25 additions & 7 deletions metricbeat/helper/socket/ptable.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@
// specific language governing permissions and limitations
// under the License.

// +build linux

package socket

import (
"os"
"strconv"
"strings"

"github.com/joeshaw/multierror"
"github.com/prometheus/procfs"

"github.com/elastic/beats/libbeat/common"
)

// process tools
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,19 +61,25 @@ func NewProcTable(mountpoint string) (*ProcTable, error) {
return nil, err
}

p := &ProcTable{fs: fs, euid: os.Geteuid()}
p := &ProcTable{fs: fs, privileged: isPrivileged(mountpoint)}
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 {
if t.privileged {
procs, err = t.fs.AllProcs()
if err != nil {
return err
Expand Down Expand Up @@ -150,3 +159,12 @@ func socketInodes(p *procfs.Proc) ([]uint32, error) {
func (t *ProcTable) ProcessBySocketInode(inode uint32) *Proc {
return t.inodes[inode]
}

const requiredCapabilities = uint64(0x0000000000080004) // sys_ptrace & dac_read_search

// isPrivileged checks if this process has privileges to read sockets
// of all users
func isPrivileged(mountpoint string) bool {
capabilities := common.GetCapabilities()
return capabilities.Check(requiredCapabilities)
}
7 changes: 4 additions & 3 deletions metricbeat/module/system/socket/socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ 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 metricbeat process " +
jsoriano marked this conversation as resolved.
Show resolved Hide resolved
"because it is running without enough privileges " +
"(sys_ptrace and dac_read_search capabilities required)")
}

m := &MetricSet{
Expand Down