feat(nzbget): route NNTP through gluetun VPN sidecar; drop dead HTTP proxy

nzbget's usenet downloads (NNTP/563) were egressing DIRECT: the HTTP_PROXY env
pointed at a standalone gluetun that isn't even running, and NNTP ignores HTTP
proxies anyway. Adopt the qbittorrent pattern instead: run gluetun as a sidecar
in the nzbget pod so the shared netns + kill-switch force ALL traffic through
the tunnel, regardless of protocol.

- Add gluetun sidecar (own AirVPN device via gluetun-wireguard-nzbget secret,
  FIREWALL_INPUT_PORTS=6789 to keep the WebUI reachable, DOT=off + DNS_ADDRESS
  per the AirVPN-blocks-DoT gotcha).
- Remove the useless HTTP_PROXY/NO_PROXY envs from nzbget.
- Delete the standalone gluetun chart/values/application (was not running; only
  nzbget referenced it).

Trade-off: if the tunnel drops, downloads stop (no leak) rather than falling
back to direct — same behaviour as qbittorrent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gilgamezh
2026-06-06 11:19:25 +02:00
parent 8da60f2ae8
commit 49cfd05bee
14 changed files with 88 additions and 453 deletions
-26
View File
@@ -1,26 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: gluetun
namespace: argocd
annotations:
spec:
project: default
source:
repoURL: http://gitea-http.gitea.svc.cluster.local:3000/admin/turingpi.git
targetRevision: HEAD
path: custom_helm_charts/gluetun
helm:
releaseName: gluetun
valueFiles:
- ../../helm-values/gluetun_values.yaml
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
-21
View File
@@ -1,21 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
-6
View File
@@ -1,6 +0,0 @@
apiVersion: v1
appVersion: "1.0"
description: Gluetun - VPN client with HTTP proxy
name: gluetun
version: 0.1.0
icon: https://raw.githubusercontent.com/qdm12/gluetun/master/.github/logo.png
-22
View File
@@ -1,22 +0,0 @@
# Usenet stack (Gluetun + NZBGet)
Service endpoints:
- NZBGet UI: nzbget.default.svc.cluster.local:6789
- Gluetun HTTP proxy: gluetun.default.svc.cluster.local:8888
AirVPN WireGuard values:
- Update `helm-values/gluetun_values.yaml`:
- `env.WIREGUARD_ADDRESSES` -> WireGuard tunnel address(es) (IPv4 /32 and optional IPv6)
- `env.SERVER_COUNTRIES` -> recommended AirVPN server selection (e.g. Netherlands)
- If your cluster does not support IPv6, remove the IPv6 address from `env.WIREGUARD_ADDRESSES`.
- Create a Secret named `gluetun-wireguard` with key `WIREGUARD_PRIVATE_KEY` from your AirVPN WireGuard config (do not commit the key).
- Add `WIREGUARD_PRESHARED_KEY` from the same AirVPN WireGuard config.
- `helm-values/gluetun_values.yaml` sets `secret.create: false` so the chart does not create a placeholder secret.
Validation:
- ArgoCD health: `argocd app get gluetun` and `argocd app get nzbget`
- WireGuard up: `kubectl -n default logs deploy/gluetun | rg -i "wireguard|tunnel"`
- VPN egress from NZBGet: `kubectl -n default exec deploy/nzbget -- curl -s ifconfig.me`
- NZBGet UI: `kubectl -n default port-forward deploy/nzbget 6789:6789`
Note: NZBGet is configured to use the HTTP proxy via `HTTP_PROXY`/`HTTPS_PROXY`. If your NNTP traffic does not honor proxy settings, consider using a proxy-aware downloader or running the downloader in the same pod as Gluetun.
@@ -1,19 +0,0 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range .Values.ingress.hosts }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "gluetun.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get svc -w {{ template "gluetun.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template "gluetun.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "gluetun.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
echo "Visit http://127.0.0.1:8888 to use your application"
kubectl port-forward $POD_NAME 8888:{{ .Values.service.port }}
{{- end }}
@@ -1,32 +0,0 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "gluetun.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "gluetun.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "gluetun.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
@@ -1,55 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ template "gluetun.fullname" . }}
labels:
app: {{ template "gluetun.name" . }}
chart: {{ template "gluetun.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ template "gluetun.name" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ template "gluetun.name" . }}
release: {{ .Release.Name }}
spec:
volumes:
{{ toYaml .Values.volumes | indent 6 }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{ toYaml .Values.securityContext | indent 12 }}
env:
{{ toYaml .Values.env | indent 12 }}
ports:
- name: http-proxy
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
{{ toYaml .Values.livenessProbe | indent 12 }}
readinessProbe:
{{ toYaml .Values.readinessProbe | indent 12 }}
volumeMounts:
{{ toYaml .Values.volumeMounts | indent 12 }}
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
@@ -1,38 +0,0 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "gluetun.fullname" . -}}
{{- $ingressPath := .Values.ingress.path -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
app: {{ template "gluetun.name" . }}
chart: {{ template "gluetun.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- with .Values.ingress.annotations }}
annotations:
{{ toYaml . | indent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ . }}
http:
paths:
- path: {{ $ingressPath }}
backend:
serviceName: {{ $fullName }}
servicePort: http
{{- end }}
{{- end }}
@@ -1,15 +0,0 @@
{{- if .Values.secret.create -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.secret.name }}
labels:
app: {{ template "gluetun.name" . }}
chart: {{ template "gluetun.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
type: Opaque
stringData:
WIREGUARD_PRIVATE_KEY: {{ .Values.secret.privateKey | quote }}
WIREGUARD_PRESHARED_KEY: {{ .Values.secret.presharedKey | quote }}
{{- end }}
@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ template "gluetun.fullname" . }}
labels:
app: {{ template "gluetun.name" . }}
chart: {{ template "gluetun.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.port }}
protocol: TCP
name: http-proxy
selector:
app: {{ template "gluetun.name" . }}
release: {{ .Release.Name }}
-102
View File
@@ -1,102 +0,0 @@
replicaCount: 1
image:
repository: qmcgaw/gluetun
tag: latest
pullPolicy: IfNotPresent
env:
- name: VPN_SERVICE_PROVIDER
value: "airvpn"
- name: VPN_TYPE
value: "wireguard"
- name: WIREGUARD_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: gluetun-wireguard
key: WIREGUARD_PRIVATE_KEY
- name: WIREGUARD_PRESHARED_KEY
valueFrom:
secretKeyRef:
name: gluetun-wireguard
key: WIREGUARD_PRESHARED_KEY
- name: WIREGUARD_ADDRESSES
value: "REPLACE_ME"
- name: SERVER_COUNTRIES
value: "REPLACE_ME"
- name: HTTPPROXY
value: "on"
- name: HTTPPROXY_LOG
value: "off"
- name: FIREWALL_INPUT_PORTS
value: "8888"
- name: TZ
value: "Europe/Amsterdam"
secret:
create: true
name: gluetun-wireguard
privateKey: "REPLACE_ME"
presharedKey: "REPLACE_ME"
service:
type: ClusterIP
port: 8888
ingress:
enabled: false
annotations:
kubernetes.io/ingress.class: nginx
kubernetes.io/tls-acme: "true"
path: /
hosts:
- gluetun.example.org
tls:
- secretName: gluetun-example-org
hosts:
- gluetun.example.org
volumes:
- name: dev-tun
hostPath:
path: /dev/net/tun
volumeMounts:
- name: dev-tun
mountPath: "/dev/net/tun"
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- NET_ADMIN
livenessProbe:
tcpSocket:
port: 8888
initialDelaySeconds: 10
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 8888
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
nodeSelector: {}
tolerations: []
affinity: {}
@@ -26,6 +26,21 @@ spec:
{{ toYaml . | indent 8 }}
{{- end }}
containers:
- name: gluetun
image: "{{ .Values.gluetun.image.repository }}:{{ .Values.gluetun.image.tag }}"
imagePullPolicy: {{ .Values.gluetun.image.pullPolicy }}
securityContext:
{{ toYaml .Values.gluetun.securityContext | indent 12 }}
env:
{{ toYaml .Values.gluetun.env | indent 12 }}
livenessProbe:
{{ toYaml .Values.gluetun.livenessProbe | indent 12 }}
readinessProbe:
{{ toYaml .Values.gluetun.readinessProbe | indent 12 }}
volumeMounts:
{{ toYaml .Values.gluetun.volumeMounts | indent 12 }}
resources:
{{ toYaml .Values.gluetun.resources | indent 12 }}
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
-88
View File
@@ -1,88 +0,0 @@
replicaCount: 1
image:
repository: qmcgaw/gluetun
tag: "latest"
pullPolicy: Always
env:
- name: VPN_SERVICE_PROVIDER
value: "airvpn"
- name: VPN_TYPE
value: "wireguard"
- name: WIREGUARD_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: gluetun-wireguard
key: WIREGUARD_PRIVATE_KEY
- name: WIREGUARD_PRESHARED_KEY
valueFrom:
secretKeyRef:
name: gluetun-wireguard
key: WIREGUARD_PRESHARED_KEY
- name: WIREGUARD_ADDRESSES
value: "10.160.17.207/32,fd7d:76ee:e68f:a993:61d7:a5fe:f834:90e1/128"
- name: SERVER_COUNTRIES
value: "Netherlands"
- name: HTTPPROXY
value: "on"
- name: HTTPPROXY_LOG
value: "off"
- name: FIREWALL_INPUT_PORTS
value: "8888"
- name: TZ
value: "Europe/Amsterdam"
secret:
create: false
name: gluetun-wireguard
privateKey: "REPLACE_ME"
service:
type: ClusterIP
port: 8888
volumes:
- name: dev-tun
hostPath:
path: /dev/net/tun
volumeMounts:
- name: dev-tun
mountPath: "/dev/net/tun"
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- NET_ADMIN
livenessProbe:
tcpSocket:
port: 8888
initialDelaySeconds: 10
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 8888
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
nodeSelector: {}
tolerations: []
affinity: {}
+73 -10
View File
@@ -12,16 +12,76 @@ env:
value: "1000"
- name: TZ
value: "Europe/Amsterdam"
- name: HTTP_PROXY
value: "http://gluetun.default.svc.cluster.local:8888"
- name: http_proxy
value: "http://gluetun.default.svc.cluster.local:8888"
- name: HTTPS_PROXY
value: "http://gluetun.default.svc.cluster.local:8888"
- name: https_proxy
value: "http://gluetun.default.svc.cluster.local:8888"
- name: NO_PROXY
value: "localhost,127.0.0.1,.svc,.cluster.local"
# gluetun runs as a sidecar in this pod (same pattern as qbittorrent): it shares
# the pod network namespace and installs the WireGuard tunnel + a kill-switch, so
# ALL of nzbget's traffic — including NNTP (port 563) to newshosting — egresses
# through the VPN. (An HTTP proxy can't cover NNTP, which is why the old
# HTTP_PROXY-to-standalone-gluetun approach left usenet downloads going direct.)
# Uses its own AirVPN device/secret (gluetun-wireguard-nzbget) to avoid sharing a
# WireGuard IP with the qbittorrent tunnel. Keep DOT=off + DNS_ADDRESS — see the
# AirVPN-blocks-DoT gotcha in CLAUDE.md.
gluetun:
image:
repository: qmcgaw/gluetun
tag: v3.41.1
pullPolicy: IfNotPresent
env:
- name: VPN_SERVICE_PROVIDER
value: "airvpn"
- name: VPN_TYPE
value: "wireguard"
- name: WIREGUARD_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: gluetun-wireguard-nzbget
key: WIREGUARD_PRIVATE_KEY
- name: WIREGUARD_PRESHARED_KEY
valueFrom:
secretKeyRef:
name: gluetun-wireguard-nzbget
key: WIREGUARD_PRESHARED_KEY
- name: WIREGUARD_ADDRESSES
value: "10.166.207.220/32,fd7d:76ee:e68f:a993:bd5e:ddfc:ad2c:d30c/128"
- name: SERVER_COUNTRIES
value: "Netherlands"
- name: DOT
value: "off"
- name: DNS_ADDRESS
value: "10.128.0.1"
- name: FIREWALL_INPUT_PORTS
value: "6789"
- name: TZ
value: "Europe/Amsterdam"
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- NET_ADMIN
livenessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 10
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
tcpSocket:
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
volumeMounts:
- name: dev-tun
mountPath: "/dev/net/tun"
# nzbget cannot read server credentials from environment variables (its
# ${...} config syntax only references other nzbget options, not env). So an
@@ -76,6 +136,9 @@ volumes:
- name: plex-data
persistentVolumeClaim:
claimName: "plex-data"
- name: dev-tun
hostPath:
path: /dev/net/tun
volumeMounts:
- name: plex-data