# πŸš€ Headscale + Headplane μ™„μ „ μ„€μΉ˜ κ°€μ΄λ“œ ## πŸ“‹ ν”„λ‘œμ νŠΈ κ°œμš” - **λͺ©ν‘œ**: Tailscale을 μ™„μ „νžˆ λŒ€μ²΄ν•˜λŠ” 자체 ν˜ΈμŠ€νŒ… VPN μ†”λ£¨μ…˜ ꡬ좕 - **기술 μŠ€νƒ**: Docker, Docker Compose, Headscale, Headplane - **ν™˜κ²½**: Ubuntu 24.04 LTS, Docker 27.2.0 ## πŸ› οΈ 사전 μš”κ΅¬μ‚¬ν•­ - Docker 및 Docker Compose μ„€μΉ˜ - 8070, 3000 포트 μ‚¬μš© κ°€λŠ₯ - root κΆŒν•œ λ˜λŠ” sudo κΆŒν•œ ## πŸ“ ν”„λ‘œμ νŠΈ ꡬ쑰 ``` headscale-setup/ β”œβ”€β”€ docker-compose.yml # Docker Compose μ„€μ • β”œβ”€β”€ .env # ν™˜κ²½λ³€μˆ˜ (API ν‚€ 포함) β”œβ”€β”€ .env.example # ν™˜κ²½λ³€μˆ˜ ν…œν”Œλ¦Ώ β”œβ”€β”€ config/ β”‚ └── config.yaml # Headscale μ΅œμ‹  μ„€μ • β”œβ”€β”€ headplane-config/ β”‚ └── config.yaml # Headplane μ„€μ • β”œβ”€β”€ data/ # SQLite λ°μ΄ν„°λ² μ΄μŠ€ (μžλ™ 생성) β”œβ”€β”€ run/ # λŸ°νƒ€μž„ 파일 (μžλ™ 생성) └── start.sh # μžλ™ μ„€μΉ˜ 슀크립트 ``` ## πŸ”§ 상세 μ„€μΉ˜ κ³Όμ • ### 1단계: ν™˜κ²½ μ€€λΉ„ ```bash # μž‘μ—… 디렉토리 생성 mkdir -p headscale-setup cd headscale-setup # ν•„μš”ν•œ ν•˜μœ„ 디렉토리 생성 mkdir -p config data run headplane-config ``` ### 2단계: Docker Compose μ„€μ • #### docker-compose.yml μž‘μ„± ```yaml version: '3.8' services: headscale: image: headscale/headscale:latest container_name: headscale restart: unless-stopped command: serve environment: - TZ=Asia/Seoul volumes: - ./config:/etc/headscale - ./data:/var/lib/headscale - ./run:/var/run/headscale ports: - "8070:8080" # μ™ΈλΆ€:λ‚΄λΆ€ (포트 좩돌 λ°©μ§€) - "9090:9090" # λ©”νŠΈλ¦­μŠ€ networks: - headscale-net healthcheck: test: ["CMD-SHELL", "nc -z localhost 8080 || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 30s headplane: image: ghcr.io/tale/headplane:latest container_name: headplane restart: unless-stopped environment: - TZ=Asia/Seoul - HEADSCALE_URL=http://headscale:8080 - HEADSCALE_API_KEY=${HEADSCALE_API_KEY} volumes: - ./headplane-config:/etc/headplane ports: - "3000:3000" depends_on: - headscale networks: - headscale-net networks: headscale-net: driver: bridge ``` ### 3단계: Headscale μ„€μ • 파일 #### config/config.yaml (μ΅œμ‹  ν˜•μ‹) ```yaml --- server_url: http://localhost:8070 listen_addr: 0.0.0.0:8080 metrics_listen_addr: 0.0.0.0:9090 private_key_path: /var/lib/headscale/private.key noise: private_key_path: /var/lib/headscale/noise_private.key # μ΅œμ‹  ν˜•μ‹: prefixes μ‚¬μš© prefixes: v4: 100.64.0.0/10 v6: fd7a:115c:a1e0::/48 derp: server: enabled: false urls: - https://controlplane.tailscale.com/derpmap/default disable_check_updates: false ephemeral_node_inactivity_timeout: 30m database: type: sqlite3 sqlite: path: /var/lib/headscale/db.sqlite # μ΅œμ‹  DNS μ„€μ • ν˜•μ‹ dns: override_local_dns: true nameservers: global: - 1.1.1.1 - 8.8.8.8 search_domains: [] magic_dns: true base_domain: headscale.local # μ΅œμ‹  μ •μ±… μ„€μ • policy: path: "" log: format: text level: info unix_socket: /var/run/headscale/headscale.sock unix_socket_permission: "0770" logtail: enabled: false randomize_client_port: false # κ°„μ†Œν™”λœ OIDC μ„€μ • oidc: only_start_if_oidc_is_available: false issuer: "" client_id: "" client_secret: "" scope: ["openid", "profile", "email"] extra_params: {} allowed_domains: [] allowed_users: [] ``` ### 4단계: Headplane μ„€μ • 파일 #### headplane-config/config.yaml ```yaml headscale: url: http://headscale:8080 api_key: YOUR_API_KEY_HERE # μžλ™ 생성됨 config_strict: false server: host: 0.0.0.0 port: 3000 cookie_secret: headscale-ui-secret-32-chars-key # μ •ν™•νžˆ 32자 cookie_secure: false settings: title: "Headscale 관리 νŒ¨λ„" favicon_url: "" custom_css: "" ``` **μ€‘μš” 섀정사항:** - `cookie_secret`: μ •ν™•νžˆ 32μžμ—¬μ•Ό 함 (μ„€μ • 검증 였λ₯˜ λ°©μ§€) - `config_strict: false`: μ„€μ • 검증 μ™„ν™” - `api_key`: μ„€μΉ˜ μ‹œ μžλ™ μƒμ„±λ˜μ–΄ ꡐ체됨 - μ„€μ • νŒŒμΌμ€ ν™˜κ²½λ³€μˆ˜λ³΄λ‹€ μš°μ„ μˆœμœ„κ°€ λ†’μŒ ### 5단계: ν™˜κ²½λ³€μˆ˜ μ„€μ • #### .env.example ```bash # Headscale API Key (μ„€μΉ˜ ν›„ μžλ™ 생성됨) HEADSCALE_API_KEY=your_api_key_here # μ„œλ²„ μ„€μ • SERVER_URL=http://localhost:8070 LISTEN_ADDR=0.0.0.0:8080 # λ°μ΄ν„°λ² μ΄μŠ€ (SQLite κΈ°λ³Έ) DB_TYPE=sqlite3 DB_PATH=/var/lib/headscale/db.sqlite # Magic DNS MAGIC_DNS=true BASE_DOMAIN=headscale.local # λ„€νŠΈμ›Œν¬ μ„€μ • IP_PREFIXES=100.64.0.0/10 # μ‹œκ°„λŒ€ TZ=Asia/Seoul ``` ### 5단계: μ„€μΉ˜ μ‹€ν–‰ #### ν™˜κ²½λ³€μˆ˜ 파일 볡사 ```bash cp .env.example .env ``` #### μžλ™ μ„€μΉ˜ 슀크립트 μ‹€ν–‰ ```bash chmod +x start.sh ./start.sh ``` #### λ˜λŠ” μˆ˜λ™ μ„€μΉ˜ ```bash # 1. Headscale μ‹œμž‘ docker-compose up -d headscale # 2. API ν‚€ 생성 (μ•½ 30초 λŒ€κΈ° ν›„) sleep 30 API_KEY=$(docker-compose exec -T headscale headscale apikeys create) echo "Generated API Key: $API_KEY" # 3. .env νŒŒμΌμ— API ν‚€ μž…λ ₯ sed -i "s/HEADSCALE_API_KEY=your_api_key_here/HEADSCALE_API_KEY=$API_KEY/" .env # 4. Headplane μ‹œμž‘ docker-compose up -d headplane ``` ## 🎯 μ€‘μš”ν•œ μ„€μ • 변경사항 ### 포트 좩돌 ν•΄κ²° - **κΈ°μ‘΄**: 8080:8080 (좩돌 λ°œμƒ) - **λ³€κ²½**: 8070:8080 (μ™ΈλΆ€ 8070 포트 μ‚¬μš©) ### μ΅œμ‹  Headscale μ„€μ • ν˜•μ‹ 적용 - `ip_prefixes` β†’ `prefixes` (v4/v6 뢄리) - `dns_config` β†’ `dns` (ꡬ쑰 λ³€κ²½) - `acl_policy_path` β†’ `policy.path` - OIDC `strip_email_domain` 제거 ### Docker ν—¬μŠ€μ²΄ν¬ κ°œμ„  - `curl` β†’ `nc` (netcat μ‚¬μš©) - Headplane μ˜μ‘΄μ„± 쑰건 μ™„ν™” ## πŸ” μ„€μΉ˜ 확인 및 검증 ### 1. μ»¨ν…Œμ΄λ„ˆ μƒνƒœ 확인 ```bash docker-compose ps ``` ### 2. Headscale API ν…ŒμŠ€νŠΈ ```bash curl -s http://localhost:8070/health # 응닡: {"status":"pass"} ``` ### 3. 둜그 확인 ```bash docker-compose logs headscale docker-compose logs headplane ``` ### 4. μ‚¬μš©μž 생성 ```bash docker-compose exec headscale headscale users create myuser ``` ### 5. μ‚¬μš©μž λͺ©λ‘ 확인 ```bash docker-compose exec headscale headscale users list ``` ### 6. Pre-auth ν‚€ 생성 ```bash docker-compose exec headscale headscale preauthkeys create --user 1 --reusable --expiration 24h ``` ## 🚨 문제 ν•΄κ²° ### 포트 좩돌 문제 ```bash # 8080 포트 μ‚¬μš© 쀑인 ν”„λ‘œμ„ΈμŠ€ 확인 lsof -i :8080 # 포트λ₯Ό 8070으둜 λ³€κ²½ν•˜μ—¬ ν•΄κ²° ``` ### Headplane μ„€μ • 파일 문제 ```bash # cookie_secret 길이 였λ₯˜ μ‹œ (μ •ν™•νžˆ 32자 ν•„μš”) echo "headscale-ui-secret-32-chars-key" | wc -c # 32자 확인 # μ„€μ • 파일 μž¬κ²€μ¦ docker-compose logs headplane --tail 10 # μ»¨ν…Œμ΄λ„ˆ μž¬μ‹œμž‘μœΌλ‘œ μ„€μ • μž¬λ‘œλ“œ docker-compose restart headplane ``` ### ν—¬μŠ€μ²΄ν¬ μ‹€νŒ¨ ```bash # wget λŒ€μ‹  netcat μ‚¬μš© # CMD-SHELL을 μ‚¬μš©ν•˜μ—¬ ν˜Έν™˜μ„± κ°œμ„  ``` ## πŸ“Š μ΅œμ’… μ„€μΉ˜ κ²°κ³Ό ### 접속 정보 - **Headscale API**: http://localhost:8070 - **Headplane UI**: http://localhost:3000/admin/ (둜그인 νŽ˜μ΄μ§€) - **μ™ΈλΆ€ 접속**: http://192.168.0.151:3000/admin/ (λ„€νŠΈμ›Œν¬ 섀정에 따라) - **λ©”νŠΈλ¦­μŠ€**: http://localhost:9090 ### μƒμ„±λœ 정보 - **μ‚¬μš©μž**: myuser (ID: 1) - **API ν‚€**: 8qRr1IB.tV95CmA0fLaCiGGIgBfeoN9daHceFkzI (μžλ™ 생성됨) - **Pre-auth ν‚€**: fc4f2dc55ee00c5352823d156129b9ce2df4db02f1d76a21 (24μ‹œκ°„ 유효, μž¬μ‚¬μš© κ°€λŠ₯) ### πŸ”‘ Headplane 둜그인 1. λΈŒλΌμš°μ €μ—μ„œ http://localhost:3000/admin/ λ˜λŠ” http://192.168.0.151:3000/admin/ 접속 2. **API Key** ν•„λ“œμ— μž…λ ₯: `8qRr1IB.tV95CmA0fLaCiGGIgBfeoN9daHceFkzI` 3. **Sign In** λ²„νŠΌ 클릭 ### λ„€νŠΈμ›Œν¬ μ„€μ • - **IPv4**: 100.64.0.0/10 - **IPv6**: fd7a:115c:a1e0::/48 - **Magic DNS**: headscale.local ## πŸ”„ Git 관리 ### 브랜치 μ „λž΅ ```bash # κΈ°λŠ₯ 브랜치 생성 git checkout -b feature/working-headscale-setup # 변경사항 컀밋 git add . git commit -m "πŸŽ‰ Working Headscale Setup Complete" # 원격 μ €μž₯μ†Œ ν‘Έμ‹œ git push -u origin feature/working-headscale-setup ``` ## πŸ“ˆ λ‹€μŒ 단계 1. Tailscale ν΄λΌμ΄μ–ΈνŠΈ μ—°κ²° ν…ŒμŠ€νŠΈ 2. HTTPS/TLS μΈμ¦μ„œ ꡬ성 3. Headplane ν•œκΈ€ν™” μž‘μ—… 4. ACL λ³΄μ•ˆ κ·œμΉ™ μ„€μ • 5. λ°±μ—… 및 λͺ¨λ‹ˆν„°λ§ ꡬ성 ## πŸŽ‰ κ²°λ‘  Headscaleκ³Ό Headplane을 μ‚¬μš©ν•œ μ™„μ „ν•œ 자체 ν˜ΈμŠ€νŒ… VPN μ†”λ£¨μ…˜μ΄ μ„±κ³΅μ μœΌλ‘œ κ΅¬μΆ•λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 이제 Tailscale을 μ™„μ „νžˆ λŒ€μ²΄ν•  수 μžˆλŠ” ν™˜κ²½μ΄ μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.