SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

WireGuard VPNをVPSに自分で構築する方法

Linux VPSでWireGuardを構築します。鍵生成、wg0.conf、IP転送、NAT、AllowedIPs、DNSに加え、KVMとLXCで異なる失敗原因やハンドシェイク不良も解説します。

構築するもの

自分で管理するサーバー上に WireGuard VPN を構築する場合、設定は約40行です。必要なのは、1組の鍵ペア、1つのインターフェースファイル、1つの sysctl 設定、1つの NAT ルール、1つのファイアウォールの許可です。インストールは簡単なので、このガイドでは主に、問題が発生する箇所、鍵の権限、AllowedIPs、転送、DNS について説明します。

WireGuard はカーネル内で動作する Layer 3 トンネルで、Linux 5.6 以降は mainline に含まれています。そのため、Ubuntu 24.04 と Debian 13 では外部モジュールなしで利用できます。暗号方式のネゴシエーション、認証局、ユーザー名とパスワードによる認証はありません。ピアは公開鍵と、その鍵で使用できる IP アドレスの組み合わせです。MAC の検証に失敗したパケットは応答なしで破棄されるため、ポートはスキャンに応答しません。一方、認証サーバーは存在しないため、アクセスを削除するにはサーバー上でピアを削除します。

仮想化方式を最初に確認する

WireGuard にはモジュールをロードできる kernel が必要です。KVM VPS では、通常そのまま動作します。ホストの kernel を共有するコンテナ型仮想化、OpenVZ、LXC では、最初のコマンドが RTNETLINK answers: Operation not supported で失敗します。この場合の代替手段は、wireguard-go userspace 実装です。最初に sudo modprobe wireguard && echo ok で確認します。

鍵を漏えいさせずに生成する

誰でも読み取れる /etc/wireguard/server.key は、VPN がないのと同じです。一般的な umask 077 && wg genkey | sudo tee ... の行でも動作します。sudoers で umask_override が設定されていない限り、デフォルトで無効です。sudo は独自のデフォルト値 0022 と umask を組み合わせますが、権限をそれより緩めることはありません。そのため、作成される tee のモードは 600 です。鍵が漏えいするのは、umask 077 を付けずにそのパイプを実行した場合、通常の 022 umask を使用している場合、または sudoers で umask_override が有効になっている場合です。モードを明示的に設定すれば、この違いは問題になりません。

sudo apt update && sudo apt install -y wireguard nftables
sudo install -d -m 700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey > /etc/wireguard/server.key'
sudo sh -c 'wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub'
sudo chmod 600 /etc/wireguard/server.key

クライアントの鍵ペアも同じ方法で生成します。wg genpsk は任意の事前共有鍵を追加します。各設定ファイルに 1 行ずつ追加されます。

サーバーのインターフェース: /etc/wireguard/wg0.conf

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/server.key>

[Peer]
PublicKey = <laptop public key>
PresharedKey = <psk, optional>
AllowedIPs = 10.8.0.2/32

chmod 600 です。ファイルが全ユーザーからアクセス可能であるという起動時の警告が表示される場合は、この設定を省略しています。Address はトンネル内でのサーバーのアドレスで、VPN サブネット全体のマスクを含みます。実際のネットワークで使われない範囲を選んでください。192.168.1.0/24 はクライアントが接続する家庭用ルーターの半数と競合し、その場合、トンネルの経路はローカルルートに負けて通信が黙って失敗します。

サーバー側でピアの AllowedIPs は /32 です。これは、そのクライアントが所有するトンネルアドレスです。2 つのピアに同じ許可 IP を指定すると、最後に設定したピアへ移動します。先に設定したピアは、どこにもエラーを出さずに通信を受信できなくなります。SaveConfig は未設定のままにしてください。設定すると、wg-quick down が稼働中の状態に基づいてこのファイルを書き換えます。

ボックスをルーターにする

Linux サーバーは、自身宛てでないパケットを破棄します。転送と送信元 NAT は、デフォルトではどちらも無効です。

printf 'net.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1\n' \
  | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
sysctl net.ipv4.ip_forward

基本的な sysctl -w は、次回の再起動までは動作しますが、その後は何も表示せず停止します。NAT には、インターネットへ接続する NIC である egress インターフェースが必要です。wg0 ではありません。eth0 だと決めつけず、ip route show default から自分の環境の値を取得してください。現在のイメージでは、enp1s0 や ens3 のような名前が使われます。

ファイアウォール: ポートと転送経路

1 つの nftables ファイルで、フィルタリングと NAT を扱います。/etc/nftables.conf を実行すると、既存のルールセットが フラッシュ されます。そのため、ufw または Docker がすでに管理しているホストでは実行しないでください。

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    ct state established,related accept
    iif lo accept
    tcp dport 22 accept
    udp dport 51820 accept
  }
  chain forward {
    type filter hook forward priority filter; policy drop;
    ct state established,related accept
    iifname "wg0" oifname "enp1s0" accept
  }
}

table ip nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    ip saddr 10.8.0.0/24 oifname "enp1s0" masquerade
  }
}

SSH の別セッションを開いたまま、sudo systemctl enable --now nftables で適用します。policy drop に加えて SSH ルールを誤記すると、自分のサーバーから締め出されます。forward chain が許可していない内容、つまり wg0 から wg0 への通信を確認してください。ピアはインターネットにはアクセスできますが、互いにはアクセスできません。ピア間 VPN が必要な場合は iifname "wg0" oifname "wg0" accept を追加します。サーバー自体でピアがアクセスできる対象は、この chain の役割ではありません。サーバー宛てのパケットは input hook を通り、forward には進まないため、input chain が判断します。現在の設定では、ピアは SSH にアクセスできますが、それ以外は許可されません。10.8.0.1 への ping も許可されません。そこに iifname "wg0" icmp type echo-request accept を追加し、トンネル経由だけで到達可能にしたいサービスごとに iifname "wg0" ルールを 1 つ追加します。これは、サーバーが tmux で Claude Code を実行するリモート開発ボックスも兼ねていて、その側を公開したくない場合に重要です。

ufw を使用するホストでは、ufw allow 51820/udp、/etc/default/ufw に DEFAULT_FORWARD_POLICY="ACCEPT"、さらに /etc/ufw/before.rules の先頭に *nat POSTROUTING MASQUERADE ルールを追加します。

systemd で起動する

sudo systemctl enable --now wg-quick@wg0
sudo wg show

wg-quick はインターフェースを作成し、アドレスを追加し、AllowedIPs から導出したルートを設定します。重要なのは enable --now です。手動で実行した wg-quick up wg0 は、次回の再起動後には失われます。カーネルのアップグレードでも再起動が発生します。再起動後に起動し直せない unit は、誰かが接続を試みるまで何も知らせません。そのため、wg-quick@wg0 に OnFailure= の drop-in を設定し、自分で用意した ntfy サーバーへ通知する方法が最も低コストです。接続できなくなったユーザーから知らされるのではなく、スマートフォンで異常を把握できます。

クライアント設定と、誰もが間違える設定

[Interface]
PrivateKey = <laptop private key>
Address = 10.8.0.2/32
DNS = 10.8.0.1

[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

AllowedIPs には 2 つの異なる役割があり、この 2 つを混同することが WireGuard に関する混乱の主な原因です。

送信方向では、ルーティングテーブルです。 宛先がピアの AllowedIPs に一致するパケットは暗号化され、そのピアへ送信されます。0.0.0.0/0, ::/0 はすべての通信をトンネル経由にします。これはフルトンネルで、サーバーをデフォルトルートにする構成です。スプリットトンネルでは、より限定したリストを指定します。AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 は VPN トラフィックと、サーバーの背後にある 1 つのプライベートネットワークを通し、それ以外の通信はローカルルートを使用します。この限定したリストにより、サービスをパブリックインターネットから完全に切り離せます。たとえば、トンネルアドレスにバインドした VPS 上のプライベートな Nextcloud インスタンスや、同じホスト上で動作する ネスト型仮想化のラボ VMを、ピアからは到達可能にし、それ以外の利用者からは見えない状態にできます。

受信方向では、アクセス制御リストです。 復号したパケットの送信元アドレスが、そのピアの AllowedIPs に含まれていない場合、そのパケットは破棄されます。そのため、サーバーではノート PC に対して 10.8.0.2/32 を指定します。そこに 0.0.0.0/0 を指定すると、そのクライアントはトンネル内の任意のアドレスを偽装できてしまいます。

PersistentKeepalive は、NAT 配下のクライアントで使用します。ルーターは、パケットが流れている間だけ UDP マッピングを維持するためです。マッピングの有効期限が切れると、サーバーはクライアントに到達できなくなります。PersistentKeepalive = 25 はマッピングを維持します。パブリック IP を持つサーバーではなく、クライアント側に設定してください。

DNS と、誰も気付かない漏えい

AllowedIPs = 0.0.0.0/0 を使用し、DNS = の行が ない 場合、クライアントはローカルネットワークから取得したリゾルバー、つまり 192.168.1.1 のカフェのルーターを使い続けます。この経路はデフォルト経路より具体的なため、DNS クエリは暗号化されていない状態でローカルリンクから送信され、それ以外の通信だけがトンネル経由になります。通信の内容は非公開でも、名前の一覧は非公開になりません。

正直な選択肢は2つあります。DNS をパブリックリゾルバー (DNS = 9.9.9.9) に向けると、クエリはトンネルを通ってサーバーから外部へ出ます。ただし、そのリゾルバーからはクエリを確認できます。もう1つは、unbound または dnsmasq を 10.8.0.1 にバインドして実行し、DNS = 10.8.0.1 を設定し、入力チェーンに udp dport 53 iifname "wg0" accept を追加する方法です。その行を設定してリゾルバーを無視すると、名前解決は一切行われません。

Linux クライアントでは wg-quick が DNS を resolvconf に適用します。これがない場合は resolvconf: command not found になります。openresolv をインストールするか、systemd-resolved クライアントで PostUp = resolvectl dns %i 10.8.0.1 を設定してください。

トンネルを停止せずにピアを追加・削除する

ユーザーを追加するためにインターフェースを再起動すると、接続中の全員が切断されます。[Peer]ブロックをwg0.confに追記し、その後、ピア一覧を稼働中のまま再読み込みします。

sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'

wg-quick stripは、wg-quick専用のキー(Address、DNS、PostUp)を除いた設定を出力し、syncconfは稼働中のセッションを維持したまま差分を適用します。更新されるのはピアだけです。Addressを変更した場合は、完全な停止・起動が必要です。sudo wg set wg0 peer <public key> removeで失効させた後、ファイルからブロックを削除してください。削除しないと、次回の再読み込みで再び追加されます。

障害パターンと表示される文字列

Handshake が完了しない。 wg show に相手が表示されますが、latest handshake はありません。クライアントのログは次のとおりです。

Handshake for peer 1 (10.0.0.10:51820) did not complete after 5 seconds, retrying (try 2)

パケットが届いていないか、受け入れられていません。次の順に確認します。VPS のファイアウォールと、プロバイダーのネットワークファイアウォール(多くの管理パネルでは別の設定項目)の両方で UDP 51820 が開いているか。Endpoint のアドレスとポートが正しいか。鍵を取り違えていないか。クライアントの [Peer] ブロックには サーバーの公開鍵 を指定する必要があります。秘密鍵やクライアント自身の公開鍵を貼り付けると、まったく同じ症状になります。サーバー上の sudo tcpdump -ni any udp port 51820 で、パケットが到着しているかどうかを確認できます。カーネルモジュールはデフォルトではログを出力しません。WireGuard のメッセージは、動的デバッグ(echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control)を有効にした後にのみ dmesg に表示されます。有効にすると、鍵の不一致は invalid-MAC drop として記録されます。

Handshake は成功するが、インターネットに接続できない。 ping 10.8.0.1 は成功しますが、ping 1.1.1.1 はタイムアウトします。転送または NAT が設定されていません。sysctl net.ipv4.ip_forward が 1 になっていることを確認し、クライアントから ping を実行しながら sudo nft list ruleset または sudo iptables -t nat -L POSTROUTING -n -v でカウンターを監視します。masquerade ルールのパケット数が 0 の場合、送信インターフェース名が間違っています。カウンターが増えるのに応答がない場合は、forward chain のポリシーを確認します。

インターネットには接続できるが、名前を解決できない。 ping 1.1.1.1 は成功しますが、curl https://example.com は Could not resolve host を返します。DNS 行がないか、トンネル内から到達できない resolver を指定しています。

一部の HTTPS サイトで接続が停止する。 SSH と ping は正常ですが、大きなページの読み込みが停止します。原因はパス MTU です。トンネルによってオーバーヘッドが追加され、中継経路上の一部のリンクが、ICMP メッセージを返さずにサイズ超過のパケットを破棄しています。クライアントの [Interface] に MTU を設定します。通常の 1500 バイトのリンクでは、wg-quick がすでに 1420(1500 からオーバーヘッド 80 バイトを引いた値)を選ぶため、1420 を記述しても変わりません。1380 を試し、次に 1280 を試します。MTU を下げると停止は解消するもののスループットが期待どおりにならない場合は、切りのよい数値を推測し続けないでください。二分探索で実際のパス MTU を特定し、TCP MSS を制限する手順を実施します。この手順では、原因がトンネル以外にある場合も切り分けられます。

インターフェースが起動しない。 Address already in use は、別のプロセスが UDP 51820 を使用していることを示します。失敗した up の後に Cannot find device wg0 が表示される場合は、設定が拒否されています。journalctl -u wg-quick@wg0 -n 50 を確認してください。

Streisand または OpenVPN からの移行

Streisand は保守されておらず、リポジトリもアーカイブされています。廃止された自動化で VPN を運用し続けると、セキュリティ上の問題が徐々に蓄積します。インプレースアップグレードはできません。また、OpenVPN の PKI を変換することもできません。WireGuard には証明書、CA、有効期限がないため、各クライアントに新しい鍵ペアを発行します。

並行して移行します。同じサーバー上で、WireGuard は UDP 51820、OpenVPN は 1194 を使用して共存できます。wg0 を構築し、クライアントを 1 台ずつ移行してから、古いサービスを停止します。OpenVPN のユーザー名・パスワードと失効モデルは引き継げません。アカウント管理や監査証跡が必要な場合は、WireGuard の上位層にその機能を追加します。

バックアップ、アップグレード、大規模運用で負荷になる点

/etc/wireguard が サーバーそのものです。バックアップを取得し(sudo tar czf wg-backup.tgz -C /etc wireguard、mode 600、ホスト外に保管)、新しい VPS 上に数分で再構築できるようにします。サーバーの private key を失うと、すべてのクライアント設定を再発行する必要があります。クライアントはサーバーの public key を固定しているためです。アップグレードは通常の apt upgrade と、kernel 更新時の再起動だけで済みます。wg-quick@wg0 を有効にしていれば、再起動後に自動的に戻ります。

ピアごとの状態は小さく、暗号処理は kernel で実行されます。そのため上限を決めるのは、この設定内の要素ではなく、VPS の CPU と帯域幅の上限です。公開されている数値をそのまま信頼せず、トンネル上で iperf3 を実行して測定してください。大規模運用で負荷になるのは、運用作業です。すべてのピアに一意のトンネル IP が必要です。60 個の [Peer] ブロックを手作業で編集すると、重複した AllowedIPs が入りやすくなります。スクリプトから設定を生成してください。1 台のサーバーは 1 つの UDP エンドポイントであり、1 つの障害点です。WireGuard にはクラスタリング機能がありません。冗長化するには、固有の鍵を持つ 2 台目のサーバーが必要です。鍵のローテーションも手動のままなので、誰がどの鍵を保持しているか、また 1 つを失効させる方法を記録してください。その管理情報がテキストファイルでは足りなくなったら、通常は同じ kernel の data plane 上に control plane を追加します。自己ホスト型の NetBird サーバーなら、アドレス割り当て、ピアへの配布、手作業で行っていた setup key の管理を処理できます。control plane 自体を運用するサーバーが 1 台増えるのも避けたい場合は、Tailscale が代わりに 1 つをホストします。無料の Personal プランでは、6 users、無制限の user devices、開始時点で最大 50 個の tagged resources(サーバーなど)が対象となるため、個人で運用するホスト群の多くは料金を支払わずに済みます。それを超えると、料金はマシン数ではなくユーザー数に応じて発生します。つまり、家庭や小規模チームが実際に支払う金額は、wg0.conf に手作業で登録するピア数ではなく、ログインを持つ人の数で決まります。その側では、split-tunnel の AllowedIPs 管理が、subnet router からプライベートレンジを広告する構成に変わります。1 台の VPS から 1 度だけ通知し、すべてのクライアントファイルに貼り付ける代わりに、中央で承認します。この移行に価値があるかどうかは、ホスト型 control plane が実際に到達できる範囲で決まります。トラフィックを暗号化する鍵を保持することはありませんが、どのピアが互いを認識するかは決定します。

これらすべてには、自分で管理する Linux ボックス、public IP、module をロードできる kernel、そして端から端まで自分で管理する firewall が必要です。

FAQ

なぜ WireGuard のハンドシェイクが完了しないのですか?

wg show でピアをリストしても latest handshake が表示されない場合、パケットが到達していないか、受け入れられていません。VPS のファイアウォールとプロバイダー側のネットワークファイアウォールの両方で UDP 51820 番ポートを確認し、Endpoint のホストとポートが正しいか確認してください。次に、鍵が入れ替わっていないか確認します。クライアントの [Peer] ブロックには、サーバーの「公開鍵」を記述する必要があります。サーバー上で sudo tcpdump -ni any udp port 51820 を実行するとパケットが到達しているか確認できます。dmesg は動的デバッグ(echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control)を有効にした後にのみ WireGuard のハンドシェイク失敗を報告するため、鍵の不一致は invalid-MAC としてドロップされます。

トンネルは接続されますがインターネットに繋がりません。何が不足していますか?

ping 10.8.0.1 は動作するが ping 1.1.1.1 がタイムアウトする場合、転送設定か NAT に問題があります。sysctl net.ipv4.ip_forward が 1 になっていること、およびそれが /etc/sysctl.d/ に設定されていることを確認してください。再起動で消える sysctl -w だけの設定では不十分です。次に、ip route show default を実行して実際の外部インターフェース名を確認し、マスカレードルールを設定します。通常は enp1s0 や ens3 ですが、まれに eth0 であることもあります。

クライアント設定に DNS = 行は必要ですか?

フルトンネル構成で DNS = 行がない場合、クライアントはローカルネットワークから取得したリゾルバーを使い続けます。その結果、他の通信はトンネルされても、DNS クエリだけはローカルリンクを介して平文で送信されます。DNS にパブリックリゾルバーを指定するか、10.8.0.1 で待ち受ける unbound/dnsmasq を実行し、入力チェーンで udp dport 53 iifname "wg0" を許可してください。

AllowedIPs は具体的に何を制御しますか?

これは2つの役割を担います。送信方向ではルーティングテーブルとして機能し、ピアの AllowedIPs に一致するトラフィックを暗号化してそのピアへ送信します。受信方向ではアクセス制御リストとして機能し、復号されたパケットの送信元がそのピアの AllowedIPs の範囲外であればドロップします。そのため、サーバー側ではクライアントごとに /32 をリストし、クライアント側では 0.0.0.0/0 をリストする構成をとることがあります。

WireGuard はどの VPS でも動作しますか?

KVM VPS であればカーネルモジュールがそのまま動作し、追加設定は不要です。OpenVZ や LXC のようにホストカーネルを共有するコンテナ仮想化環境では、modprobe wireguard は Operation not supported エラーで失敗するため、wireguard-go ユーザー空間実装へのフォールバックが必要です。作業の前に必ず sudo modprobe wireguard && echo ok を実行してください。