1. 기획 의도 및 배경
클러스터를 구축하고 나서 보니 내부 서비스들에 안전하게 접근할 수 있는 망이 필요했습니다. 예전 경험상 공유기 L2TP VPN은 속도가 너무 느렸고, 일반적인 VPN(Hub & Spoke)은 중앙 서버가 병목이 되는 구조였습니다. 또한, 제가 VPN을 통해 구현하고 싶은 목표는 다음과 같았습니다.
- 커스텀 도메인 접속:
.ts.net자동 생성 도메인이 아닌, 개인 도메인(proxmox.haru2.store) 사용. - 보안 연결 (SSL/HTTPS): 모든 통신 암호화 및 '안전하지 않음' 경고 제거.
- 사용자별 접근 제어: 그룹별로 접근 가능한 서비스를 세분화하여 제어.
따라서 일반적인 VPN 방식으로는 구축이 어렵다고 판단하여, Tailscale VPN을 도입하게 되었습니다.
2. Tailscale의 핵심 컨셉
- 제로 설정 (Zero Configuration): 복잡한 방화벽 규칙 없이 ID 공급자(Google, Microsoft) 로그인만으로 사설망 생성.
- 직접 통신 (P2P Mesh): 중앙 서버를 거치지 않고 WireGuard® 프로토콜 기반으로 기기 간 최단 경로 직접 통신 (속도 ↑, 지연 ↓).
- 신원 기반 보안 (Zero Trust): 네트워크 위치가 아닌 '신원(Identity)'을 기반으로 접근을 제어(ACL).
3. 아키텍처 의사결정 (Architecture Decision Record)
목표 기능을 구현하기 위해 3가지 아키텍처를 비교 분석했습니다.
| 구분 | 구성 방식 | 장점 | 단점 | 결정 |
|---|---|---|---|---|
| 방법 1 | NPM + Subnet Router | 커스텀 도메인/SSL 가능 | 신원 상실 (Identity Loss): 모든 트래픽이 NPM을 경유하여 ACL 제어 불가능 | 기각 |
| 방법 2 | Sidecar + Split DNS | 이상적인 구조 (도메인+ACL) | Over-engineering: 사설 DNS 구축 및 관리 복잡도 과다 | 기각 |
| 방법 3 | Sidecar + MagicDNS | 구축 용이성, 강력한 ACL | 커스텀 도메인 불가 (MagicDNS 사용) | 채택 |
결론: 사설 도메인의 편의성을 일부 양보하고, Tailscale MagicDNS와 Sidecar 패턴을 통해 운영 복잡도를 낮추면서 핵심 기능(ACL, 보안)을 확보하는 방식을 선택했습니다.
4. Kubernetes 클러스터 서비스에 Tailscale Sidecar 배포
4.1. Proxmox 호스트 설정
1. Tailscale 설치 및 활성화
# 패키지 업데이트
sudo apt update && sudo apt upgrade -y
# 설치
curl -fsSL https://tailscale.com/install.sh | sh
# 활성화 (인증 링크 접속)
sudo tailscale up
# 자동 시작 설정
sudo systemctl enable tailscaled
sudo systemctl start tailscaled
2. 방화벽/라우팅 설정
# LAN 접근 허용
sudo tailscale up --accept-routes
# 서브넷 라우팅 (필요시)
sudo tailscale up --advertise-routes=192.168.1.0/24
3. Proxmox 웹 접근 (Tailscale serve)
# https 보안연결 사용 (소켓 경로 변경 주의)
tailscale --socket=/tmp/tailscaled.sock serve --bg https+insecure://localhost:3000
4.2. Harbor: Ingress 망 분리와 Sidecar 통합 (핵심 구현)
Harbor는 Ingress Controller 의존성이 높은 서비스입니다. 저는 외부망(Public Internet)에 Ingress를 직접 노출하는 표준 방식 대신, Tailscale Mesh Network 안에 Ingress 접근점을 숨기는 '망 분리' 구조를 설계했습니다.
[구조][User] -> [Tailscale VPN Tunnel] -> [Sidecar (Tailscale + Nginx)] -> [Ingress Controller / Service] -> [Harbor]
이를 위해 Ingress Controller 또는 Harbor 서비스 앞단에 Nginx Reverse Proxy를 포함한 Tailscale Sidecar를 구성했습니다.
[설계 의도] 왜 표준 Ingress 노출이 아닌 Sidecar 패턴인가?
일반적인 Kubernetes Ingress 방식(LoadBalancer 또는 NodePort)을 사용하지 않고, 굳이 Sidecar 패턴을 고집한 이유는 다음과 같습니다.
- Network Decoupling (결합도 감소 및 독립성 확보):
- 내부 관리용 서비스(Harbor, Jenkins 등)가 클러스터의 메인 Ingress(서비스 트래픽용)에 종속되는 것을 방지하고자 했습니다.
- Tailscale Sidecar 패턴을 통해 관리용 네트워크 트래픽을 서비스용 트래픽과 물리/논리적으로 완전히 분리하여, 메인 Ingress 설정 변경이나 장애가 내부 관리 도구 접근에 영향을 주지 않는 독립적인 관리 네트워크(OOB 유사 효과)를 구성했습니다.
- Security (공격 표면 은폐):
- 표준 Ingress를 사용하면 공인 IP가 노출되어 보안 위협이 증가합니다. Sidecar 방식을 통해 인바운드 포트를 단 하나도 열지 않고, 오직 인증된 VPN 터널을 통해서만 접근 가능한 Zero-Trust 망 분리를 구현했습니다.
- L7 Proxy의 필요성 (기술적 해결):
- 초기
socat(L4) 시도 시 HTTPS 헤더 부재로 인한403 Forbidden문제를, Nginx(L7)를 Sidecar에 내장하여 헤더 주입 및 정교한 라우팅으로 해결했습니다.
A. Nginx 설정 (harbor.conf)
L4 Proxy(socat) 대신 L7 Proxy(Nginx)를 사용하여 HTTPS 헤더와 API 경로 라우팅을 처리합니다.
server {
listen 9090 ssl;
server_name harbor.tail2dac17.ts.net;
ssl_certificate /etc/nginx/certs/harbor.crt;
ssl_certificate_key /etc/nginx/certs/harbor.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Portal
location / {
proxy_pass harbor.192.168.0.111.nip.io; # Harbor Portal ClusterIP
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
# Core
location /api/ {
proxy_pass http://10.102.26.224:80; # Harbor Core ClusterIP
# ... (헤더 설정 동일)
}
# Service, Registry v2, Chartmuseum 등 각 경로별 라우팅 설정...
location /service/ { proxy_pass http://10.102.26.224:80; ... }
location /v2/ { proxy_pass http://10.102.26.224:80; ... }
location /c/ { proxy_pass http://10.102.26.224:80; ... }
}
B. Sidecar Dockerfile
Tailscale 이미지에 Nginx를 설치하고 설정을 주입합니다.
FROM tailscale/tailscale:latest
# Install nginx
RUN apk add --no-cache nginx
RUN rm /etc/nginx/http.d/default.conf
# 설정 파일 복사
COPY nginx.conf /etc/nginx/nginx.conf
COPY harbor.conf /etc/nginx/conf.d/harbor.conf
# 인증서 복사 (자체 서명 또는 공인 인증서)
RUN mkdir -p /etc/nginx/certs
COPY harbor-ca.crt /etc/nginx/certs/harbor.crt
COPY harbor-ca.key /etc/nginx/certs/harbor.key
COPY start.sh /start.sh
RUN chmod +x /start.sh
CMD ["/start.sh"]
C. 실행 스크립트 (start.sh)
Tailscale과 Nginx를 순차적으로 실행하고 tailscale serve로 포트를 매핑합니다.
#!/bin/sh
set -e
echo "Starting tailscaled..."
tailscaled &
echo "Running tailscale up..."
tailscale up --authkey=${TS_AUTHKEY} --hostname=${TS_HOSTNAME} --exit-node= --accept-dns=false
# Nginx 실행 (Background)
echo "Starting Nginx..."
nginx &
sleep 3
# Tailscale serve 실행 (HTTPS, 443 -> localhost:9090)
echo "Starting tailscale serve..."
tailscale serve --bg --tcp 443 localhost:9090
tail -f /dev/null
D. Ingress-Nginx Helm Values (Sidecar 주입)
Ingress Controller 파드에 위에서 만든 이미지를 Sidecar로 주입합니다.
controller:
service:
type: LoadBalancer
extraVolumes:
- name: tun-device
hostPath:
path: /dev/net/tun
type: CharDevice
extraContainers:
- name: tailscale
image: harbor.192.168.0.110.nip.io/harbor/tailscale-nginx-harbor:latest
imagePullPolicy: IfNotPresent
env:
- name: TS_KUBE_SECRET
value: harbor-tailscale-state
- name: TS_HOSTNAME
value: harbor
- name: TS_AUTHKEY
valueFrom:
secretKeyRef:
name: harbor-tailscale-auth-secret
key: TS_AUTHKEY
securityContext:
capabilities:
add:
- NET_ADMIN
- NET_RAW
volumeMounts:
- name: tun-device
mountPath: /dev/net/tun
E. 배포 및 RBAC 설정
1. 이미지 빌드 및 전송
# 빌드
docker buildx build --platform linux/amd64,linux/arm64 -t harbor.192.168.0.110.nip.io/harbor/tailscale-nginx-harbor:latest .
# 이미지 추출 및 노드 전송 (Ingress 배포 중엔 Pull 불가하므로 수동 전송)
docker save ... -o tailscale-nginx-harbor.tar
scp tailscale-nginx-harbor.tar worker@...:/tmp/
sudo ctr -n k8s.io images import /tmp/tailscale-nginx-harbor.tar
2. Secret 및 RBAC 생성
Tailscale 상태 저장을 위해 권한이 필요합니다.
# Secret 생성
kubectl create secret generic harbor-tailscale-auth-secret --from-literal=TS_AUTHKEY='tskey-auth-xxxx' -n ingress-nginx
kubectl create secret generic harbor-tailscale-state -n ingress-nginx
# harbor-tailscale-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ingress-tailscale-role
namespace: ingress-nginx
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "update", "patch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["get", "create", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ingress-tailscale-binding
namespace: ingress-nginx
subjects:
- kind: ServiceAccount
name: ingress-nginx
namespace: ingress-nginx
roleRef:
kind: Role
name: ingress-tailscale-role
apiGroup: rbac.authorization.k8s.io
3. 적용
kubectl apply -f harbor-tailscale-rbac.yaml
helm upgrade ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx -f values-tailscale.yaml
4.3. Grafana (Deployment Sidecar)
Grafana는 Deployment에 Sidecar를 주입하는 방식으로 간단히 구성했습니다.
1. Helm Values 수정
# values.yaml
- name: tailscale
image: tailscale/tailscale:latest
env:
- name: TS_KUBE_SECRET
value: prometheus-grafana-tailscale-state
- name: TS_HOSTNAME
value: grafana
- name: TS_AUTHKEY
valueFrom:
secretKeyRef:
name: grafana-tailscale-secret
key: TS_AUTHKEY
securityContext:
capabilities:
add: ["NET_ADMIN", "NET_RAW"]
2. Secret & RBAC
Harbor와 동일하게 grafana-tailscale-auth-secret, state secret, Role, RoleBinding을 생성하여 적용했습니다.
3. Tailscale serve
컨테이너 내부에서 로컬포트 프록시 실행:
tailscale --socket=/tmp/tailscaled.sock serve --bg http://localhost:3000
4.4. Jenkins (StatefulSet Sidecar)
Jenkins는 StatefulSet으로 구성되며, 이미지 Pull을 위해 Harbor 인증서 신뢰 설정(ConfigMap + DaemonSet)이 선행되었습니다.
1. Dockerfile (Custom Image)
FROM jenkins/jenkins:2.516.1-lts-jdk21
USER root
RUN apt-get update && apt-get install -y git nginx net-tools curl vim ca-certificates
USER jenkins
2. Helm Values (Sidecar Injection)
controller:
sidecars:
additionalSidecarContainers:
- name: tailscale
image: tailscale/tailscale:latest
env:
- name: TS_KUBE_SECRET
value: jenkins-tailscale-state
- name: TS_HOSTNAME
value: jenkins
- name: TS_AUTHKEY
valueFrom:
secretKeyRef:
name: jenkins-tailscale-auth-secret
key: TS_AUTHKEY
# Downward API로 Pod 정보 주입
- name: POD_NAME
valueFrom: { fieldRef: { fieldPath: metadata.name } }
- name: POD_UID
valueFrom: { fieldRef: { fieldPath: metadata.uid } }
securityContext:
capabilities:
add: ["NET_ADMIN", "NET_RAW"]
3. Upgrade
helm upgrade --install jenkins jenkins/jenkins -f jenkins-values.yaml -n jenkins
5. 트러블슈팅: Harbor 연동 시 기술적 챌린지
문제 상황: 403 Forbidden 및 405 Method Not Allowed
Harbor 연동 초기에는 단순 socat을 사용하여 트래픽을 포워딩했으나 오류가 발생했습니다.
- 원인 1 (403 Forbidden): Harbor는 보안을 위해 HTTPS 요청 시 특정 헤더(X-Forwarded-Proto 등)를 요구하는데,
socat은 L4(TCP) 레벨의 포워딩만 수행하므로 이 헤더들을 전달하지 못함. - 원인 2 (405 Method Not Allowed): 9090 포트로 들어오는 모든 요청을 Portal로만 보냈더니, Core 서비스로 가야 할 API 요청이 Portal UI로 잘못 전달됨.
해결: Nginx (L7 Proxy) 도입socat 대신 Nginx를 Sidecar 내부에 배치하여 해결했습니다.
- L7 라우팅:
location /api/,location /v2/등 경로에 따라 Harbor의 Core, Portal, Registry 서비스로 정확히 라우팅. - 헤더 주입:
proxy_set_header X-Forwarded-Proto https;설정을 통해 Harbor가 HTTPS 연결임을 인식하도록 강제.
6. 결론 및 성과
- Ingress 망 분리 달성: Ingress Controller를 Tailscale Mesh Network 내부에 격리하여, 공인 IP 노출 없이 VPN 연결된 사용자만 접근 가능한 보안 환경을 구축했습니다.
- Zero-Trust ACL: 사용자 신원에 따라 서비스 접근 권한을 세밀하게 제어할 수 있게 되었습니다.
- Infrastructure as Code: 모든 Sidecar 주입과 설정 과정을 Helm Values와 YAML 코드로 관리하여 재현성을 확보했습니다.
'인프라' 카테고리의 다른 글
| [K8S] configMap 과 Demonset을 활용해 인증서 배포하기 (1) | 2025.08.09 |
|---|---|
| [K8S] Harbor : LoadBalancer 및 TLS 설정 (0) | 2025.06.17 |
| [K8S] 클러스터의 리소스 분배 전략 (0) | 2025.06.15 |
| [Jenkins] Jenkins 빌드 머신 설정 하기 (0) | 2025.06.14 |
| [Jenkins] Helm으로 Jenkins 설치하기 (0) | 2025.06.13 |