helm-bjw-s-chart — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited helm-bjw-s-chart (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Published as helm-chart-generator until v2.x. Update anything still pointing at the old name.| common | Kubernetes | Helm | Status |
|---|---|---|---|
5.3.0 | >= 1.31 | >= 3.18 | Default — latest stable, all examples target it |
4.6.2 | >= 1.25 | >= 3.14 | Legacy — pin when the cluster can't meet 5.x reqs |
Everything documented here works on common 5.x by default. When a pattern is not available on 4.x it's tagged `(5.x only)` so agents pinned to the legacy track can skip it. See references/migration-4-to-5.md for the full 4 → 5 upgrade procedure.
5.3.0 (2026-10-10) is drop-in over 5.2.x: no values change is required to upgrade, and the Kubernetes and Helm minimums are unchanged.
v1/Namespace named after therelease namespace, before every other resource. namespace.labels and namespace.annotations are optional (keys and values are templated).
namespace:
create: true
labels:
pod-security.kubernetes.io/enforce: restrictedcontroller's pods. They land in the pod labels and in every generated selector (workload matchLabels, Services, native and Cilium policies, PodMonitor, PodDisruptionBudget, default topologySpreadConstraints).
controllers:
main:
extraSelectorLabels:
app.kubernetes.io/component: backendWorkload selectors are immutable: adding or changing this on a live Deployment or StatefulSet means deleting and recreating the workload. The resource-level extraSelectorLabels (Service, NetworkPolicy, ...) still only affect that resource's own selector.
probes.<n>.command (array of strings)renders exec.command, and type gains COMMAND. The type defaults to COMMAND when command is set and type is absent or AUTO. No port is emitted. type: COMMAND without command fails the render. The old custom: true plus raw spec.exec workaround is no longer needed.
probes:
liveness:
enabled: true
command: ["/usr/bin/healthcheck", "--timeout=3"]annotations, labels andextraSelectorLabels maps go through tpl too, not only the values.
passes (integer >= 1, default 1) re-evaluates a string when a templated value itself produces {{ }}, and stops early once a pass changes nothing. excludePaths lists paths (each a list of map keys or array indexes, as strings) that are left untouched with their descendants, for values that must keep a literal {{ }}.
global:
templateEvaluation:
passes: 2
excludePaths:
- [controllers, main, containers, main, env, VPN_PORT_FORWARDING_UP_COMMAND]networkpolicies.<id> (all three types), controllers: [<id or regex>] targets several controllers. Entries are exact identifiers or regular expressions anchored as ^(<expr>)$. It is mutually exclusive with controller, podSelector and endpointSelector. A reference that matches no enabled controller fails the render and lists the available identifiers.
rules.ingress[].from[] orrules.egress[].to[] may carry controllers: [...] (expands to that peer's podSelector, exclusive with podSelector and ipBlock; the singular controller is not valid inside a peer).
ciliumClusterwide rules: ingress[].fromControllers andegress[].toControllers expand into fromEndpoints / toEndpoints.
networkpolicies:
api:
controllers: ["api", "^worker-.*$"]
policyTypes: [Ingress]
rules:
ingress:
- from:
- controllers: ["frontend"]
ports:
- port: 8080Three fixes ride along. Global metadata is now the base and resource-level labels / annotations override it (the reverse on Services and several other resources). Pod labels and generated selectors share one helper, so selector labels are merged last and always win over user pod labels. clusterIP is now rendered for LoadBalancer and NodePort Services (still omitted for ExternalName).
Worked examples for most of these live in references/patterns.md.
externalSecrets (withdefaultExternalSecretStoreRef) generates ExternalSecret resources; networkpolicies.<id>.type: cilium / ciliumClusterwide renders Cilium policies (native is the default); listenerSets adds initial Gateway API ListenerSet support; controllers get default topologySpreadConstraints selectors; persistence.<id>.type: projected is native; string fields are templated globally, so {{ }} works in more places. A bugfix made NetworkPolicy extraSelectorLabels take precedence over generated selector labels.
strategy / rollingUpdate (silentlyignored on 5.0.x); serviceAccount.<id>.automountServiceAccountToken sets the field on the SA itself; route.<id>.namespaceOverride deploys a Route into another namespace and emits the ReferenceGrant (route.<id>.referenceGrant.enabled: false turns that off); rollingUpdate takes the upstream maxSurge / maxUnavailable; StatefulSets gained rollingUpdate.maxUnavailable, which needs the MaxUnavailableStatefulSet feature gate (alpha, off by default up to Kubernetes 1.34, beta from 1.35). An invalid strategy is now rejected by the values schema, so helm lint catches it earlier.
Two behavioural notes still bite. The surge / unavailable shorthands are deprecated and removed in 6.0. The library always writes automountServiceAccountToken into the pod spec (default false) and the pod spec wins over the ServiceAccount key, so set both.
Five things to know — full details in references/migration-4-to-5.md:
back to true per-pod if the workload needs to call the Kubernetes API.
Opt out with global.createDefaultServiceAccount: false when you reference an externally-managed SA.
spec: into a manifest: wrapper, and labels/annotations now live under metadata:. Only relevant if you use rawResources (rare).
app.kubernetes.io/name. Override if your Prometheus rules depended on the old metadata.name default.
data, logs), ingress, and whether sidecars or init containers apply.
assets/templates/: Chart.yaml,values.yaml, templates/common.yaml, templates/NOTES.txt.
services, ingress, persistence, secrets and configMaps.
helm dependency update (writes Chart.lock), thenvalidate_chart.py, then helm lint and helm template.
my-app/
├── Chart.yaml # Metadata and dependencies
├── values.yaml # Configuration
└── templates/
├── common.yaml # Includes the bjw-s library
└── NOTES.txt # Post-install instructionstemplates/common.yaml is always exactly one line: {{- include "bjw-s.common.loader.all" . }}. NOTES.txt covers how to reach the app, default credentials if any, and the next configuration step.
apiVersion: v2
name: <app-name>
description: <brief description>
type: application
version: 1.0.0
appVersion: "<app version>"
dependencies:
- name: common
repository: https://bjw-s-labs.github.io/helm-charts
version: 5.3.0 # Default. Pin to 4.6.2 for legacy clusters (K8s < 1.31 / Helm < 3.18).Follow this order for clarity:
# 1. Default Pod options (optional)
defaultPodOptions:
# 5.x default is false; set to true only if the pod calls the K8s API.
automountServiceAccountToken: false
securityContext: {}
annotations: {}
# 2. Controllers (required)
controllers:
main: # or custom name
containers:
main: # or custom name
image: {}
env: {}
probes: {}
# 3. Service (required if exposing)
service:
main:
controller: main
ports: {}
# 4. Ingress (optional)
ingress:
main:
className: ""
hosts: []
# 5. Persistence (optional)
persistence:
config:
type: persistentVolumeClaim
# or: emptyDir, configMap, secret, nfs, hostPath, ephemeral
# 6. ConfigMaps/Secrets (optional)
configMaps: {}
secrets: {}The baseline, a single container with a service and a PVC:
controllers:
main:
containers:
main:
image:
repository: nginx
tag: "1.25-alpine"
pullPolicy: IfNotPresent
service:
main:
controller: main
ports:
http:
port: 80
persistence:
config:
type: persistentVolumeClaim
accessMode: ReadWriteOnce
size: 1Gi
globalMounts:
- path: /configA sidecar is one more entry under containers: with dependsOn: <container> to order startup.
See references/patterns.md for worked examples:
imagePullSecretsPlus one section per version-gated key listed below.
These are ignored or rejected below the version in the Since column. Each has a worked example in references/patterns.md:
| Key | Since | What it buys you |
|---|---|---|
horizontalPodAutoscaler | 5.0.0 | Autoscaling bound to a controller identifier |
podMonitor | 5.0.0 | Prometheus scraping without a Service |
persistence.*.type: ephemeral | 5.0.0 | Per-pod PVC, deleted with the pod |
resizePolicy (pod + container) | 5.0.0 | In-place CPU/memory resize, no pod recreation |
networkpolicies | 5.0.0 | Auto-targets the only controller when it is unambiguous |
strategy on a DaemonSet | 5.1.0 | Real updateStrategy instead of a silently dropped key |
serviceAccount.*.automountServiceAccountToken | 5.1.0 | Declares the token policy on the SA itself, for consumers outside the chart |
route.*.namespaceOverride | 5.1.0 | Cross-namespace Route with an auto-generated ReferenceGrant |
rollingUpdate.maxSurge / .maxUnavailable | 5.1.0 | Upstream key names; surge / unavailable are deprecated |
externalSecrets | 5.2.0 | ExternalSecret resources generated like ConfigMap/Secret |
networkpolicies.*.type: cilium / ciliumClusterwide | 5.2.0 | Renders a CiliumNetworkPolicy / CiliumClusterwideNetworkPolicy instead of the native NetworkPolicy (type: native, default) |
listenerSets | 5.2.0 | Gateway API ListenerSet resources (initial support) |
persistence.*.type: projected | 5.2.0 | Native projected volume support |
namespace | 5.3.0 | Renders the release Namespace first, with optional labels/annotations |
controllers.*.extraSelectorLabels | 5.3.0 | Pod identity labels applied to the pods and every generated selector (immutable on live workloads) |
probes.*.command / type: COMMAND | 5.3.0 | Native exec probe, no custom: true workaround |
global.templateEvaluation | 5.3.0 | Multi-pass templating (passes) with excludePaths for literal {{ }} |
networkpolicies.*.controllers | 5.3.0 | Target several controllers by identifier or regex; rule peers take controllers (native) or fromControllers / toControllers (Cilium) |
These shape every generated chart. The reasoning, and the long form, are in references/best-practices.md.
:latest. Requests and limits on everycontainer. Liveness and readiness probes at minimum.
securityContext. Secrets for sensitive data,imagePullSecrets for private registries.
main; nameservices after their controller or their purpose.
automountServiceAccountToken: false (the 5.x default). When theworkload genuinely calls the K8s API, pair it with an explicit ServiceAccount and RBAC rather than the auto-created default.
globalMounts for simple cases, advancedMounts for multi-container,existingClaim for pre-created PVCs, type: ephemeral for scratch space tied to the pod (5.x only).
After generating a chart:
# 1. Fetch dependencies (required before helm commands)
cd /path/to/chart
helm dependency update
# 2. Validate structure
uv run skills/helm-bjw-s-chart/scripts/validate_chart.py /path/to/chart
# Or with JSON output for CI:
uv run skills/helm-bjw-s-chart/scripts/validate_chart.py --json /path/to/chart
# 3. Helm validation
helm lint .
helm template . --debug
# 4. Dry-run installation
helm install --dry-run --debug my-release .The validator warns when the chart still pins common 4.x, when rawResources uses the legacy spec: shape (removed in 5.x), when an external ServiceAccount is referenced without global.createDefaultServiceAccount: false, when Chart.lock is missing, or when a declared dependency has no vendored tarball under charts/.
Before deploying to a cluster, verify:
:latest)securityContext set: non-root, readOnlyRootFilesystem, drop ALL capabilitiesautomountServiceAccountToken: false unless explicitly neededglobal.createDefaultServiceAccount: false is setrawResources is in play, manifest uses the 5.x manifest: wrapper (not legacy spec:)helm dependency update run, with Chart.lock and the populated charts/ published (see Publishing the Chart)helm lint passes with no errorsA published chart must be self-contained: both Chart.lock and a populated charts/ have to be present before helm package runs, or offline consumers cannot resolve the common library. Run helm dependency update first, then package. Either commit charts/ to git, or gitignore it and run helm dependency build in the pipeline.
Full rationale and the two strategies: references/best-practices.md, section "Publishing and Dependency Vendoring".
Services not found: Use identifier not name in ingress paths Mounts not working: Check globalMounts vs advancedMounts usage Names too long: Use nameOverride or fullnameOverride in global settings Controller not starting: Check dependsOn order for init/sidecar containers Unexpected ServiceAccount appears (5.x): Set global.createDefaultServiceAccount: false or define your own SA Pod can't talk to the K8s API (5.x): Set automountServiceAccountToken: true on the pod AND grant RBAC `strategy` rejected by `helm lint` (5.1+): It is a string, never a map, and the valid values depend on the controller type Cross-namespace Route can't reach the Service: The ReferenceGrant is only emitted from namespaceOverride, not from a hand-written backendRefs.namespace
references/migration-4-to-5.md - Full 4 → 5 upgrade procedurereferences/patterns.md - Common deployment patterns, including Gateway API HTTPRoutereferences/best-practices.md - Kubernetes/Helm best practicesreferences/values-schema.md - Complete values.yaml referencereferences/compose-mapping.md - Docker Compose → bjw-s values.yaml mappingassets/templates/ - Base templates for quick start~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.