Skip to main content

Helm Charts

Gen0Sec publishes three charts. Which one you want depends on whether you are deploying the dataplane, the control loop, or both.

helm repo add gen0sec https://helm.gen0sec.com
helm repo update
helm search repo gen0sec
The repo root is not browsable

https://helm.gen0sec.com returns 404 in a browser. That is normal for a chart repository — Helm fetches /index.yaml, which is what helm repo add reads.

The three charts

ChartDeploysUse it when
synapseOne Synapse workload — proxy or agentYou want a single dataplane workload and manage config yourself
synapse-stackProxy + agent + the operator, from one releaseThe normal choice. One release, one namespace, config rollouts handled
synapse-operatorThe operator aloneThe dataplane is already deployed, or lives elsewhere

synapse-stack is an umbrella: it depends on the synapse chart twice, aliased as proxy and agent, so each gets its own values tree and its own identity. That aliasing is the single most important thing to understand about it — see Values live under the aliases.

You need an image pull secret

The dataplane image is private. A default helm install with no pull secret leaves pods in ImagePullBackOff, which is the most common first-run failure:

export GEN0SEC_KEY='<your API key>'

kubectl create namespace synapse-os --dry-run=client -o yaml | kubectl apply -f -

kubectl create secret docker-registry gen0sec-registry \
--docker-server=registry.gen0sec.com \
--docker-username=x \
--docker-password="$GEN0SEC_KEY" \
-n synapse-os

Then reference it, and point the image at the registry you have access to:

proxy:
image:
repository: registry.gen0sec.com/gen0sec/synapse
tag: "0.8.2"
imagePullSecrets:
- name: gen0sec-registry

The operator image is a different story — ghcr.io/gen0sec/synapse-operator is public and pulls anonymously, so operator.imagePullSecrets is only needed if you repoint it at a private mirror.

operator.imagePullSecrets needs synapse-stack 0.5.1+

Earlier charts have no such field on the operator deployments. The value is accepted and silently discarded — the pods just sit in ImagePullBackOff with no indication why. On an older chart, put the secret on the ServiceAccount instead, which Kubernetes then applies to every pod using it:

kubectl -n synapse-os patch serviceaccount synapse-operator \
-p '{"imagePullSecrets":[{"name":"gen0sec-registry"}]}'

The synapse chart

One workload, from ghcr.io/gen0sec/synapse by default.

ValueDefaultNotes
kindDeploymentSet DaemonSet for the agent role — one pod per node, XDP on the host NIC. replicaCount, autoscaling and strategy are ignored then.
hostNetworkfalseRequired to attach XDP to a host interface. dnsPolicy then defaults to ClusterFirstWithHostNet.
securityContextprivileged: truePlus SYS_ADMIN, NET_ADMIN, BPF, SYS_RESOURCE. eBPF needs them; without them the agent falls back off the eBPF path.
synapse.configa full config.yamlRendered into a ConfigMap and mounted at /etc/synapse/config.yaml.
synapse.upstreamsa file-provider exampleRendered as upstreams.yaml. Leave it empty when the operator owns routing.
synapse.upstreamsConfigMap""Mount upstreams.yaml from another ConfigMap — the one the operator's UpstreamsResolver writes with ClusterIPs already substituted.
envSYNAPSE_LOG_FORMAT: jsonYAML config outranks env vars, so use env only for what the config file does not cover.

The chart's synapse.config is a full agent configuration — capture backend, firewall mode, IDS, platform keys, proxy listeners. Every key is explained in Configuration.

Leave service.exposeHealth off

The health endpoint binds loopback inside the container, so a Service port pointing at it never routes — a request to the Pod IP on that port is refused, not answered. On type: LoadBalancer it is worse than useless: the cloud controller turns every Service port into a listener and opens the load balancer's security group to 0.0.0.0/0 for it. That is an internet-facing listener to nothing, on a port that reads as an administrative endpoint to anyone scanning.

The synapse-stack chart

One release, both dataplane workloads, and the operator.

helm upgrade --install synapse-stack gen0sec/synapse-stack \
-n synapse-os --create-namespace \
-f synapse-values.yaml

kubectl -n synapse-os rollout status deploy/synapse-proxy

By default proxy.enabled is true and agent.enabled is false — turn the agent on when you want per-node XDP observation alongside the proxy.

Both namespaces default to synapse-os:

global:
namespaces:
synapse: synapse-os
operator: synapse-os

Older deployments split these across synapse-system / ingress-synapse / synapse. For anything greenfield, keep them together.

Values live under the aliases

Because the umbrella mounts the same subchart twice, values go under proxy: and agent:never under synapse:. This is the mistake that silently does nothing:

# Correct
proxy:
image:
tag: "0.8.2"

agent:
enabled: true
kind: DaemonSet
hostNetwork: true

The chart also sets nameOverride and fullnameOverride per alias so pods come out as synapse-proxy-* and synapse-agent-* rather than <release>-synapse-*. That naming is load-bearing — the operator's label selector matches on it.

The label selector must match both workloads

operator.labelSelector defaults to the set-based form:

app.kubernetes.io/name in (synapse-proxy,synapse-agent)

The operator filters its List by this selector. Narrow it to a single name and it silently stops config-hash rolling the other workload — no error, just a workload that never picks up config changes.

clamav and valkey are dependencies of the synapse subchart and are gated off under both aliases here, so aliasing does not deploy two copies of each.

Turning on the operator's extras

The stack chart is where the operator's optional reconcilers are exposed:

operator:
enabled: true
netvarsResolver: true # fill IDS HOME_NET/EXTERNAL_NET from the cluster
upstreamsResolver: false # substitute Service FQDNs for ClusterIPs
perWorkloadConfigHash: true # only roll the workload whose own config changed
idsHotReloadHashExclude: false

What each one does is on the Synapse Operator page. The agent's ConfigMap already carries synapse.gen0sec.com/resolve-netvars: "true", so enabling netvarsResolver is all that is needed on the operator side.

Ingress mode deploys a second operator

operator.ingress.enabled stands up a separate Deployment running --ingress-mode, alongside the config-hash one. It has to be separate: ingress mode replaces config-sync within a single process, so one process cannot do both.

operator:
ingress:
enabled: true
ingressClass: synapse
upstreamsOutConfigMap: "synapse-os/synapse-proxy" # required

proxy:
synapse:
upstreams: "" # hand routing to the operator

Setting proxy.synapse.upstreams: "" is not optional. It makes the chart stop templating that key, which is what stops Helm's three-way merge from clobbering the operator's render on the next upgrade.

Routing itself — Ingress objects, path types, TLS — is covered in Kubernetes Ingress & Gateway API.

Needs synapse-stack 0.6.0 or newer

Before 0.6.0 the stack chart's ClusterRole granted no networking.k8s.io permissions, so the ingress-mode Deployment — which shares the same ServiceAccount — had its Ingress list/watch forbidden and rendered nothing. From 0.6.0 the rule is created automatically whenever operator.ingress.enabled is set.

Add operator.ingress.gatewayAPI: true to reconcile Gateway API objects as well. That one value drives both the --gateway-api flag and its RBAC, so the permission cannot go missing while the flag is on. It needs the Gateway API CRDs installed.

The synapse-operator chart

The operator on its own, for when the dataplane is deployed some other way.

helm upgrade --install synapse-operator gen0sec/synapse-operator -n synapse-os

It ships hardened defaults the stack chart does not: non-root 65532, read-only root filesystem, all capabilities dropped, RuntimeDefault seccomp. It also carries the optional extras a long-lived controller wants:

ValueDefault
operator.service.enabledtrueClusterIP in front of the metrics port
operator.serviceMonitor.enabledfalseprometheus-operator ServiceMonitor; only rendered when the CRDs exist
operator.pdb.createfalseAuto-enabled above one replica
operator.networkPolicy.enabledfalseOpens metrics + healthz ingress, unrestricted egress
operator.watchNamespace""Restrict to one namespace; needed when running operators side by side
operator.image.tag""Empty falls back to the chart's appVersion, so a chart bump moves the image
The two charts pin different operator versions

synapse-operator leaves image.tag empty and follows its own appVersion. synapse-stack pins an explicit, older operator tag. If you want a specific operator build under the stack chart, set operator.image.tag yourself rather than assuming the chart tracks the latest.

Verifying a deployment

kubectl -n synapse-os get pods
kubectl -n synapse-os rollout status deploy/synapse-proxy
kubectl -n synapse-os logs deploy/synapse-operator

A config change should roll the dataplane on its own:

kubectl -n synapse-os edit configmap synapse-proxy # change any key
kubectl -n synapse-os get pod -l app.kubernetes.io/name=synapse-proxy \
-o jsonpath='{.items[0].metadata.annotations.synapse\.gen0sec\.com/config-hash}'

If that annotation never changes, the operator is not matching the workload — check operator.labelSelector first.

See also