diff --git a/apps/qbittorrent.yaml b/apps/qbittorrent.yaml new file mode 100644 index 0000000..af54622 --- /dev/null +++ b/apps/qbittorrent.yaml @@ -0,0 +1,33 @@ +# This Argo CD Application deploys qBittorrent with Hotio's integrated Proton +# VPN support. Its dedicated namespace is privileged only because creating the +# WireGuard interface requires the NET_ADMIN Linux capability. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: qbittorrent + namespace: argocd + # Deliberately no resources finalizer: deleting this Git entry must not + # cascade into deleting qBittorrent's persistent configuration volume. +spec: + project: default + + source: + repoURL: https://tea.ahmza.com/ahmza.com/k8s-starter-pack.git + targetRevision: main + path: apps/qbittorrent + + destination: + server: https://kubernetes.default.svc + namespace: qbittorrent + + syncPolicy: + automated: + prune: true + selfHeal: true + managedNamespaceMetadata: + labels: + pod-security.kubernetes.io/enforce: privileged + pod-security.kubernetes.io/audit: privileged + pod-security.kubernetes.io/warn: privileged + syncOptions: + - CreateNamespace=true diff --git a/apps/qbittorrent/deployment.yaml b/apps/qbittorrent/deployment.yaml new file mode 100644 index 0000000..fee12ad --- /dev/null +++ b/apps/qbittorrent/deployment.yaml @@ -0,0 +1,124 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: qbittorrent + namespace: qbittorrent +spec: + # qBittorrent has one writable configuration volume and one Proton port + # forwarding lease, so upgrades replace the Pod rather than overlap two. + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: qbittorrent + template: + metadata: + labels: + app: qbittorrent + spec: + # The SOPS-managed Secret is read-only, but Hotio writes Proton's current + # forwarded port beside wg0.conf. Copy the secret into a writable emptyDir + # without ever storing the plaintext WireGuard key in Git. + initContainers: + - name: prepare-wireguard-config + image: busybox:1.37.0 + command: + - sh + - -ec + - >- + cp /vpn-secret/wg0.conf /wireguard/wg0.conf && + chmod 0600 /wireguard/wg0.conf + volumeMounts: + - name: wireguard-secret + mountPath: /vpn-secret + readOnly: true + - name: wireguard-runtime + mountPath: /wireguard + containers: + - name: qbittorrent + image: ghcr.io/hotio/qbittorrent:release + imagePullPolicy: Always + securityContext: + # Hotio needs this capability to create the WireGuard interface, + # routes, firewall rules, and VPN kill switch inside the Pod. + capabilities: + add: + - NET_ADMIN + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: UMASK + value: "002" + - name: TZ + value: Europe/London + - name: WEBUI_PORTS + value: 8080/tcp + - name: VPN_ENABLED + value: "true" + - name: VPN_CONF + value: wg0 + - name: VPN_PROVIDER + value: proton + # Permit ingress-nginx and the media application Pods to reach the + # Web UI/API without allowing ordinary internet traffic to bypass + # the VPN. The LAN range is also retained for cluster-local access. + - name: VPN_LAN_NETWORK + value: 10.10.0.0/16,10.244.0.0/21 + - name: VPN_LAN_LEAK_ENABLED + value: "false" + # Proton assigns a dynamic inbound port through NAT-PMP. Hotio + # renews it and updates qBittorrent's listening port automatically. + - name: VPN_AUTO_PORT_FORWARD + value: "true" + - name: VPN_HEALTHCHECK_ENABLED + value: "true" + - name: VPN_NAMESERVERS + value: wg + ports: + - name: http + containerPort: 8080 + protocol: TCP + startupProbe: + httpGet: + path: / + port: http + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 36 + readinessProbe: + httpGet: + path: / + port: http + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: http + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + volumeMounts: + - name: qbittorrent-config + mountPath: /config + - name: wireguard-runtime + mountPath: /config/wireguard + - name: torrents + mountPath: /data/torrents + volumes: + - name: qbittorrent-config + persistentVolumeClaim: + claimName: qbittorrent-config + - name: wireguard-secret + secret: + secretName: qbittorrent-wireguard + - name: wireguard-runtime + emptyDir: {} + - name: torrents + nfs: + server: nas.ahmza.com + path: /srv/media/torrents diff --git a/apps/qbittorrent/ingress.yaml b/apps/qbittorrent/ingress.yaml new file mode 100644 index 0000000..538ec64 --- /dev/null +++ b/apps/qbittorrent/ingress.yaml @@ -0,0 +1,24 @@ +# Routes qbittorrent.ahmza.com through the cluster's shared ingress-nginx IP. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: qbittorrent + namespace: qbittorrent + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + tls: + - hosts: + - qbittorrent.ahmza.com + secretName: qbittorrent-tls + rules: + - host: qbittorrent.ahmza.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: qbittorrent + port: + number: 8080 diff --git a/apps/qbittorrent/namespace.yaml b/apps/qbittorrent/namespace.yaml new file mode 100644 index 0000000..7dff8ff --- /dev/null +++ b/apps/qbittorrent/namespace.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: qbittorrent + # NET_ADMIN is not allowed by Kubernetes' baseline Pod Security profile. + # Scope the exception to this VPN application instead of weakening the + # cluster. + labels: + pod-security.kubernetes.io/enforce: privileged + pod-security.kubernetes.io/audit: privileged + pod-security.kubernetes.io/warn: privileged diff --git a/apps/qbittorrent/pvc.yaml b/apps/qbittorrent/pvc.yaml new file mode 100644 index 0000000..91c9f42 --- /dev/null +++ b/apps/qbittorrent/pvc.yaml @@ -0,0 +1,13 @@ +# qBittorrent's database and settings survive Pod replacements on Longhorn. +# Download data is stored separately on the NAS mount in deployment.yaml. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: qbittorrent-config + namespace: qbittorrent +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi diff --git a/apps/qbittorrent/service.yaml b/apps/qbittorrent/service.yaml new file mode 100644 index 0000000..0259a81 --- /dev/null +++ b/apps/qbittorrent/service.yaml @@ -0,0 +1,14 @@ +# Only qBittorrent's management UI/API is exposed inside the cluster. Peer +# traffic uses Proton's dynamically forwarded VPN port, not this Service. +apiVersion: v1 +kind: Service +metadata: + name: qbittorrent + namespace: qbittorrent +spec: + selector: + app: qbittorrent + ports: + - name: http + port: 8080 + targetPort: http diff --git a/apps/qbittorrent/wireguard.sops.yaml b/apps/qbittorrent/wireguard.sops.yaml new file mode 100644 index 0000000..25f9d24 --- /dev/null +++ b/apps/qbittorrent/wireguard.sops.yaml @@ -0,0 +1,25 @@ +apiVersion: isindir.github.com/v1alpha3 +kind: SopsSecret +metadata: + name: qbittorrent-wireguard + namespace: qbittorrent +spec: + secretTemplates: + - name: qbittorrent-wireguard + stringData: + wg0.conf: ENC[AES256_GCM,data:0zOMbAXi6S+3v95lDxt0FbO2WPbbqmciAzjcXMQ0+yyFmX8yVaxZHwiAbSzGZn8Prn3q1FOE0WtmM7KRg+Unpa5laivSfC9AWT8f3sFjIoWk2jdzUo5uqNGSXpHEi58tgbYqPCdHkwamxycgjll6wO7lG56vJKH/Xs/1EPgqhv1PEg+X7HfDibIYK9PVgvxzZLKB8QhrJmlU9X5IFMFiWC+w3ZGMGUSXip/q5Z22XKRtbcAF9ydWlqfyeRSct2pDO4tgrS3ErCNXe5aMBErQR0DkiKBkFd8D8WL2Z1Qu2pqtOq7rMlcR7kufSk5V+5MHUNAC3hQSk4VDYnVBj3OEhdhaYrLY3uzwpYWKtD5YaiDx2wG9aC+O5qyFAPxxpQo0a6MhEFZMh78dhMzozcpLRzjlfKzjefT2dQ+egSSSVimBXCYVsHKPC5+ODqclpf6cGBWH82m0KXisIQfjAdMUSoULcFH/9ChGy+LFemyA7IkgBJ3fHTVPjC7iWGOFWF6YsCkLZcXAIIoy985ZdY5kyc/e,iv:Y31DyDiNDYtEYTRR1AKqQUtA3SvlhsrPJObJ9lwVXYE=,tag:pvuIFRYKBmjlEVXtkbA1Bw==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiV3VUbCtrMUFxeGd4ZVEw + STFhYTNFc0lqdlJQWVpPd3h0UWV3bS8xUG1FClM3NkZvS1g5RkFLWlNYSnVYMnBX + NEExdmNIUnNJUmhxd0tnajgyYWxXbW8KLS0tIHQrWGNMYmx4Tll4aUpTcG9pb0pj + V0NIMStWK2xlODdSVGIxZW9yVkJZR0UKVKky5FJkkGmd5QnNgZBE3Pyxi8+fo6SI + 5dSXTwviUo8x/Rp6aSVrty1e1l9uDJ1fD6qagsUpXXbR7GBnpje+yg== + -----END AGE ENCRYPTED FILE----- + recipient: age1udejf5qa66rd6yvj5r8dz0fjnqfh0quf4t339c3cpqr26mkx2ydq3zel07 + encrypted_regex: ^(data|stringData)$ + lastmodified: "2026-11-01T13:07:18Z" + mac: ENC[AES256_GCM,data:p2x6f9Zslq+ftYjEXIGZZdAmrEPmM2vsgL4gB18RFOnmj9njNqeKCQoXu1A4+FOoEGB0PmwfPFSOo1pLk9LJRpL6F6LS4EP8r8YWwQ4NXUj92cC2Uqps/jwCGmqAbaTlIUsywtFX3SSWXWvf7HoQ2f7WeHtpz5AXGdTjFz3zL1g=,iv:b+N/yhHBHw9cYHPHEPnxbc/tEfYWdeTFNF455YavM5A=,tag:JbotWISa5AGrhWqdDpks9w==,type:str] + version: 3.13.3 diff --git a/docs/network.md b/docs/network.md index 0db0037..03569d4 100644 --- a/docs/network.md +++ b/docs/network.md @@ -90,6 +90,9 @@ Cloudflare; internal overrides are served by Technitium where noted. | `mc.ahmza.com` | Packwiz modpack files | `mcpacks` | Yes — `letsencrypt-prod`, real trusted certificate | [`apps/mcpacks/ingress.yaml`](../apps/mcpacks/ingress.yaml) | | `bazarr.ahmza.com` | Bazarr subtitle manager | `bazarr` | Yes — `letsencrypt-prod`, real trusted certificate; Technitium overrides Cloudflare's NetBird route on the LAN | [`apps/bazarr/ingress.yaml`](../apps/bazarr/ingress.yaml) | | `prowlarr.ahmza.com` | Prowlarr indexer manager | `prowlarr` | Configured for `letsencrypt-prod`; DNS and the certificate must be verified after the first deployment | [`apps/prowlarr/ingress.yaml`](../apps/prowlarr/ingress.yaml) | +| `qbittorrent.ahmza.com` | qBittorrent download client (outbound traffic through Proton VPN) | `qbittorrent` | Configured for `letsencrypt-prod`; DNS, certificate, and VPN health must be verified after the first deployment | [`apps/qbittorrent/ingress.yaml`](../apps/qbittorrent/ingress.yaml) | +| `radarr.ahmza.com` | Radarr movie manager | `radarr` | Yes — `letsencrypt-prod`, certificate Ready; LAN DNS resolves to `10.10.60.12` | [`apps/radarr/ingress.yaml`](../apps/radarr/ingress.yaml) | +| `sonarr.ahmza.com` | Sonarr TV manager | `sonarr` | Yes — `letsencrypt-prod`, certificate Ready; LAN DNS resolves to `10.10.60.12` | [`apps/sonarr/ingress.yaml`](../apps/sonarr/ingress.yaml) | | `soulsync.ahmza.com` | SoulSync media discovery | `soulsync` | Configured for `letsencrypt-prod`; verify the certificate after the first deployment; Technitium routes LAN clients to `10.10.60.12` | [`apps/soulsync/ingress.yaml`](../apps/soulsync/ingress.yaml) | Add a row here every time you point a new hostname at this cluster (see