Overview
Created a Syncthing pod in Kubernetes cluster managed by FluxCD with dual NFS mounts, SSL certificate via cert-manager, and consolidated LoadBalancer services.
Architecture
- Namespace:
syncthing
- Deployment: Single replica with
Recreate
strategy - Storage: Two NFS persistent volumes
- SSL: Automatic Let’s Encrypt certificate via cert-manager
- Load Balancing: Combined TCP/UDP service on single external IP
Storage Configuration
NFS Mounts
# Data mount (Dropbox sync)
xxx.xxx.xxx.xxx:/mnt/media/dropbox → /var/syncthing/dropbox
# Config mount (Syncthing configuration)
xxx.xxx.xxx.xxx:/mnt/media/home/nfs/syncthing → /var/syncthing/config
Persistent Volumes
- syncthing-dropbox-pv: 1Ti capacity for sync data
- syncthing-config-pv: 1Gi capacity for configuration
Both use NFS storage class with ReadWriteMany
access mode.
Security & Permissions
Initial Permission Issues
- Container initially ran as UID 1000, but NFS shares had restrictive permissions
- Dropbox share owned by
nobody:1001
with 770 permissions - Config files had strict NFS permissions preventing access
Solution: Root Access
- Changed container to run as
root
(UID 0) - Root can access both NFS shares regardless of ownership
- Fixed config directory permissions on NFS server:
sudo chown -R nobody:nobody /mnt/media/home/nfs/syncthing sudo chmod -R 755 /mnt/media/home/nfs/syncthing
Final Security Context (Non-Privileged)
# Pod-level security context
securityContext:
runAsNonRoot: true
runAsUser: 65534 # nobody user
runAsGroup: 1001 # read-write group
fsGroup: 1001
supplementalGroups: [1001]
seccompProfile:
type: RuntimeDefault
# Container-level security context
container.securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 1001
capabilities:
drop: [ALL]
seccompProfile:
type: RuntimeDefault
# Environment variables
env:
- name: PUID
value: "65534"
- name: PGID
value: "1001"
Network Services
Original Setup (2 External IPs)
syncthing-tcp
: TCP transfers on port 22000syncthing-udp
: UDP discovery on port 21027syncthing-web
: Web UI on port 8384 (ClusterIP)
Consolidated Setup with Static IP
# Combined LoadBalancer service with static IP annotation
apiVersion: v1
kind: Service
metadata:
name: syncthing-sync
namespace: syncthing
annotations:
metallb.universe.tf/loadBalancer-ips: "xxx.xxx.xxx.yyy"
spec:
type: LoadBalancer
ports:
- name: tcp-transfers
port: 22000
targetPort: 22000
protocol: TCP
- name: udp-discovery
port: 21027
targetPort: 21027
protocol: UDP
Benefits:
- ✅ Static IP:
xxx.xxx.xxx.yyy
remains constant across pod/node failures - ✅ Gateway Routing: IP-based routing instead of unreliable MAC-based routing
- ✅ High Availability: IP persists even when MetalLB fails over to different nodes
- ✅ Single External IP: Reduced from 2 IPs to 1 consolidated IP
SSL Certificate
Cert-Manager Configuration
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: syncthing-tls-certificate
namespace: syncthing
spec:
secretName: syncthing-tls
duration: 2160h # 90 days
renewBefore: 720h # 30 days before expiration
commonName: "syncthing.my.domain.com"
dnsNames:
- "syncthing.my.domain.com"
issuerRef:
kind: ClusterIssuer
name: letsencrypt-production
Ingress Configuration
- Host:
syncthing.my.domain.com
- SSL: Automatic redirect and forced SSL
- Backend:
syncthing-web:8384
FluxCD Integration
Directory Structure
apps/syncthing/base/
├── kustomization.yaml
├── syncthing-namespace.yaml
├── syncthing-pv-config.yaml
├── syncthing-pvc-config.yaml
├── syncthing-pv-dropbox.yaml
├── syncthing-pvc-dropbox.yaml
├── syncthing-deployment.yaml
├── syncthing-service.yaml
├── syncthing-certificate.yaml
├── syncthing-ingress.yaml
└── README.md
Deployment Integration
Added to clusters/production/apps/kustomization.yaml
:
resources:
- ../../../apps/syncthing/base
Troubleshooting Journey
Issue 1: NFS Mount Protocol Error
Problem: mount.nfs: Protocol not supported
with NFSv4.1 mount options
Solution: Removed specific NFS version and mount options to match working transmission volumes:
# Before (failed)
nfs:
server: xxx.xxx.xxx.xxx
path: /mnt/media/dropbox
mountOptions:
- nfsvers=4.1
- rsize=1048576
- wsize=1048576
# After (working)
nfs:
server: xxx.xxx.xxx.xxx
path: /mnt/media/dropbox
Issue 2: Permission Denied on Config Files
Problem: Even as root, couldn’t read /var/syncthing/config/config.xml
Root Cause: NFS config directory had overly restrictive permissions from previous UID 1000 setup
Solution: SSH to NFS server and fix permissions:
cd /mnt/media/home/nfs/syncthing
sudo chown -R nobody:nobody .
sudo find . -type d -exec chmod 755 {} \;
sudo find . -type f -exec chmod 644 {} \;
Issue 3: FluxCD Reconciliation
Problem: Changes not applying after git push
Solution: Force FluxCD reconciliation:
flux reconcile source git flux-system
flux reconcile kustomization flux-system
Issue 4: Syncthing Privilege Warning
Problem: Syncthing warned about running as privileged/system user when running as root
Root Cause: Security best practice violation and Syncthing recommendation
Solution: Changed to non-privileged user matching NFS permissions:
# Updated NFS permissions on server:
chmod 660 /mnt/media/home/nfs/syncthing # drw-rw----+
chmod 660 /mnt/media/home/nfs/syncthing/* # -rw-rw----
# Pod runs as:
# - UID 65534 (nobody) - matches NFS file owner
# - GID 1001 (read-write) - matches NFS group and dropbox share
Issue 5: Database Permission Conflicts
Problem: Existing Syncthing database created as root caused permission conflicts
Solution: Deleted index-v0.14.0.db
directory to allow fresh creation with correct ownership
Final Configuration
Access Points
- Web UI: https://syncthing.my.domain.com
- Sync Traffic:
xxx.xxx.xxx.yyy:22000
(TCP) +xxx.xxx.xxx.yyy:21027
(UDP)
File Paths in Syncthing UI
- Dropbox Share:
/var/syncthing/dropbox
- Subfolders:
/var/syncthing/dropbox/Documents
,/var/syncthing/dropbox/Photos
, etc.
Resource Usage
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "1000m"
Key Learnings
- Security First: Non-privileged containers require careful UID/GID mapping to NFS permissions
- NFS Permission Strategy: Match container user to NFS file ownership for seamless access
- FluxCD Debugging: Use
flux reconcile
commands to force updates when changes don’t apply - Service Consolidation: Multiple LoadBalancer services can share a single external IP
- Static IP Benefits: MetalLB annotations provide IP persistence across node failures
- MetalLB Behavior: LoadBalancer IPs announced via Layer 2 can change MAC addresses during failover
- Certificate Integration: cert-manager with Cloudflare DNS01 challenge works seamlessly
- Database Cleanup: Sometimes fresh start is better than fixing complex permission issues
Security Improvements Achieved
✅ Non-Root Execution: Changed from UID 0 to UID 65534 (nobody)
✅ Pod Security Standards: Meets Kubernetes restricted security requirements
✅ Capability Dropping: All capabilities dropped with drop: [ALL]
✅ Privilege Escalation: Prevented with allowPrivilegeEscalation: false
✅ Seccomp Profile: Applied RuntimeDefault
security profile
✅ No Privilege Warnings: Syncthing no longer warns about running as privileged user