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
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
| Chart | Deploys | Use it when |
|---|---|---|
synapse | One Synapse workload — proxy or agent | You want a single dataplane workload and manage config yourself |
synapse-stack | Proxy + agent + the operator, from one release | The normal choice. One release, one namespace, config rollouts handled |
synapse-operator | The operator alone | The 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.
| Value | Default | Notes |
|---|---|---|
kind | Deployment | Set DaemonSet for the agent role — one pod per node, XDP on the host NIC. replicaCount, autoscaling and strategy are ignored then. |
hostNetwork | false | Required to attach XDP to a host interface. dnsPolicy then defaults to ClusterFirstWithHostNet. |
securityContext | privileged: true | Plus SYS_ADMIN, NET_ADMIN, BPF, SYS_RESOURCE. eBPF needs them; without them the agent falls back off the eBPF path. |
synapse.config | a full config.yaml | Rendered into a ConfigMap and mounted at /etc/synapse/config.yaml. |
synapse.upstreams | a file-provider example | Rendered 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. |
env | SYNAPSE_LOG_FORMAT: json | YAML 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.
service.exposeHealth offThe 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.
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.
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:
| Value | Default | |
|---|---|---|
operator.service.enabled | true | ClusterIP in front of the metrics port |
operator.serviceMonitor.enabled | false | prometheus-operator ServiceMonitor; only rendered when the CRDs exist |
operator.pdb.create | false | Auto-enabled above one replica |
operator.networkPolicy.enabled | false | Opens 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 |
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
- Synapse Operator — modes, reconcilers and the full flag reference
- Kubernetes Ingress & Gateway API — routing with native Kubernetes objects
- Configuration — every key inside
synapse.config - Installation Guide — the pull secret, and every other channel