diff --git a/kubernetes/apps/media/kavita/app.ks.yaml b/kubernetes/apps/media/kavita/app.ks.yaml index 108f1e3..9d25243 100644 --- a/kubernetes/apps/media/kavita/app.ks.yaml +++ b/kubernetes/apps/media/kavita/app.ks.yaml @@ -15,7 +15,7 @@ spec: namespace: observability - name: storage-ready namespace: flux-system - path: ./kubernetes/apps/media/kavita/app + path: ./kubernetes/apps/media/kavita postBuild: substitute: APP: *app diff --git a/kubernetes/apps/media/qbittorrent/app.ks.yaml b/kubernetes/apps/media/qbittorrent/app.ks.yaml new file mode 100644 index 0000000..05d47e1 --- /dev/null +++ b/kubernetes/apps/media/qbittorrent/app.ks.yaml @@ -0,0 +1,28 @@ +--- +# yaml-language-server: $schema=https://schemas.tholinka.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app qbittorrent + namespace: &namespace media +spec: + interval: 1h + components: + - ../../../../components/volsync + - ../../../../components/keda/nfs-scaler + dependsOn: + - name: keda + namespace: observability + - name: storage-ready + namespace: flux-system + path: ./kubernetes/apps/media/qbittorrent/app + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 2Gi + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system + targetNamespace: *namespace diff --git a/kubernetes/apps/media/qbittorrent/app/externalsecret.yaml b/kubernetes/apps/media/qbittorrent/app/externalsecret.yaml new file mode 100644 index 0000000..61a60b1 --- /dev/null +++ b/kubernetes/apps/media/qbittorrent/app/externalsecret.yaml @@ -0,0 +1,86 @@ +--- +# yaml-language-server: $schema=https://schemas.tholinka.dev/external-secrets.io/externalsecret_v1.json +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: &secret qbittorrent +spec: + secretStoreRef: + kind: ClusterSecretStore + name: bitwarden + target: + name: *secret + template: + data: + GLUETUN_CONTROL_SERVER_API_KEY: '{{ .GLUETUN_API_KEY }}' + QBITTORRENT_HOST: qui.media.svc.cluster.local + # abusing the port sync script so it does what I want + QBITTORRENT_WEBUI_PORT: 80/proxy/{{ .GLUETUN_QB_PORT_SYNC_PROXY }} + dataFrom: + - extract: + key: gluetun +--- +# yaml-language-server: $schema=https://schemas.tholinka.dev/external-secrets.io/externalsecret_v1.json +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: &secret wireguard +spec: + secretStoreRef: + kind: ClusterSecretStore + name: bitwarden + target: + name: *secret + template: + engineVersion: v2 + data: + # wireguard + WIREGUARD_PRIVATE_KEY: '{{ .WIREGUARD_PRIVATE_KEY }}' + # WIREGUARD_ADDRESSES: '{{ .WIREGUARD_ADDRESSES }}' # no longer needed - default works fine + + # openvpn + OPENVPN_USER: '{{ .OPENVPN_USER }}' + OPENVPN_PASSWORD: '{{ .OPENVPN_PASSWORD }}' + + # service provider config + VPN_SERVICE_PROVIDER: '{{ .VPN_SERVICE_PROVIDER }}' + VPN_PORT_FORWARDING_PROVIDER: '{{ .VPN_SERVICE_PROVIDER }}' + UPDATER_VPN_SERVICE_PROVIDERS: '{{ .VPN_SERVICE_PROVIDER }}' + + HEALTH_TARGET_ADDRESSES: '{{ .HEALTH_TARGET_ADDRESS }}' + + SERVER_COUNTRIES: '{{ .SERVER_COUNTRIES }}' + SERVER_CITIES: '{{ .SERVER_CITIES }}' + + # for healthchecks + GLUETUN_CONTROL_SERVER_API_KEY: '{{ .GLUETUN_API_KEY }}' + dataFrom: + - extract: + key: gluetun +--- +# yaml-language-server: $schema=https://schemas.tholinka.dev/external-secrets.io/externalsecret_v1.json +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: &secret qbittorrent-gluetun +spec: + secretStoreRef: + kind: ClusterSecretStore + name: bitwarden + target: + name: *secret + creationPolicy: Owner + template: + data: + auth.toml: | + [[roles]] + name = "gluetun-qb-port-sync" + routes = [ + "GET /v1/publicip/ip", + "GET /v1/portforward" + ] + auth = "apikey" + apikey = "{{ .GLUETUN_API_KEY }}" + dataFrom: + - extract: + key: gluetun diff --git a/kubernetes/apps/media/qbittorrent/app/helmrelease.yaml b/kubernetes/apps/media/qbittorrent/app/helmrelease.yaml new file mode 100644 index 0000000..2428cab --- /dev/null +++ b/kubernetes/apps/media/qbittorrent/app/helmrelease.yaml @@ -0,0 +1,265 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/bjw-s-labs/helm-charts/main/charts/other/app-template/schemas/helmrelease-helm-v2.schema.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app qbittorrent +spec: + interval: 1h + chartRef: + kind: OCIRepository + name: app-template + driftDetection: + ignore: + - paths: [/spec/replicas] + values: + controllers: + *app : + annotations: + reloader.stakater.com/auto: 'true' + initContainers: + gluetun: + image: + repository: ghcr.io/qdm12/gluetun + tag: v3.41.1@sha256:1a5bf4b4820a879cdf8d93d7ef0d2d963af56670c9ebff8981860b6804ebc8ab + env: + DNS_SERVER: 'off' + DNS_ADDRESS: 10.96.0.10 # coredns is either pihole, dnsmasq, or cloudflare over tls, so it's fine to use + + HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH: &gluetunAuthPath /gluetun/auth.toml + + FIREWALL_INPUT_PORTS: 80,8388,9999 # 80: WebUI, 8388 Socks Proxy, 9999 Kube Probes + FIREWALL_OUTBOUND_SUBNETS: 10.69.0.0/16,10.96.0.0/16 # Allow access to k8s subnets + + HEALTH_SERVER_ADDRESS: :9999 + HEALTH_SUCCESS_WAIT_DURATION: 10s + + VPN_TYPE: wireguard + # VPN_TYPE: openvpn + + VPN_PORT_FORWARDING: 'on' + PORT_FORWARD_ONLY: 'on' + + VPN_INTERFACE: tun0 + UPDATER_PERIOD: 24h + + # slightly larger MTU + WIREGUARD_MTU: 1400 + OPENVPN_MSSFIX: 1320 + envFrom: + - secretRef: + name: wireguard + probes: + liveness: + enabled: true + custom: true + spec: &probe + exec: + command: + - /bin/sh + - -c + - |- + port="$(wget -O - -q --header="X-API-KEY: $GLUETUN_CONTROL_SERVER_API_KEY" http://localhost:8000/v1/portforward)" + echo $port + [ "$port" = '{"port":0}' ] && exit 1 || exit 0 + # initialDelaySeconds: 5 + # periodSeconds: 60 + # successThreshold: 1 + # timeoutSeconds: 30 + # failureThreshold: 10 + # randomly gluetun will fail to get public_ip on startup for some reason + # startup: + # enabled: true + # custom: true + # spec: + # <<: *probe + # exec: + # command: + # - /bin/sh + # - -c + # - |- + # ip="$(wget -O - -q --header="X-API-KEY: $GLUETUN_CONTROL_SERVER_API_KEY" http://localhost:8000/v1/publicip/ip)" + # echo $ip + # [ "$ip" = '{"public_ip":""}' ] && exit 1 || exit 0 + # periodSeconds: 10 + lifecycle: + postStart: + exec: + command: + - /bin/sh + - -c + - >- + (ip rule del table 51820; ip -6 rule del table 51820) || true + restartPolicy: Always + securityContext: + # can't be non-root, or it has no access to tunnel + runAsNonRoot: false + runAsUser: 0 + runAsGroup: 0 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false # wants to create a user, etc + capabilities: + add: [NET_ADMIN] + resources: + requests: + cpu: 20m + limits: + memory: 100Mi + # TODO: Replace once gluetun supports socks5, nothing supports shadowsocks + socks5: + restartPolicy: Always + image: + repository: docker.io/serjs/go-socks5-proxy + tag: latest@sha256:0af522996f402c03ecd985a87997158eabeb28935365e3a384df37eafcf740ea + env: + PROXY_PORT: &proxy-port 8388 + REQUIRE_AUTH: 'false' + resources: + requests: + cpu: 5m + limits: + memory: 32Mi + securityContext: &securityContext + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: [ALL] } + containers: + app: + image: + repository: ghcr.io/home-operations/qbittorrent + tag: 5.1.4@sha256:56ecd30a7f5e1357daf5eea2a6935134d329b7b268a3a9b9c67041b65fb87fc6 + env: + UMASK: '022' + QBT_WEBUI_PORT: &port 80 + PUSHOVER_ENABLED: true + envFrom: + - secretRef: + name: *app + probes: + startup: + enabled: true + spec: + httpGet: + path: /api/v2/app/version + port: *port + failureThreshold: 30 + periodSeconds: 10 + timeoutSeconds: 5 + securityContext: *securityContext + resources: + requests: + cpu: 100m + limits: + memory: 8Gi + port-forward: + image: + repository: ghcr.io/tholinka/gluetun-qb-port-sync + tag: 0.0.6@sha256:a6405cc2e84cc553cfb0b7796d06468ed9f6d6363690b7f656bf0e10b134346d + env: + GLUETUN_CONTROL_SERVER_HOST: localhost + GLUETUN_CONTROL_SERVER_PORT: 8000 + CRON_ENABLED: true + CRON_SCHEDULE: '*/5 * * * *' + LOG_TIMESTAMP: false + envFrom: + - secretRef: + name: *app + resources: + requests: + cpu: 5m + limits: + memory: 32Mi + securityContext: *securityContext + defaultPodOptions: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + fsGroupChangePolicy: OnRootMismatch + service: + app: + primary: true + forceRename: *app + ports: + http: + primary: true + port: *port + gluetun: + suffix: gluetun + ports: + socks-proxy: + port: *proxy-port + route: + app: + hostnames: + - qb.tholinka.dev + parentRefs: + - name: envoy-internal + namespace: network + rules: + - backendRefs: [{ identifier: app }] + + configMaps: + config: + data: + .qbt.toml: | + [qbittorrent] + addr = "http://localhost:80" + + persistence: + config: + existingClaim: *app + advancedMounts: + *app : + app: + - path: /config + config-file: + type: configMap + name: *app + advancedMounts: + *app : + app: + - path: /config/.qbt.toml + subPath: .qbt.toml + readOnly: true + + tmpfs: + type: emptyDir + advancedMounts: + *app : + port-forward: + - path: /config + subPath: config + app: + - path: /addons + subPath: addons + vuetorrent: + - path: /addons + subPath: addons + + gluetun-auth: + type: secret + name: '{{ .Release.Name }}-gluetun' + advancedMounts: + *app : + gluetun: + - path: *gluetunAuthPath + subPath: auth.toml + media: + type: nfs + server: nas.servers.internal + path: /media + advancedMounts: + *app : + app: + - path: /media/downloads/qbittorrent + subPath: downloads/qbittorrent + tunnel: + type: hostPath + hostPath: /dev/net/tun + hostPathType: CharDevice + advancedMounts: + *app : + gluetun: + - path: /dev/net/tun diff --git a/kubernetes/apps/media/qbittorrent/app/kustomization.yaml b/kubernetes/apps/media/qbittorrent/app/kustomization.yaml new file mode 100644 index 0000000..4eed917 --- /dev/null +++ b/kubernetes/apps/media/qbittorrent/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml diff --git a/kubernetes/apps/media/qbittorrent/kustomization.yaml b/kubernetes/apps/media/qbittorrent/kustomization.yaml new file mode 100644 index 0000000..7aacfdb --- /dev/null +++ b/kubernetes/apps/media/qbittorrent/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ./app.ks.yaml diff --git a/kubernetes/apps/media/qui/app.ks.yaml b/kubernetes/apps/media/qui/app.ks.yaml new file mode 100644 index 0000000..fa3af00 --- /dev/null +++ b/kubernetes/apps/media/qui/app.ks.yaml @@ -0,0 +1,30 @@ +--- +# yaml-language-server: $schema=https://schemas.tholinka.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: &app qui + namespace: &namespace media +spec: + interval: 1h + components: + - ../../../../components/volsync + - ../../../../components/keda/nfs-scaler + dependsOn: + - name: keda + namespace: observability + - name: storage-ready + namespace: flux-system + - name: authentik # does not like the oidc provider not being available at startup + namespace: security + path: ./kubernetes/apps/media/qui/app + postBuild: + substitute: + APP: *app + VOLSYNC_CAPACITY: 2Gi + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system + targetNamespace: *namespace diff --git a/kubernetes/apps/media/qui/app/externalsecret.yaml b/kubernetes/apps/media/qui/app/externalsecret.yaml new file mode 100644 index 0000000..729bfc0 --- /dev/null +++ b/kubernetes/apps/media/qui/app/externalsecret.yaml @@ -0,0 +1,21 @@ +--- +# yaml-language-server: $schema=https://schemas.tholinka.dev/external-secrets.io/externalsecret_v1.json +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: &secret qui +spec: + secretStoreRef: + kind: ClusterSecretStore + name: bitwarden + target: + name: *secret + template: + data: + QUI__SESSION_SECRET: '{{ .QUI_SESSION_SECRET }}' + QUI__OIDC_CLIENT_ID: '{{ .QUI_OIDC_CLIENT_ID }}' + QUI__OIDC_CLIENT_SECRET: '{{ .QUI_OIDC_CLIENT_SECRET }}' + + dataFrom: + - extract: + key: *secret diff --git a/kubernetes/apps/media/qui/app/helmrelease.yaml b/kubernetes/apps/media/qui/app/helmrelease.yaml new file mode 100644 index 0000000..494be6b --- /dev/null +++ b/kubernetes/apps/media/qui/app/helmrelease.yaml @@ -0,0 +1,87 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/bjw-s-labs/helm-charts/main/charts/other/app-template/schemas/helmrelease-helm-v2.schema.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app qui +spec: + interval: 1h + chartRef: + kind: OCIRepository + name: app-template + driftDetection: + ignore: + - paths: [/spec/replicas] + values: + controllers: + *app : + annotations: + reloader.stakater.com/auto: "true" + containers: + app: + image: + repository: ghcr.io/autobrr/qui + tag: v1.14.1@sha256:10b7945d4f0978f56a7cb939a011e1aeef3b8d500e825f409599ae754f95601b + env: + QUI__HOST: 0.0.0.0 + QUI__PORT: &port 80 + QUI__LOG_LEVEL: INFO + + # oidc - make it true later + QUI__OIDC_ENABLED: false + # QUI__OIDC_ISSUER: https://auth.tholinka.dev/application/o/qui/ + # QUI__OIDC_REDIRECT_URL: https://qui.tholinka.dev/api/auth/oidc/callback + # QUI__OIDC_DISABLE_BUILT_IN_LOGIN: true + envFrom: + - secretRef: + name: "{{ .Release.Name }}" + probes: + liveness: &probes + enabled: true + custom: true + spec: + httpGet: + path: /health + port: *port + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + readiness: *probes + resources: + requests: + cpu: 10m + limits: + memory: 2G + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + defaultPodOptions: + securityContext: + runAsNonRoot: true + runAsUser: 1003 + runAsGroup: 1005 + fsGroup: 1005 + fsGroupChangePolicy: OnRootMismatch + service: + app: + ports: + http: + port: *port + route: + app: + hostnames: + - "{{ .Release.Name }}.laurivan.com" + parentRefs: + - name: envoy-internal + namespace: network + persistence: + config: + existingClaim: "{{ .Release.Name }}" + media: + type: nfs + server: 10.0.0.14 + path: /mnt/Main/shares + globalMounts: + - path: /media diff --git a/kubernetes/apps/media/qui/app/kustomization.yaml b/kubernetes/apps/media/qui/app/kustomization.yaml new file mode 100644 index 0000000..b0471f3 --- /dev/null +++ b/kubernetes/apps/media/qui/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/kustomization +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./secrets.sops.yaml + - ./helmrelease.yaml diff --git a/kubernetes/apps/media/qui/app/secrets.sops.yaml b/kubernetes/apps/media/qui/app/secrets.sops.yaml new file mode 100644 index 0000000..c9e3387 --- /dev/null +++ b/kubernetes/apps/media/qui/app/secrets.sops.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Secret +metadata: + name: qui +stringData: + QUI__SESSION_SECRET: ENC[AES256_GCM,data:XkPD5CX2VLYplqSw9GQwrjAbJGmFOYKQzcCct2tOqMjb/IPxBaIuLQ==,iv:hLKHjRCtWngbSaxiOKzDKEDDLryfF4UIpn3gQieyQvw=,tag:B1xQBWWOTPWGj9kf4j+y5w==,type:str] + #ENC[AES256_GCM,data:DjPTDKJ5UEBDcA==,iv:zHbSdf/GChYY3i9cxTs4zZeV7ZIH3wOr4XCh/Fr6oGw=,tag:HbRQlXe5ZdDQbVHJkym4oQ==,type:comment] + QUI__OIDC_CLIENT_ID: ENC[AES256_GCM,data:hSioWKPmhJBWAy8=,iv:KVq82iF3iuVMyXl+w5raOgtDoyVklg0ddL4lE82r6yY=,tag:2gPa2F5cP36dMqL1qkP84w==,type:str] + QUI__OIDC_CLIENT_SECRET: ENC[AES256_GCM,data:LOv0jdRIV4BQiQU=,iv:lrKFDgUu42Ho3lZunsdoyayDPiQZKAgLRsLBXfD6d5M=,tag:ci3+Nr3G5PeK5YHx3fOE6w==,type:str] +sops: + age: + - recipient: age1yzrqhl9dk8ljswpmzsqme3enad5kxxhsptdvecy3lwlq0ms80gaqxrctst + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqRDRDT2JzZmF2RlcyREg5 + aEgyZ0QwNTJQK2JYbDBrNjRhT3BNSzdFZGlzCndQVloyK1RUU281S1Q2YnI4eXQv + RVoxa0UxOFNEVkZwQzB3ZUhTNHBMTWcKLS0tIGZLMTZ3YUs3d2FHWVBtczJzdzhp + dUtWdGJ0cjhjREI5YnVzVDk5VGJJS0kKpa+N5XC8a5/V/eUgqZoosxrio9CJMTYS + TzhILOHxY59zNtl4Jw7QtIy27jWki4+318WnQ2XGHO5yPUitc1yPuA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-02-27T12:25:54Z" + mac: ENC[AES256_GCM,data:4Q60P6znbtKtC1SuRmFMR9b4fyoiL2rhBcxjepol5KWFqyg3+zF/fAbzB/Q3UeV4V3t0e+F0CfSlGP+e+ytv1F7EBJex7RhxWMiOSKsate3oOGoQEyW1GXq2YUS9Y8ZVuUHxdi4b2qpGeSJlSyL+mLs32nC9DorzDqyyOHjpZZ4=,iv:emjMwt6NX1LgRJjqaxEd2u2QTTYGxqletxxliSVnr8I=,tag:q/kTIXTXsWig/Lhb4uIczA==,type:str] + encrypted_regex: ^(data|stringData)$ + mac_only_encrypted: true + version: 3.11.0 diff --git a/kubernetes/apps/media/qui/kustomization.yaml b/kubernetes/apps/media/qui/kustomization.yaml new file mode 100644 index 0000000..7aacfdb --- /dev/null +++ b/kubernetes/apps/media/qui/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ./app.ks.yaml