pgAdmin4 running on most restricted kubernetes environments

GitHub

EDIT: On Oct 8th 2025, I have merged an official pgadmin4 helm chart, available on oci://docker.io/dpage/pgadmin4-helm. (PR)



May 14th 2025

Running pgAdmin4 on certain kubernetes distributions that includes security policies like OpenShift 'restricted-v2' SCC or 'restricted' Pod Security Standard can be a real pain and difficult even if you are very experienced with kubernetes and security concepts of k8s.

Most people will find themselves either giving some privileges to their container or using some custom made container image or giving up. It is true that pgAdmin4 container is not well suited for DevOps security standards.

Lets get to the practical needs to run it.

First of all, the pgAdmin4 container was running as predefined user 'pgadmin' by default and I have commited to official pgAdmin4 Dockerfile (PR) to change it to user 5050 instead (pgadmin UID == 5050), because if k8s securityContext runAsNonRoot=true is set, k8s wouldn't know what uid the user would run with by reading the image's metadata which will result in CreateContainerConfigError. Implementation since pgAdmin4 v9.4.0 which doesn't exist at the time im writing this but tag 'snapshot' can be used instead.




I will number the things we have to handle to run this container securely


1. Change listen port to higher than 1023 (Must if using OpenShift or choosing the second option in section 7)

2. Disable postfix service (If you don't disable it, the application will throw a warning but won't crash)

3. Writing pgadmin data to /var/lib/pgadmin during runtime (Not a must if running as user 5050 and readOnlyFileSystem=false is set, otherwise a must)

4. Writing logs to /var/log/pgadmin during runtime (Not a must if running as user 5050 and readOnlyFileSystem=false is set, otherwise a must)

5. Writing cache to /tmp during runtime (Must only if using securityContext readOnlyFileSystem=true)

6. /pgadmin4/config_distro.py file is getting overriden during runtime, (Not a must if running as user 5050 and readOnlyFileSystem=false is set, otherwise a must)

7. Unlock the usage of 'python3' CLI (/venv/bin/python3: Operation not permitted) error. (2 ways, depends on your security policy)




Lets look at each of them


1. Probably the easiest along the next one, adding the next environment variable to your pod. PGADMIN_LISTEN_PORT="8080"

env:
- name: PGADMIN_LISTEN_PORT
  value: "8080"


2. Adding the next environment variable to your pod. PGADMIN_DISABLE_POSTFIX=true
env:
- name: PGADMIN_DISABLE_POSTFIX
  value: "true"


3. Adding a volume to /var/lib/pgadmin path, can be either PVC or emptyDir
volumes:
- name: pgadmin-data
  emptyDir: {}
volumeMounts:
- name: pgadmin-data
  mountPath: /var/lib/pgadmin


4, 5. Adding an emptyDir volume to /var/log/pgadmin, /tmp paths.
volumes:
- name: empty-dir
  emptyDir: {}
volumeMounts:
- name: empty-dir
  mountPath: /var/log/pgadmin
  subPath: logs
- name: empty-dir
  mountPath: /tmp
  subPath: tmp


6. As for this config_distro.py file, it is empty and getting overriden during runtime, you can either use a configMap to mount your own configuration or change the file permissions with an initContainer and emptyDir and then mounting it in the main container as I did. Im adding volumes, initContainer and main container's configuration.
volumes:
  - name empty-dir
    emptyDir: {}
initContainers:
- name: modify-config-distro-py-permissions
  workingDir: /emptydir
  command: ["sh", "-x", "-c"]
  args: ["cp /pgadmin4/config_distro.py . && chmod 777 config_distro.py"]
  image: docker.io/dpage/pgadmin4:<MAKE SURE ITS THE SAME IMAGE AS THE ORIGINAL CONTAINER>
  securityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]
    privileged: false
    readOnlyRootFilesystem: true
    runAsNonRoot: true
    seLinuxOptions: {}
    seccompProfile:
      type: RuntimeDefault
    volumeMounts:
    - name: empty-dir
      mountPath: /emptydir
containers:
- volumeMounts
  - name: empty-dir
    mountPath: /pgadmin4/config_distro.py
    subPath: config_distro.py
    ...
    ...


7. If you take a look at the Dockerfile of the pgAdmin image Here, at the end you can see that they are using setcap command which allows the binary of python3.VER to run and expose at ports lower than 1024 even if you are using non root user, they were trying to allow this because they are using UID 5050 in their container and wanted to maintain usage of default port 80.


There are 2 ways solving this, depends on your security policy:

A. When using restricted PSS or OpenShift SCCs, you must drop all capabilities excluding the one pgAdmin4 attached to the python3 CLI. We are lucky that the only capability that is allowed with these k8s security policies is the same one, NET_BIND_SERVICE. The solution is to drop all capabilities and add this capability. Im adding how it supposed to look like.
securityContext:
  capabilities:
    drop: ["ALL"]
    add: ["NET_BIND_SERVICE"]

B. THIS METHOD ALWAYS WORKS.
If you're using a custom admission controller framework policy (Kyverno/OPA-Gatekeeper) that doesn't allow you to add the NET_BIND_SERVICE capability you must use an initContainer and an emptyDir to unset the capability and override the python3 cli file, Im adding the initContainer and the volumeMount of the main container
initContainers:
- name: unset-python3-cli-net-cap
  workingDir: /emptydir
  command: ["sh", "-x", "-c"]
  args: ["ls /usr/bin/python3.* | sort -V -r | head -n 1 | xargs -I {} cp {} python3"]
  image: docker.io/dpage/pgadmin4:<MAKE SURE ITS THE SAME IMAGE AS THE ORIGINAL CONTAINER>
  securityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]
    privileged: false
    readOnlyRootFilesystem: true
    runAsNonRoot: true
    seLinuxOptions: {}
    seccompProfile:
      type: RuntimeDefault
  volumeMounts:
  - name: empty-dir
    mountPath: /emptydir
containers:
- volumeMounts:
  - name: empty-dir
    mountPath: /usr/bin/python3
    subPath python3
  ...
  ...



I have added a deployment.yaml example file in my github pages repository. deployment.yaml
kubectl create -f  -n YOUR_NAMESPACE


Thanks for reading everyone. Koren.