Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ LOCAL_NODE_BIN := $(LOCAL_NODE_DIR)/bin
LOCAL_NODE := $(LOCAL_NODE_BIN)/node
LOCAL_NPM := $(LOCAL_NODE_BIN)/npm

.PHONY: proto build test test-nested-modules tidy lint generate build-sdk docker-build docker-build-e2e-client docker-build-etcd-tools docker-clean ensure-minio start-minio stop-containers release-broker-ports test-produce-consume test-produce-consume-debug test-consumer-group test-ops-api test-mcp test-multi-segment-durability test-full test-operator test-acl demo demo-platform demo-platform-bootstrap iceberg-demo kafsql-demo platform-demo help clean-kind-all ensure-local-node check vet race fmt fmt-check test-fuzz code-ql code-ql-summary code-ql-gate commit-check
.PHONY: proto build test test-nested-modules tidy lint generate build-sdk docker-build docker-build-e2e-client docker-build-etcd-tools docker-clean ensure-minio start-minio stop-containers release-broker-ports test-produce-consume test-produce-consume-debug test-consumer-group test-ops-api test-mcp test-multi-segment-durability test-full test-operator test-acl demo demo-platform demo-platform-bootstrap iceberg-demo kafsql-demo platform-demo help clean-kind-all ensure-local-node check vet race fmt fmt-check test-fuzz test-chart-psa code-ql code-ql-summary code-ql-gate commit-check

REGISTRY ?= ghcr.io/kafscale
STAMP_DIR ?= .build
Expand Down Expand Up @@ -205,6 +205,9 @@ fmt-check: ## Check formatting (fails if unformatted)
test-fuzz: ## Run Go fuzz test(s)
bash scripts/test_fuzz.sh

test-chart-psa: ## Run the PodSecurity restricted conformance chart test (helm only, no cluster)
bash test/chart/psa-restricted_test.sh

code-ql: ensure-local-node ## Run local CodeQL and emit SARIF under .tmp/codeql/
bash scripts/codeql_local.sh

Expand Down
2 changes: 1 addition & 1 deletion deploy/helm/kafscale/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ description: Helm chart for the Kafscale operator and console
home: https://github.com/KafScale/platform
icon: https://raw.githubusercontent.com/KafScale/platform/main/docs/assets/icon.png
type: application
version: 0.4.0
version: 0.4.2
appVersion: "v1.5.0"
8 changes: 8 additions & 0 deletions deploy/helm/kafscale/templates/console-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,19 @@ spec:
{{- range .Values.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- with .Values.console.podSecurityContext }}
securityContext:
{{ toYaml . | indent 8 }}
{{- end }}
containers:
- name: console
image: "{{ .Values.console.image.repository }}:{{ ternary "latest" (default .Chart.AppVersion .Values.console.image.tag) .Values.console.image.useLatest }}"
imagePullPolicy: {{ ternary "Always" .Values.console.image.pullPolicy .Values.console.image.useLatest }}
{{- with .Values.console.containerSecurityContext }}
securityContext:
{{ toYaml . | indent 12 }}
{{- end }}
env:
- name: KAFSCALE_CONSOLE_HTTP_ADDR
value: ":8080"
Expand Down
8 changes: 8 additions & 0 deletions deploy/helm/kafscale/templates/mcp-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,18 @@ spec:
{{- end }}
{{- end }}
serviceAccountName: {{ include "kafscale.mcpServiceAccountName" . }}
{{- with .Values.mcp.podSecurityContext }}
securityContext:
{{ toYaml . | indent 8 }}
{{- end }}
containers:
- name: mcp
image: "{{ .Values.mcp.image.repository }}:{{ ternary "latest" (default .Chart.AppVersion .Values.mcp.image.tag) .Values.mcp.image.useLatest }}"
imagePullPolicy: {{ ternary "Always" .Values.mcp.image.pullPolicy .Values.mcp.image.useLatest }}
{{- with .Values.mcp.containerSecurityContext }}
securityContext:
{{ toYaml . | indent 12 }}
{{- end }}
env:
- name: KAFSCALE_MCP_HTTP_ADDR
value: ":8090"
Expand Down
8 changes: 8 additions & 0 deletions deploy/helm/kafscale/templates/operator-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,19 @@ spec:
{{- range .Values.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- with .Values.operator.podSecurityContext }}
securityContext:
{{ toYaml . | indent 8 }}
{{- end }}
containers:
- name: operator
image: "{{ .Values.operator.image.repository }}:{{ ternary "latest" (default .Chart.AppVersion .Values.operator.image.tag) .Values.operator.image.useLatest }}"
imagePullPolicy: {{ ternary "Always" .Values.operator.image.pullPolicy .Values.operator.image.useLatest }}
{{- with .Values.operator.containerSecurityContext }}
securityContext:
{{ toYaml . | indent 12 }}
{{- end }}
ports:
- name: metrics
containerPort: {{ .Values.operator.metrics.port }}
Expand Down
18 changes: 18 additions & 0 deletions deploy/helm/kafscale/templates/proxy-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,19 @@ spec:
{{- range .Values.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- with .Values.proxy.podSecurityContext }}
securityContext:
{{ toYaml . | indent 8 }}
{{- end }}
containers:
- name: proxy
image: "{{ .Values.proxy.image.repository }}:{{ ternary "latest" (default .Chart.AppVersion .Values.proxy.image.tag) .Values.proxy.image.useLatest }}"
imagePullPolicy: {{ ternary "Always" .Values.proxy.image.pullPolicy .Values.proxy.image.useLatest }}
{{- with .Values.proxy.containerSecurityContext }}
securityContext:
{{ toYaml . | indent 12 }}
{{- end }}
env:
- name: KAFSCALE_PROXY_ADDR
value: ":{{ .Values.proxy.service.port }}"
Expand Down Expand Up @@ -107,6 +115,16 @@ spec:
{{- else }}
{}
{{- end }}
# The proxy LFS verify path writes a temp file via os.CreateTemp("", ...)
# which resolves to /tmp. With readOnlyRootFilesystem: true the root FS is
# read-only, so /tmp must be a writable mount or LFS verify returns HTTP 500.
# This emptyDir keeps the restricted default and LFS coexisting.
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
{{- with .Values.proxy.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
Expand Down
75 changes: 75 additions & 0 deletions deploy/helm/kafscale/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ operator:
nodeSelector: {}
tolerations: []
affinity: {}
# Pod-level security context. PSA-restricted compliant defaults.
# These defaults assume the shipped images (UID 10001). If you override
# image.repository with a root-running image, set
# operator.podSecurityContext.runAsNonRoot=false. Override per-environment
# via `--set operator.podSecurityContext.<key>=<value>` or a values file.
podSecurityContext:
runAsNonRoot: true
# runAsUser/runAsGroup pin the Dockerfile-baked USER 10001. If the image
# USER changes, change this in lockstep. PSA restricted itself only
# requires runAsNonRoot, not a specific UID.
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
# Container-level security context. PSA-restricted-required defaults.
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
metrics:
enabled: true
port: 8080
Expand Down Expand Up @@ -107,6 +127,23 @@ console:
nodeSelector: {}
tolerations: []
affinity: {}
# PSA-restricted compliant defaults. The console image runs as
# USER 10001 (Dockerfile-baked). These defaults assume the shipped image;
# if you override image.repository with a root-running image, set
# console.podSecurityContext.runAsNonRoot=false.
podSecurityContext:
runAsNonRoot: true
# runAsUser/runAsGroup pin the Dockerfile-baked USER 10001. Change in
# lockstep with the image USER. PSA restricted requires only runAsNonRoot.
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
service:
type: ClusterIP
port: 80
Expand Down Expand Up @@ -146,6 +183,27 @@ proxy:
nodeSelector: {}
tolerations: []
affinity: {}
# PSA-restricted compliant defaults. The proxy image runs as
# USER 10001 (Dockerfile-baked). These defaults assume the shipped image;
# if you override image.repository with a root-running image, set
# proxy.podSecurityContext.runAsNonRoot=false.
# fsGroup is kept here (unlike operator/console) because the proxy mounts a
# writable /tmp emptyDir for the LFS verify path; fsGroup sets that volume's
# group ownership so the non-root process can write to it.
podSecurityContext:
runAsNonRoot: true
# runAsUser/runAsGroup pin the Dockerfile-baked USER 10001. Change in
# lockstep with the image USER. PSA restricted requires only runAsNonRoot.
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
service:
type: LoadBalancer
port: 9092
Expand Down Expand Up @@ -211,6 +269,23 @@ mcp:
nodeSelector: {}
tolerations: []
affinity: {}
# PSA-restricted compliant defaults. The mcp image runs as
# USER 10001 (Dockerfile-baked, deploy/docker/mcp.Dockerfile). These
# defaults assume the shipped image; if you override image.repository with
# a root-running image, set mcp.podSecurityContext.runAsNonRoot=false.
podSecurityContext:
runAsNonRoot: true
# runAsUser/runAsGroup pin the Dockerfile-baked USER 10001. Change in
# lockstep with the image USER. PSA restricted requires only runAsNonRoot.
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
service:
type: ClusterIP
port: 80
Expand Down
112 changes: 112 additions & 0 deletions test/chart/psa-restricted_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# Copyright 2026 KafScale team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Chart conformance gate for the PodSecurity "restricted" profile.
#
# Asserts that every chart-templated Deployment (operator, proxy, console,
# mcp) renders the five restricted controls:
# 1. pod-level runAsNonRoot: true
# 2. container allowPrivilegeEscalation: false
# 3. container capabilities.drop includes ALL
# 4. pod-level seccompProfile.type: RuntimeDefault
# 5. pod-level runAsUser: <non-zero> (a non-root UID)
#
# This is the gate that catches a future Deployment being added without the
# securityContext blocks (e.g. the mcp gap this test was written to close).
# Self-contained: needs only helm and awk, no helm plugins, no cluster.
# Run directly or via `make test-chart-psa`.

set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CHART_DIR="${ROOT_DIR}/deploy/helm/kafscale"

command -v helm >/dev/null 2>&1 || { echo "helm is required"; exit 1; }

fail=0

# Render a single Deployment template with every workload enabled. The proxy
# and mcp are opt-in (enabled: false by default), so we enable all four here.
render_deployment() {
local template="$1"
helm template kafscale "${CHART_DIR}" \
--show-only "templates/${template}" \
--set proxy.enabled=true \
--set console.enabled=true \
--set mcp.enabled=true
}

# assert_present <component> <description> <rendered-yaml> <grep-pattern>
assert_present() {
local component="$1" desc="$2" yaml="$3" pattern="$4"
if printf '%s\n' "${yaml}" | grep -Eq "${pattern}"; then
echo "PASS: ${component}: ${desc}"
else
echo "FAIL: ${component}: ${desc} (pattern: ${pattern})"
fail=1
fi
}

# assert_runasuser_nonzero <component> <rendered-yaml>
# runAsUser must be present and not 0 (0 is root, which violates restricted).
assert_runasuser_nonzero() {
local component="$1" yaml="$2" uid
uid="$(printf '%s\n' "${yaml}" | awk '/^[[:space:]]*runAsUser:/ { print $2; exit }')"
if [ -n "${uid}" ] && [ "${uid}" != "0" ]; then
echo "PASS: ${component}: runAsUser is a non-root UID (runAsUser=${uid})"
else
echo "FAIL: ${component}: runAsUser must be a non-root UID (got \"${uid:-<absent>}\")"
fail=1
fi
}

echo "==> chart PSA-restricted conformance gate"

# The chart-templated Deployments and their template files. Broker/etcd are
# operator-reconciled (label app=kafscale-broker), not chart-templated, and
# are intentionally out of scope for this chart-level gate.
declare -a COMPONENTS=(
"operator:operator-deployment.yaml"
"proxy:proxy-deployment.yaml"
"console:console-deployment.yaml"
"mcp:mcp-deployment.yaml"
)

for entry in "${COMPONENTS[@]}"; do
component="${entry%%:*}"
template="${entry##*:}"
yaml="$(render_deployment "${template}")"

# 1. runAsNonRoot: true
assert_present "${component}" "runAsNonRoot: true" "${yaml}" \
'^[[:space:]]*runAsNonRoot:[[:space:]]*true[[:space:]]*$'
# 2. allowPrivilegeEscalation: false
assert_present "${component}" "allowPrivilegeEscalation: false" "${yaml}" \
'^[[:space:]]*allowPrivilegeEscalation:[[:space:]]*false[[:space:]]*$'
# 3. capabilities.drop includes ALL
assert_present "${component}" "capabilities.drop includes ALL" "${yaml}" \
'^[[:space:]]*-[[:space:]]*ALL[[:space:]]*$'
# 4. seccompProfile RuntimeDefault
assert_present "${component}" "seccompProfile.type: RuntimeDefault" "${yaml}" \
'^[[:space:]]*type:[[:space:]]*RuntimeDefault[[:space:]]*$'
# 5. non-root runAsUser
assert_runasuser_nonzero "${component}" "${yaml}"
done

if [ "${fail}" -ne 0 ]; then
echo "==> chart PSA-restricted conformance gate FAILED"
exit 1
fi
echo "==> chart PSA-restricted conformance gate passed (4 Deployments x 5 controls)"
Loading