diff options
| author | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-06-30 22:29:07 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-06-30 22:29:07 +0900 |
| commit | 58f7917662ff92a3712289acccff8310c8f1baff (patch) | |
| tree | cf7290a3eaf0e08c170199ac831f562949785014 | |
| parent | 1ad98cc2f9f8a5950aec998a6daacec381cbe2c4 (diff) | |
Add strongSwan IKEv2 VPN service
- vpn/: Dockerfile, docker-compose.yml, swanctl.conf.tpl, entrypoint.sh
- install.sh: Caddy 証明書流用 + コンテナ起動
- renew-cert.sh: 証明書更新 + swanctl ホットリロード
- caddy/Caddyfile: vpn.yyamashita.com 追加(Let's Encrypt 証明書取得用)
- post-receive: vpn/deploy.sh を追加
- setup-route53.py: WEB_SUBDOMAINS に vpn を追加
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | CLAUDE.md | 42 | ||||
| -rw-r--r-- | caddy/Caddyfile | 6 | ||||
| -rwxr-xr-x | git/hooks/hetzner-infra/post-receive | 1 | ||||
| -rwxr-xr-x | mail/dns/setup-route53.py | 1 | ||||
| -rw-r--r-- | vpn/.gitignore | 2 | ||||
| -rw-r--r-- | vpn/Dockerfile | 7 | ||||
| -rw-r--r-- | vpn/deploy.sh | 11 | ||||
| -rw-r--r-- | vpn/docker-compose.yml | 14 | ||||
| -rw-r--r-- | vpn/entrypoint.sh | 54 | ||||
| -rw-r--r-- | vpn/install.sh | 59 | ||||
| -rw-r--r-- | vpn/renew-cert.sh | 18 | ||||
| -rw-r--r-- | vpn/strongswan.conf | 15 | ||||
| -rw-r--r-- | vpn/swanctl.conf.tpl | 41 |
13 files changed, 270 insertions, 1 deletions
@@ -11,12 +11,13 @@ git/ ベアリポジトリ・フック管理(repos.txt, hooks/, ins claude/ Claude Code セッション管理(sessions.txt, systemd unit, sync.sh) hetzner-admin/ サーバー管理 Web UI(app.py, Dockerfile, docker-compose.yml, deploy.sh) mail/ MTA(Postfix + OpenDKIM, docker-compose.yml, install.sh, dns/setup-route53.py) +vpn/ strongSwan IKEv2 VPN(Dockerfile, docker-compose.yml, install.sh, renew-cert.sh) server/ サーバー共通設定(authorized_keys, requirements.md) ``` ## デプロイの仕組み -- `git push origin master` → post-receive フック → `caddy/deploy.sh` / `cgit/deploy.sh` / `mail/deploy.sh` / `hetzner-admin/deploy.sh` を実行 +- `git push origin master` → post-receive フック → `caddy/deploy.sh` / `cgit/deploy.sh` / `mail/deploy.sh` / `hetzner-admin/deploy.sh` / `vpn/deploy.sh` を実行 - cgit の `lighttpd.conf` / `Dockerfile` 変更はイメージ再ビルドが必要(`deploy.sh` で `--build` するため push だけで反映される) - cgit の `cgitrc` はボリュームマウントのため push だけで反映される - フック・リポジトリの変更は push 後に `bash /app/infra/git/install.sh` を手動実行 @@ -85,4 +86,43 @@ EOF bash /app/infra/hetzner-admin/install.sh # → SSH鍵生成 + authorized_keys登録(command= 制限付き)+ コンテナ起動 # → hetzner-admin.yyamashita.com でアクセス可能 + +# VPN 初回セットアップ(サーバー上で実行) +# 前提: ファイアウォールで UDP 500, 4500 を開放済み +git push origin master +ssh root@<server> 'bash /app/infra/git/install.sh' # post-receive フック更新 +# → Caddy が vpn.yyamashita.com の証明書を取得するまで数分待つ +ssh root@<server> +cat > /app/infra/vpn/users.conf <<EOF +user1 <strong-password> +EOF +chmod 600 /app/infra/vpn/users.conf +bash /app/infra/vpn/install.sh +# → Caddy の証明書を流用 + コンテナ起動 +# → vpn.yyamashita.com:500/4500 (IKEv2) で接続可能 + +# VPN 証明書更新(Let's Encrypt 更新後 90 日ごと) +ssh root@<server> 'bash /app/infra/vpn/renew-cert.sh' + +# VPN ユーザー追加・変更 +ssh root@<server> +vi /app/infra/vpn/users.conf # 1行1ユーザー: "username password" +docker compose -f /app/infra/vpn/docker-compose.yml restart ``` + +## VPN クライアント設定 + +**iOS / macOS** (ネイティブ IKEv2): +- サーバー: `vpn.yyamashita.com` +- タイプ: IKEv2 +- 認証: ユーザー名 + パスワード (EAP) +- 証明書インストール不要(Let's Encrypt は自動信頼) + +**Meta Quest 3S / FireTV** (Android): +- strongSwan VPN Client アプリをインストール + - Quest: sideload `org.strongswan.android` APK + - FireTV: 同様に APK をサイドロード +- プロファイル設定: + - タイプ: IKEv2 EAP (Username/Password) + - サーバー: `vpn.yyamashita.com` + - ユーザー名・パスワード: `users.conf` の内容 diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 86242b8..66a9d05 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -40,3 +40,9 @@ blog.yyamashita.com { header X-Robots-Tag "noindex, nofollow" reverse_proxy microblog-app:3000 } + +vpn.yyamashita.com { + header Referrer-Policy "no-referrer" + header X-Robots-Tag "noindex, nofollow" + respond "VPN Server" 200 +} diff --git a/git/hooks/hetzner-infra/post-receive b/git/hooks/hetzner-infra/post-receive index 190df69..6eeff49 100755 --- a/git/hooks/hetzner-infra/post-receive +++ b/git/hooks/hetzner-infra/post-receive @@ -5,4 +5,5 @@ bash /app/infra/caddy/deploy.sh bash /app/infra/cgit/deploy.sh bash /app/infra/mail/deploy.sh bash /app/infra/hetzner-admin/deploy.sh +bash /app/infra/vpn/deploy.sh echo "Deploy complete: hetzner-infra" diff --git a/mail/dns/setup-route53.py b/mail/dns/setup-route53.py index 2a6aee9..f42e273 100755 --- a/mail/dns/setup-route53.py +++ b/mail/dns/setup-route53.py @@ -37,6 +37,7 @@ WEB_SUBDOMAINS = [ "git", "hetzner-admin", "blog", + "vpn", ] diff --git a/vpn/.gitignore b/vpn/.gitignore new file mode 100644 index 0000000..b68b01a --- /dev/null +++ b/vpn/.gitignore @@ -0,0 +1,2 @@ +certs/ +users.conf diff --git a/vpn/Dockerfile b/vpn/Dockerfile new file mode 100644 index 0000000..86644be --- /dev/null +++ b/vpn/Dockerfile @@ -0,0 +1,7 @@ +FROM alpine:3.21 +RUN apk add --no-cache strongswan iptables iproute2 gettext +COPY swanctl.conf.tpl /etc/swanctl/swanctl.conf.tpl +COPY strongswan.conf /etc/strongswan.conf +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/vpn/deploy.sh b/vpn/deploy.sh new file mode 100644 index 0000000..0005d9a --- /dev/null +++ b/vpn/deploy.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +# 初期化前はスキップ(install.sh を先に実行する必要がある) +if [ ! -f users.conf ] || [ ! -f certs/server.crt ]; then + echo "VPN: 未初期化のためスキップ (install.sh を実行してください)" + exit 0 +fi + +docker compose up -d --build diff --git a/vpn/docker-compose.yml b/vpn/docker-compose.yml new file mode 100644 index 0000000..83ff270 --- /dev/null +++ b/vpn/docker-compose.yml @@ -0,0 +1,14 @@ +services: + vpn: + build: . + restart: unless-stopped + network_mode: host + cap_add: + - NET_ADMIN + - SYS_MODULE + volumes: + - ./certs/server.crt:/etc/swanctl/x509/server.crt:ro + - ./certs/server.key:/etc/swanctl/private/server.key:ro + - ./users.conf:/etc/strongswan/users.conf:ro + environment: + - VPN_DOMAIN=vpn.yyamashita.com diff --git a/vpn/entrypoint.sh b/vpn/entrypoint.sh new file mode 100644 index 0000000..4efe500 --- /dev/null +++ b/vpn/entrypoint.sh @@ -0,0 +1,54 @@ +#!/bin/sh +set -e + +# IP フォワーディング有効化 +sysctl -w net.ipv4.ip_forward=1 +sysctl -w net.ipv6.conf.all.forwarding=1 + +# VPN クライアントのトラフィックを MASQUERADE +IFACE=$(ip route show default | awk '/default/ {print $5}' | head -1) +iptables -t nat -C POSTROUTING -s 10.10.0.0/24 -o "${IFACE}" -j MASQUERADE 2>/dev/null || \ + iptables -t nat -A POSTROUTING -s 10.10.0.0/24 -o "${IFACE}" -j MASQUERADE +iptables -C FORWARD -s 10.10.0.0/24 -j ACCEPT 2>/dev/null || \ + iptables -A FORWARD -s 10.10.0.0/24 -j ACCEPT +iptables -C FORWARD -d 10.10.0.0/24 -j ACCEPT 2>/dev/null || \ + iptables -A FORWARD -d 10.10.0.0/24 -j ACCEPT + +# users.conf から EAP シークレットセクションを生成 +VPN_SECRETS="" +while IFS=' ' read -r user pass || [ -n "$user" ]; do + [ -z "$user" ] && continue + case "$user" in \#*) continue ;; esac + VPN_SECRETS="${VPN_SECRETS} + eap-${user} { + id = ${user} + secret = \"${pass}\" + }" +done < /etc/strongswan/users.conf + +export VPN_SECRETS +export VPN_DOMAIN="${VPN_DOMAIN:-vpn.yyamashita.com}" + +# テンプレートから swanctl.conf を生成 +envsubst < /etc/swanctl/swanctl.conf.tpl > /etc/swanctl/swanctl.conf + +# charon (IKE デーモン) 起動 +ipsec start + +# 初期化待ち +sleep 2 + +# 設定ロード +swanctl --load-conns +swanctl --load-creds --noprompt +swanctl --load-pools + +echo "VPN ready. Domain: ${VPN_DOMAIN}" + +# シグナルハンドラ +trap 'ipsec stop; exit 0' TERM INT + +# charon が生きている間ループ +while ipsec status > /dev/null 2>&1; do + sleep 10 +done diff --git a/vpn/install.sh b/vpn/install.sh new file mode 100644 index 0000000..9a2f33d --- /dev/null +++ b/vpn/install.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +DOMAIN="vpn.yyamashita.com" +CADDY_DIR="/app/infra/caddy" +CERT_DIR="./certs" +CADDY_CERT_BASE="/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}/${DOMAIN}" + +echo "=== VPN Install: ${DOMAIN} ===" + +# users.conf チェック +if [ ! -f users.conf ]; then + echo "" + echo "Error: users.conf が見つかりません。先に作成してください:" + echo " echo 'username password' > /app/infra/vpn/users.conf" + echo " chmod 600 /app/infra/vpn/users.conf" + echo "" + echo "複数ユーザーは1行ずつ追加:" + echo " user1 pass1" + echo " user2 pass2" + exit 1 +fi + +mkdir -p "$CERT_DIR" + +# Caddy コンテナから証明書を取得 +echo "Caddy から証明書を取得中..." +if ! docker compose -f "$CADDY_DIR/docker-compose.yml" exec -T caddy \ + cat "${CADDY_CERT_BASE}.crt" > "$CERT_DIR/server.crt"; then + echo "" + echo "Error: 証明書の取得に失敗しました。" + echo "Caddyfile に ${DOMAIN} が追加されているか確認してください。" + echo "追加後 'git push origin master' してから再実行してください。" + exit 1 +fi + +docker compose -f "$CADDY_DIR/docker-compose.yml" exec -T caddy \ + cat "${CADDY_CERT_BASE}.key" > "$CERT_DIR/server.key" + +chmod 600 "$CERT_DIR/server.key" +echo "証明書を $CERT_DIR/ に保存しました。" + +# VPN コンテナ起動 +echo "VPN サービスを起動中..." +docker compose up -d --build + +echo "" +echo "=== VPN 起動完了 ===" +echo "" +echo "クライアント接続情報:" +echo " サーバー: ${DOMAIN}" +echo " 種別: IKEv2" +echo " 認証: EAP (ユーザー名/パスワード)" +echo "" +echo "iOS/macOS: 設定 > VPN > VPN 構成を追加 > IKEv2" +echo "Android (Quest/FireTV): strongSwan VPN Client アプリをインストール" +echo "" +echo "証明書の更新: bash /app/infra/vpn/renew-cert.sh" diff --git a/vpn/renew-cert.sh b/vpn/renew-cert.sh new file mode 100644 index 0000000..50f8784 --- /dev/null +++ b/vpn/renew-cert.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +DOMAIN="vpn.yyamashita.com" +CADDY_DIR="/app/infra/caddy" +CADDY_CERT_BASE="/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}/${DOMAIN}" + +echo "証明書を更新中..." + +docker compose -f "$CADDY_DIR/docker-compose.yml" exec -T caddy \ + cat "${CADDY_CERT_BASE}.crt" > certs/server.crt +docker compose -f "$CADDY_DIR/docker-compose.yml" exec -T caddy \ + cat "${CADDY_CERT_BASE}.key" > certs/server.key +chmod 600 certs/server.key + +docker compose exec vpn swanctl --load-creds --noprompt +echo "完了。証明書を更新してリロードしました。" diff --git a/vpn/strongswan.conf b/vpn/strongswan.conf new file mode 100644 index 0000000..b42f8c0 --- /dev/null +++ b/vpn/strongswan.conf @@ -0,0 +1,15 @@ +charon { + load_modular = yes + plugins { + include strongswan.d/charon/*.conf + } + filelog { + stderr { + default = 1 + ike = 2 + knl = 3 + } + } + send_vendor_id = no +} +include strongswan.d/*.conf diff --git a/vpn/swanctl.conf.tpl b/vpn/swanctl.conf.tpl new file mode 100644 index 0000000..f3a9e2e --- /dev/null +++ b/vpn/swanctl.conf.tpl @@ -0,0 +1,41 @@ +connections { + ikev2-eap { + version = 2 + # iOS/macOS/Android 互換の暗号スイート + proposals = aes256gcm16-prfsha256-ecp256,aes256-sha256-ecp256,aes256gcm16-prfsha384-ecp384,aes256-sha256-modp2048,aes256-sha1-modp2048 + + local { + auth = pubkey + certs = server.crt + id = ${VPN_DOMAIN} + } + + remote { + auth = eap-mschapv2 + eap_id = %any + } + + children { + vpn { + local_ts = 0.0.0.0/0 + esp_proposals = aes256gcm16,aes256-sha256,aes256-sha1 + dpd_action = clear + } + } + + pools = pool4 + send_certreq = no + dpd_delay = 30s + } +} + +pools { + pool4 { + addrs = 10.10.0.0/24 + dns = 1.1.1.1,8.8.8.8 + } +} + +secrets { +${VPN_SECRETS} +} |
