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

container users and groups from process root #677

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions test/e2e/containers/sinsp.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM debian:buster

ENV HOST_ROOT /host

RUN apt-get update && \
apt-get install -y \
libcurl4 \
Expand Down
14 changes: 9 additions & 5 deletions test/e2e/tests/commons/sinspqa/sinsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,12 @@ def validate_event(expected_fields: dict, event: dict) -> bool:

expected = expected_fields[k]

if isinstance(expected, str) or expected is None:
if expected == event[k]:
if isinstance(expected, SinspField):
if expected.compare(str(event[k])):
continue
return False

if not expected.compare(str(event[k])):
if expected != event[k]:
return False

return True
Expand Down Expand Up @@ -202,8 +202,10 @@ def container_spec(image: str = 'sinsp-example:latest', args: list = [], env: di
A dictionary describing how to run the sinsp-example container
"""
mounts = [
docker.types.Mount("/dev", "/dev", type="bind",
consistency="delegated", read_only=True)
docker.types.Mount("/host/dev", "/dev", type="bind",
consistency="delegated", read_only=True),
docker.types.Mount("/host/proc", "/proc", type="bind",
consistency="delegated", read_only=True),
]

return {
Expand All @@ -212,6 +214,8 @@ def container_spec(image: str = 'sinsp-example:latest', args: list = [], env: di
'mounts': mounts,
'env': env,
'privileged': True,
'pid_mode': 'host',
'network_mode': 'host',
'init_wait': 2,
'post_validation': sinsp_validation,
}
Expand Down
8 changes: 7 additions & 1 deletion test/e2e/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ def run_containers(request, docker_client: docker.client.DockerClient):
additional_wait = container.get('init_wait', 0)
post_validation = container.get('post_validation', None)
stop_signal = container.get('signal', None)
user = container.get('user', '')
pid_mode = container.get('pid_mode', '')
network_mode = container.get('network_mode', '')

handle = docker_client.containers.run(
image,
Expand All @@ -74,7 +77,10 @@ def run_containers(request, docker_client: docker.client.DockerClient):
detach=True,
privileged=privileged,
mounts=mounts,
environment=environment
environment=environment,
user=user,
pid_mode=pid_mode,
network_mode=network_mode,
)

containers[name] = handle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_db_program_spawned_process(run_containers: dict):
},
{
"container.id": generator_id,
"evt.args": SinspField.regex_field(r'^res=0 exe=/bin/ls args=NULL tid=\d+\(ls\) pid=\d+\(ls\) ptid=\d+\(mysqld\) .* tty=0 pgid=1\(sinsp-example\) loginuid=-1 flags=1\(EXE_WRITABLE\) cap_inheritable=0 cap_permitted=3FFFFFFFFF cap_effective=3FFFFFFFFF $'),
"evt.args": SinspField.regex_field(r'^res=0 exe=/bin/ls args=NULL tid=\d+\(ls\) pid=\d+\(ls\) ptid=\d+\(mysqld\) .* tty=0 pgid=1\(systemd\) loginuid=-1 flags=1\(EXE_WRITABLE\) cap_inheritable=0 cap_permitted=3FFFFFFFFF cap_effective=3FFFFFFFFF $'),
"evt.category": "process",
"evt.num": SinspField.numeric_field(),
"evt.time": SinspField.numeric_field(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_run_shell_untrusted(run_containers: dict):
expected_events = [
{
"container.id": generator_id,
"evt.args": SinspField.regex_field(r'^res=0 exe=\/tmp\/falco-event-generator\d+\/httpd args=--loglevel.info.run.\^helper.RunShell\$. tid=\d+\(httpd\) pid=\d+\(httpd\) ptid=\d+\(event-generator\) .* tty=0 pgid=\d+\(sinsp-example\) loginuid=-1 flags=1\(EXE_WRITABLE\) cap_inheritable=0 cap_permitted=3FFFFFFFFF cap_effective=3FFFFFFFFF $'),
"evt.args": SinspField.regex_field(r'^res=0 exe=\/tmp\/falco-event-generator\d+\/httpd args=--loglevel.info.run.\^helper.RunShell\$. tid=\d+\(httpd\) pid=\d+\(httpd\) ptid=\d+\(event-generator\) .* tty=0 pgid=\d+\(systemd\) loginuid=-1 flags=1\(EXE_WRITABLE\) cap_inheritable=0 cap_permitted=3FFFFFFFFF cap_effective=3FFFFFFFFF $'),
"evt.category": "process",
"evt.num": SinspField.numeric_field(),
"evt.time": SinspField.numeric_field(),
Expand All @@ -39,7 +39,7 @@ def test_run_shell_untrusted(run_containers: dict):
},
{
"container.id": generator_id,
"evt.args": SinspField.regex_field(r'^res=0 exe=bash args=-c.ls > \/dev\/null. tid=\d+\(bash\) pid=\d+\(bash\) ptid=\d+\(httpd\) .* tty=0 pgid=\d+\(sinsp-example\) loginuid=-1 flags=1\(EXE_WRITABLE\) cap_inheritable=0 cap_permitted=3FFFFFFFFF cap_effective=3FFFFFFFFF $'),
"evt.args": SinspField.regex_field(r'^res=0 exe=bash args=-c.ls > \/dev\/null. tid=\d+\(bash\) pid=\d+\(bash\) ptid=\d+\(httpd\) .* tty=0 pgid=\d+\(systemd\) loginuid=-1 flags=1\(EXE_WRITABLE\) cap_inheritable=0 cap_permitted=3FFFFFFFFF cap_effective=3FFFFFFFFF $'),
"evt.category": "process",
"evt.num": SinspField.numeric_field(),
"evt.time": SinspField.numeric_field(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_system_user_interactive(run_containers: dict):
expected_events = [
{
"container.id": generator_id,
"evt.args": SinspField.regex_field(r'^res=0 exe=\/bin\/login args=NULL tid=\d+\(login\) pid=\d+\(login\) ptid=\d+\(event-generator\) .* pgid=\d+\(sinsp-example\) loginuid=-1 flags=0 cap_inheritable=0 cap_permitted=0 cap_effective=0 $'),
"evt.args": SinspField.regex_field(r'^res=0 exe=\/bin\/login args=NULL tid=\d+\(login\) pid=\d+\(login\) ptid=\d+\(event-generator\) .* pgid=\d+\(systemd\) loginuid=-1 flags=0 cap_inheritable=0 cap_permitted=0 cap_effective=0 $'),
"evt.category": "process",
"evt.num": SinspField.numeric_field(),
"evt.time": SinspField.numeric_field(),
Expand Down
31 changes: 20 additions & 11 deletions test/e2e/tests/test_process/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,38 @@
from sinspqa.sinsp import assert_events
from sinspqa.docker import get_container_id

sinsp_filters = ["-f", "evt.category=process and not container.id=host"]
sinsp_args = [
"-f", "evt.category=process and not container.id=host",
"-o", "%container.id %evt.args %evt.category %evt.type %proc.cmdline %proc.exe %user.uid %user.name %group.gid %group.name"
]

containers = [
{
'sinsp': sinsp_container,
'nginx': {
'image': 'nginx:1.14-alpine',
'app': {
'image': 'hashicorp/http-echo:alpine',
'args': ['-text=hello'],
'user': '11:100'
}
} for sinsp_container in sinsp.generate_specs(args=sinsp_filters)
} for sinsp_container in sinsp.generate_specs(args=sinsp_args)
]

ids = [ sinsp.generate_id(c['sinsp']) for c in containers ]

@pytest.mark.parametrize("run_containers", containers, indirect=True, ids=ids)
def test_exec_in_container(run_containers: dict):
nginx_container = run_containers['nginx']
app_container = run_containers['app']
sinsp_container = run_containers['sinsp']

container_id = get_container_id(nginx_container)
container_id = get_container_id(app_container)

nginx_container.exec_run("sleep 5")
nginx_container.exec_run("sh -c ls")
app_container.exec_run("sleep 5")
app_container.exec_run("sh -c ls")

expected_events = [
{
'container.id': container_id,
'evt.args': 'filename=/usr/sbin/nginx ',
'evt.args': 'filename=/http-echo ',
'evt.category': 'process',
'evt.type': 'execve',
'proc.exe': 'runc',
Expand All @@ -38,8 +43,12 @@ def test_exec_in_container(run_containers: dict):
'container.id': container_id,
'evt.category': 'process',
'evt.type': 'execve',
'proc.exe': 'nginx',
'proc.cmdline': 'nginx -g daemon off;'
'proc.exe': '/http-echo',
'proc.cmdline': 'http-echo -text=hello',
'user.uid': 11,
'user.name': 'operator',
'group.gid': 100,
'group.name': 'users',
}, {
'container.id': container_id,
'evt.category': 'process',
Expand Down
5 changes: 4 additions & 1 deletion userspace/libsinsp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ set(SINSP_SOURCES
sinsp_ppm_sc.cpp
sinsp_tp.cpp)

if(NOT WIN32)
list(APPEND SINSP_SOURCES procfs_utils.cpp)
endif()

if(WITH_CHISEL)
list(APPEND SINSP_SOURCES
../chisel/chisel_api.cpp
Expand Down Expand Up @@ -180,7 +184,6 @@ if(NOT MINIMAL_BUILD)
container_engine/mesos.cpp
container_engine/rkt.cpp
container_engine/bpm.cpp
procfs_utils.cpp
runc.cpp)
endif()

Expand Down
63 changes: 61 additions & 2 deletions userspace/libsinsp/procfs_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ limitations under the License.

*/
#include "procfs_utils.h"
#include "logger.h"

#include <string>
#include "sinsp.h"
#include <cstring>
#include <sstream>
#include <unistd.h>

int libsinsp::procfs_utils::get_userns_root_uid(std::istream& uid_map)
{
Expand Down Expand Up @@ -63,3 +65,60 @@ std::string libsinsp::procfs_utils::get_systemd_cgroup(std::istream& cgroups)

return "";
}

//
// ns_helper
//
libsinsp::procfs_utils::ns_helper::ns_helper(const std::string& host_root):
m_host_root(host_root)
{
// (try to) init m_host_init_ns_mnt
char buf[NS_MNT_SIZE] = {0};
if(-1 == readlink((m_host_root + "/proc/1/ns/mnt").c_str(), buf, NS_MNT_SIZE - 1))
{
g_logger.format(sinsp_logger::SEV_WARNING,
"Cannot read host init ns/mnt: %d", errno);
m_cannot_read_host_init_ns_mnt = true;
}
else
{
auto size = strlen(buf) + 1;
m_host_init_ns_mnt = (char*)malloc(size);
strncpy(m_host_init_ns_mnt, buf, size);
}
}

libsinsp::procfs_utils::ns_helper::~ns_helper()
{
if(m_host_init_ns_mnt)
{
free(m_host_init_ns_mnt);
m_host_init_ns_mnt = nullptr;
}
}

bool libsinsp::procfs_utils::ns_helper::in_own_ns_mnt(int64_t pid) const
{
if(m_host_init_ns_mnt == nullptr)
{
return false;
}

std::string path = m_host_root + "/proc/" + std::to_string(pid) + "/ns/mnt";

char proc_ns_mnt[NS_MNT_SIZE] = {0};
if(-1 == readlink(path.c_str(), proc_ns_mnt, NS_MNT_SIZE-1))
{
g_logger.format(sinsp_logger::SEV_DEBUG,
"Cannot read process ns/mnt");
return false;
}

if(0 == strncmp(m_host_init_ns_mnt, proc_ns_mnt, NS_MNT_SIZE))
FedeDP marked this conversation as resolved.
Show resolved Hide resolved
{
// Still in the host namespace
return false;
}

return true;
}
42 changes: 40 additions & 2 deletions userspace/libsinsp/procfs_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,43 @@ int get_userns_root_uid(std::istream& uid_map);
*/
std::string get_systemd_cgroup(std::istream& cgroups);

}
}
/**
* @brief Access container data through proc
*/
class ns_helper
{
public:
ns_helper(const std::string& host_root);
~ns_helper();

bool can_read_host_init_ns_mnt() const
{
return !m_cannot_read_host_init_ns_mnt;
}

const char* get_host_init_ns_mnt() const { return m_host_init_ns_mnt; }

//! Return true if not in the host init mount namespace
bool in_own_ns_mnt(int64_t pid) const;

std::string get_pid_root(int64_t pid) const
{
return m_host_root + "/proc/" + std::to_string(pid) + "/root";
}

private:
const std::string& m_host_root;
char* m_host_init_ns_mnt{nullptr};
FedeDP marked this conversation as resolved.
Show resolved Hide resolved
bool m_cannot_read_host_init_ns_mnt{false};

private:
//
// NOTE: at the time of writing 16 would have been enough, being
// the format `mnt:[<unsigned>]`, but the only call to `ns_get_name`
// I could find in the kernel was using a buffer of 50, so 50 it is.
//
static constexpr const std::size_t NS_MNT_SIZE{50};
};

} // namespace procfs_utils
} // namespace libsinsp
1 change: 1 addition & 0 deletions userspace/libsinsp/sinsp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ sinsp::sinsp(bool static_container, const std::string &static_id, const std::str
m_external_event_processor(),
m_evt(this),
m_lastevent_ts(0),
m_host_root(scap_get_host_root()),
m_container_manager(this, static_container, static_id, static_name, static_image),
m_usergroup_manager(this),
m_suppressed_comms(),
Expand Down
5 changes: 5 additions & 0 deletions userspace/libsinsp/sinsp.h
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,9 @@ class SINSP_PUBLIC sinsp : public capture_stats_source, public wmi_handle_source

uint64_t get_lastevent_ts() const { return m_lastevent_ts; }

const std::string& get_host_root() const { return m_host_root; }
void set_host_root(const std::string& s) { m_host_root = s; }

VISIBILITY_PROTECTED
bool add_thread(const sinsp_threadinfo *ptinfo);
void set_mode(scap_mode_t value)
Expand Down Expand Up @@ -1264,6 +1267,8 @@ VISIBILITY_PRIVATE

sinsp_network_interfaces* m_network_interfaces;

std::string m_host_root;

public:
sinsp_thread_manager* m_thread_manager;

Expand Down
3 changes: 2 additions & 1 deletion userspace/libsinsp/test/user.ut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ class usergroup_manager_host_root_test : public sinsp_with_test_input
m_host_root += "/host";

ASSERT_EQ(mkdir(m_host_root.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH), 0);
m_inspector.set_host_root(m_host_root);

std::string etc = m_host_root + "/etc";
ASSERT_EQ(mkdir(etc.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH), 0);

Expand Down Expand Up @@ -146,7 +148,6 @@ TEST_F(usergroup_manager_host_root_test, host_root_lookup)
std::string container_id{""};

sinsp_usergroup_manager mgr(&m_inspector);
sinsp_usergroup_manager::s_host_root = m_host_root;

mgr.add_user(container_id, 0, 0, nullptr, nullptr, nullptr);
auto* user = mgr.get_user(container_id, 0);
Expand Down
34 changes: 30 additions & 4 deletions userspace/libsinsp/threadinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -505,8 +505,21 @@ void sinsp_threadinfo::set_user(uint32_t uid)
scap_userinfo *user = m_inspector->m_usergroup_manager.get_user(m_container_id, uid);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mind that sinsp_threadinfo::set_user gets called by parse_chroot_exit, parse_clone_exit, parse_execve_exit

if (!user)
{
// this can fail if import_user is disabled
user = m_inspector->m_usergroup_manager.add_user(m_container_id, uid, m_group.gid, NULL, NULL, NULL, m_inspector->is_live());
// these can fail if import_user is disabled
if(m_container_id.empty())
{
user = m_inspector->m_usergroup_manager.add_user(m_container_id, uid, m_group.gid, NULL, NULL, NULL, m_inspector->is_live());
}
else if(uid != 0)
{
//
// When a container is running with a specific user and this
// get called with 0, it's too early to make an attempt.
// As a downside we won't load users for containers running as
// root, but we will load them if e.g.docker exec -u <specific-user>.
//
user = m_inspector->m_usergroup_manager.add_container_user(m_container_id, m_pid, uid, m_inspector->is_live());
}
Comment on lines +513 to +522
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is it "too early"? Because we caught a thread switching into the container? What if the container is started as root?

What is uid here? If we're calling this from all sorts of parsers, it's not a special uid, like the docker -u user, right? So how does the uid != 0 check work?

Maybe hardcoding uid=0 as "root" would be good enough btw?

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Why is it "too early"?
    By trial and error, I found that the thread had the container id but wasn't really inside the container (i.e. runc:[1:CHILD] in my tests).
  2. What if the container is started as root?
    We'll have <NA> 🤷 but if / when it switches to a different user we'll load the users.
  3. What is uid here?
    a) sinsp_threadinfo::set_user is being called by parse_chroot_exit, parse_clone_exit, parse_execve_exit, parse_setuid_exit, parse_setresuid_exit.
    b) It's actually the user of docker -u or k8s runAsUser.
    c) you can see that it's being tested (test/e2e/tests/test_process/test_container.py) by starting a hashicorp/http-echo:alpine container with 11:110
  4. Maybe hardcoding uid=0 as "root" would be good enough btw?
    I was a bit uncertain about doing so, but if it's already two of us I'd go with that 😀

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe hardcoding uid=0 as "root" would be good enough btw?

Makes sense.

}

if (user)
Expand All @@ -528,8 +541,21 @@ void sinsp_threadinfo::set_group(uint32_t gid)
scap_groupinfo *group = m_inspector->m_usergroup_manager.get_group(m_container_id, gid);
if (!group)
{
// this can fail if import_user is disabled
group = m_inspector->m_usergroup_manager.add_group(m_container_id, gid, NULL, m_inspector->is_live());
// these can fail if import_user is disabled
if(m_container_id.empty())
{
group = m_inspector->m_usergroup_manager.add_group(m_container_id, gid, NULL, m_inspector->is_live());
}
else if(gid != 0)
{
//
// When a container is running with a specific user and this
// get called with 0, it's too early to make an attempt.
// As a downside we won't load users for containers running as
// root, but we will load them if e.g.docker exec -u <specific-user>.
//
group = m_inspector->m_usergroup_manager.add_container_group(m_container_id, m_pid, gid, m_inspector->is_live());
}
}

if (group)
Expand Down
Loading