Skip to content

Hardening (Security improvements)#302

Open
jwkaltz wants to merge 35 commits into
GeoNodeUserGroup-DE:mainfrom
jwkaltz:hardening
Open

Hardening (Security improvements)#302
jwkaltz wants to merge 35 commits into
GeoNodeUserGroup-DE:mainfrom
jwkaltz:hardening

Conversation

@jwkaltz

@jwkaltz jwkaltz commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Description

Hardening (security improvement) as described in #301.

Current status:

  • Validate in a Dev environment with RKE2, NFS
  • Consider Gemini suggestions to this PR
  • Validate in a Production environment
  • Check if custom Geonode-Geoserver image can be avoided (currently needed to run as non root) or propose improvement to Geonode project
  • Check if custom Geonode image can be avoided -> yes, helm chart was now refactored to allow for running standard geonode image as non-root including settings-additions.
  • Document usage and update steps (notably PVC changes)

Type of Change

Please select the relevant option:

  • Bug fix
  • New feature
  • Documentation update
  • Refactoring
  • Other (please describe)

Related Issue

closes #301

@jwkaltz jwkaltz marked this pull request as draft May 29, 2026 08:05

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances security across the GeoNode Helm chart by configuring non-root execution, dropping capabilities, and setting seccomp profiles for various components. It also migrates from a single shared persistent volume to four distinct PVCs, updates Nginx to run unprivileged on port 8080 with a read-only root filesystem, and configures Celery to log to stdout/stderr. Feedback highlights a potential permission conflict on the shared statics volume due to mismatched fsGroup values, resource waste from using a single hardcoded storage size for all split PVCs, the need to conditionally render storageClassName, and cleanup of unused favicon values.

Comment thread charts/geonode/templates/nginx/nginx-deploy.yaml Outdated
Comment thread charts/geonode/templates/geonode/geonode-pvc.yaml Outdated
Comment thread charts/geonode/templates/geonode/geonode-pvc.yaml
Comment thread charts/geonode/templates/nginx/nginx-deploy.yaml
@mwallschlaeger

Copy link
Copy Markdown
Member

@jwkaltz I appreciate your PR. But we will need an upgrade path, as you changes to the PVC setup seems like a breaking change to me on first view.

Comment thread charts/geonode/templates/geonode/jobs/geonode-statics-job.yaml
Comment thread charts/geonode/templates/geonode/jobs/geonode-statics-job.yaml
Comment thread charts/geonode/templates/geonode/geonode-deploy.yaml
@jwkaltz

jwkaltz commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

@jwkaltz I appreciate your PR. But we will need an upgrade path, as you changes to the PVC setup seems like a breaking change to me on first view.

Good point, we will at least need some migration notes (and a new release).
But for now, it is still a work in progress anyway, although this is a first working helm chart, there are still a couple of non-trivial things I am not happy with (see description), and I will also consider the Gemini feedback, and your review comments as well (thanks for that).

jwkaltz added 6 commits June 2, 2026 07:32
…ng and better control of subdirectory creation (avoid subPath)
…Cs, to allow more granular handling and better control of subdirectory creation (avoid subPath); Celery: do not write to log files within container; nginx: use other image (unprivileged)
… creates directory creation permission issues and is already copied anyway in another process.
…Cs, to allow more granular handling and better control of subdirectory creation (avoid subPath)
…Cs, to allow more granular handling and better control of subdirectory creation (avoid subPath). Note: requires a Geoserver Docker image which supports non-root runtime
@mwallschlaeger

Copy link
Copy Markdown
Member

I would like to do a major version for this changes along with the introduction of gateway api.

@mwallschlaeger mwallschlaeger added this to the v2.0.0 milestone Jun 2, 2026
@jwkaltz

jwkaltz commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

I have addressed the previous review comments. Please review the latest revision of this pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant security and storage enhancements to the GeoNode Helm chart. Key changes include splitting the single shared persistent volume into separate dedicated PVCs for statics, geoserver data, backup/restore, and data directories, as well as applying strict security contexts (non-root execution, dropped capabilities, and read-only root filesystems) across all components. Additionally, Nginx is migrated to an unprivileged image running on port 8080 with temporary writable scratch directories. Review feedback highlights critical improvements: wrapping octal file modes (e.g., 0555) in double quotes to prevent YAML 1.2 parsing errors, defaulting Nginx's runAsUser to 101 to avoid template rendering issues, and adding fsGroupChangePolicy: OnRootMismatch to the GeoNode deployment to prevent slow pod startup times when mounting large volumes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread charts/geonode/templates/geonode/geonode-deploy.yaml
Comment thread charts/geonode/templates/geonode/jobs/geonode-init-db-job.yaml
Comment thread charts/geonode/templates/pycsw/pycsw-deploy.yaml
Comment thread charts/geonode/templates/nginx/nginx-deploy.yaml Outdated
Comment thread charts/geonode/templates/nginx/nginx-deploy.yaml Outdated
Comment thread charts/geonode/templates/geonode/geonode-deploy.yaml Outdated
@jwkaltz

jwkaltz commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

I have addressed the previous review comments, and integrated your suggestions, except changing defaultMode to a string, which does not work. Please review the latest revision of this pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces security context configurations across various GeoNode components to support running containers as non-root users, updates Nginx to an unprivileged image listening on port 8080, and splits the single persistent volume claim into separate PVCs for statics, GeoServer data, backup/restore, and general data. Feedback on these changes focuses on preventing potential Helm rendering errors by safely defaulting the newly introduced component-specific securityContext values to empty dictionaries before accessing nested fields. Additionally, it is recommended to use the -D flag with the install command in the statics job to ensure target directories are created automatically.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread charts/geonode/templates/geonode/jobs/geonode-statics-job.yaml Outdated
Comment thread charts/geonode/templates/geonode/geonode-deploy.yaml Outdated
Comment thread charts/geonode/templates/geonode/geonode-deploy.yaml Outdated
Comment thread charts/geonode/templates/geonode/geonode-deploy.yaml Outdated
Comment thread charts/geonode/templates/nginx/nginx-deploy.yaml Outdated
Comment thread charts/geonode/templates/nginx/nginx-deploy.yaml Outdated
Comment thread charts/geonode/templates/geoserver/geoserver-deploy.yaml Outdated
Comment thread charts/geonode/templates/pycsw/pycsw-deploy.yaml Outdated
@jwkaltz

jwkaltz commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

I have added refactoring in order to be able to run the standard geonode container as non-root, and also introduced a temporary fix in order to be able to run celery as non-root. Please review the latest revision of this pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant security and architectural improvements to the GeoNode Helm chart, including running containers as non-root, configuring Nginx as unprivileged on port 8080, splitting the single PVC into dedicated PVCs for different storage needs, and implementing a Python settings wrapper to handle configuration overrides. Feedback on these changes highlights a copy-paste typo in the Celery temporary fix script, potential nil pointer evaluation errors across multiple templates if nested keys under persistant or component securityContext are omitted in custom values, and a recommendation to pass globals() to exec() in the settings wrapper.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread charts/geonode/templates/geonode/geonode-pvc.yaml Outdated
Comment thread charts/geonode/files/celery-cmd-temporary-fix Outdated
Comment thread charts/geonode/templates/geonode/geonode-pvc.yaml Outdated
Comment thread charts/geonode/templates/geonode/geonode-pvc.yaml Outdated
Comment thread charts/geonode/templates/geonode/geonode-pvc.yaml Outdated
Comment thread charts/geonode/templates/geoserver/geoserver-deploy.yaml Outdated
Comment thread charts/geonode/templates/geoserver/geoserver-deploy.yaml Outdated
Comment thread charts/geonode/templates/pycsw/pycsw-deploy.yaml Outdated
Comment thread charts/geonode/templates/pycsw/pycsw-deploy.yaml Outdated
Comment thread charts/geonode/templates/nginx/nginx-deploy.yaml Outdated
@mwallschlaeger

Copy link
Copy Markdown
Member

@jwkaltz I haven't checked your latest changes. That said first i will give this version a try in the upcoming week. Also we will need default values.yaml working with the default geonode/geonode image. And have some documentation on how the hardened version can be deployed together with the steps on howto build a geonode image capable to support the hardened features.

As we are building our geonode ourself, too. I will evaluate your recommendations on this, too. Further more we need an update path for updating from chart version 1.3.3 to version 2.0.0 (incl. hardening). Furthermore we will add support for gateway API to this repository as a last feature of 1.x chart version next week.

@jwkaltz

jwkaltz commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

@jwkaltz I haven't checked your latest changes. That said first i will give this version a try in the upcoming week. Also we will need default values.yaml working with the default geonode/geonode image. And have some documentation on how the hardened version can be deployed together with the steps on howto build a geonode image capable to support the hardened features.

I changed the default now so that the default geonode/geoserver image can be used. (The default geonode/geonode image can already be used with hardened settings, because we have an included patch for the celery-cmd script, this is the only issue I have found so far).
And there is some documentation on how to harden geonode/geoserver as well.

As we are building our geonode ourself, too. I will evaluate your recommendations on this, too. Further more we need an update path for updating from chart version 1.3.3 to version 2.0.0 (incl. hardening). Furthermore we will add support for gateway API to this repository as a last feature of 1.x chart version next week.

I have added a note regarding update path, it is not much but I am not sure what further information would be useful.

@mwallschlaeger mwallschlaeger requested a review from Zalfsten June 9, 2026 08:04
@mwallschlaeger

Copy link
Copy Markdown
Member

@jwkaltz I managed to do my first tests on your PR. What I did:

  1. Run the new default configuration using minikube-values.yaml:

Could deploy the chart without problems. GeoNode was working properly on first glance.

  1. Build Non Root Geoserver image:

build.sh worked as expected. With this step I'm a bit afraid that this build.sh will always work in future as things can change in geoserver Dockerfile. So the expectation would be that geonode-k8s maintaince this build.sh, what i'm atm not quite sure about. But nevertheless its working for the sake of this PR.

  1. I deployed non-root geoserver without editing minikube-values.yaml

Image was not starting properly as it has missing permission. This change was applied to existing deployment.

  1. I updated minikube-values.yaml along your documentation

Geoserver still has missing permission as the files were created with root user permission before.

so applying this changes to existing infrastructure will need to update permissions of the files on geoserver before updating. This need to be investigated and documented.

In general thinks look quite ready to me, but are missing more documentation regarding updates. I will to do some more tests on:

  • container level to see if thinks work properly inside the containers (executing manage.py, logfiles ...)
  • apply this changes to an existing deployment and gather information on how an update strategy could look like
  • review new pvc setup in more detail

@Zalfsten Zalfsten left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first glance, everything looks fine

Only I for myself came to the conclusion that UID/GID 1000 is not the best idea.

Comment thread charts/geonode/values.yaml
Comment thread geoserver-nonroot/build.sh
Comment thread geoserver-nonroot/Dockerfile
@jwkaltz

jwkaltz commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

With this step I'm a bit afraid that this build.sh will always work in future as things can change in geoserver Dockerfile. So the expectation would be that geonode-k8s maintaince this build.sh, what i'm atm not quite sure about. But nevertheless its working for the sake of this PR.

Yes, we don't want to maintain this in the long run, the goal definitely is to run a standard geonode/geoserver image as non-root. There is a discussion about that here: GeoNode/geonode-docker#59 (comment)

So the custom image is just a workaround if you need geonode now in a non-root K8S, hopefully we can get rid of it with one of the next versions of geonode.

@jwkaltz

jwkaltz commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

Geoserver still has missing permission as the files were created with root user permission before.

Yes this is tricky and requires manual intervention on the storage. If Geoserver was run as root before, the file ownerships in the geoserver pvc need to be manually changed once.

Zalfsten
Zalfsten previously approved these changes Jun 9, 2026

@Zalfsten Zalfsten left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

@mwallschlaeger

Copy link
Copy Markdown
Member

I have run claude for some deeper security checks on this changes. He came up with many ways of improvements, most of them are not the scope of this PR, so i will create individual issues for them. This will be the target on the first minor patch of the next version. But some parts of the review are relevant for this PR, too.

Please review the remarks below, i think these are reasonable change requests for the current PR:

Pod Security Matrix

Pod Runs As Capabilities seccomp readOnlyRootFS
geonode / celery UID 1000 ALL dropped RuntimeDefault No
geoserver UID 1000 ALL dropped RuntimeDefault No
nginx UID 101 ALL dropped RuntimeDefault Yes
pycsw UID 1000 ALL dropped RuntimeDefault No
redis UID 999 ALL dropped RuntimeDefault Yes
memcached UID 11211 ALL dropped RuntimeDefault No
postgres-operator UID 1000 ALL dropped RuntimeDefault Yes
postgres UID 0 (root) 14 caps: DAC_OVERRIDE, NET_RAW, SYS_CHROOT, SETFCAP, SETPCAP … none No

H4 — Statics PVC is shared and writable by geonode, celery, and geoserver

The pvc-geonode-statics PVC is mounted read-write in geonode, celery, and geoserver. nginx serves its contents to end users. Confirmed:

STATICS PVC WRITABLE from geoserver!
STATICS PVC WRITABLE from celery!

If an attacker compromises GeoServer (e.g., via the GeoServer RCE CVEs from 2024), they can write malicious JavaScript or HTML into /mnt/volumes/statics/static/, which nginx then serves to all users — enabling persistent stored XSS or a supply-chain-style attack against users of the platform.

Recommendation: Mount the statics PVC as readOnly: true in the geoserver deployment. GeoServer has no legitimate reason to write to the statics volume.

# geoserver-deploy.yaml volumeMounts
- name: pvc-statics
  mountPath: /mnt/volumes/statics
  readOnly: true       # add this

MEDIUM

M1 — Setuid binaries present in the geoserver-nonroot image

-rwsr-xr-x 1 root root /usr/bin/su
-rwsr-xr-x 1 root root /usr/bin/passwd
-rwsr-xr-x 1 root root /usr/bin/newgrp
-rwsr-xr-x 1 root root /usr/bin/mount
-rwsr-xr-x 1 root root /usr/bin/umount

With all Linux capabilities dropped (CapEff: 0000000000000000) and runAsUser: 1000, these binaries cannot currently be exploited to gain root — CAP_SETUID is absent. However, they are unnecessary attack surface inherited from the full Debian base image.

Recommendation: Strip setuid bits in geoserver-nonroot/Dockerfile:

RUN find / -xdev -perm -4000 -exec chmod u-s {} \; 2>/dev/null || true

M2 — readOnlyRootFilesystem not set on geonode, celery, geoserver, pycsw

nginx, redis, and postgres-operator all have readOnlyRootFilesystem: true. The geonode, celery, geoserver, and pycsw containers do not. The overlayfs mount does prevent writes to / in practice (confirmed: touch /test_rw fails), but without the explicit security context field the Kubernetes admission layer cannot enforce it, and it could silently regress if the image or entrypoint changes.

Recommendation: Add readOnlyRootFilesystem: true to all remaining containers. This requires mapping out which paths are written at startup and adding explicit emptyDir mounts for each (e.g., /var/log, /tmp already has one, /usr/src/geonode/invoke.log, etc.).


M4 — geoserver-nonroot/Dockerfile hardcodes UID in chown despite using ARG

geoserver-nonroot/Dockerfile:

ARG GEOSERVER_UID=1000
ARG GEOSERVER_GID=1000

RUN chown -R 1000:1000 /usr/local/tomcat \   # <-- hardcoded, ignores ARG
    && chown -R 1000:1000 /opt \
    ...
USER ${GEOSERVER_UID}:${GEOSERVER_GID}        # correctly uses ARG

Building with --build-arg GEOSERVER_UID=500 produces an image where USER 500 tries to run a process in directories still owned by 1000:1000, causing a startup failure.

Recommendation:

RUN chown -R ${GEOSERVER_UID}:${GEOSERVER_GID} /usr/local/tomcat \
    && chown -R ${GEOSERVER_UID}:${GEOSERVER_GID} /opt \
    ...

M5 — celery-cmd ConfigMap mounted with defaultMode: 0755 instead of 0555

All other ConfigMaps in this PR were tightened from 07440555. The celery-cmd ConfigMap is mounted 0755 (charts/geonode/templates/geonode/geonode-deploy.yaml), which is inconsistent with the hardening intent — the owner-write bit is set on a file owned by root and executed by the non-root container user.

Recommendation: Change to defaultMode: 0555.


LOW

L1 — fsGroupChangePolicy: OnRootMismatch missing on init-db and statics jobs

The main geonode deployment and geoserver have fsGroupChangePolicy: OnRootMismatch, avoiding expensive recursive chown on every start. The geonode-init-db-job and geonode-statics-job omit this field and will perform a full recursive chown on each run. Minor performance inconsistency.


L3 — memcached runAsUser: 11211 uses a port number as UID

11211 is the memcached port, not a real user ID. The official memcached image runs as UID 999 (memcache user). Running as a non-existent UID works in Linux but produces an unnamed process and may cause confusion in audit logs. Functionally harmless.


@jwkaltz

jwkaltz commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Please review the remarks below, i think these are reasonable change requests for the current PR:

Thanks @mwallschlaeger I will test them all (today if possible) with my use cases, and commit everything that does not cause problems.

@mwallschlaeger

Copy link
Copy Markdown
Member

@jwkaltz i will review your changes in the upcoming week, again. i'm planning to do some tests on chart update from current to hardened version with the goal to know how to update our deployments, and to provide some docs on how to deal with the update. Again thx to your great work on this PR.

@jwkaltz

jwkaltz commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

@jwkaltz i will review your changes in the upcoming week, again. i'm planning to do some tests on chart update from current to hardened version with the goal to know how to update our deployments, and to provide some docs on how to deal with the update. Again thx to your great work on this PR.

Thanks @mwallschlaeger , I have just now committed the final changes for the review comments up to "M2 — readOnlyRootFilesystem", I think these are complete now.
I hope to have time to address the remaining issues (they are lower prio ones, but they look useful to me) by the end of the week.

@jwkaltz

jwkaltz commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

11211 is the memcached port, not a real user ID. The official memcached image runs as UID 999 (memcache user). Running as a non-existent UID works in Linux but produces an unnamed process and may cause confusion in audit logs. Functionally harmless.

This finding is not correct for the image that the helm chart currently uses. 11211 is correct:
kubectl exec -n geonode deploy/geonode-memcached -- grep memcache /etc/passwd memcache:x:11211:11211::/home/memcache:/bin/sh

So no change to this pull request here (but all other findings have been implemented now).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Hardening of Geonode K8S

3 participants