Featured image of post VPN + qBittorrent 설치

VPN + qBittorrent 설치

K8S에 NordVPN으로 우회되는 원격 qBittorrent 서버 만들기


⚠ 주의 : 본 포스트에서는 k8s에 VPN으로 우회되는 Torrent 서버 구성에 대한 정보를 담고 있습니다.

부적절한 컨텐츠는 VPN이고 뭐고 절대 받지 맙시다! 👮


문제의 시작

  • Jellyfin으로 지인들과 편하게 미디어를 함께 보고 싶어졌다. 귀칼이 그렇게 재밌다길래
  • 미디어 콘텐츠를 구하는 가장 좋은 방법은 Torrent이다.
  • 하지만 이를 위해 메인 PC를 하루종일 켜두고 싶지는 않다.
  • 보안을 위해 VPN은 쓰되, 메인 PC 전체 트래픽을 VPN에 묶고 싶지는 않다.

해결방안

  • k8s 클러스터에 qBittorrent 배포

  • VPN 구성을 위해 NordVPN의 서비스인 NordLynx를 적용

  • Ingress를 구성해서 보유한 도메인을 통해 접근 가능하도록 설정 (선택)

  • qBittorrent의 기본 UI는 너무 못생겼으므로 UI도 VueTorrent로 변경 (선택)

    참고로 기본 UI는 이렇게 생겼다.

    VueTorrent UI는 이렇게 생겼다. 이쁘다!

구성 개요

  • 시크릿: NordLynx 개인키(nord-lynx-private-key)를 담는 Opaque 타입 시크릿.
  • 서비스(Service): ClusterIP로 qBittorrent 웹 및 토렌트 포트 노출.
  • 디플로이먼트(Deployment):
    • 사이드카 컨테이너 ghcr.io/bubuntux/nordlynx:latest (VPN)
    • 메인 컨테이너 lscr.io/linuxserver/qbittorrent:latest
    • initContainer로 커널 파라미터 설정(sysctl)
    • NET_ADMIN capability 부여(터널 동작용)
    • fsGroup: 1000으로 퍼미션 정리
  • Ingress(선택): NGINX Ingress



네임스페이스 생성

늘 그렇듯 처음은 Namespace부터 만든다. 이번 포스트에서는 일관성 있게 torrent라는 이름을 사용한다.

1
kubectl create namespace torrent



NordVPN Access Token 발급

  1. NordVPN Dashboard에 접속 후 좌상단 NordVPN을 클릭한다.

2. 하단에 Get Access Token 버튼 클릭

3. 등록된 이메일로 인증코드 전송

4. 새 토큰을 생성한다



Private Key 생성

Docker 명령어를 통해 Private Key를 생성한다.

<YOUR_ACCESS_TOKEN> 자리에 위에서 생성한 토큰 값을 넣어준다.

1
docker run --rm --cap-add=NET_ADMIN -e TOKEN=<YOUR_ACCESS_TOKEN> ghcr.io/bubuntux/nordvpn:get_private_key | grep "Private Key:" | cut -d' ' -f3 | tr -d '\n'



시크릿 생성

<YOUR_PRIVATE_KEY> 자리를 위에서 Docker Command로 생성한 Private Key 값으로 교체한다.

1
2
kubectl -n torrent create secret generic torrent-nord-lynx-private-key \
  --from-literal=nord-lynx-private-key='<YOUR_PRIVATE_KEY>'



Service 생성

service-qbittorrent.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: Service
metadata:
  name: qbittorrent
  namespace: torrent
spec:
  type: ClusterIP
  selector:
    app: qbittorrent
  ports:
    - name: web
      port: 8080
      targetPort: 8080
      protocol: TCP
    - name: torrenting-tcp
      port: 6881
      targetPort: 6881
      protocol: TCP
    - name: torrenting-udp
      port: 6881
      targetPort: 6881
      protocol: UDP

적용:

1
kubectl apply -f service-qbittorrent.yaml



PVC 생성

StorageClass가 별도로 없다면 비워도 상관 없다.

가급적 qbittorrent-completeHDDqbittorrent-incompleteSSD에 저장하는 것이 좋다.

pvc-qbittorrent.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# qBittorrent의 설정을 저장하는 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: qbittorrent-config
  namespace: torrent
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: <your-storage-class> # 클러스터 StorageClass 명에 맞게 수정
---

# 다운로드가 완료 된 파일들이 저장될 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: qbittorrent-complete
  namespace: torrent
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Gi # 용량은 본인이 원하는 만큼 할당
  storageClassName: <your-storage-class> 
---

# 아직 다운로드 중인 파일들이 저장될 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: qbittorrent-incomplete
  namespace: torrent
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 200Gi # 용량은 본인이 원하는 만큼 할당
  storageClassName: <your-storage-class> 

적용:

1
kubectl apply -f pvc-qbittorrent.yaml



Deployment 생성

deployment-qbittorrent.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
apiVersion: apps/v1
kind: Deployment
metadata:
  name: qbittorrent
  namespace: torrent
spec:
  replicas: 1
  selector:
    matchLabels:
      app: qbittorrent
  template:
    metadata:
      labels:
        app: qbittorrent
        sidecar.istio.io/inject: 'false' # Istio Sidecar와 NordLynx간의 충돌이 있다.
    spec:
      securityContext:
        fsGroup: 1000
      initContainers:
        - name: init-sysctl
          image: busybox
          command:
            - /bin/sh
            - -c
            - |
              sysctl -w net.ipv6.conf.all.disable_ipv6=1 &&
              sysctl -w net.ipv4.conf.all.src_valid_mark=1
          securityContext:
            privileged: true
      containers:
        - name: nordlynx
          image: ghcr.io/bubuntux/nordlynx:latest
          imagePullPolicy: Always
          env:
            - name: TZ
              value: Asia/Seoul
            - name: NET_LOCAL
              value: '10.244.0.0/16' # 클러스터 Pod CIDR에 맞게 수정
            - name: ALLOW_LIST
              value: qbittorrent.torrent.svc.cluster.local
            - name: DNS
              value: '1.1.1.1,8.8.8.8'
            - name: PRIVATE_KEY
              valueFrom:
                secretKeyRef:
                  name: torrent-nord-lynx-private-key
                  key: nord-lynx-private-key
            - name: QUERY
              value: 'filters[servers_groups][identifier]=legacy_p2p'
            - name: COUNTRY_CODE
              value: JP # 우회를 원하는 국가 코드 기입
          securityContext:
            capabilities:
              add:
                - NET_ADMIN
        - name: web
          image: lscr.io/linuxserver/qbittorrent:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080
              protocol: TCP
            - containerPort: 6881
              protocol: TCP
            - containerPort: 6881
              protocol: UDP
          env:
            - name: PUID
              value: '1000'
            - name: PGID
              value: '1000'
            - name: TZ
              value: Asia/Seoul
            - name: WEBUI_PORT
              value: '8080'
            - name: TORRENTING_PORT
              value: '6881'
            # VueTorrent UI 적용
            - name: DOCKER_MODS
              value: ghcr.io/gabe565/linuxserver-mod-vuetorrent
          volumeMounts:
            - name: qbittorrent-config
              mountPath: /config
            - name: qbittorrent-complete
              mountPath: /downloads
            - name: qbittorrent-incomplete
              mountPath: /downloads/incomplete
      volumes:
        # qBittorrent 설정 파일
        - name: qbittorrent-config
          persistentVolumeClaim:
            claimName: qbittorrent-config
        # 다운로드가 완료된 파일들
        - name: qbittorrent-complete
          persistentVolumeClaim:
            claimName: qbittorrent-complete
        # 다운로드 중인 파일들
        - name: qbittorrent-incomplete
          persistentVolumeClaim:
            claimName: qbittorrent-incomplete

적용:

1
kubectl apply -f deployment-qbittorrent.yaml



Ingress 생성(선택)

Ingress는 옵션이다. 실 사용에서는 Cloudflare 레코드로 도메인을 연결하고

k8s 내부에는 nginx ingress controller를 설치해서 사용하고 있다.

또한, 인증된 사용자만 접근할 수 있도록 별도로 OAuth2 Proxy로 보호받는다.

여기에 대해서는 추후 포스팅 예정.

ingress-qbittorrent.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: qbittorrent
  namespace: torrent
  annotations:
    nginx.ingress.kubernetes.io/backend-protocol: 'HTTP'
    nginx.ingress.kubernetes.io/rewrite-target: '/'
spec:
  ingressClassName: nginx
  rules:
    - host: torrent.your-domain.com # 실제로 연결할 도메인 선택
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: qbittorrent
                port:
                  number: 8080

적용:

1
kubectl apply -f ingress-qbittorrent.yaml

참고

  • NET_LOCAL은 클러스터 Pod CIDR에 맞춘다.

  • PRIVATE_KEY는 위 시크릿 참조와 일치시킨다.

  • initContainer는 커널 파라미터를 조정하므로 privileged가 필요하다.

  • NET_ADMIN capability가 없으면 VPN 터널이 정상 동작하지 않는다.

  • VPN 네트워크가 끊어지면 모든 토렌트 서비스가 셧다운 된다.

  • Service Type은 Cluster IP로 설정했는데, 이는 Nginx Ingress Controller와 연결하기 위함이다.

    환경에 따라 NodePortLoadBalancer를 사용해서 접근 할 수 있도록 하자.

마치며

WebUI에서 로그를 검색해보면 정상적으로 VPN IP가 할당되었음을 알 수 있다.

최종적으론 이렇게 나온다

Hugo로 만듦
JimmyStack 테마 사용 중