Python socket bind in a rootless container/world

My original task was to create a Python script to send ICMP echo-request (aka. ping) messages to some hosts and then do some basic reporting. This is a simple task right? Well, not really, at least, if you care about security. To be able to ping a host, we need to open a socket, which cannot be done by a regular user, either on a Windows nor on a Linux machine. Now I will concentrate on Linux systems.

The internet is full of with fantastic ideas, but sometimes it is maybe a better idea to think a little before you copy something from stackoverflow. In a bunch of topics, they advice you to run your script as root, because what can go wrong, right? Or disable SELinux, what could go wrong? Well, say no to these! (This can apply to other topics too.. don’t always accept the first solution, think, think, think.)

So my idea is to create a simple script in Python. I wanted to use a full Python and async implementation, so I choose the aioping (https://github.com/M-o-a-T/aioping) library. They state that the library can only be used, if you run it as root. This is not really acceptable, so let’s see, what can be done to solve this issue..

Let’s see, what’s going on under the hood. In the core of the library, it tries to open a raw socket:

import socket
socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
socket.bind(('', 0))

If you try to run just this, as a non-root user, you will get the following error message:

bash-4.2$ python3.6
Python 3.6.7 (default, Dec  5 2018, 15:02:05)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib64/python3.6/socket.py", line 144, in __init__
    _socket.socket.__init__(self, family, type, proto, fileno)
PermissionError: [Errno 1] Operation not permitted

Well, nobody lied, it really need to be ran as ‘root’ (well.. yeah no, maybe). But that sucks, so what options do we have here? We can use the magic of setuid/setgid or from kernel version 2.2, we can use Linux kernel capabilities. Next we will check these.

SETUID/SETGID

setuid and setgid (short for “set user ID” and “set group ID”)[1] are Unix access rights flags that allow users to run an executable with the permissions of the executable’s owner or group respectively and to change behaviour in directories.

https://en.wikipedia.org/wiki/Setuid

So we can just simply set the owner of the script to be “root” and then enable the setuid bit and bamm we are done. What an awful idea! Security folks probably started to heavily breath now! But they are right, don’t use that.

Linux kernel capabilities

For the purpose of performing permission checks, traditional UNIX implementations distinguish two categories of processes: privileged processes (whose effective user ID is 0, referred to as superuser or root), and unprivileged processes (whose effective UID is nonzero). Privileged processes bypass all kernel permission checks, while unprivileged processes are subject to full permission checking based on the process’s credentials (usually: effective UID, effective GID, and supplementary group list).

Starting with kernel 2.2, Linux divides the privileges traditionally associated with superuser into distinct units, known as capabilities, which can be independently enabled and disabled. Capabilities are a per-thread attribute.

http://man7.org/linux/man-pages/man7/capabilities.7.htm

This sounds good, let’s see what capabilities are out there and which can be interesting for us! You can find all the capabilities and details in the manual, here I collected the most interesting for this topic:

CAP_NET_ADMIN

Perform various network-related operations:
– interface configuration;
– administration of IP firewall, masquerading, and accounting;
– modify routing tables;
– bind to any address for transparent proxying;
– set type-of-service (TOS)
– clear driver statistics;
– set promiscuous mode;
– enabling multicasting;
– use setsockopt(2) to set the following socket options:
– SO_DEBUG, SO_MARK, SO_PRIORITY (for a priority outside the range 0 to 6), SO_RCVBUFFORCE, and SO_SNDBUFFORCE.

http://man7.org/linux/man-pages/man7/capabilities.7.htm

CAP_NET_BIND_SERVICE

Bind a socket to Internet domain privileged ports (portnumbers less than 1024).

http://man7.org/linux/man-pages/man7/capabilities.7.htm

CAP_NET_RAW

Use RAW and PACKET sockets;
bind to any address for transparent proxying.

http://man7.org/linux/man-pages/man7/capabilities.7.htm

Okay, so now we know, that in the Linux world, there’s no more only root or non-root, we can use these capabilities, so we can grant permissions more granular. But if we follow the “principle of least privilege”, we can ask: do we need to allow all these capabilities? Well no, not really.

If we add CAP_NET_ADMIN capability to a process, it will be able to do various things to our networking stack, like interface configuration, change of firewall settings and so on. We clearly don’t want this, and if we check the list of permissions, we don’t need them.

On the other hand, CAP_NET_BIND_SERVICE allows the process to bind on a privileges port (any port under 1024). We also don’t need this, so we can just exclude this capability as well.

The last thing is CAP_NET_RAW. This allows exactly what we need: allows the process to use RAW sockets and to bind to any IP address. Perfect, just what we need.

Linux capability sets

Now we know, that we need the CAP_NET_RAW capability. But how should we add this to our process? We can me these permitted, inheritable and effective (and many more, but these are the most important now).

Each thread has the following capability sets containing zero or more of the above capabilities:

Permitted
This is a limiting superset for the effective capabilities that the thread may assume. It is also a limiting superset for the capabilities that may be added to the inheritable set by a thread that does not have the CAP_SETPCAP capability in its effective set.

Inheritable
This is a set of capabilities preserved across an execve(2). Inheritable capabilities remain inheritable when executing any program, and inheritable capabilities are added to the permitted set when executing a program that has the corresponding bits set in the file inheritable set.

Effective
This is the set of capabilities used by the kernel to perform permission checks for the thread.

http://man7.org/linux/man-pages/man7/capabilities.7.html

Add Linux capabilities to a process

Adding Linux capabilities to a process is fairly easy with the use of the setcap command. As this is going to be a Python script, we need to add it to the Python interpreter. Be careful, if you add this to the global interpreter of your system, all scripts will have these capabilities! To be on the safe side, it’s worth to create a virtualenv, and only pimp that interpreter!

(+ means, that we add the capability, e=effective, p=permitted)

setcap cap_net_raw+ep /path/to/your/interpreter

We can check the capabilities with the getcap command (this is an example of the ping command):

getcap /usr/bin/ping
/usr/bin/ping = cap_net_admin,cap_net_raw+p

And that’s it. Now you should be able to run your script without running it as root. It has now more permissions, than a regular process, but still not running as root!

Running in a rootless Docker container

Okay, now we can run that thing without root. So now let’s put it in a container! If we run a Docker container, by default, it drops a bunch of capabilities, as we wont need most of them. In this case, we will only need the cap_net_raw capability, so we can drop all others.

To read more about capabilities: https://www.redhat.com/en/blog/secure-your-containers-one-weird-trick

Example command to run the container with only the net_raw capability:

docker run -it --cap-drop=all --cap-add=net_raw <image>

With the –cap=drop=all command, we can drop all the capabilities. With the –cap-add=<name>, we can add capabilities.

That’s it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s