skip to Main Content

I am expecting the following command to fail with a permission exception because it is running as an unprivileged user. But instead, it appears to succeed.

% docker run --rm -u nobody  alpine  nc -l 0.0.0.0 443
% docker exec -it b2b471d05398 sh
~ $ id
uid=65534(nobody) gid=65534(nobody)
~ $ ps
PID   USER     TIME  COMMAND
    1 nobody    0:00 nc -l 0.0.0.0 443
    8 nobody    0:00 sh
   15 nobody    0:00 ps
~ $ %

I have tried to disable various capabilities, but still, none of this prevents the nc from running successfully and binding to the port.

docker run --rm -u nobody --cap-drop=SETUID --cap-drop=NET_BIND_SERVICE --cap-drop=SETFCAP --cap-drop=NET_RAW  alpine  nc -l 0.0.0.0 443

In response to David Maze’s answer,
I built an image with Debian GNU/Linux.

Here is the Dockerfile:

FROM python:slim-buster
EXPOSE 80
USER nobody
CMD python -m http.server 80

The docker build command

docker build -t test .

Still able to bind on a privileged port by non-root user

docker run  --rm test

I also tried this for dropping the capability:

docker run  --rm --cap-drop=SETUID --cap-drop=NET_BIND_SERVICE --cap-drop=SETFCAP --cap-drop=NET_RAW  test

Any idea what should I do to remove the capability and trigger the error that I am trying to reproduce?

2

Answers


  1. TL;DR:

    include option --sysctl "net.ipv4.ip_unprivileged_port_start=1024" in your command


    Long answer:

    Introduction

    in docker docs it is stated that

    Warning

    The docker group grants privileges equivalent to the root user.

    this does not refer to the user you are intending to use inside the container but to your ${USER}. It is important because that will grant all the things docker can do!

    However while nobody is not a proper user that can logon the uid exists and docker will just run the program with that uid. see here

    Testing the stated issue

    I tried the netcat issue, but I did not find any proper resources on the alpine nc. Testing it seemed to not properly open ports. 🤷
    The python server gives more insight when testing.

    If you try the following you will see that the security is properly working for the host network.

    correctly permission denied:

    docker run -u nobody --cap-drop=all --network host --rm python:slim-buster python -m http.server 80
    
    docker run  --cap-drop=all --network host --rm python:slim-buster python -m http.server 80
    
    docker run -u nobody --network host --rm python:slim-buster python -m http.server 80
    

    granted for root ( as you can see from the introduction, you are root equivalent ):

    docker run --network host --rm python:slim-buster python -m http.server 80
    

    But yes the "issue" exists for bridges:

    docker run -u nobody --cap-drop=all --rm python:slim-buster python -m http.server 80
    

    Explanation

    Looking around you can find the docker bridge interface is not included in the port opening limitation. Please see issue merge.

    And as usual looking at their tests you will find "the answer" to your underlying question.

    Permission denied in all 3 flavors with bridge

    docker run -u nobody --cap-drop=all --sysctl "net.ipv4.ip_unprivileged_port_start=1024" --rm python:slim-buster  python -m http.server 80
    
    docker run -u nobody --sysctl "net.ipv4.ip_unprivileged_port_start=1024" --rm python:slim-buster  python -m http.server 80
    
    docker run --cap-drop=all --sysctl "net.ipv4.ip_unprivileged_port_start=1024" --rm python:slim-buster  python -m http.server 80
    

    And here it is granted correctly for root (a privileged user, while demanding privileged user for access)

    docker run --sysctl "net.ipv4.ip_unprivileged_port_start=1024" --rm python:slim-buster  python -m http.server 80
    
    Login or Signup to reply.
  2. Alpine is based on a minimal tool set called BusyBox, which contains its own implementation of many standard command-line utilities. The nc syntax that BusyBox supports is

    nc -l -p 443 0.0.0.0
    

    where -l sets up "listen" mode and -p 443 indicates the listening port. (Indeed, without the -p option, I’d expect BusyBox nc to exit immediately with a -l option and two positional arguments, and then you wouldn’t be able to docker exec into the container.)

    Without a -p option, BusyBox nc will pick an arbitrary port and print it to stderr. That won’t be a privileged port, which is why you’re not getting an error there. docker logs should show the port number, and netstat inside the container should show the listener on the alternate port.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search