From ef709e15f58225745264d0b67d36364393db0544 Mon Sep 17 00:00:00 2001 From: Timofey Zhukov-Khovanskiy Date: Mon, 9 Mar 2026 15:36:19 -0400 Subject: [PATCH 1/7] feat(security): enable secure defaults for kagent chart Add security context defaults to improve pod and container security: - Set runAsNonRoot: true for pod security context - Set readOnlyRootFilesystem: true for container security context - Add UI-specific security context overrides - Add emptyDir volumes for Next.js cache and tmp (required for read-only filesystem) - Update tool charts (grafana-mcp, querydoc) to make securityContext optional - Add comprehensive security context tests This change enables secure-by-default configuration while maintaining backward compatibility through values.yaml overrides. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Timofey Zhukov-Khovanskiy --- .../mcp-grafana/templates/deployment.yaml | 4 +- helm/kagent/templates/ui-deployment.yaml | 20 +- helm/kagent/tests/security-context_test.yaml | 189 ++++++++++++++++++ helm/kagent/values.yaml | 42 +++- helm/tools/querydoc/templates/deployment.yaml | 4 +- 5 files changed, 244 insertions(+), 15 deletions(-) create mode 100644 helm/kagent/tests/security-context_test.yaml diff --git a/contrib/tools/mcp-grafana/templates/deployment.yaml b/contrib/tools/mcp-grafana/templates/deployment.yaml index b882b8990..0ad7a89fe 100644 --- a/contrib/tools/mcp-grafana/templates/deployment.yaml +++ b/contrib/tools/mcp-grafana/templates/deployment.yaml @@ -27,8 +27,10 @@ spec: - "-t" - "streamable-http" - -debug + {{- with .Values.securityContext }} securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} + {{- toYaml . | nindent 12 }} + {{- end }} image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }} resources: diff --git a/helm/kagent/templates/ui-deployment.yaml b/helm/kagent/templates/ui-deployment.yaml index 3b009a1e9..f8a1ff637 100644 --- a/helm/kagent/templates/ui-deployment.yaml +++ b/helm/kagent/templates/ui-deployment.yaml @@ -23,9 +23,18 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} + {{- with (.Values.ui.podSecurityContext | default .Values.podSecurityContext) }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- toYaml . | nindent 8 }} + {{- end }} serviceAccountName: {{ include "kagent.fullname" . }}-ui + volumes: + - name: nextjs-cache + emptyDir: + sizeLimit: {{ .Values.ui.volumes.nextjsCache }} + - name: tmp + emptyDir: + sizeLimit: {{ .Values.ui.volumes.tmp }} {{- with .Values.ui.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -36,8 +45,10 @@ spec: {{- end }} containers: - name: ui + {{- with (.Values.ui.securityContext | default .Values.securityContext) }} securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} + {{- toYaml . | nindent 12 }} + {{- end }} image: "{{ .Values.ui.image.registry | default .Values.registry }}/{{ .Values.ui.image.repository }}:{{ coalesce .Values.tag .Values.ui.image.tag .Chart.Version }}" imagePullPolicy: {{ .Values.ui.image.pullPolicy | default .Values.imagePullPolicy }} env: @@ -50,6 +61,11 @@ spec: - name: http containerPort: {{ .Values.ui.service.ports.targetPort }} protocol: TCP + volumeMounts: + - name: nextjs-cache + mountPath: /.next/cache + - name: tmp + mountPath: /tmp resources: {{- toYaml .Values.ui.resources | nindent 12 }} startupProbe: diff --git a/helm/kagent/tests/security-context_test.yaml b/helm/kagent/tests/security-context_test.yaml new file mode 100644 index 000000000..fc7093685 --- /dev/null +++ b/helm/kagent/tests/security-context_test.yaml @@ -0,0 +1,189 @@ +suite: test security context configuration +templates: + - _helpers.tpl + - controller-deployment.yaml + - ui-deployment.yaml +tests: + # ============================================================================= + # Controller Security Context Tests + # ============================================================================= + - it: should apply default pod security context to controller + template: controller-deployment.yaml + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + + - it: should apply default container security context to controller + template: controller-deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: true + + - it: should allow controller pod security context override + template: controller-deployment.yaml + set: + controller: + podSecurityContext: + runAsNonRoot: true + fsGroup: 2000 + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 2000 + + - it: should allow controller container security context override + template: controller-deployment.yaml + set: + controller: + securityContext: + readOnlyRootFilesystem: false + runAsUser: 1000 + asserts: + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: false + - equal: + path: spec.template.spec.containers[0].securityContext.runAsUser + value: 1000 + + - it: should fallback to global pod security context for controller + template: controller-deployment.yaml + set: + podSecurityContext: + runAsNonRoot: true + fsGroup: 3000 + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 3000 + + - it: should fallback to global container security context for controller + template: controller-deployment.yaml + set: + securityContext: + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + asserts: + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: true + - contains: + path: spec.template.spec.containers[0].securityContext.capabilities.drop + content: ALL + + # ============================================================================= + # UI Security Context Tests + # ============================================================================= + - it: should apply UI-specific container security context override + template: ui-deployment.yaml + asserts: + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: true + + - it: should have nextjs-cache volume for UI + template: ui-deployment.yaml + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: nextjs-cache + emptyDir: + sizeLimit: 100Mi + + - it: should have tmp volume for UI + template: ui-deployment.yaml + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: tmp + emptyDir: + sizeLimit: 50Mi + + - it: should have nextjs-cache volume mount for UI + template: ui-deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: nextjs-cache + mountPath: /.next/cache + + - it: should have tmp volume mount for UI + template: ui-deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: tmp + mountPath: /tmp + + - it: should allow UI pod security context override + template: ui-deployment.yaml + set: + ui: + podSecurityContext: + runAsNonRoot: true + fsGroup: 5000 + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 5000 + + - it: should allow UI container security context override + template: ui-deployment.yaml + set: + ui: + securityContext: + readOnlyRootFilesystem: false + runAsUser: 2000 + asserts: + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: false + - equal: + path: spec.template.spec.containers[0].securityContext.runAsUser + value: 2000 + + - it: should fallback to global pod security context for UI + template: ui-deployment.yaml + set: + ui: + podSecurityContext: {} + podSecurityContext: + runAsNonRoot: true + runAsUser: 3001 + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 3001 + + - it: should prefer UI security context over global + template: ui-deployment.yaml + set: + ui: + securityContext: + readOnlyRootFilesystem: false + securityContext: + readOnlyRootFilesystem: true + asserts: + - equal: + path: spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem + value: false diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 5f5d18878..2fcbbc910 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -25,16 +25,18 @@ labels: {} # environment: production # team: platform -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 +# -- Security context for all pods +podSecurityContext: + runAsNonRoot: true +# fsGroup: 2000 + +# -- Security context for all containers +securityContext: + readOnlyRootFilesystem: true +# capabilities: +# drop: +# - ALL +# runAsUser: 1000 # ============================================================================== # CORE KAGENT COMPONENTS @@ -146,7 +148,25 @@ ui: port: 8080 targetPort: 8080 env: {} # Additional configuration key-value pairs for the ui ConfigMap - + # -- Pod-level security context for the UI pod. Overrides the global podSecurityContext. + # @default -- (uses global podSecurityContext) + podSecurityContext: {} + # fsGroup: 2000 + # -- Container-level security context for the UI container. Overrides the global securityContext. + # @default -- (uses global securityContext) + securityContext: + readOnlyRootFilesystem: true + # -- EmptyDir volume sizes for Next.js UI workload (required for readOnlyRootFilesystem) + volumes: + # -- Size limit for Next.js build cache (.next/cache). Default 100Mi is sufficient for typical Next.js apps with moderate caching needs. + nextjsCache: 100Mi + # -- Size limit for temporary files (/tmp). Default 50Mi provides ample space for Next.js runtime temporary data. + tmp: 50Mi + # capabilities: + # drop: + # - ALL + # runAsNonRoot: true + # runAsUser: 1000 # -- Node taints which will be tolerated for `Pod` [scheduling](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/). tolerations: [] diff --git a/helm/tools/querydoc/templates/deployment.yaml b/helm/tools/querydoc/templates/deployment.yaml index fde1a70dd..659132050 100644 --- a/helm/tools/querydoc/templates/deployment.yaml +++ b/helm/tools/querydoc/templates/deployment.yaml @@ -31,8 +31,10 @@ spec: {{- end }} containers: - name: querydoc + {{- with .Values.securityContext }} securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} + {{- toYaml . | nindent 12 }} + {{- end }} image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }} resources: From 98c926848712d61410dcd725e673361d6016232e Mon Sep 17 00:00:00 2001 From: Timofey Zhukov-Khovanskiy Date: Mon, 9 Mar 2026 21:44:49 -0400 Subject: [PATCH 2/7] security context + helm test + ui with rofs Signed-off-by: Timofey Zhukov-Khovanskiy --- helm/kagent/templates/controller-deployment.yaml | 8 ++++++-- helm/kagent/tests/security-context_test.yaml | 1 + ui/Dockerfile | 6 ++++-- ui/scripts/init.sh | 15 +++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 ui/scripts/init.sh diff --git a/helm/kagent/templates/controller-deployment.yaml b/helm/kagent/templates/controller-deployment.yaml index cdfa8de85..879c2febb 100644 --- a/helm/kagent/templates/controller-deployment.yaml +++ b/helm/kagent/templates/controller-deployment.yaml @@ -25,8 +25,10 @@ spec: imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} + {{- with (.Values.controller.podSecurityContext | default .Values.podSecurityContext) }} securityContext: - {{- toYaml (.Values.controller.podSecurityContext | default .Values.podSecurityContext) | nindent 8 }} + {{- toYaml . | nindent 8 }} + {{- end }} serviceAccountName: {{ include "kagent.fullname" . }}-controller {{- if or (eq .Values.database.type "sqlite") (gt (len .Values.controller.volumes) 0) }} volumes: @@ -72,8 +74,10 @@ spec: protocol: TCP resources: {{- toYaml .Values.controller.resources | nindent 12 }} + {{- with (.Values.controller.securityContext | default .Values.securityContext) }} securityContext: - {{- toYaml .Values.controller.securityContext | nindent 12 }} + {{- toYaml . | nindent 12 }} + {{- end }} startupProbe: httpGet: path: /health diff --git a/helm/kagent/tests/security-context_test.yaml b/helm/kagent/tests/security-context_test.yaml index fc7093685..007396c89 100644 --- a/helm/kagent/tests/security-context_test.yaml +++ b/helm/kagent/tests/security-context_test.yaml @@ -1,6 +1,7 @@ suite: test security context configuration templates: - _helpers.tpl + - controller-configmap.yaml - controller-deployment.yaml - ui-deployment.yaml tests: diff --git a/ui/Dockerfile b/ui/Dockerfile index ca9aad7f3..407228fee 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -86,6 +86,7 @@ RUN mkdir -p /app/ui/public /tmp/nginx/client_temp /tmp/nginx/proxy_temp /tmp/ng WORKDIR /app COPY conf/nginx.conf /etc/nginx/nginx.conf COPY conf/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY scripts/init.sh /usr/local/bin/init.sh WORKDIR /app/ui COPY --from=builder /app/ui/next.config.ts ./ @@ -96,7 +97,8 @@ COPY --from=builder --chown=nextjs:nginx /app/ui/.next/static ./.next/static # Ensure correct permissions RUN chown -R nextjs:nginx /app/ui && \ - chmod -R 755 /app + chmod -R 755 /app && \ + chmod +x /usr/local/bin/init.sh EXPOSE 8080 ARG VERSION @@ -108,4 +110,4 @@ LABEL org.opencontainers.image.version="$VERSION" USER nextjs -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file +CMD ["/usr/local/bin/init.sh"] \ No newline at end of file diff --git a/ui/scripts/init.sh b/ui/scripts/init.sh new file mode 100644 index 000000000..f8d88469a --- /dev/null +++ b/ui/scripts/init.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +# Create nginx temp directories +# These are required when running with readOnlyRootFilesystem: true +# The /tmp emptyDir volume is mounted empty at runtime, so we need to +# recreate the directory structure that was created during the Docker build +mkdir -p /tmp/nginx/client_temp \ + /tmp/nginx/proxy_temp \ + /tmp/nginx/fastcgi_temp \ + /tmp/nginx/uwsgi_temp \ + /tmp/nginx/scgi_temp + +# Start supervisord +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf From 193bb4ae929f6cbe221d6c8517b83d77f5ad201b Mon Sep 17 00:00:00 2001 From: Timofey Zhukov-Khovanskiy Date: Tue, 10 Mar 2026 10:53:25 -0400 Subject: [PATCH 3/7] fix(ui): use numeric UID for runAsNonRoot compatibility Signed-off-by: Timofey Zhukov-Khovanskiy --- ui/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/Dockerfile b/ui/Dockerfile index ebe22bef0..59b3206ce 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -108,6 +108,6 @@ LABEL org.opencontainers.image.description="Kagent app is the UI and apiserver f LABEL org.opencontainers.image.authors="Kagent Creators 🤖" LABEL org.opencontainers.image.version="$VERSION" -USER nextjs +USER 1001 CMD ["/usr/local/bin/init.sh"] \ No newline at end of file From 5add8f56ff18c0f32359761ec830e86fb1b4fa55 Mon Sep 17 00:00:00 2001 From: Timofey Zhukov-Khovanskiy Date: Tue, 10 Mar 2026 15:42:21 -0400 Subject: [PATCH 4/7] adding XDG_CACHE_HOME=/sqlite-volume/.cache Signed-off-by: Timofey Zhukov-Khovanskiy --- helm/kagent/templates/controller-deployment.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/helm/kagent/templates/controller-deployment.yaml b/helm/kagent/templates/controller-deployment.yaml index d4019b041..e8057ab98 100644 --- a/helm/kagent/templates/controller-deployment.yaml +++ b/helm/kagent/templates/controller-deployment.yaml @@ -67,6 +67,10 @@ spec: valueFrom: fieldRef: fieldPath: spec.nodeName + {{- if eq .Values.database.type "sqlite" }} + - name: XDG_CACHE_HOME + value: /sqlite-volume/.cache + {{- end }} {{- with .Values.controller.env }} {{- toYaml . | nindent 12 }} {{- end }} From 0e0af39298514ee4089a1497b71b07dd1a1c04c8 Mon Sep 17 00:00:00 2001 From: Tim Zhukov <51675972+tzhukov@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:17:31 -0400 Subject: [PATCH 5/7] Update helm/kagent/values.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Tim Zhukov <51675972+tzhukov@users.noreply.github.com> --- helm/kagent/values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index 1e0fe0c6d..acd8c0b40 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -174,9 +174,9 @@ ui: # fsGroup: 2000 # -- Container-level security context for the UI container. Overrides the global securityContext. # @default -- (uses global securityContext) - securityContext: - readOnlyRootFilesystem: true - # -- EmptyDir volume sizes for Next.js UI workload (required for readOnlyRootFilesystem) + securityContext: {} + # readOnlyRootFilesystem: true + # -- EmptyDir volume sizes for Next.js UI workload (typically used when enabling readOnlyRootFilesystem) volumes: # -- Size limit for Next.js build cache (.next/cache). Default 100Mi is sufficient for typical Next.js apps with moderate caching needs. nextjsCache: 100Mi From 8b31d1dd5d373870f0f0064c28dec0b6825eb653 Mon Sep 17 00:00:00 2001 From: Tim Zhukov <51675972+tzhukov@users.noreply.github.com> Date: Wed, 11 Mar 2026 09:17:54 -0400 Subject: [PATCH 6/7] Update helm/kagent/templates/ui-deployment.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Tim Zhukov <51675972+tzhukov@users.noreply.github.com> --- helm/kagent/templates/ui-deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/kagent/templates/ui-deployment.yaml b/helm/kagent/templates/ui-deployment.yaml index f8a1ff637..e0109c631 100644 --- a/helm/kagent/templates/ui-deployment.yaml +++ b/helm/kagent/templates/ui-deployment.yaml @@ -63,7 +63,7 @@ spec: protocol: TCP volumeMounts: - name: nextjs-cache - mountPath: /.next/cache + mountPath: /app/ui/.next/cache - name: tmp mountPath: /tmp resources: From 2f5796ba0307b704a3b19e14d761ce9b78912e0a Mon Sep 17 00:00:00 2001 From: Timofey Zhukov-Khovanskiy Date: Wed, 11 Mar 2026 09:43:56 -0400 Subject: [PATCH 7/7] updating the helm test Signed-off-by: Timofey Zhukov-Khovanskiy --- helm/kagent/tests/security-context_test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/kagent/tests/security-context_test.yaml b/helm/kagent/tests/security-context_test.yaml index 007396c89..ab2a71813 100644 --- a/helm/kagent/tests/security-context_test.yaml +++ b/helm/kagent/tests/security-context_test.yaml @@ -119,7 +119,7 @@ tests: path: spec.template.spec.containers[0].volumeMounts content: name: nextjs-cache - mountPath: /.next/cache + mountPath: /app/ui/.next/cache - it: should have tmp volume mount for UI template: ui-deployment.yaml