diff --git a/cloudformation/values/dns-override.values.yaml b/cloudformation/values/dns-override.values.yaml index 4a2b306..19636c2 100644 --- a/cloudformation/values/dns-override.values.yaml +++ b/cloudformation/values/dns-override.values.yaml @@ -14,7 +14,9 @@ replicaCount: 2 image: repository: socketdev/socket-registry-firewall - tag: "1.1.346" # pin; do not use :latest for a security product + # tag intentionally omitted: it inherits the chart's appVersion (helm/Chart.yaml), + # the single source of truth for the firewall version. Never use :latest for a + # security product. Set a value here only to override the pinned version. socket: # Injected at install time from AWS Secrets Manager by the CloudFormation diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 5c1af8f..1f406b5 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -2,7 +2,9 @@ apiVersion: v2 name: socket-firewall description: Socket.dev Registry Firewall - Block vulnerable packages before they reach your cluster type: application -version: 0.4.3 +version: 0.5.0 +# appVersion is the single source of truth for the firewall image version. +# image.tag in values.yaml defaults to this (see templates/_helpers.tpl). appVersion: "1.1.346" keywords: - security diff --git a/helm/README.md b/helm/README.md index b19cddc..0243641 100644 --- a/helm/README.md +++ b/helm/README.md @@ -90,8 +90,8 @@ registries: | Parameter | Description | Default | |-----------|-------------|---------| | `image.repository` | Docker image | `socketdev/socket-registry-firewall` | -| `image.tag` | Image tag | `latest` | -| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.tag` | Image tag. Empty means track the chart's `appVersion` (single source of truth). Set to override. | `""` | +| `image.pullPolicy` | Image pull policy | `Always` | | `replicaCount` | Number of replicas (ignored if autoscaling enabled) | `1` | | `socket.apiToken` | Socket API token | `""` | | `socket.existingSecret` | Use existing secret | `""` | @@ -122,6 +122,7 @@ registries: | `ingress.className` | Ingress class (nginx, alb, traefik) | `""` | | `autoscaling.enabled` | Enable HorizontalPodAutoscaler | `false` | | `podDisruptionBudget.enabled` | Keep pods available during node maintenance | `true` | +| `topologySpreadConstraints` | Evenly spread replicas across zones/nodes | `[]` | | `extraContainers` | Sidecar containers (auth proxies, log collectors) | `[]` | | `resources.limits.cpu` | CPU limit | `4` | | `resources.limits.memory` | Memory limit | `8Gi` | @@ -492,6 +493,35 @@ kubectl get hpa socket-firewall kubectl describe hpa socket-firewall ``` +## Spreading Replicas Across Zones + +When you run more than one replica (via `replicaCount` or autoscaling), use +`topologySpreadConstraints` to distribute pods evenly across availability zones +(or nodes) so a single zone/node failure can't take down a disproportionate share +of the fleet. This is preferred over soft pod anti-affinity, which the scheduler is +free to ignore and can pile replicas into one zone. + +```yaml +replicaCount: 3 +topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway # best-effort even spread; never blocks scheduling + labelSelector: + matchLabels: + app.kubernetes.io/name: socket-firewall +``` + +- `maxSkew: 1` keeps zones within one pod of each other. +- `whenUnsatisfiable: ScheduleAnyway` is a soft guarantee (recommended). Use + `DoNotSchedule` for a hard guarantee — but be aware pods can stay `Pending` if a + zone is full or you have fewer zones than replicas. +- Pin spreading to a specific revision by adding `matchLabelKeys: [pod-template-hash]` + (requires Kubernetes 1.27+, satisfied by all currently-supported EKS and GKE versions). + +**Compatibility:** `topologySpreadConstraints` is GA since Kubernetes 1.19, so it works +on every currently-supported cluster. The chart sets no `kubeVersion` floor. + ## Using an Existing Secret for API Token ```bash diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 619c04f..6c20d93 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -21,6 +21,15 @@ Create a default fully qualified app name. {{- end }} {{- end }} +{{/* +Fully-qualified firewall image reference. +The tag defaults to the chart's appVersion so the firewall version is defined in +exactly one place (Chart.yaml appVersion). Override via image.tag in values.yaml. +*/}} +{{- define "socket-firewall.image" -}} +{{- printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) -}} +{{- end }} + {{/* Create chart name and version as used by the chart label. */}} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 0b510d3..b6a9f3a 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -35,7 +35,7 @@ spec: {{- end }} initContainers: - name: copy-app - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + image: "{{ include "socket-firewall.image" . }}" imagePullPolicy: {{ .Values.image.pullPolicy }} command: ["/bin/sh", "-c", "cp -r /app/. /mnt/app/"] {{- with .Values.initContainers.copyApp.securityContext }} @@ -175,7 +175,7 @@ spec: {{- end }} containers: - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + image: "{{ include "socket-firewall.image" . }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- with .Values.securityContext }} securityContext: @@ -369,6 +369,10 @@ spec: affinity: {{- toYaml . | nindent 8 }} {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} diff --git a/helm/values.yaml b/helm/values.yaml index ddf571c..6e4a0d8 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -3,10 +3,10 @@ # Image configuration image: repository: socketdev/socket-registry-firewall - # Defaults to the latest published image so deployments pick up new releases - # without a chart bump. Pin a version for reproducibility, e.g. tag: "1.1.327". - tag: "1.1.346" - # Always (not IfNotPresent) so "latest" actually refreshes on pod restart. + # Leave empty to track the chart's appVersion (Chart.yaml), which is the single + # source of truth for the firewall version. Set a value here only to override + # the pinned version, e.g. tag: "1.1.327". + tag: "" pullPolicy: Always # Image pull secrets for private registries @@ -432,6 +432,22 @@ tolerations: [] # Affinity affinity: {} +# Topology spread constraints (evenly distribute replicas across failure domains +# such as zones or nodes). Preferred over soft pod anti-affinity for even spreading. +# Empty by default; only takes effect with replicaCount > 1 or autoscaling enabled. +# The base field is GA since Kubernetes 1.19 (universally available). The optional +# matchLabelKeys field needs Kubernetes 1.27+ (satisfied by all current EKS/GKE versions). +topologySpreadConstraints: [] + # - maxSkew: 1 + # topologyKey: topology.kubernetes.io/zone + # whenUnsatisfiable: ScheduleAnyway # use DoNotSchedule for a hard guarantee + # labelSelector: + # matchLabels: + # app.kubernetes.io/name: socket-firewall + # # matchLabelKeys requires Kubernetes 1.27+ + # # matchLabelKeys: + # # - pod-template-hash + # Extra containers to run alongside the firewall (e.g., auth sidecars, log collectors) extraContainers: [] # - name: auth-sidecar