Skip to content
Merged
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
227 changes: 168 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,83 +1,192 @@
![Gen0Sec logo](./images/logo.svg)
<p align="center">
<img src="./images/logo.svg" alt="Gen0Sec" width="280">
</p>

<p align="center">
<a href="https://github.com/gen0sec/synapse-operator/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache 2-green" alt="License - Apache 2"></a> &nbsp;
<a href="https://github.com/gen0sec/synapse-operator/actions?query=branch%3Amain"><img src="https://github.com/gen0sec/synapse-operator/actions/workflows/release.yaml/badge.svg" alt="CI Build"></a> &nbsp;
<a href="https://github.com/gen0sec/synapse-operator/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-green" alt="License - Apache 2.0"></a> &nbsp;
<a href="https://github.com/gen0sec/synapse-operator/releases"><img src="https://img.shields.io/github/release/gen0sec/synapse-operator.svg?label=Release" alt="Release"></a> &nbsp;
<img alt="GitHub Downloads (all assets, all releases)" src="https://img.shields.io/github/downloads/gen0sec/synapse-operator/total"> &nbsp;
<a href="https://docs.gen0sec.com/"><img alt="Static Badge" src="https://img.shields.io/badge/gen0sec-documentation-page?style=flat&link=https%3A%2F%2Fdocs.gen0sec.com%2F"></a> &nbsp;
<a href="https://docs.gen0sec.com/"><img alt="Documentation" src="https://img.shields.io/badge/gen0sec-documentation-page?style=flat&link=https%3A%2F%2Fdocs.gen0sec.com%2F"></a> &nbsp;
<a href="https://discord.gg/jzsW5Q6s9q"><img src="https://img.shields.io/discord/1377189913849757726?label=Discord" alt="Discord"></a> &nbsp;
<a href="https://x.com/gen0sec"><img src="https://img.shields.io/twitter/follow/gen0sec?style=flat" alt="X (formerly Twitter) Follow" /> </a>
<a href="https://x.com/gen0sec"><img src="https://img.shields.io/twitter/follow/gen0sec?style=flat" alt="X (formerly Twitter) Follow" /></a>
</p>

<p align="center">
<a href="https://discord.gg/jzsW5Q6s9q"><img src="https://img.shields.io/badge/Join%20Us%20on-Discord-5865F2?logo=discord&logoColor=white" alt="Join us on Discord"></a>
<a href="https://gen0sec.substack.com/"><img src="https://img.shields.io/badge/Substack-FF6719?logo=substack&logoColor=fff" alt="Substack"></a>
</p>

# Community
[![Join us on Discord](https://img.shields.io/badge/Join%20Us%20on-Discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/jzsW5Q6s9q)
[![Substack](https://img.shields.io/badge/Substack-FF6719?logo=substack&logoColor=fff)](https://gen0sec.substack.com/)
---

## Keep Synapse in Sync on Kubernetes

A Go [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) operator for running [Synapse](https://github.com/gen0sec/synapse) on Kubernetes. It runs in **two modes** from the same binary: a **config-sync controller** that rolls Synapse pods when their config changes, and an **Ingress + Gateway API controller** that renders native Kubernetes routing into Synapse's upstreams.

## Synapse Operator (Go)
**What it does:**
- **Config-sync (default)** — hashes every ConfigMap/Secret matching a label selector and stamps the hash onto the Synapse workload, so Kubernetes rolls the pods whenever config content changes (no manual restarts)
- **Ingress / Gateway API mode** (`--ingress-mode`) — reconciles class-matched `Ingress` and Gateway API `HTTPRoute` objects into Synapse's `upstreams.yaml` on a shared volume, hot-reloaded via inotify + `SIGHUP`
- **TLS projection** — projects referenced Ingress/Gateway TLS Secrets into Synapse's certificates directory, operator-owned and hot-reloaded
- **Status & HA** — optionally publishes load-balancer addresses on matched Ingresses and gates shared status writes behind a Lease when running more than one proxy replica
- **Helm-native** — keys off `app.kubernetes.io/name=synapse`, so it plugs straight into Synapse Helm releases

This Go operator watches Synapse configuration ConfigMaps and Secrets and keeps the running pods in sync by forcing a rollout any time config content changes. It relies on matching labels (default `app.kubernetes.io/name=synapse`) so it naturally plugs into Helm releases of Synapse.
> **Go 1.24+** · any conformant **Kubernetes** cluster · Gateway API CRDs required only for `--gateway-api`

### How It Works
- Reconciles ConfigMaps and Secrets that match the configured label selector.
- Hashes the combined data across all matching config sources in the namespace, with optional per-key ignores (for example, hot-reloadable `upstreams.yaml`).
- Patches Synapse workloads (Deployments, DaemonSets, StatefulSets) with the hash stored under `synapse.gen0sec.com/config-hash` by default.
- Updating the annotation bumps the workload template hash, causing Kubernetes to roll the pods and pick up the new configuration.
---

### Project Layout
- `main.go` bootstraps a controller-runtime manager with health probes and optional namespace scoping.
- `controllers/configmap_controller.go` contains the reconciliation logic and hashing helper.
- `config/` holds a kustomize deployment (service account, RBAC, manager deployment). It references the published image `ghcr.io/gen0sec/synapse-operator:latest`.
## Quick start

### Build

### Building
```bash
GOOS=linux GOARCH=amd64 go build -o bin/synapse-operator
```
Adjust the target architecture if you are building for another platform.

To containerize:
### Container

```bash
docker build -t ghcr.io/gen0sec/synapse-operator:latest .
docker push ghcr.io/gen0sec/synapse-operator:latest
docker build -t ghcr.io/<org>/synapse-operator:latest .
docker push ghcr.io/<org>/synapse-operator:latest
```

Update `config/manager.yaml` with the pushed image reference.

### Deploying with Kustomize
### Deploy with Kustomize

```bash
kubectl apply -k config
```
This creates the `synapse-system` namespace, service account, RBAC, and a single replica of the operator.

### Testing From WSL
1. **Prepare tools** - ensure WSL has `docker`, `kubectl`, `kind`, and `helm` installed and on `$PATH`.
2. **Build & load the image** - inside WSL build the Linux image and use `kind load docker-image ghcr.io/gen0sec/synapse-operator:latest` (or push to a registry reachable by your cluster).
3. **Create a test cluster** - `kind create cluster --name synapse`.
4. **Deploy Synapse via Helm** - use the public chart repo:
```bash
helm repo add gen0sec https://helm.gen0sec.com
helm repo update
helm upgrade --install synapse-stack gen0sec/synapse-stack \
-n synapse --create-namespace \
--set global.namespaces.synapse="synapse" \
--set global.namespaces.operator="synapse-system" \
--set synapse.image.repository="ghcr.io/gen0sec/synapse" \
--set synapse.image.tag="latest" \
--set synapse.synapse.server.upstream="http://example.com" \
--set synapse.synapse.network.disableXdp=true \
--set operator.enabled=true \
--set operator.image.repository="ghcr.io/gen0sec/synapse-operator" \
--set operator.image.tag="latest"
```
5. **Apply/verify operator** - if the chart already deployed the operator, check it with `kubectl -n synapse-system rollout status deployment/synapse-operator`.
6. **Trigger a config change** - edit the Synapse ConfigMap (`kubectl edit configmap synapse-stack -n synapse`) or use `kubectl patch`.
7. **Verify restart** - watch the rollout: `kubectl rollout status deployment/synapse-stack -n synapse` and ensure pod annotation `synapse.gen0sec.com/config-hash` updates.

### Helm Integration Notes
The Helm chart already labels both the ConfigMap and workloads with `app.kubernetes.io/name=synapse`. The operator leans on that selector to discover which objects belong together. When Helm updates config sources (e.g., via `helm upgrade`), the operator sees the new data, recalculates the hash, and patches the workloads so the change propagates without any manual restarts.

### Configuration Flags
- `--label-selector` - Label selector for config sources and workloads (default `app.kubernetes.io/name=synapse`).
- `--config-hash-annotation` - Annotation key used for the hash (default `synapse.gen0sec.com/config-hash`).
- `--ignore-configmap-keys` - Comma-separated ConfigMap keys to ignore when hashing (default `upstreams.yaml`).
- `--ignore-secret-keys` - Comma-separated Secret keys to ignore when hashing (default empty).

Creates the `synapse-system` namespace, ServiceAccount, RBAC, and a single operator replica.

### Deploy with Helm (alongside Synapse)

```bash
helm repo add gen0sec https://helm.gen0sec.com
helm repo update

export GEN0SEC_API_KEY="REPLACE_ME"
helm upgrade --install synapse-stack gen0sec/synapse-stack \
-n synapse --create-namespace \
--set global.namespaces.synapse="synapse" \
--set global.namespaces.operator="synapse-system" \
--set synapse.image.repository="ghcr.io/gen0sec/synapse" \
--set synapse.image.tag="latest" \
--set synapse.synapse.gen0sec.apiKey="$GEN0SEC_API_KEY" \
--set operator.enabled=true \
--set operator.image.repository="ghcr.io/<org>/synapse-operator" \
--set operator.image.tag="latest"
```

Verify, then trigger a config change and watch the rollout:

```bash
kubectl -n synapse-system rollout status deployment/synapse-operator
kubectl -n synapse edit configmap synapse-stack # change any key
kubectl -n synapse rollout status deployment/synapse-stack
# the pod annotation synapse.gen0sec.com/config-hash updates
```

---

## Modes

The operator runs as **one** of two controllers per process, selected by `--ingress-mode`. Both share the same manager, health probes, and optional namespace scoping.

> **Config-sync** is the default — it never touches routing, it only forces rollouts when watched config changes.
>
> **Ingress / Gateway API** turns native Kubernetes routing objects into Synapse's `upstreams.yaml` and keeps Synapse hot-reloaded in place.

| | Config-sync | Ingress / Gateway API |
|---|:---:|:---:|
| Flag | _(default)_ | `--ingress-mode` |
| Watches | ConfigMaps + Secrets (by label) | `Ingress` (+ `HTTPRoute` with `--gateway-api`) |
| Action | stamp config hash → roll workload | render `upstreams.yaml` → inotify + `SIGHUP` |
| TLS Secret projection | — | ✅ via `--certs-out` |
| Publishes LB status on Ingress | — | ✅ via `--publish-status-address` |
| One-shot initContainer prime | — | ✅ `--render-once` |
| Multi-replica shared-status HA | — | ✅ `--status-leader-election` |

---

## Architecture

```mermaid
flowchart TD
subgraph K8s[Kubernetes API]
CM[ConfigMaps / Secrets]
ING["Ingress / HTTPRoute<br/>(+ Gateway API)"]
TLS[TLS Secrets]
end

subgraph OP[synapse-operator · controller-runtime]
direction TB
C1[Config-sync controller]
C2["Ingress / Gateway controller<br/>(--ingress-mode)"]
end

CM --> C1
ING --> C2
TLS --> C2

C1 -->|patch synapse.gen0sec.com/config-hash| WL[Synapse workload<br/>Deployment / DaemonSet / StatefulSet]
WL -->|rolls pods| SYN[Synapse pods]
C2 -->|render upstreams.yaml + project certs| SHV[(Shared volume)]
SHV -->|inotify| SYN
C2 -.SIGHUP.-> SYN
```

---

## Configuration flags

**Common**

| Flag | Default | Purpose |
|---|---|---|
| `--metrics-bind-address` | `:8080` | Metrics endpoint address |
| `--health-probe-bind-address` | `:8081` | Health probe address |
| `--leader-elect` | `false` | Leader election for the controller manager |
| `--namespace` | _(all)_ | Restrict the watch to one namespace |

**Config-sync mode**

| Flag | Default | Purpose |
|---|---|---|
| `--label-selector` | `app.kubernetes.io/name=synapse` | Selects config sources and workloads |
| `--config-hash-annotation` | `synapse.gen0sec.com/config-hash` | Annotation key for the hash |
| `--ignore-configmap-keys` | `upstreams.yaml` | Comma-separated ConfigMap keys excluded from the hash |
| `--ignore-secret-keys` | _(none)_ | Comma-separated Secret keys excluded from the hash |

**Ingress / Gateway API mode** (`--ingress-mode`)

| Flag | Default | Purpose |
|---|---|---|
| `--render-once` | `false` | One-shot: render `upstreams.yaml` and exit (initContainer prime) |
| `--ingress-class` | `synapse` | `spec.ingressClassName` this controller serves |
| `--upstreams-out` | `/shared/upstreams.yaml` | Path of the rendered upstreams file |
| `--cluster-domain` | `cluster.local` | Cluster DNS domain for backend FQDNs |
| `--certs-out` | _(disabled)_ | Directory to project referenced TLS Secrets into |
| `--gateway-api` | `false` | Also reconcile Gateway API (requires the CRDs) |
| `--publish-status-address` | _(none)_ | IPs/hostnames to publish on matched Ingresses' status |
| `--reload-process-name` | `synapse` | argv0 of the co-located proxy to `SIGHUP` |
| `--status-leader-election` | `false` | Only the Lease holder writes shared status (>1 replica) |
| `--status-leader-election-id` | `synapse-ingress-status` | Lease name for the shared-status election |
| `--leader-election-namespace` | `$POD_NAMESPACE` | Namespace for the shared-status Lease |

---

## Documentation

| | |
|---|---|
| [Gen0Sec Docs](https://docs.gen0sec.com/) | Product documentation and guides |
| [Synapse](https://github.com/gen0sec/synapse) | The NDR/proxy this operator manages |
| [`config/`](config/) | Kustomize deployment: namespace, ServiceAccount, RBAC, manager |
| [`SECURITY.md`](SECURITY.md) | Security policy and disclosure |

---

## Thank you!

- [Kubernetes SIGs](https://github.com/kubernetes-sigs/controller-runtime) for controller-runtime
- [Kubernetes Gateway API](https://github.com/kubernetes-sigs/gateway-api) for the Gateway API
Binary file modified images/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 1 addition & 33 deletions images/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.