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

Node fails to bind port 443 due to missing setcap on /usr/bin/node binary #25385

Closed
rklaren opened this issue Dec 30, 2024 · 13 comments · Fixed by #25456
Closed

Node fails to bind port 443 due to missing setcap on /usr/bin/node binary #25385

rklaren opened this issue Dec 30, 2024 · 13 comments · Fixed by #25456
Labels
problem Something isn't working

Comments

@rklaren
Copy link
Contributor

rklaren commented Dec 30, 2024

What happened?

I was trying to run zigbee2mqtt via docker compose on port 443 running as a non root user. Despite adding CAP_NET_BIND_SERVICE the service failed to start with an access denied error.

What did you expect to happen?

I expected it to started properly after properly configuring the docker compose set up.

How to reproduce it (minimal and precise)

Environment: Synology DSM 7.2. Docker 20.10.23, docker-compose v2.9.0. Container connected to a macvlan. Zigbee2mqtt started via portainer with this stack:

version: "3"
services:
  zigbee2mqtt:
    container_name: zigbee2mqtt
    image: koenkk/zigbee2mqtt:1.42.0
    user: 1000:1000
    restart: unless-stopped
    volumes:
      - /volume1/docker/Zigbee2mqtt/data:/app/data
    networks:
      macvlan:
        ipv4_address: 192.168.2.195
    environment:
      - TZ=America/Chicago
    devices:
      - /dev/ttyUSB0:/dev/ttyUSB0:rw
    cap_add:
      - NET_BIND_SERVICE
networks:
  macvlan:
    name: 'macvlan'
    external: true

Zigbee2mqtt config:

homeassistant: true
permit_join: false
frontend:
  port: 443
  url: https://zigbee2mqtt.my.domain
  ssl_cert: /app/data/certs/fullchain.pem
  ssl_key: /app/data/certs/privkey.pem
mqtt:
  base_topic: zigbee2mqtt
  server: mqtts://mqtt.my.domain
  user: user
  password: mqtt-pass
serial:
  adapter: ember
  port: /dev/ttyUSB0
  rtscts: false
device_options:
  legacy: false
advanced:
  homeassistant_legacy_entity_attributes: false
  legacy_api: false
  legacy_availability_payload: false
  log_level: info
  network_key: ...
  pan_id: ...
  ext_pan_id: ...
devices:
  ...

This results in the following error in the log:

....
[2024-12-30 12:28:24] info:     z2m: Zigbee2MQTT started!
Error: listen EACCES: permission denied 0.0.0.0:443
    at Server.setupListenHandle [as _listen2] (node:net:1800:21)
    at listenInCluster (node:net:1865:12)

I was able to get the above stack running by executing setcap 'cap_net_bind_service=+ep' /usr/bin/node using a small Dockerfile like this and replacing the original container with my patched one:

FROM koenkk/zigbee2mqtt:1.42.0

RUN apk add --no-cache libcap
RUN setcap 'cap_net_bind_service=+ep' /usr/bin/node

Zigbee2MQTT version

1.42.0

Adapter firmware version

NA

Adapter

NA

Setup

docker-compose/portainer on synology DSM 7.2

Debug log

No response

@rklaren rklaren added the problem Something isn't working label Dec 30, 2024
rklaren added a commit to rklaren/zigbee2mqtt that referenced this issue Dec 30, 2024
In order for the container to bind to low ports the node binary needs to have cap_net_bind_service. This is needed besides adding the  capability when starting the container.
rklaren added a commit to rklaren/zigbee2mqtt that referenced this issue Dec 30, 2024
In order for the container to bind to low ports the node binary needs to have cap_net_bind_service. This is needed besides adding the  capability when starting the container.
rklaren added a commit to rklaren/zigbee2mqtt that referenced this issue Jan 2, 2025
In order for the container to bind to low ports the node binary needs to have cap_net_bind_service. This is needed besides adding the  capability when starting the container.
rklaren added a commit to rklaren/zigbee2mqtt that referenced this issue Jan 2, 2025
In order for the container to bind to low ports the node binary needs to have cap_net_bind_service. This is needed besides adding the  capability when starting the container.
@bo0tzz
Copy link

bo0tzz commented Jan 3, 2025

This change breaks the container for any nonroot deployment that doesn't set CAP_NET_BIND_SERVICE.

@onedr0p
Copy link

onedr0p commented Jan 3, 2025

Yeah I run z2m as a non-root user on Kubernetes and now I get

Using '/config' as data directory
[FATAL tini (7)] exec node failed: Operation not permitted

Setting the securityContext like this helped, but I was already running zigbee2mqtt on a privileged port (80) successfully without this change.

            securityContext:
              allowPrivilegeEscalation: false
              readOnlyRootFilesystem: true
              capabilities:
                drop: ["ALL"]
                add: ["NET_BIND_SERVICE"] # Added this

I think this change to the Dockerfile should be reverted.

@rklaren
Copy link
Contributor Author

rklaren commented Jan 3, 2025

Ugh, that's annoying. If we leave the libcap/setcap in the image then we can fix it by conditionally doing the setcap call in the docker-entrypoint.sh I'd guess.
I can probably make a fix up PR later in the evening.

@onedr0p
Copy link

onedr0p commented Jan 3, 2025

conditionally doing the setcap call in the docker-entrypoint.sh I'd guess.

This would break people running z2m rootless like me, unless you define an env to enable it (with default off)

@rklaren
Copy link
Contributor Author

rklaren commented Jan 3, 2025

In my case I was trying to run z2m as non root on a low port (due to macvlan I'm not able to map ports). It is required to be root in order to execute the setcap tool to change the capabilities of the node process. So that's not really the right solution.

Testing if the capability is there does seem to work as non root e.g. check with capsh --has-b=cap_net_bind_service whether the capability was added to the container.

So I'm thinking of adding a wrapper script for node that has NET_BIND_SERVICE (inheritable) that executes node. And in the docker entry point select the wrapper or plain node based on whether the capability was granted to the container.

The other option is to add the wrapper for node with the capability and use a dumb environment variable to switch things.

Or a combination of the two, try to be smart, and an environment override to disable things for unforeseen consequences.

@rklaren
Copy link
Contributor Author

rklaren commented Jan 4, 2025

I played around a bit and do not see a graceful way to do this without breaking backwards compatibility in some way.

Any use of setcap/setpriv still requires root so the wrapper script would require to be run as root. The best solution so I've found so far is to make a copy of node and do the setcap on there, and override the CMD in the container in my docker compose to point at the copy. In theory the node binary used in the args passed to the docker-entrypoint script could be patched but that gets all too magical for my taste.

Adding the copy of node adds 40Mb to the container which I'm not a fan of either. I'll stick to maintaining a local version of the zigbee2mqtt container.

@Koenkk it's probably best to revert the change. This looked easier on the surface, apologies for the noise.

@onedr0p
Copy link

onedr0p commented Jan 4, 2025

A reasonable thing to do would be to put a reverse proxy in front of z2m. I don't know why you have requirements of using 443 and to terminate SSL at z2m instead of a reverse proxy.

@rklaren
Copy link
Contributor Author

rklaren commented Jan 4, 2025

@onedr0p I'm currently setting up HA+mosquitto+z2mt on a synology NAS, this all works reasonably ok, with separate docker compose stacks, macvlan for networking and letsencrypt for the certificates. The whole set up will currently survive DSM upgrades. Adding a reverse proxy requires hacking the nginx config which may not survive an upgrade.
So far it works well enough and the DSM has enough CPU/memory to run the containers. But the DSM does add some extra nuisance to setting things up. Anyway patching the z2m container isn't that hard.
If it becomes a pain I can still move the HA set up to a raspberry Pi or so.

@onedr0p
Copy link

onedr0p commented Jan 4, 2025

You can run traefik, caddy or https://nginxproxymanager.com/ as a container for the reverse proxy in DSM Container Manager or Portainer. All those options support SSL with letsencrypt too, I don't see why using those options wouldn't survive a reboot or upgrade of DSM.

@rklaren
Copy link
Contributor Author

rklaren commented Jan 4, 2025

Thank you for the pointer! I hadn't run across that one yet.

@onedr0p
Copy link

onedr0p commented Jan 4, 2025

I opened a PR to revert the change #25456

@Ongy
Copy link

Ongy commented Jan 4, 2025

Also in favor of reverting. This just cost me a while on the 2.0.0 update since I prefer to drop all capabilities.

I also think it's "wrong" to try and modify the root image, since I also like to set my container root image as read-only.

For this specific thing, the best way I know of to achieve the original goal is via sysctls.
I.e. https://docs.docker.com/reference/cli/docker/container/run/#sysctl with https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html#:~:text=Default%3A%20Empty-,ip_unprivileged_port_start,-%2D%20INTEGER

Should be roughly:
docker run --sysctl net.ipv4.ip_unprivileged_port_start=0

Though I only ever do it on k8s, so some experimentation might be necessary.
(besides, I'm not entirely sure why you run it on --network=host instead of dockerfied network and a port mapping from 443)

@Koenkk
Copy link
Owner

Koenkk commented Jan 5, 2025

Merged #25456, assuming this can be closed now.

Changes will be available in the dev branch in a few hours from now.

@Koenkk Koenkk closed this as completed Jan 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
problem Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants