From b0eccdb730f45743dd6f3f9f2d15f00804704477 Mon Sep 17 00:00:00 2001 From: dc-larsen Date: Tue, 1 Sep 2026 13:59:42 -0400 Subject: [PATCH] helm: validate values with a schema, render-time guards, and helm tests Three failure modes this chart previously accepted silently: - A values file where an indentation accident turns a section (e.g. pathRouting) into null and promotes its children to top-level keys. The stray keys were ignored and the firewall deployed with zero routes, returning 404 for all traffic. - Dead config: top-level keys no template reads (e.g. an env: block holding an API token). - An ElastiCache cluster-mode configuration endpoint (clustercfg.*) in redis.host, which the firewall's single-endpoint Redis client cannot use (no MOVED-redirect support). What's added: - values.schema.json (draft-07) covering every top-level key, with additionalProperties: false at the root and in structured sections. Kubernetes passthrough maps (annotations, resources, affinity, tolerations, extraConfig, ...) stay free-form. Validated against the chart defaults and all bundled examples. - templates/validations.yaml render-time guards: pathRouting enabled with no route source, clustercfg.* Redis endpoints, and missing socket.apiToken/existingSecret all fail with actionable messages. Guards are no-ops under helm lint (lint mode) and enforce on install/upgrade/template. - helm test pod (templates/tests/): checks the /health endpoint, egress + TLS to the Socket API (expects exactly 401 from /v0/quota), redis_available via /metrics when Redis is enabled, and Host-header route matching for the first pathRouting route. - NOTES.txt warnings for socket.failOpen=true and for internet-facing ALB ingress with no client auth gate (open proxy). - README section on the validation layers and the strictness change. Breaking change: unknown top-level values keys now fail instead of being ignored. Use extraConfig for raw socket.yml passthrough. --- helm/Chart.yaml | 2 +- helm/README.md | 25 + helm/templates/NOTES.txt | 32 + helm/templates/tests/test-connection.yaml | 80 +++ helm/templates/validations.yaml | 44 ++ helm/values.schema.json | 693 ++++++++++++++++++++++ 6 files changed, 875 insertions(+), 1 deletion(-) create mode 100644 helm/templates/tests/test-connection.yaml create mode 100644 helm/templates/validations.yaml create mode 100644 helm/values.schema.json diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 9a39b68..e19fa67 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -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" diff --git a/helm/README.md b/helm/README.md index 0af7e04..5bc31e6 100644 --- a/helm/README.md +++ b/helm/README.md @@ -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 -n + ``` + +> **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) diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt index b63a674..3da25c7 100644 --- a/helm/templates/NOTES.txt +++ b/helm/templates/NOTES.txt @@ -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. diff --git a/helm/templates/tests/test-connection.yaml b/helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000..a582c80 --- /dev/null +++ b/helm/templates/tests/test-connection.yaml @@ -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)" + {{- 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." diff --git a/helm/templates/validations.yaml b/helm/templates/validations.yaml new file mode 100644 index 0000000..6acd62e --- /dev/null +++ b/helm/templates/validations.yaml @@ -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 -}} diff --git a/helm/values.schema.json b/helm/values.schema.json new file mode 100644 index 0000000..7f5221a --- /dev/null +++ b/helm/values.schema.json @@ -0,0 +1,693 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "socket-firewall Helm chart values", + "description": "Validates values on install/upgrade/lint. The root rejects unknown top-level keys so indentation accidents (e.g. a pathRouting block whose children land at the top level) and dead config (e.g. a stray env: block no template reads) fail fast instead of being silently ignored. Use extraConfig for raw socket.yml passthrough.", + "type": "object", + "additionalProperties": false, + "definitions": { + "ecosystem": { + "type": "string", + "enum": ["npm", "pypi", "maven", "cargo", "rubygems", "nuget", "go", "conda", "openvsx", "huggingface"] + }, + "portLike": { + "type": ["integer", "string"] + }, + "nullablePort": { + "type": ["integer", "string", "null"] + }, + "nullableBool": { + "type": ["boolean", "null"] + }, + "nullableNumber": { + "type": ["number", "null"] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" } + }, + "freeformObject": { + "type": "object" + }, + "objectArray": { + "type": "array", + "items": { "type": "object" } + }, + "decisionLogLevel": { + "type": "string", + "enum": ["", "error", "warn", "notice", "info", "debug"] + }, + "confirmAllowMode": { + "type": "string", + "enum": ["serve_stale", "wait", "confirm_when_degraded", "fail_when_degraded"] + }, + "route": { + "type": "object", + "additionalProperties": false, + "required": ["path", "registry"], + "properties": { + "path": { "type": "string" }, + "upstream": { "type": "string" }, + "registry": { "$ref": "#/definitions/ecosystem" }, + "mode": { "type": "string", "enum": ["rewrite", "proxy"] }, + "upstreamType": { "type": "string", "enum": ["", "artifactory"] }, + "upstreamHost": { "type": "string" }, + "upstreamToken": { "type": "string" }, + "upstreamTokenSecret": { "type": "string" }, + "baseUrl": { "type": "string" }, + "artifactoryRepo": { "type": "string" }, + "cooldownName": { "type": "string" }, + "cooldownEcosystem": { "type": "string" }, + "repoKey": { "type": "string" } + } + }, + "registryEntry": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "domains": { "$ref": "#/definitions/stringArray" }, + "upstream": { "type": "string" }, + "upstreamToken": { "type": "string" }, + "upstreamTokenSecret": { "type": "string" } + } + }, + "containerSecurityContext": { + "type": "object" + } + }, + "properties": { + "global": { "$ref": "#/definitions/freeformObject" }, + "nameOverride": { "type": "string" }, + "fullnameOverride": { "type": "string" }, + "image": { + "type": "object", + "additionalProperties": false, + "properties": { + "repository": { "type": "string" }, + "tag": { "type": "string" }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "imagePullSecrets": { "$ref": "#/definitions/objectArray" }, + "replicaCount": { "type": "integer" }, + "podDisruptionBudget": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "minAvailable": { "$ref": "#/definitions/portLike" }, + "maxUnavailable": { "$ref": "#/definitions/portLike" } + } + }, + "autoscaling": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "minReplicas": { "type": "integer" }, + "maxReplicas": { "type": "integer" }, + "targetCPUUtilizationPercentage": { "type": "integer" }, + "targetMemoryUtilizationPercentage": { "type": "integer" } + } + }, + "socket": { + "type": "object", + "additionalProperties": false, + "properties": { + "apiToken": { "type": "string" }, + "existingSecret": { "type": "string" }, + "existingSecretKey": { "type": "string" }, + "failOpen": { "type": "boolean" }, + "failOpenUnscanned": { "type": "boolean" }, + "useRemoteConfig": { "type": "boolean" }, + "configRefreshInterval": { "type": "string" }, + "bearerTokenType": { "type": "string", "enum": ["string", "env"] }, + "bearerToken": { "type": "string" }, + "bearerTokenExistingSecret": { "type": "string" }, + "bearerTokenExistingSecretKey": { "type": "string" }, + "saveIgnoreReason": { "type": "boolean" }, + "requestIdHeader": { "type": "string" }, + "exposeUnscannedHeader": { "type": "boolean" }, + "cacheTtl": { "type": "integer" }, + "logLevel": { "type": "string", "enum": ["", "error", "warn", "info", "debug"] }, + "logMaxBodySize": { "type": "integer" }, + "logNoRoute": { "type": "boolean" }, + "debugLoggingEnabled": { "type": "boolean" }, + "rewritePackageUrls": { "type": "boolean" }, + "recentlyPublishedEnabledEcosystems": { + "type": "array", + "items": { "$ref": "#/definitions/ecosystem" } + }, + "apiUrl": { "type": "string" }, + "apiSslVerify": { "type": "boolean" }, + "apiSslCaCert": { "type": "string" }, + "upstreamSslVerify": { "$ref": "#/definitions/nullableBool" }, + "upstreamSslCaCert": { "type": "string" }, + "outboundProxy": { "type": "string" }, + "noProxy": { "type": "string" }, + "deployment": { "type": "string" }, + "basicAuthUsername": { "type": "string" }, + "basicAuthPassword": { "type": "string" }, + "healthApiToken": { "type": "string" }, + "debugUserAgentFilter": { "type": "string" }, + "apiReadTimeout": { "$ref": "#/definitions/nullableNumber" }, + "apiConnectTimeout": { "$ref": "#/definitions/nullableNumber" }, + "apiSendTimeout": { "$ref": "#/definitions/nullableNumber" }, + "blockLogLevel": { "$ref": "#/definitions/decisionLogLevel" }, + "warnLogLevel": { "$ref": "#/definitions/decisionLogLevel" }, + "monitorLogLevel": { "$ref": "#/definitions/decisionLogLevel" }, + "ignoreLogLevel": { "$ref": "#/definitions/decisionLogLevel" }, + "ecosystemParams": { + "type": "object", + "additionalProperties": { "type": "object" } + }, + "resilience": { + "type": "object", + "additionalProperties": false, + "properties": { + "circuitBreaker": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "latencyEwmaThresholdMs": { "type": "number" }, + "ewmaAlpha": { "type": "number" }, + "errorRateThreshold": { "type": "number" }, + "errorMinRequests": { "type": "number" }, + "errorWindowS": { "type": "number" }, + "openWindowS": { "type": "number" }, + "maxOpenWindowS": { "type": "number" }, + "backoffFactor": { "type": "number" }, + "halfopenProbes": { "type": "number" }, + "halfopenProbeSpacingS": { "type": "number" }, + "probeTimeoutMs": { "type": "number" } + } + } + } + } + } + }, + "cache": { + "type": "object", + "additionalProperties": false, + "properties": { + "revalidationLockLeaseSeconds": { "type": "integer" }, + "revalidationJitterSeconds": { "type": "integer" }, + "revalidationAsync": { "type": "boolean" }, + "confirmAllowMode": { "$ref": "#/definitions/confirmAllowMode" }, + "warmEnabled": { "type": "boolean" }, + "warmInterval": { "type": "integer" } + } + }, + "keepaliveTimeout": { "type": "integer" }, + "upstreamTokens": { + "type": "object", + "additionalProperties": false, + "properties": { + "values": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + }, + "pathRouting": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "domain": { "type": "string" }, + "allowedDomain": { "$ref": "#/definitions/stringArray" }, + "mode": { "type": "string", "enum": ["", "local", "artifactory", "nexus", "downstream"] }, + "configMode": { "type": "string", "enum": ["", "upstream", "middle"] }, + "upstreamFqdn": { "type": "string" }, + "rewriteScheme": { "type": "string", "enum": ["", "http", "https"] }, + "clientRewriteScheme": { "type": "string", "enum": ["", "http", "https"] }, + "useIncomingDomain": { "$ref": "#/definitions/nullableBool" }, + "forwardForDomain": { "$ref": "#/definitions/nullableBool" }, + "overrideLocationRewrite": { "$ref": "#/definitions/nullableBool" }, + "overridePortHttp": { "$ref": "#/definitions/portLike" }, + "overridePortHttps": { "$ref": "#/definitions/portLike" }, + "routesFile": { "type": "string" }, + "routes": { + "type": "array", + "items": { "$ref": "#/definitions/route" } + }, + "privateRegistry": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "type": { "type": "string", "enum": ["", "nexus", "artifactory"] }, + "apiUrl": { "type": "string" }, + "apiKey": { "type": "string" }, + "username": { "type": "string" }, + "password": { "type": "string" }, + "interval": { "type": "string" }, + "includePattern": { "type": "string" }, + "excludePattern": { "type": "string" }, + "ignoreSslErrors": { "type": "boolean" }, + "supportedEcosystemsOnly": { "type": "boolean" }, + "includeVirtual": { "type": "boolean" } + } + } + } + }, + "registries": { + "type": "object", + "additionalProperties": false, + "properties": { + "npm": { "$ref": "#/definitions/registryEntry" }, + "pypi": { "$ref": "#/definitions/registryEntry" }, + "maven": { "$ref": "#/definitions/registryEntry" }, + "rubygems": { "$ref": "#/definitions/registryEntry" }, + "cargo": { "$ref": "#/definitions/registryEntry" }, + "openvsx": { "$ref": "#/definitions/registryEntry" }, + "nuget": { "$ref": "#/definitions/registryEntry" }, + "go": { "$ref": "#/definitions/registryEntry" }, + "conda": { "$ref": "#/definitions/registryEntry" }, + "huggingface": { "$ref": "#/definitions/registryEntry" } + } + }, + "dnsRouting": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "registries": { + "type": "array", + "items": { "$ref": "#/definitions/ecosystem" } + } + } + }, + "proxy": { + "type": "object", + "additionalProperties": false, + "properties": { + "bufferSize": { "type": "string" }, + "buffersCount": { "type": "integer" }, + "buffersSize": { "type": "string" }, + "busyBuffersSize": { "type": "string" }, + "connectTimeout": { "type": "integer" }, + "sendTimeout": { "type": "integer" }, + "readTimeout": { "type": "integer" }, + "stripRequestHeaders": { "$ref": "#/definitions/stringArray" } + } + }, + "nginx": { + "type": "object", + "additionalProperties": false, + "properties": { + "workerProcesses": { "$ref": "#/definitions/portLike" }, + "workerConnections": { "type": "integer" }, + "workerRlimitNofile": { "type": "integer" }, + "keepalivePoolSize": { "type": "integer" }, + "typesHashMaxSize": { "type": "integer" }, + "clientMaxBodySize": { "type": "string" }, + "acceptMutex": { "type": ["string", "boolean"] }, + "http2": { "type": "boolean" }, + "resolver": { "type": "string" }, + "resolverTimeout": { "type": "string" }, + "accessLogBuffer": { "type": "string" }, + "gzip": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": ["string", "boolean"] }, + "minLength": { "type": "integer" } + } + }, + "openFileCache": { + "type": "object", + "additionalProperties": false, + "properties": { + "max": { "type": "integer" }, + "inactive": { "type": "string" } + } + }, + "proxyCache": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": ["string", "boolean"] }, + "path": { "type": "string" }, + "size": { "type": "string" }, + "inactive": { "type": "string" } + } + } + } + }, + "ports": { + "type": "object", + "additionalProperties": false, + "properties": { + "http": { "$ref": "#/definitions/nullablePort" }, + "https": { "$ref": "#/definitions/nullablePort" }, + "disableHttp": { "type": "boolean" }, + "disableHttps": { "type": "boolean" } + } + }, + "ssl": { + "type": "object", + "additionalProperties": false, + "properties": { + "cert": { "type": "string" }, + "key": { "type": "string" }, + "caCert": { "type": "string" } + } + }, + "lua": { + "type": "object", + "additionalProperties": false, + "properties": { + "cacheSize": { "type": "string" }, + "statsSize": { "type": "string" } + } + }, + "clientIp": { + "type": "object", + "additionalProperties": false, + "properties": { + "header": { "type": "string" }, + "trustedProxies": { "$ref": "#/definitions/stringArray" }, + "recursive": { "type": "boolean" } + } + }, + "forwardProxy": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "port": { "type": "integer" }, + "maxTunnelLifetimeSeconds": { "type": "integer" }, + "maxConnectionsPerSource": { "type": "integer" }, + "proxyProtocolPort": { "type": "integer" }, + "skipStreamLuaCheck": { "type": "boolean" }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "type": { "type": "string", "enum": ["ClusterIP", "NodePort", "LoadBalancer"] }, + "annotations": { "$ref": "#/definitions/freeformObject" }, + "nodePort": { "$ref": "#/definitions/portLike" }, + "externalTrafficPolicy": { "type": "string", "enum": ["", "Cluster", "Local"] }, + "loadBalancerSourceRanges": { "$ref": "#/definitions/stringArray" } + } + } + } + }, + "metadataFiltering": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "filterBlocked": { "type": "boolean" }, + "filterWarn": { "type": "boolean" }, + "includeUncheckedVersions": { "type": "boolean" }, + "maxVersions": { "type": "integer" }, + "cacheTtl": { "type": "integer" }, + "batchSize": { "type": "integer" }, + "maxBodySize": { "type": "string" }, + "excludedEcosystems": { "$ref": "#/definitions/stringArray" }, + "prefetchEnabled": { "type": "boolean" }, + "prefetchTtl": { "type": "integer" }, + "prefetchMaxConcurrent": { "type": "integer" }, + "prefetchBatchConcurrency": { "type": "integer" }, + "metadataConcurrentBatch": { "type": "integer" }, + "condaPrefetchArchs": { "$ref": "#/definitions/stringArray" }, + "maxConcurrent": { "type": "integer" }, + "packageFilterTimeout": { "type": "integer" }, + "semaphoreWaitTimeout": { "type": "integer" }, + "packageFilterRetry": { "type": "integer" }, + "responseCacheEnabled": { "type": "boolean" }, + "responseCacheTtl": { "type": "integer" }, + "responseCacheFresh": { "type": "integer" }, + "cacheAuthenticated": { "type": "boolean" }, + "responseCacheExcludedEcosystems": { "$ref": "#/definitions/stringArray" }, + "confirmAllowMode": { "$ref": "#/definitions/confirmAllowMode" } + } + }, + "redis": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "host": { "type": "string" }, + "port": { "type": "integer" }, + "password": { "type": "string" }, + "existingSecret": { "type": "string" }, + "existingSecretKey": { "type": "string" }, + "db": { "type": "integer" }, + "timeout": { "type": "integer" }, + "poolSize": { "type": "integer" }, + "ttl": { "type": "integer" }, + "ssl": { "type": "boolean" }, + "sslVerify": { "type": "boolean" }, + "sslServerName": { "type": "string" }, + "sslCaCert": { "type": "string" }, + "sslCaCertExistingSecret": { "type": "string" }, + "sslCaCertExistingSecretKey": { "type": "string" }, + "sslClientCert": { "type": "string" }, + "sslClientCertExistingSecret": { "type": "string" }, + "sslClientCertExistingSecretKey": { "type": "string" }, + "sslClientKey": { "type": "string" }, + "sslClientKeyExistingSecret": { "type": "string" }, + "sslClientKeyExistingSecretKey": { "type": "string" } + } + }, + "splunk": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "hecUrl": { "type": "string" }, + "hecToken": { "type": "string" }, + "existingSecret": { "type": "string" }, + "existingSecretKey": { "type": "string" }, + "index": { "type": "string" }, + "source": { "type": "string" }, + "sourcetype": { "type": "string" }, + "sslVerify": { "type": "boolean" }, + "timeout": { "type": "integer" }, + "batchSize": { "type": "integer" } + } + }, + "webhook": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "url": { "type": "string" }, + "authHeader": { "type": "string" }, + "sslVerify": { "type": "boolean" }, + "timeout": { "type": "integer" }, + "onBlock": { "type": "boolean" }, + "onWarn": { "type": "boolean" }, + "onMonitor": { "type": "boolean" }, + "onIgnore": { "type": "boolean" }, + "batchEnabled": { "type": "boolean" }, + "batchSize": { "type": "integer" }, + "batchPeriod": { "type": "integer" } + } + }, + "externalRegistryCooldown": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "enablePublicQuery": { "type": "boolean" }, + "cooldownPeriod": { "type": "string" }, + "checkInterval": { "type": "string" }, + "redisKeyPrefix": { "type": "string" }, + "cacheTtl": { "type": "integer" }, + "mode": { "type": "string", "enum": ["", "api", "local"] }, + "fallback": { "type": "string", "enum": ["", "external", "artifactory", "nexus"] }, + "registries": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "url", "ecosystem"], + "properties": { + "name": { "type": "string" }, + "url": { "type": "string" }, + "ecosystem": { "type": "string" }, + "authType": { "type": "string", "enum": ["none", "basic", "bearer"] }, + "authCredential": { "type": "string" }, + "rateLimitHeader": { "type": "string" }, + "ignoreSslErrors": { "type": "boolean" }, + "timeout": { "type": "integer" }, + "cooldownPeriod": { "type": "string" } + } + } + }, + "privateRegistry": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "authType": { "type": "string", "enum": ["bearer_token", "basic"] }, + "authCredential": { "type": "string" }, + "source": { "type": "string", "enum": ["auto", "artifactory", "nexus"] }, + "includeUnsupportedOnly": { "type": "boolean" }, + "includePattern": { "type": "string" }, + "excludePattern": { "type": "string" }, + "cooldownPeriod": { "type": "string" } + } + } + } + }, + "extraConfig": { "$ref": "#/definitions/freeformObject" }, + "metrics": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "minImageVersion": { "type": "string" }, + "port": { "type": "integer" }, + "podAnnotations": { "type": "boolean" }, + "serviceMonitor": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "namespace": { "type": "string" }, + "interval": { "type": "string" }, + "scrapeTimeout": { "type": "string" }, + "labels": { "$ref": "#/definitions/freeformObject" }, + "honorLabels": { "type": "boolean" }, + "relabelings": { "$ref": "#/definitions/objectArray" }, + "metricRelabelings": { "$ref": "#/definitions/objectArray" } + } + } + } + }, + "service": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { "type": "string", "enum": ["ClusterIP", "NodePort", "LoadBalancer"] }, + "httpPort": { "type": "integer" }, + "httpsPort": { "type": "integer" }, + "containerHttpPort": { "$ref": "#/definitions/portLike" }, + "containerHttpsPort": { "$ref": "#/definitions/portLike" }, + "httpsTargetPort": { "$ref": "#/definitions/portLike" }, + "externalTrafficPolicy": { "type": "string", "enum": ["", "Cluster", "Local"] }, + "annotations": { "$ref": "#/definitions/freeformObject" } + } + }, + "ingress": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "className": { "type": "string" }, + "annotations": { "$ref": "#/definitions/freeformObject" }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "host": { "type": "string" }, + "paths": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { "type": "string" }, + "pathType": { "type": "string" } + } + } + } + } + } + }, + "tls": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "secretName": { "type": "string" }, + "hosts": { "$ref": "#/definitions/stringArray" } + } + } + } + } + }, + "tls": { + "type": "object", + "additionalProperties": false, + "properties": { + "generateSelfSigned": { "type": "boolean" }, + "existingSecret": { "type": "string" }, + "certManager": { "type": "boolean" }, + "includeCaCrt": { "type": "boolean" }, + "certificate": { "type": "string" }, + "privateKey": { "type": "string" } + } + }, + "resources": { "$ref": "#/definitions/freeformObject" }, + "healthCheck": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "path": { "type": "string" }, + "initialDelaySeconds": { "type": "integer" }, + "periodSeconds": { "type": "integer" }, + "timeoutSeconds": { "type": "integer" }, + "failureThreshold": { "type": "integer" } + } + }, + "terminationGracePeriodSeconds": { "$ref": "#/definitions/portLike" }, + "podAnnotations": { "$ref": "#/definitions/freeformObject" }, + "podSecurityContext": { "$ref": "#/definitions/freeformObject" }, + "securityContext": { "$ref": "#/definitions/freeformObject" }, + "nodeSelector": { "$ref": "#/definitions/freeformObject" }, + "tolerations": { "$ref": "#/definitions/objectArray" }, + "affinity": { "$ref": "#/definitions/freeformObject" }, + "topologySpreadConstraints": { "$ref": "#/definitions/objectArray" }, + "extraContainers": { "$ref": "#/definitions/objectArray" }, + "initContainers": { + "type": "object", + "additionalProperties": false, + "properties": { + "copyApp": { + "type": "object", + "additionalProperties": false, + "properties": { + "securityContext": { "$ref": "#/definitions/containerSecurityContext" } + } + }, + "certGenerator": { + "type": "object", + "additionalProperties": false, + "properties": { + "image": { + "type": "object", + "additionalProperties": false, + "properties": { + "repository": { "type": "string" }, + "tag": { "type": "string" }, + "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] } + } + }, + "securityContext": { "$ref": "#/definitions/containerSecurityContext" } + } + } + } + }, + "serviceAccount": { + "type": "object", + "additionalProperties": false, + "properties": { + "create": { "type": "boolean" }, + "name": { "type": "string" }, + "annotations": { "$ref": "#/definitions/freeformObject" } + } + } + } +}