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
2 changes: 1 addition & 1 deletion helm/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: socket-firewall
description: Socket.dev Registry Firewall - Block vulnerable packages before they reach your cluster
type: application
version: 0.11.4
version: 0.12.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: "2.3.3"
Expand Down
25 changes: 25 additions & 0 deletions helm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,31 @@ helm install socket-firewall . -f examples/remote-first.yaml \
--set ingress.hosts[0].host=sfw.yourcompany.com
```

## Validating Your Configuration

The chart validates values at three points:

1. **Schema validation** (`values.schema.json`) runs automatically on every
`helm install`, `helm upgrade`, `helm lint`, and `helm template`. It checks
types and rejects unknown keys.
2. **Render-time guards** (`templates/validations.yaml`) fail the render with an
actionable message for configurations that would deploy but not work:
path routing enabled with no routes, an ElastiCache cluster-mode
(`clustercfg.*`) Redis endpoint, or no Socket API token / existing secret.
3. **Post-install tests** verify the live deployment — health endpoint, egress
to the Socket API, Redis connectivity (via `/metrics`), and route matching:

```bash
helm test <release> -n <namespace>
```

> **Strictness change:** unknown top-level keys in your values now fail
> validation instead of being silently ignored. This is deliberate — it catches
> indentation accidents (e.g. a `pathRouting:` block whose children slip to the
> top level and leave the firewall with zero routes) and dead config (e.g. an
> `env:` block no template reads). If you need to pass config keys the chart
> doesn't expose, use `extraConfig` (raw `socket.yml` passthrough).

## Proxy Modes

### Path-Based Routing (Recommended)
Expand Down
32 changes: 32 additions & 0 deletions helm/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,36 @@ Set metrics.serviceMonitor.enabled=true (Prometheus Operator) or metrics.podAnno
{{- end }}
{{- end }}

{{- $socket := .Values.socket | default dict }}
{{- $ingress := .Values.ingress | default dict }}
{{- $albScheme := index ($ingress.annotations | default dict) "alb.ingress.kubernetes.io/scheme" | default "" }}
{{- $noClientAuth := and (not $socket.bearerToken) (not $socket.bearerTokenExistingSecret) (not $socket.basicAuthUsername) }}
{{- $openProxy := and $ingress.enabled (eq $albScheme "internet-facing") $noClientAuth }}
{{- if or $socket.failOpen $openProxy }}

## Warnings
{{- if $socket.failOpen }}

- socket.failOpen is true: whenever the Socket API is unreachable, packages are
allowed through UNSCANNED. Set socket.failOpen=false for fail-closed behavior
(blocks all packages while the API is down).
{{- end }}
{{- if $openProxy }}

- The ALB ingress scheme is "internet-facing" and no client auth is configured
(socket.bearerToken, socket.bearerTokenExistingSecret, and
socket.basicAuthUsername are all empty). The firewall will accept
unauthenticated internet traffic and can be used as an open proxy to the
upstream registries. Enable the client auth gate (see "Client Auth Gate" in
README.md) and/or restrict the ALB's security groups to trusted CIDRs.
{{- end }}
{{- end }}

## Validate

Run the chart's built-in checks against the live deployment
(health endpoint, Socket API egress, Redis, and route matching):

helm test {{ .Release.Name }} -n {{ .Release.Namespace }}

See deployment recommendations in README.md for production setup.
80 changes: 80 additions & 0 deletions helm/templates/tests/test-connection.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{{- $fullname := include "socket-firewall.fullname" . -}}
{{- $apiUrl := (.Values.socket).apiUrl | default "https://api.socket.dev" -}}
{{- $healthPath := (.Values.healthCheck).path | default "/health" -}}
{{- $pathRouting := .Values.pathRouting | default dict -}}
apiVersion: v1
kind: Pod
metadata:
name: {{ $fullname }}-test-connection
labels:
{{- include "socket-firewall.labels" . | nindent 4 }}
annotations:
helm.sh/hook: test
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
restartPolicy: Never
containers:
- name: test-connection
image: curlimages/curl:8.11.1
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
runAsNonRoot: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
command:
- /bin/sh
- -c
- |
set -u
BASE="http://{{ $fullname }}:{{ ((.Values.service).httpPort) | default 80 }}"
fail() {
echo "FAILED: $1"
exit 1
}

echo "==> [1] Health endpoint: GET ${BASE}{{ $healthPath }}"
code=$(curl -s --max-time 15 -o /tmp/health.out -w '%{http_code}' "${BASE}{{ $healthPath }}") || true
[ "$code" = "200" ] || fail "health check returned HTTP ${code} (expected 200). The firewall Service or pods are not serving traffic."
grep -q "Health OK" /tmp/health.out || fail "health check returned 200 but the body does not contain 'Health OK'."
echo " OK (HTTP 200, body contains 'Health OK')"

echo "==> [2] Socket API egress: GET {{ $apiUrl }}/v0/quota (expect HTTP 401)"
code=$(curl -s --max-time 15 -o /dev/null -w '%{http_code}' "{{ $apiUrl }}/v0/quota") || true
if [ "$code" != "401" ]; then
echo " Hint: HTTP 000 or a timeout means the cluster cannot reach the Socket API"
echo " (egress blocked, DNS failure, or TLS interception). The firewall needs"
echo " outbound HTTPS to {{ $apiUrl }} to fetch package verdicts."
fail "Socket API returned HTTP ${code} (expected exactly 401 — an unauthenticated 401 proves egress and TLS work)."
fi
echo " OK (HTTP 401 — egress and TLS to the Socket API verified)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API test ignores egress settings

Medium Severity

Check 2 curls socket.apiUrl from the test pod and requires exactly HTTP 401. It does not use socket.outboundProxy, socket.apiSslCaCert, or socket.apiSslVerify, so a working firewall behind a corporate proxy or custom CA still fails helm test.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b0eccdb. Configure here.

{{- if and (eq (include "socket-firewall.metricsExposed" .) "true") ((.Values.redis).enabled) }}

echo "==> [3] Redis connectivity: GET http://{{ $fullname }}:{{ ((.Values.metrics).port) | default 9145 }}/metrics (expect redis_available 1)"
curl -s --max-time 15 -o /tmp/metrics.out "http://{{ $fullname }}:{{ ((.Values.metrics).port) | default 9145 }}/metrics" || fail "could not fetch /metrics from the firewall."
if ! grep -q '^redis_available 1' /tmp/metrics.out; then
echo " Hint: redis_available 0 means the firewall cannot reach Redis —"
echo " check the endpoint, TLS settings (redis.ssl / CA cert), and auth"
echo " (redis.password / redis.existingSecret)."
fail "metrics do not report 'redis_available 1'."
fi
echo " OK (redis_available 1)"
{{- end }}
{{- if and $pathRouting.enabled $pathRouting.routes }}
{{- $firstRoute := first $pathRouting.routes }}

echo "==> [4] Path routing: GET ${BASE}{{ $firstRoute.path }}/ with Host: {{ $pathRouting.domain }} (expect anything but 404)"
code=$(curl -s --max-time 15 -o /dev/null -w '%{http_code}' -H "Host: {{ $pathRouting.domain }}" "${BASE}{{ $firstRoute.path }}/") || true
if [ "$code" = "404" ]; then
echo " Hint: routing is Host-header based — a 404 means the request did not"
echo " match pathRouting.domain (or allowedDomain) plus a configured route"
echo " path. Verify clients send the same Host the firewall is configured for."
fail "route {{ $firstRoute.path }}/ returned 404 for Host {{ $pathRouting.domain }}."
fi
echo " OK (HTTP ${code})"
{{- end }}

echo "All checks passed."
44 changes: 44 additions & 0 deletions helm/templates/validations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{{- /*
Render-time configuration guards. This file renders no manifests — it only
fails fast on configurations that would deploy but not work. Structural /
type validation lives in values.schema.json; the checks here are the
cross-field rules a schema can't express.

Note: `fail` is a no-op under `helm lint` (lint mode); these guards enforce
on `helm install`, `helm upgrade`, and `helm template`.
*/ -}}

{{- $socket := .Values.socket | default dict -}}
{{- $redis := .Values.redis | default dict -}}
{{- $pathRouting := .Values.pathRouting | default dict -}}

{{- /* Path routing enabled with no route source: the firewall starts fine but
has nothing to match, so every request 404s. Routes can come from the
inline list, a routes CSV file, or private-registry auto-discovery. */ -}}
{{- if $pathRouting.enabled -}}
{{- $hasRoutes := not (empty $pathRouting.routes) -}}
{{- $hasRoutesFile := not (empty $pathRouting.routesFile) -}}
{{- $hasAutoDiscovery := and $pathRouting.privateRegistry (($pathRouting.privateRegistry).enabled) -}}
{{- if not (or $hasRoutes $hasRoutesFile $hasAutoDiscovery) -}}
{{- fail "pathRouting.enabled is true but pathRouting.routes is empty and no routesFile is set. The firewall would deploy with no routes and return 404 for all traffic. Define pathRouting.routes, set pathRouting.routesFile, or enable pathRouting.privateRegistry auto-discovery." -}}
{{- end -}}
{{- end -}}

{{- /* ElastiCache cluster-mode configuration endpoints (clustercfg.*) hand out
MOVED redirects across shards. The firewall's Redis client is a
single-endpoint client and cannot follow them, so every cache operation
fails at runtime even though the deployment looks healthy. */ -}}
{{- if $redis.enabled -}}
{{- $redisHost := (toString ($redis.host | default "")) | trimAll " " -}}
{{- if hasPrefix "clustercfg." $redisHost -}}
{{- fail (printf "redis.host %q is an ElastiCache cluster-mode configuration endpoint (clustercfg.*). The firewall uses a single-endpoint Redis client and cannot follow the MOVED redirects a cluster-mode-enabled group issues, so caching would fail at runtime. Use a cluster-mode-disabled replication group and point redis.host at its primary endpoint." $redisHost) -}}
{{- end -}}
{{- end -}}

{{- /* The deployment always mounts SOCKET_SECURITY_API_TOKEN from a Secret,
but the chart only creates that Secret when socket.apiToken is set.
With neither value the pods reference a Secret that doesn't exist and
stay stuck in CreateContainerConfigError. */ -}}
{{- if and (empty $socket.apiToken) (empty $socket.existingSecret) -}}
{{- fail "Set socket.apiToken or socket.existingSecret. The deployment mounts SOCKET_SECURITY_API_TOKEN from a Secret; without either value the pods reference a Secret Helm does not create. If you manage that Secret out of band, set socket.existingSecret to its name." -}}
{{- end -}}
Loading