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/controller-deployment.yaml b/helm/kagent/templates/controller-deployment.yaml index 2909cdc78..e8057ab98 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: @@ -65,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 }} @@ -80,8 +86,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/templates/ui-deployment.yaml b/helm/kagent/templates/ui-deployment.yaml index 3b009a1e9..e0109c631 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: /app/ui/.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..ab2a71813 --- /dev/null +++ b/helm/kagent/tests/security-context_test.yaml @@ -0,0 +1,190 @@ +suite: test security context configuration +templates: + - _helpers.tpl + - controller-configmap.yaml + - 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: /app/ui/.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 7f6142dd8..acd8c0b40 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -24,16 +24,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 @@ -166,7 +168,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 (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 + # -- 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 02249c8aa..8ed01623c 100644 --- a/helm/tools/querydoc/templates/deployment.yaml +++ b/helm/tools/querydoc/templates/deployment.yaml @@ -35,8 +35,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: diff --git a/ui/Dockerfile b/ui/Dockerfile index b77571f37..2670fc1e2 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -60,6 +60,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 ./ @@ -70,7 +71,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 @@ -80,6 +82,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/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +CMD ["/usr/local/bin/init.sh"] 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