Kubernetes Operator for Synology Reverse Proxy Rules
Stop Manually Configuring Synology Reverse Proxy Rules — Let Kubernetes Do It
In my home lab, Synology is managing all certificates for domains to work with the DSM apps and other services like the on-board directory server or RADIUS server. So why manage domains or certificates in multiple places? Let’s just use the resources that are already configured and managed for external services running in a Kubernetes cluster too.
Every time I deployed a new service to my home Kubernetes cluster, I had the same ritual.
Open DSM. Navigate to Application Portal → Reverse Proxy. Fill in the hostname. Copy the LoadBalancer IP from kubectl. Set the port. Assign the certificate. Save.
Three minutes of clicking for every single deployment. And when I removed an app, I had to remember to clean up the rule manually — which I didn’t always do.
I decided to automate it away.
The Problem
A Synology NAS makes a great home lab gateway. It has a real HTTPS reverse proxy built in, handles wildcard certificates from Let’s Encrypt, and DSM’s Application Portal gives you a clean UI for managing rules. It’s polished and it works.
The friction is the manual part. Every new service you expose means:
- Finding the backend IP (which changes when pods restart)
- Creating the DSM rule by hand
- Assigning a certificate
- Remembering to clean up when the service is gone
If you run ArgoCD for GitOps, this gets worse — you’re deploying frequently and the gap between “app deployed” and “accessible via HTTPS” is always manual DSM config.
The Solution: Synology Proxy Operator
The Synology Proxy Operator is a Kubernetes operator that watches your cluster and keeps your Synology DSM reverse proxy configuration in sync — automatically.
Deploy an app, get a reverse proxy rule. Delete it, the rule is gone. Change the backend, the rule is updated. No touching DSM.
It also:
- Assigns TLS certificates automatically — matches against the certificates in DSM (wildcard and SAN), falls back to your default cert
- Enforces access control — apply a Synology ACL profile (e.g. “LAN Only”) globally or per-service
- Handles multiple hostnames — one service, multiple public FQDNs, each with its own DSM record
- Cleans up reliably — a Kubernetes finalizer ensures the DSM record is removed before the rule object disappears, even if the operator restarts mid-deletion
How It Works
The operator runs three controllers:
ServiceIngressReconciler watches Services and Ingresses annotated with synology.proxy/enabled: "true" and creates or deletes SynologyProxyRule objects in response.
ArgoApplicationReconciler watches ArgoCD Application objects with the same annotation and does the same — translating GitOps deployments into proxy rules.
SynologyProxyRuleReconciler watches all SynologyProxyRule objects and is the only controller that ever calls the DSM API. It handles upsert, certificate assignment, and deletion via finalizer.
Get Julian Hassler’s stories in your inbox
Join Medium for free to get updates from this writer.
The first two controllers are purely Kubernetes-side — they translate annotations into SynologyProxyRule custom resources. All DSM API calls flow through the third controller only. This separation keeps the DSM sync logic in one place and makes the system easy to reason about.
Three Ways to Use It
1. Annotate a Service
The simplest approach. Add one annotation to any existing Service:
apiVersion: v1
kind: Service
metadata:
name: myapp
namespace: myapp
annotations:
synology.proxy/enabled: "true"
# Optional if DEFAULT_DOMAIN is configured — becomes "myapp.home.example.com"
synology.proxy/source-host: "myapp.home.example.com"
spec:
type: LoadBalancer
ports:
- port: 80The operator picks up the annotation, discovers the LoadBalancer IP, creates a DSM reverse proxy rule, and assigns a certificate. Remove the annotation or delete the Service and the DSM rule is removed automatically.
2. Annotate an ArgoCD Application
If you use ArgoCD, annotate the Application object instead:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
annotations:
synology.proxy/enabled: "true"
synology.proxy/acl-profile: "LAN Only"
spec:
destination:
namespace: myapp
server: https://kubernetes.default.svcThe operator auto-scans the destination namespace for a LoadBalancer service to use as the backend. No need to know the IP address at deploy time.
3. Create a SynologyProxyRule directly
For full control — or for services outside Kubernetes entirely:
apiVersion: proxy.synology.io/v1alpha1
kind: SynologyProxyRule
metadata:
name: nas-photos
namespace: synology-proxy-operator
spec:
sourceHost: photos.home.example.com
destinationHost: 192.168.1.100
destinationPort: 8080
assignCertificate: true
aclProfile: "LAN Only"This is useful for pointing DSM rules at VMs, NAS packages, or IoT devices — anything that isn’t in Kubernetes.
Zero-Touch Mode
If you set WATCH_NAMESPACE to a glob pattern like app-*, every Service, Ingress, and ArgoCD Application in matching namespaces is managed automatically — no annotation required at all.
Combined with DEFAULT_DOMAIN, the workflow becomes:
- Deploy an app to a namespace matching the glob
- The operator detects it, creates a DSM rule for
appname.home.example.com - The best matching certificate is assigned
- If a default ACL profile is set, access restrictions are applied
- Delete the app, the DSM rule is gone
No manual steps anywhere in that flow.
Optional — automatic DNS:
If you run the Synology DNS Server package, it can automatically create A records for hostnames the reverse proxy manages. Combined with forwarding your internal DNS queries to the NAS, even DNS is zero-touch.
If you already use some DNS server like e.g. Pi-Hole you can set the upstream DNS to your NAS IP. This will provide an end-to-end workflow of provisioning new services to become available.
Observability
kubectl get spr -ANAMESPACE NAME SOURCE HOST SYNCED RECORDS AGE
myapp myapp--myapp myapp.home.example.com true 1 12m
nas nas-photos photos.home.example.com true 1 3dEach SynologyProxyRule tracks its DSM records, resolved backend, last sync time, and Kubernetes-standard conditions (Synced, Ready). If a DSM call fails, the condition shows the error and the operator retries automatically.
Installation
Option A — Helm from GHCR (stable releases):
helm upgrade --install synology-proxy-operator \
oci://ghcr.io/phoeluga/charts/synology-proxy-operator \
--namespace synology-proxy-operator \
--create-namespace \
--set synology.url="https://192.168.1.x:5001" \
--set synology.username="admin" \
--set synology.password="secret" \
--set synology.skipTLSVerify=true \
--set operator.defaultDomain="home.example.com"Option B — GitOps with ArgoCD:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: synology-proxy-operator
namespace: argocd
spec:
source:
repoURL: ghcr.io/phoeluga/charts
chart: synology-proxy-operator
targetRevision: ">=0.0.0"
helm:
valuesObject:
operator:
defaultDomain: "home.example.com"
synology:
existingSecret: synology-credentials
destination:
server: https://kubernetes.default.svc
namespace: synology-proxy-operator
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=trueNote:
ServerSideApply=trueis required for ArgoCD to upgrade CRDs on sync.
Requirements
- Synology DSM ≥ 7.0
- Kubernetes ≥ 1.28
- ArgoCD ≥ 2.8 — optional, only for the ArgoCD watcher
Where to Find It
The project is open source and available on GitHub: github.com/phoeluga/synology-proxy-operator
The Helm chart is published to GHCR and listed on ArtifactHub. Issues, PRs, and feedback are welcome.
If you run Synology + Kubernetes and this solves a problem you have, give it a try and let me know how it goes.





