In my Docker container, why can I still bind the port 1 without `NET_BIND_SERVICE` capability?
Asked Answered
A

1

6

I'm using Ubuntu 18.04 Desktop. Here are more details about my question.

Recently, I'm writing some test code that wants to do this: when it is run as a non-privileged user, the test code tries to bind a privileged port (port 1 in my case) and expects the binding to fail.

On my host machine, my current non-privileged user has the following capsh --print output:

Current: =
Bounding set =cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=1000(ywen)
gid=1000(ywen)
groups=4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare),999(docker),1000(ywen)

Therefore, I can get the permission denial error as expected when trying to bind port 1 using the current non-privileged user:

Python 3.6.9 (default, Oct  8 2020, 12:12:24) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket as s
>>> o = s.socket(s.AF_INET)
>>> o.bind(("127.0.0.1", 1))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
PermissionError: [Errno 13] Permission denied
>>> exit()

Because my test code will eventually be run inside a Docker container, I built an image using the following Dockerfile:

ARG UBUNTU_VERSION=18.04
FROM ubuntu:${UBUNTU_VERSION}
ARG USER_NAME=ywen
ARG USER_ID=1000
ARG GROUP_ID=1000

RUN apt-get update

# Install the needed packages.
RUN DEBIAN_FRONTEND=noninteractive apt-get -y install \
    bash-completion \
    libcap2-bin \
    openssh-server \
    openssh-client \
    sudo \
    tree \
    vim

# Add a non-privileged user.
RUN groupadd -g ${GROUP_ID} ${USER_NAME} && \
    useradd -r --create-home -u ${USER_ID} -g ${USER_NAME} ${USER_NAME}

# Give the non-privileged user the privilege to run `sudo` without a password.
RUN echo "${USER_NAME} ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/${USER_NAME}

# Switch to the non-root user.
USER ${USER_NAME}

# The default command when the container is run.
CMD ["/bin/sleep", "infinity"]

by running the following docker build command:

docker build -f ./Dockerfile.ubuntu --tag port-binding .

The resulting image was called port-binding:latest.

Then I ran it, firstly with the default capabilities as listed here:

docker run --rm -it --name binding port-binding /bin/bash

Then I logged into the container and ran capsh --print. I got:

Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+i
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=1000(ywen)
gid=1000(ywen)
groups=

Currently, I had the cap_net_bind_service capability. Therefore, when I ran the test code at the beginning of this post, the port binding could succeed and I didn't get any error:

Python 3.6.9 (default, Oct  8 2020, 12:12:24) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket as s
>>> o = s.socket(s.AF_INET)
>>> o.bind(("127.0.0.1", 1))    # Succeeded here.
>>>

I thought the success was expected because the container had the cap_net_bind_service capability. So I stopped the container and started a new one that dropped the cap_net_bind_service:

docker run --rm -it --cap-drop=NET_BIND_SERVICE --name binding port-binding /bin/bash

Inside the new container, capsh --print didn't show cap_net_bind_service:

Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+i
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
uid=1000(ywen)
gid=1000(ywen)
groups=

But when I ran the test code, I found I could still succeed in binding the port 1:

Python 3.6.9 (default, Oct  8 2020, 12:12:24) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket as s
>>> o = s.socket(s.AF_INET)
>>> o.bind(("127.0.0.1", 1))    # Didn't raise an error. Still succeeded here.
>>>

However, by reading the following posts, I think removing NET_BIND_SERVICE should be the right thing to do. Obviously, I've made a mistake somewhere. Could someone tell me what I did wrong?

Awful answered 22/1, 2021 at 22:5 Comment(0)
R
16

I had the opposite problem - wanted to bind to port 80 but couldn't. Two days of debugging lead to this: https://github.com/moby/moby/pull/41030 - since docker 20.03.0 the default sysctl net.ipv4.ip_unprivileged_port_start for containers is set to 0 which has the same effect as cap_net_bind_service - all processes inside the container can now bind to any port (of the container) even as an unprivileged user. It can be set externally by docker run --sysctl net.ipv4.ip_unprivileged_port_start=0 ... or docker-compose.yml setting

  sysctls:
    - net.ipv4.ip_unprivileged_port_start=0

Set it to 1024 to get the same behavior as prior to docker 20.03.0

Rafflesia answered 31/3, 2021 at 17:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.