Skip to content

Commit 0d11dbc

Browse files
authored
fix(docker): make CAP_NET_BIND_SERVICE optional for restricted runtimes (#9985)
The container previously applied CAP_NET_BIND_SERVICE to the python interpreter so the non-root pgadmin user could bind to ports 80/443. Some platforms refuse to honor file capabilities: - --cap-drop=ALL / OpenShift restricted-v2 SCC zero the bounding set, so the kernel returns EPERM on exec of any capability-tagged binary. This makes the image fail to start (issue #9657). - --security-opt=no-new-privileges / allowPrivilegeEscalation: false causes the kernel to silently strip file capabilities on exec, so the binary runs but a subsequent bind() to <1024 still fails. Split the interpreter so neither default behavior nor restricted-runtime support has to give up the other: - Dockerfile copies python3.X to /usr/local/bin/python3-cap and applies setcap to the copy. /usr/local/bin/python3.X stays un-capped, so /venv/bin/python3 (which symlinks to it) execs cleanly under restricted SCCs. A parallel /venv/bin/python3-cap symlink keeps the venv activation working when the capped interpreter is used. - entrypoint.sh reads /proc/self/status at startup. If NoNewPrivs is set, or CAP_NET_BIND_SERVICE is missing from the bounding set, gunicorn is invoked through the un-capped python and (when PGADMIN_LISTEN_PORT is unset) the default port falls back to 8080 for plain HTTP or 8443 for TLS. A startup message records the choice. - Existing deployments with the default 80/443 mapping are unaffected: on every unrestricted runtime the bounding set still contains NET_BIND_SERVICE and gunicorn runs through the capped interpreter exactly as before. - PGADMIN_LISTEN_PORT, if set, is honored in both paths. Docs gain a "Restricted Security Contexts" subsection covering the new auto-detected fallback and the OpenShift / --cap-drop=ALL invocation. Fixes #9657
1 parent 533aed1 commit 0d11dbc

3 files changed

Lines changed: 91 additions & 3 deletions

File tree

Dockerfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,10 @@ RUN /venv/bin/python3 -m pip install --no-cache-dir gunicorn==23.0.0 && \
204204
chown pgadmin:root /pgadmin4/config_distro.py && \
205205
chmod g=u /pgadmin4/config_distro.py && \
206206
chmod g=u /etc/passwd && \
207-
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/python3.[0-9][0-9] && \
207+
PYBIN="$(ls /usr/local/bin/python3.[0-9][0-9] 2>/dev/null | head -n1)" && \
208+
cp "$PYBIN" /usr/local/bin/python3-cap && \
209+
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/python3-cap && \
210+
ln -s /usr/local/bin/python3-cap /venv/bin/python3-cap && \
208211
echo "pgadmin ALL = NOPASSWD: /usr/sbin/postfix start" > /etc/sudoers.d/postfix && \
209212
echo "pgadminr ALL = NOPASSWD: /usr/sbin/postfix start" >> /etc/sudoers.d/postfix
210213

docs/en_US/container_deployment.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,44 @@ Run a TLS secured container using a shared config/storage directory in
320320
-e 'PGADMIN_ENABLE_TLS=True' \
321321
-d dpage/pgadmin4
322322
323+
Restricted Security Contexts (OpenShift, ``--cap-drop=ALL``)
324+
************************************************************
325+
326+
Some platforms refuse to honor Linux file capabilities. The two situations
327+
the pgAdmin container handles automatically are:
328+
329+
- ``--cap-drop=ALL`` (or an equivalent restricted Kubernetes SecurityContext
330+
such as OpenShift's ``restricted-v2`` SCC), which zeros the bounding set
331+
and removes ``CAP_NET_BIND_SERVICE``. Exec of a capability-tagged binary
332+
then fails with ``Operation not permitted``.
333+
334+
- ``--security-opt=no-new-privileges`` (or
335+
``allowPrivilegeEscalation: false``), which causes the kernel to silently
336+
strip file capabilities on exec. The binary runs, but a subsequent
337+
``bind()`` to a port below 1024 fails with ``EPERM``.
338+
339+
The container's entrypoint reads ``/proc/self/status`` at startup, detects
340+
either condition, switches gunicorn to the non-capability python
341+
interpreter, and (when *PGADMIN_LISTEN_PORT* is not set) defaults the
342+
listen port to **8080** for plain HTTP and **8443** for TLS instead of
343+
80/443. A message is logged so the choice is visible.
344+
345+
In practice this means a typical OpenShift deployment requires no special
346+
build, no setcap, and no custom configuration — only a Service / Route
347+
that targets the chosen non-privileged port:
348+
349+
.. code-block:: bash
350+
351+
docker run --rm -p 8080:8080 \
352+
--security-opt=no-new-privileges \
353+
--cap-drop=ALL \
354+
-e 'PGADMIN_DEFAULT_EMAIL=user@domain.com' \
355+
-e 'PGADMIN_DEFAULT_PASSWORD=SuperSecret' \
356+
dpage/pgadmin4
357+
358+
If you explicitly set *PGADMIN_LISTEN_PORT*, that value is honored in both
359+
the restricted and unrestricted paths.
360+
323361
Reverse Proxying
324362
****************
325363

pkg/docker/entrypoint.sh

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,53 @@ else
7575
fi
7676
fi
7777

78+
# Decide which python interpreter to use for gunicorn.
79+
#
80+
# /venv/bin/python3-cap is a symlink to /usr/local/bin/python3-cap, a copy
81+
# of the system python carrying CAP_NET_BIND_SERVICE. It is needed to bind
82+
# to privileged ports (the default 80/443) as the non-root pgadmin user.
83+
#
84+
# Some platforms refuse to honor file capabilities, in which case execing
85+
# the capped binary either fails outright or silently strips the caps so
86+
# bind() to a port <1024 still returns EPERM:
87+
#
88+
# - NoNewPrivs=1 (--security-opt=no-new-privileges, OpenShift's
89+
# allowPrivilegeEscalation: false): the kernel silently strips file
90+
# capabilities on exec.
91+
# - CAP_NET_BIND_SERVICE missing from the bounding set (--cap-drop=ALL,
92+
# OpenShift restricted-v2 SCC): exec of the capped binary returns
93+
# EPERM.
94+
#
95+
# Detect either condition via /proc/self/status. When restricted, fall
96+
# back to the un-capped venv python and (if the user has not picked a
97+
# port) default PGADMIN_LISTEN_PORT to 8080/8443 so the server can
98+
# actually bind.
99+
PYTHON_BIN=/venv/bin/python3-cap
100+
restricted=0
101+
102+
if grep -q '^NoNewPrivs:[[:space:]]*1' /proc/self/status 2>/dev/null; then
103+
restricted=1
104+
fi
105+
106+
if [ "$restricted" = "0" ]; then
107+
cap_bnd=$(awk '/^CapBnd:/ { print $2 }' /proc/self/status 2>/dev/null)
108+
if [ -n "$cap_bnd" ] && [ "$(( 0x${cap_bnd} & 0x400 ))" -eq 0 ]; then
109+
restricted=1
110+
fi
111+
fi
112+
113+
if [ "$restricted" = "1" ] || [ ! -x /venv/bin/python3-cap ]; then
114+
PYTHON_BIN=/venv/bin/python3
115+
if [ -z "${PGADMIN_LISTEN_PORT}" ]; then
116+
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
117+
export PGADMIN_LISTEN_PORT=8443
118+
else
119+
export PGADMIN_LISTEN_PORT=8080
120+
fi
121+
echo "Restricted security context detected; defaulting PGADMIN_LISTEN_PORT to ${PGADMIN_LISTEN_PORT}."
122+
fi
123+
fi
124+
78125
# usage: file_env VAR [DEFAULT] ie: file_env 'XYZ_DB_PASSWORD' 'example'
79126
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
80127
# "$XYZ_DB_PASSWORD" from a file, for Docker's secrets feature)
@@ -275,7 +322,7 @@ else
275322
fi
276323

277324
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
278-
exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
325+
exec $SU_EXEC "${PYTHON_BIN}" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" --keyfile /certs/server.key --certfile /certs/server.cert -c gunicorn_config.py run_pgadmin:app
279326
else
280-
exec $SU_EXEC /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
327+
exec $SU_EXEC "${PYTHON_BIN}" /venv/bin/gunicorn --limit-request-line "${GUNICORN_LIMIT_REQUEST_LINE:-8190}" --limit-request-fields "${GUNICORN_LIMIT_REQUEST_FIELDS:-100}" --limit-request-field_size "${GUNICORN_LIMIT_REQUEST_FIELD_SIZE:-8190}" --timeout "${TIMEOUT}" --bind "${BIND_ADDRESS}" -w 1 --threads "${GUNICORN_THREADS:-25}" --access-logfile "${GUNICORN_ACCESS_LOGFILE:--}" -c gunicorn_config.py run_pgadmin:app
281328
fi

0 commit comments

Comments
 (0)