diff --git a/README.md b/README.md index 5be000e..f3c028a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - [GeoWhat?](#Geonode) - [Geonode-k8s](#geonode-k8s) -- [Install Guilde](#install) +- [Install Guide](#install) **Homepage:** @@ -69,6 +69,31 @@ Define your own values.yaml to configure your geonode installation. Use the [doc vi my-values.yaml ``` +## Hardened K8S environments + +By default, this Helm Chart is intended to run in hardened K8S environments, notably with non-root permissions. To use it, ensure that your storage will be writeable by the geonode technical user; UID and GID can be configured in the values file. + +There are two exceptions to this rule: +- the `geonode-postgres` Pod does not have full securityContext out-of-the-box, because the subchart `postgres-operator` does not support that yet. A workaround is available if you want the full securityContext, for that, set `postgres.kyvernoSecurityContext`to `enabled` in your values YAML. +- the `geonode/geoserver` Pod, because the current `geonode/geoserver` Docker Image does not support running as non-root out-of-the-box, therefore, default chart settings for this image are set to run as root. If you wish to run this Pod as non-root, proceed as follows: + +Create a custom image with the provided script: +```bash +cd geoserver-nonroot +./build.sh +``` +And then override the settings in your values file, for example: +```bash +geoserver: + image: + name: geonode/geoserver-nonroot + securityContext: + runAsNonRoot: true + fsGroup: 1000 + runAsUser: 1000 + runAsGroup: 1000 +``` + ## Install chart ```bash @@ -81,6 +106,11 @@ helm upgrade --cleanup-on-fail --install --namespace geonode --create-namespace helm delete --namespace geonode geonode geonode ``` +## Migrating from earlier versions of this Helm Chart + +The hardened version of this helm chart introduced a split from one common PVC for geonode to four distinct PVC. +When upgrading from earlier versions of the helm chart, you may need to copy user manually to the new PVC. + ## Contribution ### Create an Issue diff --git a/charts/geonode/README.md b/charts/geonode/README.md index ff0e898..46b374b 100644 --- a/charts/geonode/README.md +++ b/charts/geonode/README.md @@ -224,6 +224,7 @@ If `geonode.gatewayApi.hostnames` is left empty, the route defaults to | postgres.external.ssl | string | `"prefer"` | | | postgres.geodata_databasename_and_username | string | `"geodata"` | geoserver database name and username | | postgres.geonode_databasename_and_username | string | `"geonode"` | geonode database name and username | +| postgres.kyvernoSecurityContext.enabled | bool | `false`| Provide full securityContext to postgres Pod via Kyverno, as postgres-operator does not yet fully support securityContext | | postgres.operator.allowedSourceRanges | list | `[]` | when one or more load balancers are enabled for the cluster, this parameter defines the comma-separated range of IP networks (in CIDR-notation). The corresponding load balancer is accessible only to the networks defined by this parameter. Optional, when empty the load balancer service becomes inaccessible from outside of the Kubernetes cluster. | | postgres.operator.annotations | object | `{}` | additional annotation for postgresql object | | postgres.operator.clone | object | `{}` | | diff --git a/charts/geonode/files/celery-cmd-temporary-fix b/charts/geonode/files/celery-cmd-temporary-fix new file mode 100644 index 0000000..4872be0 --- /dev/null +++ b/charts/geonode/files/celery-cmd-temporary-fix @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# A configurable celery command. +# Luca Pasquali +CELERY_BIN=${CELERY_BIN:-"$(which celery||echo celery)"} +CELERY_APP=${CELERY_APP:-"geonode.celery_app:app"} +CELERY__STATE_DB=${CELERY__STATE_DB:-"/mnt/volumes/statics/worker@%h.state"} +# expressed in KB +CELERY__MAX_MEMORY_PER_CHILD=${CELERY__MAX_MEMORY_PER_CHILD:-"200000"} +CELERY__AUTOSCALE_VALUES=${CELERY__AUTOSCALE_VALUES:-"10,5"} +CELERY__MAX_TASKS_PER_CHILD=${CELERY__MAX_TASKS_PER_CHILD:-"10"} +CELERY__OPTS=${CELERY__OPTS:-"--without-gossip --without-mingle -Ofair -E"} +CELERY__LOG_LEVEL=${CELERY__LOG_LEVEL:-"ERROR"} +CELERY__LOG_FILE=${CELERY__LOG_FILE:-"/var/log/celery.log"} +CELERY__WORKER_NAME=${CELERY__WORKER_NAME:-"worker1@%h"} +CELERY__WORKER_CONCURRENCY=${CELERY__WORKER_CONCURRENCY:-"4"} + +# Celery beat settings +CELERY__BEAT_SCHEDULE=${CELERY__BEAT_SCHEDULE:-"celery.beat:PersistentScheduler"} +CELERY__BEAT_DB=${CELERY__BEAT_DB:-"/mnt/volumes/statics/celerybeat-schedule"} +CELERY__BEAT_LOG=${CELERY__BEAT_LOG:-"/var/log/celery_beat.log"} + +# Harvester settings +CELERY__HARVESTER_WORKER_NAME=${CELERY__HARVESTER_WORKER_NAME:-"harvesting_worker@%h"} +CELERY__HARVESTER_CONCURRENCY=${CELERY__HARVESTER_CONCURRENCY:-"10"} +CELERY__HARVESTER_AUTOSCALE_VALUES=${CELERY__HARVESTER_AUTOSCALE_VALUES:-"15,10"} +CELERY__HARVESTER_MAX_MEMORY_PER_CHILD=${CELERY__HARVESTER_MAX_MEMORY_PER_CHILD:-"500000"} + +# --- FIX: Remove stale Beat pidfile before starting beat --- +BEAT_PIDFILE="/tmp/celerybeat.pid" + +if [ -f "$BEAT_PIDFILE" ]; then + PID=$(cat "$BEAT_PIDFILE" 2>/dev/null) + + # If PID exists and is running → warn but continue (avoid killing) + if kill -0 "$PID" 2>/dev/null; then + echo "WARNING: Celery Beat seems to be running already (PID $PID). Removing stale pidfile anyway." + else + echo "Removing stale Celery Beat pidfile: $BEAT_PIDFILE" + fi + + rm -f "$BEAT_PIDFILE" +fi +# --- END FIX --- + +echo "Starting Celery Beat..." +$CELERY_BIN -A $CELERY_APP beat --scheduler=$CELERY__BEAT_SCHEDULE \ + --schedule=$CELERY__BEAT_DB \ + --loglevel=$CELERY__LOG_LEVEL -f $CELERY__BEAT_LOG --pidfile=/tmp/celerybeat.pid & + +echo "Starting Default Celery Worker..." +$CELERY_BIN -A $CELERY_APP worker --autoscale=$CELERY__AUTOSCALE_VALUES \ + --max-memory-per-child=$CELERY__MAX_MEMORY_PER_CHILD $CELERY__OPTS \ + --statedb=$CELERY__STATE_DB \ + --loglevel=$CELERY__LOG_LEVEL -n $CELERY__WORKER_NAME -f $CELERY__LOG_FILE \ + --concurrency=$CELERY__WORKER_CONCURRENCY --max-tasks-per-child=$CELERY__MAX_TASKS_PER_CHILD \ + -X harvesting & + +echo "Starting Harvester Celery Worker..." +$CELERY_BIN -A $CELERY_APP worker -Q harvesting \ + --autoscale=$CELERY__HARVESTER_AUTOSCALE_VALUES \ + --max-memory-per-child=$CELERY__HARVESTER_MAX_MEMORY_PER_CHILD \ + --loglevel=$CELERY__LOG_LEVEL \ + -n $CELERY__HARVESTER_WORKER_NAME \ + --concurrency=$CELERY__HARVESTER_CONCURRENCY \ + -f $CELERY__LOG_FILE & + +# Wait for any process to exit +wait -n + +# Exit with the status of the process that exited first +# Docker will restart the container if this is non-zero (i.e., a failure) +exit $? diff --git a/charts/geonode/files/settings_wrapper.py b/charts/geonode/files/settings_wrapper.py new file mode 100644 index 0000000..87b7cb0 --- /dev/null +++ b/charts/geonode/files/settings_wrapper.py @@ -0,0 +1,9 @@ +# Overlay for existing GeoNode settings + +from pathlib import Path +from geonode.settings import * + +settings_additions = Path("/opt/geonode-custom/settings_additions.py") + +if settings_additions.exists(): + exec(settings_additions.read_text(encoding="utf-8")) diff --git a/charts/geonode/templates/_helpers.tpl b/charts/geonode/templates/_helpers.tpl index 4637136..73c9ef3 100644 --- a/charts/geonode/templates/_helpers.tpl +++ b/charts/geonode/templates/_helpers.tpl @@ -117,8 +117,20 @@ postgres-password {{- end -}} # Volume names -{{- define "persistant_volume_name" -}} -persistence +{{- define "pvc_statics_name" -}} +pvc-{{ .Release.Name }}-geonode-statics +{{- end -}} + +{{- define "pvc_geoserver_data_name" -}} +pvc-{{ .Release.Name }}-geonode-geoserver-data +{{- end -}} + +{{- define "pvc_backup_restore_name" -}} +pvc-{{ .Release.Name }}-geonode-backup-restore +{{- end -}} + +{{- define "pvc_data_name" -}} +pvc-{{ .Release.Name }}-geonode-data {{- end -}} # ports and endpoints diff --git a/charts/geonode/templates/geonode/celery-cmd-temporary-fix.yaml b/charts/geonode/templates/geonode/celery-cmd-temporary-fix.yaml new file mode 100644 index 0000000..d4f3a65 --- /dev/null +++ b/charts/geonode/templates/geonode/celery-cmd-temporary-fix.yaml @@ -0,0 +1,8 @@ +# Temporary fix pending https://github.com/GeoNode/geonode/pull/14292 +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-celery-cmd-temporary-fix +data: + celery-cmd: | +{{ .Files.Get "files/celery-cmd-temporary-fix" | indent 4 }} diff --git a/charts/geonode/templates/geonode/geonode-deploy.yaml b/charts/geonode/templates/geonode/geonode-deploy.yaml index 320b545..c25ae1e 100644 --- a/charts/geonode/templates/geonode/geonode-deploy.yaml +++ b/charts/geonode/templates/geonode/geonode-deploy.yaml @@ -31,12 +31,15 @@ spec: checksum/geonode-env: {{ include (print $.Template.BasePath "/geonode/geonode-env.yaml") . | sha256sum }} checksum/geonode-secret: {{ include (print $.Template.BasePath "/geonode/geonode-secret.yaml") . | sha256sum }} checksum/geonode-tasks-py: {{ include (print $.Template.BasePath "/geonode/geonode-tasks-py-conf.yaml") . | sha256sum }} - checksum/geonode-local-settings: {{ include (print $.Template.BasePath "/geonode/geonode-local-settings.yaml") . | sha256sum }} checksum/geonode-settings-additions: {{ include (print $.Template.BasePath "/geonode/geonode-settings-additions.yaml") . | sha256sum }} checksum/geonode-uwsig: {{ include (print $.Template.BasePath "/geonode/geonode-uwsgi-ini-conf.yaml") . | sha256sum }} spec: +{{- $gnctx := default dict .Values.geonode.securityContext }} + securityContext: + runAsNonRoot: true + fsGroup: {{- if hasKey $gnctx "fsGroup" }} {{ $gnctx.fsGroup }} {{- else }} {{ .Values.global.securityContext.fsGroup }} {{- end }} + fsGroupChangePolicy: OnRootMismatch terminationGracePeriodSeconds: 3 - {{- if not (empty .Values.geonode.imagePullSecret) }} imagePullSecrets: - name: {{ .Values.geonode.imagePullSecret }} @@ -45,6 +48,17 @@ spec: initContainers: # Wait for Postgres and rabbit - name: {{ .Values.geonode.init.container_name }} + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault image: "{{ .Values.geonode.init.image.name }}:{{ .Values.geonode.init.image.tag }}" imagePullPolicy: {{ .Values.geonode.init.imagePullPolicy }} args: @@ -59,34 +73,56 @@ spec: - -wait - tcp://{{ include "redis_host" . }} {{ end }} + - name: seed-home + image: "{{ .Values.geonode.image.name }}:{{ .Values.geonode.image.tag }}" + imagePullPolicy: {{ .Values.geonode.imagePullPolicy }} + command: ["sh", "-c"] + args: + - | + cp -r /home/ubuntu/. /seed-geonode/ + cp -r /home/ubuntu/. /seed-celery/ + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: home-geonode + mountPath: /seed-geonode + - name: home-celery + mountPath: /seed-celery containers: # This is the django app server - name: {{ .Values.geonode.container_name }} + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault image: "{{ .Values.geonode.image.name }}:{{ .Values.geonode.image.tag }}" imagePullPolicy: {{ .Values.geonode.imagePullPolicy }} command: - bash - -c - | - # Install xmljson for GeoNode monitoring module (missing dependency in 4.4.x) - if ! python -c "import xmljson" 2>/dev/null; then - echo "Installing xmljson..." - pip install --no-cache-dir xmljson - else - echo "xmljson already installed, skipping..." - fi - # execute prescript if defined {{ if .Values.geonode.tasks_pre_script }} invoke prescript {{ end }} - cd {{ include "geonode_root_path" .}}/ - # Add config overrides - cat /usr/src/geonode/geonode/geonode-k8s-settings.py >> {{ include "geonode_path" .}}/settings.py - cat /usr/src/geonode/geonode/geonode-k8s-settings-additions.py >> {{ include "geonode_path" .}}/settings.py - # Setup {{ include "geonode_root_path" .}}/entrypoint.sh uwsgi --ini {{ include "geonode_root_path" .}}/uwsgi.ini @@ -121,22 +157,22 @@ spec: value: "postgis://$(GEONODE_GEODATABASE):$(GEONODE_GEODATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(GEONODE_GEODATABASE)" - name: DATABASE_URL value: "postgis://$(GEONODE_DATABASE):$(GEONODE_DATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(GEONODE_DATABASE)" + - name: PYTHONPATH + value: "/opt/geonode-custom" volumeMounts: - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-statics mountPath: /mnt/volumes/statics - subPath: statics - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-geoserver-data mountPath: /geoserver_data/data - subPath: geoserver-data-dir - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-backup-restore mountPath: /backup_restore - subPath: backup-restore - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-data mountPath: /data - subPath: data - name: cache-volume mountPath: /tmp + - name: home-geonode + mountPath: /home/ubuntu - name: tasks-py mountPath: "{{ include "geonode_root_path" .}}/tasks.py" subPath: tasks.py @@ -148,12 +184,13 @@ spec: mountPath: "{{ include "geonode_root_path" .}}/uwsgi.ini" subPath: uwsgi.ini readOnly: true - - name: geonode-k8s-settings-py - mountPath: "/usr/src/geonode/geonode/geonode-k8s-settings.py" - subPath: geonode-k8s-settings.py - - name: geonode-k8s-settings-additions-py - mountPath: "/usr/src/geonode/geonode/geonode-k8s-settings-additions.py" - subPath: geonode-k8s-settings-additions.py + - name: settings-wrapper + mountPath: /opt/geonode-custom/settings_wrapper.py + subPath: settings_wrapper.py + readOnly: true + - name: settings-additions + mountPath: /opt/geonode-custom/settings_additions.py + subPath: settings_additions.py readOnly: true {{ if .Values.geonodeFixtures }} - name: geonode-fixtures @@ -171,32 +208,32 @@ spec: # Celery is the task worker - name: {{ .Values.geonode.celery.container_name }} + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault image: "{{ .Values.geonode.image.name }}:{{ .Values.geonode.image.tag }}" imagePullPolicy: {{ .Values.geonode.celery.imagePullPolicy }} command: - bash - -c - | - # Install xmljson for GeoNode monitoring module (missing dependency in 4.4.x) - if ! python -c "import xmljson" 2>/dev/null; then - echo "Installing xmljson..." - pip install --no-cache-dir xmljson - else - echo "xmljson already installed, skipping..." - fi - - cd {{ include "geonode_root_path" .}}/ - {{ if .Values.geonode.sentry.enabled }} pip install sentry-sdk {{ end }} - # Add config overrides - cat /usr/src/geonode/geonode/geonode-k8s-settings.py >> {{ include "geonode_path" .}}/settings.py - cat /usr/src/geonode/geonode/geonode-k8s-settings-additions.py >> {{ include "geonode_path" .}}/settings.py + set -e + echo "Starting celery-cmd..." + # Setup - touch /var/log/celery.log - {{ include "geonode_root_path" .}}/entrypoint.sh celery-cmd + exec {{ include "geonode_root_path" .}}/entrypoint.sh celery-cmd envFrom: - configMapRef: @@ -209,6 +246,16 @@ spec: env: - name: IS_CELERY value: 'True' + # Override Celery Log file location as defined in "celery-cmd". Container should write to stdout, not files. + - name: CELERY__LOG_FILE + value: "/dev/stdout" + - name: CELERY__BEAT_LOG + value: "/dev/stdout" + - name: CELERY__LOG_LEVEL + value: "INFO" + - name: INVOKE_LOG_STDOUT + value: "True" + # - name: GEONODE_DATABASE_PASSWORD valueFrom: secretKeyRef: @@ -228,25 +275,26 @@ spec: value: "postgis://$(GEONODE_GEODATABASE):$(GEONODE_GEODATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(GEONODE_GEODATABASE)" - name: DATABASE_URL value: "postgis://$(GEONODE_DATABASE):$(GEONODE_DATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(GEONODE_DATABASE)" + # + - name: PYTHONPATH + value: "/opt/geonode-custom" ports: - containerPort: 5555 volumeMounts: - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-statics mountPath: /mnt/volumes/statics - subPath: statics - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-geoserver-data mountPath: /geoserver_data/data - subPath: geoserver-data-dir - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-backup-restore mountPath: /backup_restore - subPath: backup-restore - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-data mountPath: /data - subPath: data - name: cache-volume mountPath: /tmp + - name: home-celery + mountPath: /home/ubuntu - name: entrypoint-sh mountPath: "{{ include "geonode_root_path" .}}/entrypoint.sh" subPath: entrypoint.sh @@ -254,18 +302,24 @@ spec: mountPath: "{{ include "geonode_root_path" .}}/tasks.py" subPath: tasks.py readOnly: true - - name: geonode-k8s-settings-py - mountPath: "/usr/src/geonode/geonode/geonode-k8s-settings.py" - subPath: geonode-k8s-settings.py - - name: geonode-k8s-settings-additions-py - mountPath: "/usr/src/geonode/geonode/geonode-k8s-settings-additions.py" - subPath: geonode-k8s-settings-additions.py + - name: settings-wrapper + mountPath: /opt/geonode-custom/settings_wrapper.py + subPath: settings_wrapper.py + readOnly: true + - name: settings-additions + mountPath: /opt/geonode-custom/settings_additions.py + subPath: settings_additions.py readOnly: true {{ if .Values.geonodeFixtures }} - name: geonode-fixtures mountPath: "{{ include "geonode_path" .}}/fixtures" readOnly: true {{ end }} + # Temporary fix pending fix in upstream Geonode https://github.com/GeoNode/geonode/pull/14292 + - name: celery-cmd + mountPath: /usr/bin/celery-cmd + subPath: celery-cmd + readOnly: true resources: requests: @@ -276,9 +330,18 @@ spec: cpu: {{ .Values.geonode.celery.resources.limits.cpu }} volumes: - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-statics + persistentVolumeClaim: + claimName: {{ include "pvc_statics_name" . }} + - name: pvc-geoserver-data + persistentVolumeClaim: + claimName: {{ include "pvc_geoserver_data_name" . }} + - name: pvc-backup-restore + persistentVolumeClaim: + claimName: {{ include "pvc_backup_restore_name" . }} + - name: pvc-data persistentVolumeClaim: - claimName: pvc-{{ .Release.Name }}-geonode + claimName: {{ include "pvc_data_name" . }} - name: tasks-py configMap: name: {{ .Release.Name }}-geonode-tasks-py @@ -288,36 +351,30 @@ spec: - name: entrypoint-sh configMap: name: {{ .Release.Name }}-geonode-entrypoint-sh - defaultMode: 0744 + defaultMode: 0555 items: - key: entrypoint.sh path: "entrypoint.sh" - name: uwsgi-ini configMap: name: {{ .Release.Name }}-geonode-uwsgi-ini - defaultMode: 0744 + defaultMode: 0555 items: - key: uwsgi.ini path: "uwsgi.ini" - - name: geonode-k8s-settings-py + - name: settings-wrapper configMap: - name: {{ .Release.Name }}-geonode-k8s-settings-py - defaultMode: 0744 - items: - - key: geonode-k8s-settings.py - path: "geonode-k8s-settings.py" - - name: geonode-k8s-settings-additions-py + name: {{ .Release.Name }}-geonode-settings-wrapper + defaultMode: 0555 + - name: settings-additions configMap: - name: {{ .Release.Name }}-geonode-k8s-settings-additions-py - defaultMode: 0744 - items: - - key: geonode-k8s-settings-additions.py - path: "geonode-k8s-settings-additions.py" + name: {{ .Release.Name }}-geonode-settings-additions + defaultMode: 0555 {{- if .Values.geonodeFixtures }} - name: geonode-fixtures configMap: name: {{ .Release.Name }}-geonode-fixtures - defaultMode: 0744 + defaultMode: 0555 items: {{- range $key, $value := .Values.geonodeFixtures }} - key: {{ $key | quote }} @@ -327,4 +384,13 @@ spec: # Using an emptyDir to cache compiled statics... it will survive container crashes, but not pod restarts - name: cache-volume emptyDir: {} + - name: home-geonode + emptyDir: {} + - name: home-celery + emptyDir: {} + # Temporary fix pending fix in upstream Geonode https://github.com/GeoNode/geonode/pull/14292 + - name: celery-cmd + configMap: + name: {{ .Release.Name }}-celery-cmd-temporary-fix + defaultMode: 0555 diff --git a/charts/geonode/templates/geonode/geonode-entrypoint-sh-conf.yaml b/charts/geonode/templates/geonode/geonode-entrypoint-sh-conf.yaml index b0e3cda..84c522e 100644 --- a/charts/geonode/templates/geonode/geonode-entrypoint-sh-conf.yaml +++ b/charts/geonode/templates/geonode/geonode-entrypoint-sh-conf.yaml @@ -11,18 +11,15 @@ data: INVOKE_LOG_STDOUT=${INVOKE_LOG_STDOUT:-FALSE} invoke () { - if [ $INVOKE_LOG_STDOUT = 'true' ] || [ $INVOKE_LOG_STDOUT = 'True' ] + if [ "$INVOKE_LOG_STDOUT" = 'true' ] || [ "$INVOKE_LOG_STDOUT" = 'True' ] then - /usr/local/bin/invoke $@ + /usr/local/bin/invoke "$@" else - /usr/local/bin/invoke $@ > {{ include "geonode_root_path" . }}/invoke.log 2>&1 + /usr/local/bin/invoke "$@" > /tmp/invoke.log 2>&1 fi echo "$@ tasks done" } - # Start cron && memcached services - service cron restart - echo $"\n\n\n" echo "-----------------------------------------------------" echo "STARTING DJANGO ENTRYPOINT $(date)" diff --git a/charts/geonode/templates/geonode/geonode-favicon.yaml b/charts/geonode/templates/geonode/geonode-favicon.yaml index f34d401..191c2b1 100644 --- a/charts/geonode/templates/geonode/geonode-favicon.yaml +++ b/charts/geonode/templates/geonode/geonode-favicon.yaml @@ -1,3 +1,4 @@ +{{- if .Values.favicon }} apiVersion: v1 kind: ConfigMap metadata: @@ -5,3 +6,4 @@ metadata: namespace: {{ .Release.Namespace }} binaryData: favicon.ico: {{ .Values.favicon }} +{{- end }} diff --git a/charts/geonode/templates/geonode/geonode-local-settings.yaml b/charts/geonode/templates/geonode/geonode-local-settings.yaml deleted file mode 100644 index c6f80c0..0000000 --- a/charts/geonode/templates/geonode/geonode-local-settings.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-geonode-k8s-settings-py -data: - geonode-k8s-settings.py: | - # add these import lines to the top of your geonode settings file - {{ if .Values.geonode.sentry.enabled }} - import sentry_sdk - BUILD_NUMBER = os.environ.get("BUILD_NUMBER", "0") - AUTHENTICATION_BACKENDS += ('allauth.account.auth_backends.AuthenticationBackend',) - - SENTRY_ENABLED = ast.literal_eval(os.getenv("SENTRY_ENABLED", "False")) - if SENTRY_ENABLED: - import sentry_sdk - - print("sentry enabled ...") - SENTRY_DSN = os.getenv("SENTRY_DSN") - print(SENTRY_DSN) - sentry_sdk.init( - dsn=SENTRY_DSN, - release="geonodex@{}.{}".format(VERSION, BUILD_NUMBER), - environment=os.getenv("SENTRY_ENVIRONMENT", "development"), - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production. - traces_sample_rate=1.0, - # If you wish to associate users to errors (assuming you are using - # django.contrib.auth) you may enable sending PII data. - send_default_pii=True, - ) - {{ end }} - - THESAURUS_DEFAULT_LANG = os.environ.get("THESAURUS_DEFAULT_LANG", "en") \ No newline at end of file diff --git a/charts/geonode/templates/geonode/geonode-pvc.yaml b/charts/geonode/templates/geonode/geonode-pvc.yaml index 63f7808..6584d91 100644 --- a/charts/geonode/templates/geonode/geonode-pvc.yaml +++ b/charts/geonode/templates/geonode/geonode-pvc.yaml @@ -1,12 +1,59 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: pvc-{{ .Release.Name }}-geonode + name: pvc-{{ .Release.Name }}-geonode-statics namespace: {{ .Release.Namespace }} spec: accessModes: - {{ .Values.global.accessMode }} + {{- if .Values.global.storageClass }} storageClassName: {{ .Values.global.storageClass }} + {{- end }} resources: requests: - storage: {{ .Values.geonode.persistant.storageSize }} + storage: {{ (default dict .Values.geonode.persistant.statics).storageSize | default .Values.geonode.persistant.storageSize }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pvc-{{ .Release.Name }}-geonode-geoserver-data + namespace: {{ .Release.Namespace }} +spec: + accessModes: + - {{ .Values.global.accessMode }} + {{- if .Values.global.storageClass }} + storageClassName: {{ .Values.global.storageClass }} + {{- end }} + resources: + requests: + storage: {{ (default dict .Values.geonode.persistant.geoserver_data).storageSize | default .Values.geonode.persistant.storageSize }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pvc-{{ .Release.Name }}-geonode-backup-restore + namespace: {{ .Release.Namespace }} +spec: + accessModes: + - {{ .Values.global.accessMode }} + {{- if .Values.global.storageClass }} + storageClassName: {{ .Values.global.storageClass }} + {{- end }} + resources: + requests: + storage: {{ (default dict .Values.geonode.persistant.backup_restore).storageSize | default .Values.geonode.persistant.storageSize }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pvc-{{ .Release.Name }}-geonode-data + namespace: {{ .Release.Namespace }} +spec: + accessModes: + - {{ .Values.global.accessMode }} + {{- if .Values.global.storageClass }} + storageClassName: {{ .Values.global.storageClass }} + {{- end }} + resources: + requests: + storage: {{ (default dict .Values.geonode.persistant.data).storageSize | default .Values.geonode.persistant.storageSize }} diff --git a/charts/geonode/templates/geonode/geonode-settings-additions.yaml b/charts/geonode/templates/geonode/geonode-settings-additions.yaml index 92bd509..38e765f 100644 --- a/charts/geonode/templates/geonode/geonode-settings-additions.yaml +++ b/charts/geonode/templates/geonode/geonode-settings-additions.yaml @@ -1,7 +1,38 @@ apiVersion: v1 kind: ConfigMap metadata: - name: {{ .Release.Name }}-geonode-k8s-settings-additions-py + name: {{ .Release.Name }}-geonode-settings-additions + namespace: {{ .Release.Namespace }} data: - geonode-k8s-settings-additions.py: {{- .Values.geonode.general.settings_additions | toYaml | indent 1 }} + settings_additions.py: | + import os +{{- if .Values.geonode.sentry.enabled }} + import ast + import sentry_sdk + BUILD_NUMBER = os.environ.get("BUILD_NUMBER", "0") + AUTHENTICATION_BACKENDS += ('allauth.account.auth_backends.AuthenticationBackend',) + SENTRY_ENABLED = ast.literal_eval(os.getenv("SENTRY_ENABLED", "False")) + if SENTRY_ENABLED: + import sentry_sdk + + print("sentry enabled ...") + SENTRY_DSN = os.getenv("SENTRY_DSN") + print(SENTRY_DSN) + sentry_sdk.init( + dsn=SENTRY_DSN, + release="geonodex@{}.{}".format(VERSION, BUILD_NUMBER), + environment=os.getenv("SENTRY_ENVIRONMENT", "development"), + # Set traces_sample_rate to 1.0 to capture 100% + # of transactions for performance monitoring. + # We recommend adjusting this value in production. + traces_sample_rate=1.0, + # If you wish to associate users to errors (assuming you are using + # django.contrib.auth) you may enable sending PII data. + send_default_pii=True, + ) +{{- end }} + + THESAURUS_DEFAULT_LANG = os.environ.get("THESAURUS_DEFAULT_LANG", "en") + +{{ .Values.geonode.general.settings_additions | indent 4 }} diff --git a/charts/geonode/templates/geonode/geonode-settings-wrapper.yaml b/charts/geonode/templates/geonode/geonode-settings-wrapper.yaml new file mode 100644 index 0000000..15c4c2b --- /dev/null +++ b/charts/geonode/templates/geonode/geonode-settings-wrapper.yaml @@ -0,0 +1,10 @@ +# charts/geonode/templates/geonode/geonode-settings-wrapper.yaml + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-geonode-settings-wrapper + namespace: {{ .Release.Namespace }} +data: + settings_wrapper.py: | +{{- .Files.Get "files/settings_wrapper.py" | nindent 4 }} diff --git a/charts/geonode/templates/geonode/jobs/geonode-cleanup-job.yaml b/charts/geonode/templates/geonode/jobs/geonode-cleanup-job.yaml index 5910a86..7e8eb4c 100644 --- a/charts/geonode/templates/geonode/jobs/geonode-cleanup-job.yaml +++ b/charts/geonode/templates/geonode/jobs/geonode-cleanup-job.yaml @@ -57,10 +57,25 @@ spec: spec: serviceAccountName: "{{ include "geonode_pod_name" . }}-cleanup" restartPolicy: Never +{{- $gnctx := default dict .Values.geonode.securityContext }} + securityContext: + runAsNonRoot: true + fsGroup: {{- if hasKey $gnctx "fsGroup" }} {{ $gnctx.fsGroup }} {{- else }} {{ .Values.global.securityContext.fsGroup }} {{- end }} initContainers: - name: cleanup-init-db-job image: "{{ .Values.geonode.hooks.kubectlImage }}:{{ .Values.geonode.hooks.kubectlTag }}" imagePullPolicy: IfNotPresent + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault command: - kubectl args: @@ -70,9 +85,24 @@ spec: - -n - {{ .Release.Namespace }} - --ignore-not-found=true + - --cache-dir=/tmp/kubecache + volumeMounts: + - name: tmp + mountPath: /tmp - name: cleanup-statics-job image: "{{ .Values.geonode.hooks.kubectlImage }}:{{ .Values.geonode.hooks.kubectlTag }}" imagePullPolicy: IfNotPresent + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault command: - kubectl args: @@ -82,14 +112,34 @@ spec: - -n - {{ .Release.Namespace }} - --ignore-not-found=true + - --cache-dir=/tmp/kubecache + volumeMounts: + - name: tmp + mountPath: /tmp + containers: - name: cleanup-done image: "{{ .Values.geonode.hooks.kubectlImage }}:{{ .Values.geonode.hooks.kubectlTag }}" imagePullPolicy: IfNotPresent + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault command: - kubectl args: - version - --client=true + + volumes: + - name: tmp + emptyDir: {} {{- end }}{{/* ne $desiredImage $existingImage */}} {{- end }}{{/* cleanupOnUpgrade */}} diff --git a/charts/geonode/templates/geonode/jobs/geonode-init-db-job.yaml b/charts/geonode/templates/geonode/jobs/geonode-init-db-job.yaml index 8573d18..42d08f2 100644 --- a/charts/geonode/templates/geonode/jobs/geonode-init-db-job.yaml +++ b/charts/geonode/templates/geonode/jobs/geonode-init-db-job.yaml @@ -22,11 +22,28 @@ spec: - name: {{ .Values.geonode.imagePullSecret }} {{ end }} restartPolicy: OnFailure +{{- $gnctx := default dict .Values.geonode.securityContext }} + securityContext: + runAsNonRoot: true + fsGroup: {{- if hasKey $gnctx "fsGroup" }} {{ $gnctx.fsGroup }} {{- else }} {{ .Values.global.securityContext.fsGroup }} {{- end }} + fsGroupChangePolicy: OnRootMismatch + initContainers: # Wait for Postgres and rabbit - name: {{ .Values.geonode.init.container_name }}-init-db-job image: "{{ .Values.geonode.init.image.name }}:{{ .Values.geonode.init.image.tag }}" imagePullPolicy: {{ .Values.geonode.init.imagePullPolicy }} + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault args: - -timeout=60s - -wait @@ -44,15 +61,23 @@ spec: - name: {{ .Values.geonode.container_name }}-init-db-job image: "{{ .Values.geonode.image.name }}:{{ .Values.geonode.image.tag }}" imagePullPolicy: {{ .Values.geonode.imagePullPolicy }} + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault command: - bash - -c - | set -e cd {{ include "geonode_root_path" .}}/ - # Add config overrides - cat /usr/src/geonode/geonode/geonode-k8s-settings.py >> {{ include "geonode_path" .}}/settings.py - cat /usr/src/geonode/geonode/geonode-k8s-settings-additions.py >> {{ include "geonode_path" .}}/settings.py echo "-----------------------------------------------------" echo "Starting Geonode migration tasks (waitfordbs, migrations) $(date)" @@ -109,47 +134,59 @@ spec: value: "postgis://$(GEONODE_GEODATABASE):$(GEONODE_GEODATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(GEONODE_GEODATABASE)" - name: DATABASE_URL value: "postgis://$(GEONODE_DATABASE):$(GEONODE_DATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(GEONODE_DATABASE)" + - name: PYTHONPATH + value: /opt/geonode-custom volumeMounts: - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-data mountPath: /data - subPath: data - name: cache-volume mountPath: /tmp - name: tasks-py mountPath: "{{ include "geonode_root_path" .}}/tasks.py" subPath: tasks.py readOnly: true - - name: geonode-k8s-settings-py - mountPath: "/usr/src/geonode/geonode/geonode-k8s-settings.py" - subPath: geonode-k8s-settings.py - - name: geonode-k8s-settings-additions-py - mountPath: "/usr/src/geonode/geonode/geonode-k8s-settings-additions.py" - subPath: geonode-k8s-settings-additions.py + - name: settings-wrapper + mountPath: /opt/geonode-custom/settings_wrapper.py + subPath: settings_wrapper.py + readOnly: true + - name: settings-additions + mountPath: /opt/geonode-custom/settings_additions.py + subPath: settings_additions.py + readOnly: true + {{- if .Values.geonodeFixtures }} + - name: geonode-fixtures + mountPath: "{{ include "geonode_path" .}}/fixtures" readOnly: true + {{- end }} volumes: - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-data persistentVolumeClaim: - claimName: pvc-{{ .Release.Name }}-geonode + claimName: {{ include "pvc_data_name" . }} - name: tasks-py configMap: name: {{ .Release.Name }}-geonode-tasks-py items: - key: tasks.py path: "tasks.py" - - name: geonode-k8s-settings-py + - name: settings-wrapper configMap: - name: {{ .Release.Name }}-geonode-k8s-settings-py - defaultMode: 0744 - items: - - key: geonode-k8s-settings.py - path: "geonode-k8s-settings.py" - - name: geonode-k8s-settings-additions-py + name: {{ .Release.Name }}-geonode-settings-wrapper + defaultMode: 0555 + - name: settings-additions + configMap: + name: {{ .Release.Name }}-geonode-settings-additions + defaultMode: 0555 + {{- if .Values.geonodeFixtures }} + - name: geonode-fixtures configMap: - name: {{ .Release.Name }}-geonode-k8s-settings-additions-py - defaultMode: 0744 + name: {{ .Release.Name }}-geonode-fixtures + defaultMode: 0555 items: - - key: geonode-k8s-settings-additions.py - path: "geonode-k8s-settings-additions.py" + {{- range $key, $value := .Values.geonodeFixtures }} + - key: {{ $key | quote }} + path: {{ $key | quote }} + {{- end }} + {{- end }} # Using an emptyDir to cache compiled statics... it will survive container crashes, but not pod restarts - name: cache-volume diff --git a/charts/geonode/templates/geonode/jobs/geonode-statics-job.yaml b/charts/geonode/templates/geonode/jobs/geonode-statics-job.yaml index 2d9b7f0..a83c1af 100644 --- a/charts/geonode/templates/geonode/jobs/geonode-statics-job.yaml +++ b/charts/geonode/templates/geonode/jobs/geonode-statics-job.yaml @@ -22,25 +22,46 @@ spec: - name: {{ .Values.geonode.imagePullSecret }} {{ end }} restartPolicy: OnFailure +{{- $gnctx := default dict .Values.geonode.securityContext }} + securityContext: + runAsNonRoot: true + fsGroup: {{- if hasKey $gnctx "fsGroup" }} {{ $gnctx.fsGroup }} {{- else }} {{ .Values.global.securityContext.fsGroup }} {{- end }} + fsGroupChangePolicy: OnRootMismatch + containers: - name: {{ .Values.geonode.container_name }}-statics-job image: "{{ .Values.geonode.image.name }}:{{ .Values.geonode.image.tag }}" imagePullPolicy: {{ .Values.geonode.imagePullPolicy }} + securityContext: + runAsUser: {{- if hasKey $gnctx "runAsUser" }} {{ $gnctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gnctx "runAsGroup" }} {{ $gnctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + command: - bash - -c - | set -e cd {{ include "geonode_root_path" .}}/ - # Add config overrides - cat /usr/src/geonode/geonode/geonode-k8s-settings.py >> {{ include "geonode_path" .}}/settings.py - cat /usr/src/geonode/geonode/geonode-k8s-settings-additions.py >> {{ include "geonode_path" .}}/settings.py echo "-----------------------------------------------------" echo "Starting GeoNode static tasks (migrations) $(date)" echo "-----------------------------------------------------" invoke initialized invoke statics + if [ -f /favicon/favicon.ico ]; then + echo "Installing custom favicon" + install -D -m 0644 \ + /favicon/favicon.ico \ + /mnt/volumes/statics/static/geonode/img/favicon.ico + fi echo "-----------------------------------------------------" echo "GeoNode static tasks done $(date)" echo "-----------------------------------------------------" @@ -80,22 +101,22 @@ spec: value: "postgis://$(GEONODE_GEODATABASE):$(GEONODE_GEODATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(GEONODE_GEODATABASE)" - name: DATABASE_URL value: "postgis://$(GEONODE_DATABASE):$(GEONODE_DATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(GEONODE_DATABASE)" + - name: PYTHONPATH + value: /opt/geonode-custom volumeMounts: - - name: "{{ include "persistant_volume_name" . }}" - mountPath: /data - subPath: data - name: cache-volume mountPath: /tmp - name: tasks-py mountPath: "{{ include "geonode_root_path" .}}/tasks.py" subPath: tasks.py readOnly: true - - name: geonode-k8s-settings-py - mountPath: "/usr/src/geonode/geonode/geonode-k8s-settings.py" - subPath: geonode-k8s-settings.py - - name: geonode-k8s-settings-additions-py - mountPath: "/usr/src/geonode/geonode/geonode-k8s-settings-additions.py" - subPath: geonode-k8s-settings-additions.py + - name: settings-wrapper + mountPath: /opt/geonode-custom/settings_wrapper.py + subPath: settings_wrapper.py + readOnly: true + - name: settings-additions + mountPath: /opt/geonode-custom/settings_additions.py + subPath: settings_additions.py readOnly: true {{- if .Values.geonode.mapstore.nominatim_patch.enabled }} - name: mapstore-nominatim-patch-py @@ -103,39 +124,41 @@ spec: subPath: mapstore_nominatim_patch.py readOnly: true {{- end }} - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-statics mountPath: /mnt/volumes/statics - subPath: statics +{{- if .Values.favicon }} + - name: favicon + mountPath: /favicon + readOnly: true +{{- end }} volumes: - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-statics persistentVolumeClaim: - claimName: pvc-{{ .Release.Name }}-geonode + claimName: {{ include "pvc_statics_name" . }} - name: tasks-py configMap: name: {{ .Release.Name }}-geonode-tasks-py items: - key: tasks.py path: "tasks.py" - - name: geonode-k8s-settings-py + - name: settings-wrapper configMap: - name: {{ .Release.Name }}-geonode-k8s-settings-py - defaultMode: 0744 - items: - - key: geonode-k8s-settings.py - path: "geonode-k8s-settings.py" - - name: geonode-k8s-settings-additions-py + name: {{ .Release.Name }}-geonode-settings-wrapper + defaultMode: 0555 + - name: settings-additions configMap: - name: {{ .Release.Name }}-geonode-k8s-settings-additions-py - defaultMode: 0744 - items: - - key: geonode-k8s-settings-additions.py - path: "geonode-k8s-settings-additions.py" + name: {{ .Release.Name }}-geonode-settings-additions + defaultMode: 0555 {{- if .Values.geonode.mapstore.nominatim_patch.enabled }} - name: mapstore-nominatim-patch-py configMap: name: "{{ include "geonode_pod_name" . }}-mapstore-nominatim-patch" {{- end }} - +{{- if .Values.favicon }} + - name: favicon + configMap: + name: {{ .Release.Name }}-geonode-favicon +{{- end }} # Using an emptyDir to cache compiled statics... it will survive container crashes, but not pod restarts - name: cache-volume emptyDir: {} diff --git a/charts/geonode/templates/geoserver/geoserver-deploy.yaml b/charts/geonode/templates/geoserver/geoserver-deploy.yaml index 26575d5..1bacb5c 100644 --- a/charts/geonode/templates/geoserver/geoserver-deploy.yaml +++ b/charts/geonode/templates/geoserver/geoserver-deploy.yaml @@ -19,9 +19,28 @@ spec: checksum/geoserver-secret: {{ include (print $.Template.BasePath "/geoserver/geoserver-secret.yaml") . | sha256sum }} spec: + {{- $gsctx := default dict .Values.geoserver.securityContext }} + securityContext: + runAsNonRoot: {{- if hasKey $gsctx "runAsNonRoot" }} {{ $gsctx.runAsNonRoot }} {{- else }} true {{- end }} + fsGroup: {{- if hasKey $gsctx "fsGroup" }} {{ $gsctx.fsGroup }} {{- else }} {{ .Values.global.securityContext.fsGroup }} {{- end }} + fsGroupChangePolicy: OnRootMismatch + terminationGracePeriodSeconds: 3 initContainers: - name: {{ .Values.geoserver_data.container_name }} + securityContext: + runAsNonRoot: {{- if hasKey $gsctx "runAsNonRoot" }} {{ $gsctx.runAsNonRoot }} {{- else }} true {{- end }} + runAsUser: {{- if hasKey $gsctx "runAsUser" }} {{ $gsctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gsctx "runAsGroup" }} {{ $gsctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + + capabilities: + drop: + - ALL + + seccompProfile: + type: RuntimeDefault image: "{{ .Values.geoserver_data.image.name }}:{{ .Values.geoserver_data.image.tag }}" imagePullPolicy: {{ .Values.geoserver_data.imagePullPolicy }} command: @@ -46,11 +65,42 @@ spec: - secretRef: name: {{ .Values.geoserver.secret.existingSecretName | default (include "geoserver_secret_name" .) | quote }} volumeMounts: - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-geoserver-data mountPath: /geoserver_data/data - subPath: geoserver-data-dir - name: geoserver-printing-config-yaml mountPath: /overrides + # initContainer to copy necessary files and be able to mount readonly afterwards + - name: seed-geoserver + securityContext: + runAsNonRoot: {{- if hasKey $gsctx "runAsNonRoot" }} {{ $gsctx.runAsNonRoot }} {{- else }} true {{- end }} + runAsUser: {{- if hasKey $gsctx "runAsUser" }} {{ $gsctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gsctx "runAsGroup" }} {{ $gsctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + image: "{{ .Values.geoserver.image.name }}:{{ .Values.geoserver.image.tag }}" + imagePullPolicy: {{ .Values.geoserver.imagePullPolicy }} + command: + - sh + - -c + - | + cp -r /usr/local/tomcat/conf/. /seed-conf/ + cp -r /home/geoserver/. /seed-home/ + cp -r /usr/local/tomcat/tmp/. /seed-tmp/ + chmod 0755 /seed-tmp/entrypoint.sh + echo "=== Image-Inhalt der übrigen maskierten Dirs ===" + for d in work temp logs; do echo "--- /usr/local/tomcat/$d ---"; ls -A /usr/local/tomcat/$d 2>/dev/null; done + volumeMounts: + - name: gs-conf + mountPath: /seed-conf + - name: gs-home + mountPath: /seed-home + - name: gs-tmp + mountPath: /seed-tmp {{- if not (empty .Values.geoserver.imagePullSecret) }} imagePullSecrets: @@ -59,6 +109,17 @@ spec: containers: - name: {{ .Values.geoserver.container_name }} + securityContext: + runAsNonRoot: {{- if hasKey $gsctx "runAsNonRoot" }} {{ $gsctx.runAsNonRoot }} {{- else }} true {{- end }} + runAsUser: {{- if hasKey $gsctx "runAsUser" }} {{ $gsctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $gsctx "runAsGroup" }} {{ $gsctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault image: "{{ .Values.geoserver.image.name }}:{{ .Values.geoserver.image.tag }}" imagePullPolicy: {{ .Values.geoserver.imagePullPolicy }} ports: @@ -82,13 +143,30 @@ spec: {{- end }} volumeMounts: - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-geoserver-data mountPath: /geoserver_data/data - subPath: geoserver-data-dir - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-statics mountPath: /mnt/volumes/statics - subPath: statics + readOnly: true + - name: gs-conf + mountPath: /usr/local/tomcat/conf + - name: gs-home + mountPath: /home/geoserver + - name: gs-work + mountPath: /usr/local/tomcat/work + - name: gs-temp + mountPath: /usr/local/tomcat/temp + - name: gs-tmp + mountPath: /usr/local/tomcat/tmp + - name: gs-logs + mountPath: /usr/local/tomcat/logs + - name: tmp + mountPath: /tmp + {{- with .Values.geoserver.startupProbe }} + startupProbe: + {{- toYaml . | nindent 10 }} + {{- end }} readinessProbe: {{- toYaml $.Values.geoserver.readinessProbe | nindent 10 }} @@ -102,6 +180,23 @@ spec: - name: geoserver-printing-config-yaml configMap: name: {{ .Release.Name }}-geoserver-printing-config-yaml - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-geoserver-data + persistentVolumeClaim: + claimName: {{ include "pvc_geoserver_data_name" . }} + - name: pvc-statics persistentVolumeClaim: - claimName: pvc-{{ .Release.Name }}-geonode + claimName: {{ include "pvc_statics_name" . }} + - name: gs-conf + emptyDir: {} + - name: gs-home + emptyDir: {} + - name: gs-work + emptyDir: {} + - name: gs-temp + emptyDir: {} + - name: gs-tmp + emptyDir: {} + - name: gs-logs + emptyDir: {} + - name: tmp + emptyDir: {} diff --git a/charts/geonode/templates/nginx/nginx-conf.yaml b/charts/geonode/templates/nginx/nginx-conf.yaml index 368d449..9c134d5 100644 --- a/charts/geonode/templates/nginx/nginx-conf.yaml +++ b/charts/geonode/templates/nginx/nginx-conf.yaml @@ -6,8 +6,8 @@ metadata: data: geonode.conf: | server { - listen 80 default_server; - listen [::]:80 default_server; + listen 8080 default_server; + listen [::]:8080 default_server; # resolver is required because we use variables as upstream # resolver kube-dns.kube-system.svc 8.8.8.8 8.8.4.4 valid=300s; @@ -48,7 +48,7 @@ data: application/json; location = /favicon.ico { - alias /mnt/volumes/statics/static/img/favicon.ico; + alias /mnt/volumes/statics/static/geonode/img/favicon.ico; } diff --git a/charts/geonode/templates/nginx/nginx-deploy.yaml b/charts/geonode/templates/nginx/nginx-deploy.yaml index ec768e3..f413d45 100644 --- a/charts/geonode/templates/nginx/nginx-deploy.yaml +++ b/charts/geonode/templates/nginx/nginx-deploy.yaml @@ -1,4 +1,3 @@ - apiVersion: apps/v1 kind: Deployment metadata: @@ -17,44 +16,59 @@ spec: checksum/config: {{ include (print $.Template.BasePath "/nginx/nginx-conf.yaml") . | sha256sum }} spec: terminationGracePeriodSeconds: 3 - {{- if not (empty .Values.nginx.imagePullSecret) }} imagePullSecrets: - name: {{ .Values.nginx.imagePullSecret }} {{ end }} - +{{- $ngxctx := default dict .Values.nginx.securityContext }} + securityContext: + runAsNonRoot: true + fsGroup: {{- if hasKey $ngxctx "fsGroup" }} {{ $ngxctx.fsGroup }} {{- else }} {{ .Values.global.securityContext.fsGroup }} {{- end }} containers: - name: {{ .Values.nginx.container_name }} image: "{{ .Values.nginx.image.name }}:{{ .Values.nginx.image.tag }}" imagePullPolicy: {{ .Values.nginx.imagePullPolicy }} ports: - name: http - containerPort: 80 - + containerPort: 8080 + securityContext: + runAsUser: {{- if hasKey $ngxctx "runAsUser" }} {{ $ngxctx.runAsUser }} {{- else }} 101 {{- end }} + runAsGroup: {{- if hasKey $ngxctx "runAsGroup" }} {{ $ngxctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault volumeMounts: - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-statics mountPath: /mnt/volumes/statics - subPath: statics - - name: geonode-favicon - mountPath: /mnt/volumes/statics/static/geonode/img/favicon.ico - subPath: favicon.ico - name: nginx-confd mountPath: /etc/nginx/conf.d - - # this will fail as long as statics are not build by the geonode sts + # Writable scratch directories for nginx + - name: nginx-cache + mountPath: /var/cache/nginx + - name: nginx-run + mountPath: /var/run + - name: nginx-tmp + mountPath: /tmp livenessProbe: {{- toYaml $.Values.nginx.livenessProbe | nindent 10 }} - resources: {{- toYaml $.Values.nginx.resources | nindent 10 }} - volumes: - name: nginx-confd configMap: name: {{ .Release.Name }}-nginx-confd - - name: geonode-favicon - configMap: - name: {{ .Release.Name }}-geonode-favicon - - name: "{{ include "persistant_volume_name" . }}" + - name: pvc-statics persistentVolumeClaim: - claimName: pvc-{{ .Release.Name }}-geonode \ No newline at end of file + claimName: {{ include "pvc_statics_name" . }} + # emptyDir for all nginx scratch directories + - name: nginx-cache + emptyDir: {} + - name: nginx-run + emptyDir: {} + - name: nginx-tmp + emptyDir: {} diff --git a/charts/geonode/templates/nginx/nginx-svc.yaml b/charts/geonode/templates/nginx/nginx-svc.yaml index 826ea03..b29d483 100644 --- a/charts/geonode/templates/nginx/nginx-svc.yaml +++ b/charts/geonode/templates/nginx/nginx-svc.yaml @@ -8,6 +8,6 @@ spec: selector: org.geonode.instance: "{{ include "nginx_pod_name" . }}" ports: - - targetPort: 80 + - targetPort: 8080 port: 80 type: ClusterIP diff --git a/charts/geonode/templates/postgres/postgres-securitycontext.yaml b/charts/geonode/templates/postgres/postgres-securitycontext.yaml new file mode 100644 index 0000000..2742b00 --- /dev/null +++ b/charts/geonode/templates/postgres/postgres-securitycontext.yaml @@ -0,0 +1,39 @@ +{{- /* +Inspired by https://github.com/zalando/postgres-operator/issues/2223#issuecomment-2202042612 +Necessary until postgres-operator fully supports securityContext configuration + +Kyverno-Mutating-Policy: injects the missing securityContext attributes into the Spilo-Pods + +Prerequisite: Kyverno is installed in the cluster +*/ -}} +{{- if .Values.postgres.kyvernoSecurityContext.enabled }} +apiVersion: kyverno.io/v1 +kind: Policy +metadata: + name: postgresql-securitycontext + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/managed-by: {{ .Release.Service }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + rules: + - name: mutate-postgresql-sts + match: + any: + - resources: + kinds: [Pod] + selector: + matchLabels: + application: spilo + mutate: + patchStrategicMerge: + spec: + containers: + - (name): "*" + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: [ALL] } + seccompProfile: { type: RuntimeDefault } + securityContext: + runAsNonRoot: true +{{- end }} diff --git a/charts/geonode/templates/pycsw/pycsw-deploy.yaml b/charts/geonode/templates/pycsw/pycsw-deploy.yaml index d10405a..3fd33e8 100644 --- a/charts/geonode/templates/pycsw/pycsw-deploy.yaml +++ b/charts/geonode/templates/pycsw/pycsw-deploy.yaml @@ -20,7 +20,10 @@ spec: checksum/pycsw-mappings-py: {{ include (print $.Template.BasePath "/pycsw/pycsw-mappings-py.yaml") . | sha256sum }} spec: terminationGracePeriodSeconds: 3 - +{{- $pyctx := default dict .Values.pycsw.securityContext }} + securityContext: + runAsNonRoot: true + fsGroup: {{- if hasKey $pyctx "fsGroup" }} {{ $pyctx.fsGroup }} {{- else }} {{ .Values.global.securityContext.fsGroup }} {{- end }} {{- if not (empty .Values.pycsw.imagePullSecret) }} imagePullSecrets: - name: {{ .Values.pycsw.imagePullSecret }} @@ -32,6 +35,17 @@ spec: - name: {{ .Values.pycsw.init.container_name }} image: "{{ .Values.pycsw.init.image.name }}:{{ .Values.pycsw.init.image.tag }}" imagePullPolicy: {{ .Values.pycsw.init.imagePullPolicy }} + securityContext: + runAsUser: {{- if hasKey $pyctx "runAsUser" }} {{ $pyctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $pyctx "runAsGroup" }} {{ $pyctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault command: ["/bin/sh","-c"] args: ['while [ $(curl -ksw "%{http_code}" "{{ include "nginx_pod_name" . }}" -o /dev/null) -ne 200 ]; do sleep 5; echo "health check failed . Waiting for GeoNode ({{ include "nginx_pod_name" . }}) ..."; done'] env: @@ -42,6 +56,17 @@ spec: - name: {{ .Values.pycsw.container_name }} image: "{{ .Values.pycsw.image.name }}:{{ .Values.pycsw.image.tag }}" imagePullPolicy: {{ .Values.pycsw.imagePullPolicy }} + securityContext: + runAsUser: {{- if hasKey $pyctx "runAsUser" }} {{ $pyctx.runAsUser }} {{- else }} {{ .Values.global.securityContext.runAsUser }} {{- end }} + runAsGroup: {{- if hasKey $pyctx "runAsGroup" }} {{ $pyctx.runAsGroup }} {{- else }} {{ .Values.global.securityContext.runAsGroup }} {{- end }} + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault envFrom: - configMapRef: name: {{ include "pycsw_pod_name" . }}-env @@ -66,7 +91,8 @@ spec: mountPath: /etc/pycsw/pycsw-mappings.py subPath: pycsw-mappings.py readOnly: true - + - name: tmp + mountPath: /tmp readinessProbe: {{- toYaml $.Values.pycsw.readinessProbe | nindent 10 }} livenessProbe: @@ -78,15 +104,17 @@ spec: - name: pycsw-cfg configMap: name: {{ .Release.Name }}-pycsw-cfg - defaultMode: 0744 + defaultMode: 0555 items: - key: pycsw.cfg path: "pycsw.cfg" - name: pycsw-mappings-py configMap: name: {{ .Release.Name }}-pycsw-mappings-py - defaultMode: 0744 + defaultMode: 0555 items: - key: pycsw-mappings.py path: "pycsw-mappings.py" + - name: tmp + emptyDir: {} {{ end }} diff --git a/charts/geonode/values.yaml b/charts/geonode/values.yaml index d296934..40dd120 100644 --- a/charts/geonode/values.yaml +++ b/charts/geonode/values.yaml @@ -3,6 +3,15 @@ global: storageClass: # -- storage access mode used by helm dependency pvc accessMode: ReadWriteMany + # -- Global security context defaults for all pods + # Can be overridden per component via component.securityContext.runAsUser etc. + securityContext: + # -- User ID to run the containers + runAsUser: 1000 + # -- Group ID to run the containers + runAsGroup: 1000 + # -- Group ID for volume mounts + fsGroup: 1000 # geonode configuration geonode: @@ -10,9 +19,23 @@ geonode: container_name: geonode # -- number of geonode replicas (! not working properly yet) replicaCount: 1 + # -- Security context for geonode pods (overrides global.securityContext) + securityContext: {} persistant: - # -- size of persistant geonode storage + # -- size of statics storage + statics: + storageSize: 2Gi + # -- size of statics storage + geoserver_data: + storageSize: 2Gi + # -- size of statics storage + backup_restore: + storageSize: 2Gi + # -- size of statics storage + data: + storageSize: 4Gi + # -- fallback value for all persistant geonode storage storageSize: 10Gi image: @@ -379,9 +402,16 @@ geoserver: image: # -- geoserver image docker image name: geonode/geoserver + # NOTE: to run this image as non-root, you currently need to wrap it into a custom image, see README file on how to build it. + # name: geonode/geoserver-nonroot # -- geoserver docker image tag tag: "2.27.4-latest" - + # -- Security context for geoserver pods (overrides global.securityContext) + securityContext: + runAsNonRoot: false + fsGroup: 0 + runAsUser: 0 + runAsGroup: 0 # -- geoserver image pull policy imagePullPolicy: "IfNotPresent" # -- pull secret to use for geoserver image @@ -393,11 +423,19 @@ geoserver: shapefile_datetime: false # -- geoserver port port: 8080 + # -- configuration of startup probe (before liveliness) + startupProbe: + tcpSocket: + port: 8080 + periodSeconds: 10 + failureThreshold: 30 + # -- configuration of special parameters for print functionality printing: extraHosts: | # - !dnsMatch # host: data.geopf.fr # port: 80 + secret: # -- name of an existing Secret to use. Set, if you want to separately maintain the Secret. existingSecretName: "" @@ -473,13 +511,16 @@ nginx: replicaCount: 1 image: # -- nginx docker image - name: nginx + name: nginxinc/nginx-unprivileged # -- nginx docker image tag - tag: "1.28" + tag: "1.26.3-alpine3.20" # -- nginx image pull policy imagePullPolicy: "IfNotPresent" # -- pull secret to use for nginx image imagePullSecret: "" + # Use nginx typical userId for running + securityContext: + runAsUser: 101 # -- max file upload size for geonode upload. Only set this value if it should be different from geonode.general.upload.size. # to use e.g. if geonode.general.upload.document_size > geonode.general.upload.size @@ -530,6 +571,8 @@ pycsw: name: geopython/pycsw # -- pycsw docker image tag tag: "3.0.0-beta2" + # -- Security context for pycsw pods (overrides global.securityContext) + securityContext: {} # -- pycsw image pull policy imagePullPolicy: "IfNotPresent" # -- pull secret to use for pycsw image @@ -719,6 +762,18 @@ memcached: enabled: true # Basic installation replicaCount: 1 + containerSecurityContext: + # Use memcached typical userid for runtime + runAsUser: 11211 + runAsNonRoot: true + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + config: memoryLimit: 128 maxConnections: 2048 @@ -729,7 +784,6 @@ memcached: requests: cpu: 100m memory: 128Mi - # RABBITMQ CONFIGURATION # https://artifacthub.io/packages/helm/cloudpirates-rabbitmq/rabbitmq # Activate if you are running geonode below 5.0.0 @@ -869,6 +923,10 @@ postgres: postgres_password: postgres geonode_password: geonode geodata_password: geogeonode + # -- set to true to enable full compliance to "restricted" policy + # -- (needed as workaround for postgres-operator limited handling of securityContext) + kyvernoSecurityContext: + enabled: false # VALUES DEFINITION: https://github.com/zalando/postgres-operator/blob/master/charts/postgres-operator/values.yaml postgres-operator: @@ -882,20 +940,37 @@ postgres-operator: # -- not setting the podServiceAccount name will leed to generation of this name. This allows to run multiple postgres-operators in a single kubernetes cluster. just seperating them by namespace. podServiceAccount: name: "" - + # Override securityContext of subchart + securityContext: + runAsUser: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + configKubernetes: + spilo_runasuser: 101 + spilo_runasgroup: 103 + spilo_fsgroup: 103 + spilo_privileged: false + spilo_allow_privilege_escalation: false # -- (map of fixture files) Fixture files which shall be made available under /usr/src/geonode/geonode/fixtures (refer to https://docs.djangoproject.com/en/4.2/howto/initial-data/) geonodeFixtures: # @gignore - somefixture.json: | - [ - { - "pk": 0, - "model": "myapp.sample" - "description": "nice little content" - } - ] +# somefixture.json: | +# [ +# { +# "pk": 0, +# "model": "myapp.sample", +# "fields": { +# "description": "nice little content" +# } +# } +# ] # -- A base64 encoded favicon # @default -- AAABAAMAEBAAAAEAIABoBA ... AAAA== favicon: | - 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k/+y7vn/su75/7Lu+f+y7vn/su75/7Hs9/8uLi3/ncUs/57GLP+exiz/nsYs/57GLP+exiv/cowr/ywsLf8sLCz/25tP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y4uLf/bnE//3Z1Q/92dUP/dnVD/3Z1Q/92dUP+FZT7/b42T/7Lu+f+y7vn/su75/7Lu+f+y7vn/sez3/y4uLf+dxSz/nsYs/57GLP+exiz/mb8r/1ZmLP9IPjL/LCws/ywsLP/bm0//3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnVD/Li4t/9ucT//dnVD/3Z1Q/92dUP/dnVD/3Z1Q/4VlPv9vjZP/su75/7Lu+f+y7vn/su75/7Lu+f+x7Pf/Li4t/53FLP+exiz/nsYs/4qsK/8/Riz/b1Y5/9OWTv8sLCz/LCws/9ubT//dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9ydUP8uLi3/25xP/92dUP/dnVD/3Z1Q/92dUP/dnVD/hWU+/2+Nk/+y7vn/su75/7Lu+f+y7vn/su75/7Hs9/8uLi3/ncUs/57GK/9zjCz/ODcu/5tyQv/dnVD/25tP/ywsLP8sLCz/25tP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y4uLf/bnE//3Z1Q/92dUP/dnVD/3Z1Q/92dUP+FZT7/b42T/7Lu+f+y7vn/su75/7Lu+f+y7vn/sez3/y4uLf+Yvyv/VmYs/0k/Mv+/ikn/3Z1Q/92dUP/bm0//LCws/ywsLP/bm0//3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnVD/Li4t/9ucT//dnVD/3Z1Q/82STP+edUP/blY5/zw2L/8zNTb/RVFT/1dqbf9og4j/epyj/4u1vf+cztf/LS0t/z9GLP9vVjn/1ZhO/92dUP/dnVD/3Z1Q/9ubT/8sLCz/LCws/9ubT//dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9ydUP8uLi3/qnxF/3xfPP9NQTP/NDc3/1Vna/95nKL/YHh8/11vLP+CoSv/dI0s/2V5LP9WZSz/R1Es/zk9LP8tLS3/m3JC/92dUP/dnVD/3Z1Q/92dUP/dnVD/25tP/ywsLP8sLCz/25tP/92dUP/dnVD/3Z1Q/92dUP/cnE//lnBB/y0tLf9LWVv/b42S/5PByf+v6vX/su75/7Lu+f9vjZP/ZXks/57GLP+exiz/nsYs/57GLP+exiz/ncUr/y4uLf/cnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/bm0//LCws/ywsLP/bm0//3Z1Q/92dUP/dnVD/y5FM/19NNv9ETSz/LS0t/7Hs9/+y7vn/su75/7Lu+f+y7vn/su75/2+Nk/9leSz/nsYs/57GLP+exiz/nsYs/57GLP+dxSz/Li4t/9ydUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9ubT/8sLCz/LCws/9ubT//dnVD/3Z1Q/6F2Q/87Ny7/Znsr/5vDK/8uLi3/sez3/7Lu+f+y7vn/su75/7Lu+f+y7vn/b42T/2V5LP+exiz/nsYs/57GLP+exiz/nsYs/53FLP8uLi3/3J1Q/92dUP/dnVD/3Z1Q/92dUP/CjEr/dVs6/ywsLP8sLCz/25tP/9CVTf9pUzj/P0Ys/4enK/+exiz/ncUs/y4uLf+x7Pf/su75/7Lu+f+y7vn/su75/7Lu+f9vjZP/ZXks/57GLP+exiz/nsYs/57GLP+exiz/ncUs/y4uLf/cnVD/3Z1Q/9ycT/+vf0b/YU43/zxCQ/9ykZf/LCws/ywsLP+oe0X/Pzkw/2ByLP+awSv/nsYs/57GLP+dxSz/Li4t/7Hs9/+y7vn/su75/7Lu+f+y7vn/su75/2+Nk/9leSz/nsYs/57GLP+exiz/nsYs/57GLP+dxSz/Li4t/9ydUP/dnVD/jGk//0JLTf+CqbD/r+r1/7Dr9v8sLCz/LCwt/zxBLP+CoSv/nsYs/57GLP+exiz/nsYs/53FLP8uLi3/sez3/7Lu+f+y7vn/su75/7Lu+f+x7Pf/UmRn/05aLP+dxSv/nsYs/57GLP+exiz/nsYs/53FLP8uLi3/3J1Q/92dUP+DYz7/cY+V/7Lu+f+y7vn/sOv2/ywsLP8sLC3/l70r/57GLP+exiz/nsYs/57GLP+exiz/ncUs/y4uLf+x7Pf/su75/7Lu+f+y7vn/su75/2qGjP80NFz/MzNX/2V5K/+exiz/nsYs/57GLP+exiz/ncUs/y4uLf/cnVD/3Z1Q/4NjPv9xj5X/su75/7Lu+f+w6/b/LCws/ywsLf+cxCz/nsYs/57GLP+exiz/nsYs/57GLP+dxSz/Li4t/7Hs9/+y7vn/su75/7Lu+f+CqbD/MDFI/0hI2/9ISNj/MDFE/3iT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cP+CqK//p9/o/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/53FLP+exiz/nsYs/57GLP+dxSv/jbAr/3CJLP9RXyz/PkUs/zc7LP8vMSz/Kysr0iwsLP9kTzf/sIBG/82STP/cnE//3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9OWTf+zgkf/RDwx/zg9Pv+NuMD/pdzm/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/53FLP+exiz/nsYs/57GLP+exiz/nsYs/57GLP+dxSv/lLgr/4KgLP9QXCz/LCwt/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnE//TkIz/z5FSP+x7fj/su75/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/53FLP+exiz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9jdyz/LCwt/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnE//TkIz/z5FSP+x7fj/su75/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/53FLP+exiz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/nsYr/5K2K/9SYCz/LCwt/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnE//TkIz/z5FSP+x7fj/su75/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/53FLP+exiz/nsYs/57GLP+exiz/nsYs/57GLP+cxCv/gJ4r/0tWLP8tLiz/LS0t/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnE//TkIz/z5FSP+x7fj/su75/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/53FLP+exiz/nsYs/57GLP+exiz/nsYs/5vDK/9rgiv/Njos/z03L/9aSTX/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnE//TkIz/z5FSP+x7fj/su75/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/53FLP+exiz/nsYs/57GLP+dxSv/krcr/15xK/80My3/XEo1/8ePS/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnE//TkIz/z5FSP+x7fj/su75/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/53FLP+exiz/nsYs/5zEK/+Aniv/SVQs/zYyLv+EZD7/z5RN/9ycT/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnE//TkIz/z5FSP+x7fj/su75/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/53FLP+exiz/nsYr/3CJLP81OSz/Qjow/7OBR//Ymk//3Z1Q/9ycT/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/cnE//TkIz/z5FSP+x7fj/su75/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/5zEK/+RtSv/WWks/y8uLf9iTjb/x45L/92dUP/dnVD/3Z1Q/9ycT/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9CUTP+vf0b/Qzsx/zc7PP9/pav/irW9/5bGzv+i1+H/rejy/7Lu+f+y7vn/su75/7Ht+P9ujJH/Li8t/4KhLP9KVCz/OjYu/4JiPf/Mkkz/3Z1Q/92dUP/dnVD/3Z1Q/9ycT/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf+DZD7/3JxP/92dUP/dnVD/3Z1Q/8+UTP+ZcUL/Yk43/z44MP83My//Li0t/y0tLf8xMjP/Mzc3/zY7O/85P0D/PENE/0pXWv9fdXr/cpKY/4evt/9gd3v/LS0t/zM2LP9JPjL/soFG/9maT//dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9ycT/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3J1Q/y8uLf98Xzz/x49L/7eER/+HZj7/VUY0/y8uLv81OTr/PUVG/0xbXv9ykpj/PERG/zg8LP99miv/bYUr/15wLP9NWiz/QUks/z1DLP85Piz/Njks/zI0LP8uLyz/LS0t/1lINf/Ci0r/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9ycT/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/0JVN/y4uLf9GPDH/TEAy/zEwLv8+Rkj/Vmhr/22Kj/+GrrX/ndDZ/7Dr9v+x7fj/RlFU/ztALP+dxSz/nsYs/57GLP+dxiv/m8Ir/5K2K/+HqCv/fZsr/3SOK/9NWCz/Li4t/8+UTf/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9ycT/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9ycT/+5hkj/U0Uz/y0tLf8/SEn/bIiN/5PCyv+l3Ob/q+Tv/7Ds9/+y7vn/su75/7Lu+f+x7fj/RlFU/ztALP+dxSz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FK/9keCz/Ly4t/9ydUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9ycT/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/92dUP/cnE//0pZN/591Q/8+ODD/OT4s/y0tLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Lu+f+x7fj/RlFU/ztALP+dxSz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9ycT/+CYz3/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/9ucT/+5hkj/bVU5/zMyLf9UYyz/hqcr/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Lu+f+x7fj/RlFU/ztALP+dxSz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/9WYTv9yWDr/LCws/ywsLP+CYz3/3JxP/92dUP/dnVD/3Z1Q/510Qv8/ODD/NDYs/3SOK/+Zvyv/ncQr/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Lu+f+x7fj/RlFU/ztALP+dxSz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/92dUP/dnVD/3Z1Q/92dUP/WmE7/m3JB/0xAMv8zMS7/LCws/ywsLP+CYz3/3JxP/9ucT//EjUr/blY5/y4vLP9LViz/i6wr/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Lu+f+x7fj/RlFU/ztALP+dxSz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/9ycT//XmU7/x49L/5lxQv9JPzL/NTk5/0VRU/9HU1X/LCws/ywsLP+CYz3/2ZpP/6B2Q/9QQzP/Oj0s/2Z6K/+Ttyv/ncUr/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Lu+f+x7fj/RlFU/ztALP+dxSz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/9OWTf9/YTz/Sj8y/zY7O/9ddHj/hKyz/6jf6f9tipD/LCws/ywsLP9wVzn/aFI4/zUyLv9KVCz/iqsr/5zDK/+exiz/nsYs/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Lu+f+x7fj/RlFU/ztALP+dxSz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/8WNSv8vLi3/aIOI/6DV3/+t5/H/se34/7Ht+P9tipD/LCws/ywsLf8tLC3/NTgs/1ttK/+WvCv/nsYs/57GLP+exiz/nsYs/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/7Ht+P+j2OL/QElL/zg8LP+Stiv/ncUr/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/8SNSv8uLi//mcrT/7Lu+f+y7vn/su75/7Ht+P9tipD/LCws/ywsLf9DSyz/epYr/53EK/+exiz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/6vl8P9gd3z/MDI6/y8wM/9bbSz/l70r/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/8SNSv8uLi//mcrT/7Lu+f+y7vn/su75/7Ht+P9tipD/LCws/ywsLf9idSz/nMQr/57GLP+exiz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+y7vn/su75/3COlP8vMTX/Pj6f/z4+oP8vMTD/Z30r/57GLP+exiz/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/8SNSv8uLi//mcrT/7Lu+f+y7vn/su75/7Ht+P9tipD/LCws/ywsLf9jdyz/ncUs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Lu+f+v6/X/gqiw/y0uMP85OYD/SUng/0lJ4f84OHr/Li4w/3eRK/+cxCv/nsYs/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/8SNSv8uLi//mcrT/7Lu+f+y7vn/su75/7Ht+P9tipD/LCws/ywsLf9jdyz/ncUs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/7Ht+P+Pu8P/O0JE/zQ0Xv9HR9T/S0vo/0tL6P9HRtH/NDRg/zc8LP+CoSv/ncUr/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/8SNSv8uLi//mcrT/7Lu+f+y7vn/su75/7Ht+P9tipD/LCws/ywsLf9jdyz/ncUs/57GLP+exiz/nsYs/57GLP+exiz/nsYs/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/su75/6jg6v9LWVz/MTFH/0hH1v9LSuj/S0vo/0tL6P9KSuf/R0fU/zAwQ/9JUyz/krcr/57GLP+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/8SNSv8uLi//mcrT/7Lu+f+y7vn/su75/7Ht+P9tipD/LCws/ywsLf9cbSz/k7cr/5rBK/+dxSv/nsYs/57GLP+exiz/nsYs/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Lu+f+y7vn/sOv2/22Kj/8uLjv/QkK4/0tL6P9LS+j/S0vo/0tL6P9LS+j/S0vo/0NDvf8uLjr/YXQs/5zDK/+exiz/nsYs/53FLP9keCz/Ly4t/9ydUP/dnVD/3Z1Q/8SNSv8uLi//mcrT/7Lu+f+y7vn/r+r1/6jf6f9mgIT/LCws/ywsLOI1OCz+SVMs/1tsK/9vhyv/gqAr/5S5K/+dxSv/nsYs/57GLP+exiz/ncUs/y4vLf9ujJH/se34/7Ht+P+h1t//eJuh/zEzN/86Oob/SUnh/0tL6P9LS+j/S0vo/0tL6P9LS+j/S0vo/0pK4/86OoX/MTIx/2uCK/+Psyv/ncUr/53F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 diff --git a/geoserver-nonroot/Dockerfile b/geoserver-nonroot/Dockerfile new file mode 100644 index 0000000..cecfcc6 --- /dev/null +++ b/geoserver-nonroot/Dockerfile @@ -0,0 +1,40 @@ +# Custom geonode/geoserver Docker image building for non-root environments +# Necessary until solving in upstream, +# see also https://github.com/GeoNode/geonode-docker/pull/59 + +ARG GEOSERVER_VERSION=2.27.4-latest + +FROM geonode/geoserver:${GEOSERVER_VERSION} + +ARG GEOSERVER_UID=1000 +ARG GEOSERVER_GID=1000 + +USER root + +# Create group and user with configurable UID/GID +RUN groupadd -g ${GEOSERVER_GID} geoserver && \ + useradd -u ${GEOSERVER_UID} -g ${GEOSERVER_GID} -m geoserver + +# Ensure HOME is set +ENV HOME=/home/geoserver + +# Fix writable directories +RUN mkdir -p /usr/local/tomcat/conf \ + && chown -R ${GEOSERVER_UID}:${GEOSERVER_GID} /usr/local/tomcat \ + && chown -R ${GEOSERVER_UID}:${GEOSERVER_GID} /opt \ + && chmod -R g=u /usr/local/tomcat \ + && chmod -R g=u /opt + +# Replace hardcoded /root/ references with ${HOME} in entrypoint +# so the script works for any user, not just root or geoserver. +# Fail if "/root" can not be removed. +RUN sed -i 's|/root/\(\.bashrc\)|${HOME}/\1|g' \ + /usr/local/tomcat/tmp/entrypoint.sh && \ + sed -i 's|/root/\(\.override_env\)|${HOME}/\1|g' \ + /usr/local/tomcat/tmp/entrypoint.sh && \ + ! grep -q '/root/\.\(bashrc\|override_env\)' /usr/local/tomcat/tmp/entrypoint.sh + +# Strip setuid bits, for enhanced security +RUN find / -xdev -perm -4000 -exec chmod u-s {} \; 2>/dev/null || true + +USER ${GEOSERVER_UID}:${GEOSERVER_GID} diff --git a/geoserver-nonroot/build.sh b/geoserver-nonroot/build.sh new file mode 100755 index 0000000..ebf767a --- /dev/null +++ b/geoserver-nonroot/build.sh @@ -0,0 +1,3 @@ +docker build --build-arg GEOSERVER_UID=1000 --build-arg GEOSERVER_GID=1000 \ + --build-arg GEOSERVER_VERSION=2.27.4-latest \ + -t geonode/geoserver-nonroot:2.27.4-latest .