Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,10 @@ RUN /venv/bin/python3 -m pip install --no-cache-dir gunicorn==23.0.0 && \
chown pgadmin:root /pgadmin4/config_distro.py && \
chmod g=u /pgadmin4/config_distro.py && \
chmod g=u /etc/passwd && \
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/python3.[0-9][0-9] && \
PYBIN="$(ls /usr/local/bin/python3.[0-9][0-9] 2>/dev/null | head -n1)" && \
cp "$PYBIN" /usr/local/bin/python3-cap && \
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/python3-cap && \
ln -s /usr/local/bin/python3-cap /venv/bin/python3-cap && \
echo "pgadmin ALL = NOPASSWD: /usr/sbin/postfix start" > /etc/sudoers.d/postfix && \
echo "pgadminr ALL = NOPASSWD: /usr/sbin/postfix start" >> /etc/sudoers.d/postfix

Expand Down
38 changes: 38 additions & 0 deletions docs/en_US/container_deployment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,44 @@ Run a TLS secured container using a shared config/storage directory in
-e 'PGADMIN_ENABLE_TLS=True' \
-d dpage/pgadmin4

Restricted Security Contexts (OpenShift, ``--cap-drop=ALL``)
************************************************************

Some platforms refuse to honor Linux file capabilities. The two situations
the pgAdmin container handles automatically are:

- ``--cap-drop=ALL`` (or an equivalent restricted Kubernetes SecurityContext
such as OpenShift's ``restricted-v2`` SCC), which zeros the bounding set
and removes ``CAP_NET_BIND_SERVICE``. Exec of a capability-tagged binary
then fails with ``Operation not permitted``.

- ``--security-opt=no-new-privileges`` (or
``allowPrivilegeEscalation: false``), which causes the kernel to silently
strip file capabilities on exec. The binary runs, but a subsequent
``bind()`` to a port below 1024 fails with ``EPERM``.

The container's entrypoint reads ``/proc/self/status`` at startup, detects
either condition, switches gunicorn to the non-capability python
interpreter, and (when *PGADMIN_LISTEN_PORT* is not set) defaults the
listen port to **8080** for plain HTTP and **8443** for TLS instead of
80/443. A message is logged so the choice is visible.

In practice this means a typical OpenShift deployment requires no special
build, no setcap, and no custom configuration — only a Service / Route
that targets the chosen non-privileged port:

.. code-block:: bash

docker run --rm -p 8080:8080 \
--security-opt=no-new-privileges \
--cap-drop=ALL \
-e 'PGADMIN_DEFAULT_EMAIL=user@domain.com' \
-e 'PGADMIN_DEFAULT_PASSWORD=SuperSecret' \
dpage/pgadmin4

If you explicitly set *PGADMIN_LISTEN_PORT*, that value is honored in both
the restricted and unrestricted paths.

Reverse Proxying
****************

Expand Down
51 changes: 49 additions & 2 deletions pkg/docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,53 @@ else
fi
fi

# Decide which python interpreter to use for gunicorn.
#
# /venv/bin/python3-cap is a symlink to /usr/local/bin/python3-cap, a copy
# of the system python carrying CAP_NET_BIND_SERVICE. It is needed to bind
# to privileged ports (the default 80/443) as the non-root pgadmin user.
#
# Some platforms refuse to honor file capabilities, in which case execing
# the capped binary either fails outright or silently strips the caps so
# bind() to a port <1024 still returns EPERM:
#
# - NoNewPrivs=1 (--security-opt=no-new-privileges, OpenShift's
# allowPrivilegeEscalation: false): the kernel silently strips file
# capabilities on exec.
# - CAP_NET_BIND_SERVICE missing from the bounding set (--cap-drop=ALL,
# OpenShift restricted-v2 SCC): exec of the capped binary returns
# EPERM.
#
# Detect either condition via /proc/self/status. When restricted, fall
# back to the un-capped venv python and (if the user has not picked a
# port) default PGADMIN_LISTEN_PORT to 8080/8443 so the server can
# actually bind.
PYTHON_BIN=/venv/bin/python3-cap
restricted=0

if grep -q '^NoNewPrivs:[[:space:]]*1' /proc/self/status 2>/dev/null; then
restricted=1
fi

if [ "$restricted" = "0" ]; then
cap_bnd=$(awk '/^CapBnd:/ { print $2 }' /proc/self/status 2>/dev/null)
if [ -n "$cap_bnd" ] && [ "$(( 0x${cap_bnd} & 0x400 ))" -eq 0 ]; then
restricted=1
fi
fi

if [ "$restricted" = "1" ] || [ ! -x /venv/bin/python3-cap ]; then
PYTHON_BIN=/venv/bin/python3
if [ -z "${PGADMIN_LISTEN_PORT}" ]; then
if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
export PGADMIN_LISTEN_PORT=8443
else
export PGADMIN_LISTEN_PORT=8080
fi
echo "Restricted security context detected; defaulting PGADMIN_LISTEN_PORT to ${PGADMIN_LISTEN_PORT}."
fi
fi

# usage: file_env VAR [DEFAULT] ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
# "$XYZ_DB_PASSWORD" from a file, for Docker's secrets feature)
Expand Down Expand Up @@ -275,7 +322,7 @@ else
fi

if [ -n "${PGADMIN_ENABLE_TLS}" ]; then
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
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
else
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
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
fi
Loading