wireproxyでDockerから--cap-addせずにWireGuardに繋ぐ

はじめに

試用期間を無事に乗り越えた

お久しぶりです。今年の7月からさくらインターネットで働き始めておりました。

febc-yamamoto.hatenablog.jp

先月で試用期間が終わり、今月から正式採用となりました。
無事に試用期間を乗り切れて一安心です。

担当業務は?

所属部署はこれまで通りSRE室です。
ボスであるkazeburoさんをはじめ頼もしい同僚たちに囲まれて毎日楽しく仕事しております。

そんな中での私の業務はというと、引き続きUsacloudTerraformプロバイダーといったOSSの開発をしつつSREとしての業務も担当しています。

Embedded SRE/Enabling SREとして、開発/運用の両者が共通のゴールをもって、運用性に優れたソフトウェアを開発すべくさまざまな取り組みをしています。

今日は最近のSREとしての取り組みの中から、デプロイの自動化にまつわる話を紹介させていただきます。

本題: wireproxyでDockerコンテナ内からWireGuard VPNに繋ぐ

SREとしての取り組みの一環として、これまで手動で行われていたとあるアプリケーションのデプロイ作業を自動化しました。

CI/CDという取り組みは既に一般的に広まっており、私自身これまで何度もデプロイの自動化を行なってきました。
今回対象にしたアプリケーションは比較的小規模で単純なGo製のアプリケーションでしたので、割とすんなりと自動化できるだろうと思っていたのですが少々引っかかる点もありました。

というのも、今回のデプロイ対象サーバへはWireGuard VPNを経由して接続する必要があり、さらにデプロイに用いているDrone上で起動されるDockerコンテナからそれを行う必要があるというのが難点でした。

wireproxyというツールを併用することでこの問題を解決しましたので以下で今回のシステム構成〜使い方まで含めて紹介します。

github.com

今回のシステム構成

今回の構成は以下の通りです。

アプリケーションのソースコードは社内ネットワーク上のGHEに置かれており、CI/CDには同じく社内ネットワーク上においたDroneを利用しています。
DroneではDockerパイプラインを利用しています。

今回デプロイ対象のシステムはさくらのクラウドに置いており、VPCルータを用いてVPCを構築、その中にサーバを複数台置くという構成です。

VPCルータのWireGuardサーバ機能でVPNを構築している

今回の構成ではVPNのためにVPCルータのWireGuardサーバ機能を利用しています。 VPCの中のサーバには外部から直接SSH接続出来ないようにしており、VPCルータとWireGuardでVPN接続してからサーバに接続する形となっています。

サーバ上で動かすアプリケーションはWeb APIを提供するもので、クライアントからのアクセス経路は別途確保する必要がありますが、今回のデプロイの話には関係ないので記載を省略しました。

問題: DockerからどうやってWireGuard VPNに繋ぐ?

普通にdocker runするとエラーになる

上記の通り、VPCの中のサーバにSSH接続するにはまずWireGuardでVPN接続する必要があります。
これをDroneのDockerパイプラインからやろうとするとエラーになってしまいます。

以下は手元でエラーを再現してみたものです。

# docker run -it --rm ubuntu:22.04

# 必要なものをインストール
$ apt-get update; apt-get install -y iproute2 wireguard

# インターフェース作成
$ ip link add dev wg0 type wireguard
RTNETLINK answers: Operation not permitted

RTNETLINK answers: Operation not permittedって怒られてしまいますね。

権限が足りない -> --cap-add NET_ADMIN or --privilegedで実行できる

これは権限が足りないからで、docker runする時に--cap-add NET_ADMINを指定することで実行できるようになります。
参考: Docker run リファレンス - Linuxケイパビリティ

--privilegedでも良いですが、不要な権限は与えない方が良いでしょう。
Dockerのドキュメントにも以下のように書かれてます。

ネットワーク・スタックとやりとりするには、 --privileged を使う替わりに、ネットワーク・インターフェースの変更には --cap-add=NET_ADMIN を使うべきでしょう。

DockerはOK、次にDroneではどうすればいい?

Droneも--cap-add相当のことができる設定があればよさそうです。 しかしドキュメントを見る限りどうも--cap-addは出来ないようです。
代わりに--privilegedであれば指定できますが、Drone上で該当リポジトリをTrustedとしてマークする必要があります。
(各リポジトリSettingsから設定可能です)

これで一応対応出来るとはいえデプロイのためにコンテナへ特権を与えたくないです。
ということで別の方法を探します。

別の方法: wireproxyを使う

この件についてSRE室の朝会(業務内容の共有だったり雑談したりする会)で話題に出したところ、kazeburoさんから「wireproxyというのがあるよ」と教えていただきました。

github.com

wireproxyとは

READMEにはwireproxy is a completely userspace application that connects to a wireguard peer, and exposes a socks5 proxyと書かれています。
どう実装されているのでしょうか?

ざっと見たところ以下のものを使ってユーザースペースでの実装を実現しているようです。

wireguard-go/tun/netstackについてはこちらの記事でも触れられていますね。
0x6b.github.io

これなら--cap-add NET_ADMINしなくても大丈夫そうです。

ちなみにSOCKS5プロキシの実装にはこちらが使われています。

ということで早速試してみます。

wireproxyを手元から試してみる

WireGuardのクライアント設定ファイルを用意する

wireproxyはWireGuardクライアント設定ファイルを読み込めますので、まずは動くクライアント設定ファイルを用意した上でwireproxyの設定という手順で進めます。 まずWireGuardクライアント設定ファイルを用意し、その設定でWireGuardに繋がることを確認します。

今回は以下のような感じで用意しました。

$ vi wg0.conf
[Interface]
PrivateKey = xxx
Address = 192.168.0.11/32

[Peer]
PublicKey = xxx
AllowedIPs = 192.168.0.0/24
Endpoint = 192.0.2.1:51820
PersistentKeepalive = 25

これで繋がるかDockerで試してみます。--cap-add NET_ADMINをつけるのを忘れないようにします。

# wg0.confを置いたディレクトリで実行
$ docker run -it --rm --cap-add NET_ADMIN -v $PWD:/etc/wireguard -v ~/.ssh:/root/.ssh ubuntu:latest

# 必要なものをインストール
$ apt-get update; apt-get install -y iproute2 wireguard openssh-client

# WireGuardでVPN接続してみる
$ wg-quick up wg0

# 確認: VPC内のサーバへSSHしてみる
$ ssh user@192.168.0.x 

設定が正しければ繋がるはずです。

WireGuardクライアント設定ファイルを参照するようにwireproxyの設定ファイルを作成

次に先ほど動作確認したWireGuardクライアント設定ファイルを参照するような形でwireproxyの設定ファイルを作成します。
今回のwireproxyの設定ファイルは以下のような構成となります。

# WireGuardのクライアント設定ファイルへのパス
WGConfig = /path/to/wg0.conf

# 以下でwireproxyの設定
[Socks5]
BindAddress = 127.0.0.1:1080

wireproxyにはTCPClientTunnelTCPServerTunnelSocks5という設定項目がありますが、今回はSocks5を利用します。
Socks5を利用すると、指定したアドレスでリッスンするSOCKS5プロキシを起動してくれます。
sshコマンドからはProxyCommand='nc -X 5 -x 127.0.0.1:1080 %h %p'のように指定することでSOCKS5プロキシを利用できます。

ということで動作確認してみましょう。今度は--cap-add不要です。

# wg0.confを置いたディレクトリで実行
$ docker run -it --rm -v $PWD:/etc/wireguard -v ~/.ssh:/root/.ssh ubuntu:latest

# 必要なものをインストール
$ apt-get update; apt-get install -y iproute2 wireguard openssh-client curl netcat

# wireproxyをダウンロード&インストール
$ curl -LO https://github.com/octeep/wireproxy/releases/download/v1.0.5/wireproxy_linux_amd64.tar.gz 
$ tar zxvf wireproxy_linux_amd64.tar.gz && rm wireproxy_linux_amd64.tar.gz 
$ install wireproxy /usr/local/bin

# wireproxy用の設定ファイルを作成
$ vi proxy.conf
WGConfig = /path/to/wg0.conf
[Socks5]
BindAddress = 127.0.0.1:1080

# wireproxyをデーモンモードで起動
$ wireproxy -d -c proxy.conf

# SSH接続してみる
$ ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:1080 %h %p' user@192.168.0.x 

これでうまく動きました。これならDrone上でTrustedにしたりprivileged: trueにしたりしなくても動きそうです。

Droneから使えるようにDockerfile作成

Droneから使いたいのでDockerfileを作っておきます。これをビルドしてレジストリにプッシュしておきます。

$ vi Dockerfile
FROM ubuntu:latest

ENV DEBIAN_FRONTEND noninteractive
RUN  apt-get update \
     && apt-get -y install \
          iproute2 \
          wireguard \
          openssh-client \
          curl \
          netcat \
     && apt-get clean \
     && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* \
     && curl -LO https://github.com/octeep/wireproxy/releases/download/v1.0.5/wireproxy_linux_amd64.tar.gz \
     && tar zxvf wireproxy_linux_amd64.tar.gz \
     && rm wireproxy_linux_amd64.tar.gz \
     && install wireproxy /usr/local/bin

ADD deploy.sh /deploy.sh

deploy.shは以下の値を環境変数で受け取る形にしました。

  • WG_CONF: WireGuardクライアント設定ファイルの内容
  • SSH_USER_NAME: SSH接続時のユーザー名
  • SSH_PRIVATE_KEY: SSHための秘密鍵
  • TARGET_HOSTS: 対象ホスト(スペース区切り)

今回はscp/sshコマンドをベタ書きしてますが、その辺を書き直せばもう少し汎用的に使えるはずです。

#!/bin/sh 

set -e

if [ -z "$WG_CONF" ]; then
  echo "\$WG_CONF is required"
  exit 1
fi

if [ -z "$SSH_USER_NAME" ]; then
  echo "\$SSH_USER_NAME is required"
  exit 1
fi

if [ -z "$SSH_PRIVATE_KEY" ]; then
  echo "\$SSH_PRIVATE_KEY is required"
  exit 1
fi

if [ -z "$TARGET_HOSTS" ]; then
  echo "\$TARGET_HOSTS is required"
  exit 1
fi

# 各種ファイルを生成
echo "$WG_CONF" > wg0.conf
echo "$SSH_PRIVATE_KEY" > /deploy_key; chmod 0600 /deploy_key
cat << EOL > proxy.conf
WGConfig = wg0.conf
[Socks5]
BindAddress = 127.0.0.1:1080
EOL

# wireproxy起動
wireproxy -d -c proxy.conf
# 接続完了するまで数秒待つ
sleep 5

for host in $TARGET_HOSTS; do
  # scpでファイルを送り込んだり
  scp -i /deploy_key -o StrictHostKeyChecking=no -o ProxyCommand='nc -X 5 -x 127.0.0.1:1080 %h %p' ./your-application-file ${SSH_USER_NAME}@${host}:/path/to/your/application
  # sshで何かしたり
  ssh -i /deploy_key -o StrictHostKeyChecking=no -o ProxyCommand='nc -X 5 -x 127.0.0.1:1080 %h %p' ${SSH_USER_NAME}@${host}  : do something
done

Droneの設定

最後にDroneでこのDockerイメージを使ってデプロイするように設定します。

$ vi .drone.yml

---
kind: pipeline
type: docker
name: deploy

# タグをトリガーとする
trigger:
  ref:
    - refs/tags/**

steps:
  # ...中略...

  - name: deploy
    image: your-image:tags
    environment:
      WG_CONF:
        from_secret: WG_CONF
      SSH_USER_NAME:
        from_secret: SSH_USER_NAME
      SSH_PRIVATE_KEY:
        from_secret: SSH_PRIVATE_KEY
      TARGET_HOSTS:
        from_secret: TARGET_HOSTS

これでGHEにタグをpushするとWireGuard経由でデプロイが行えるようになりました。

終わりに

ということでwireproxyを使ってDockerから--cap-addなしでWireGuard VPNに繋いでデプロイするようにした話を紹介しました。
Drone特有の処理はないのでGitHub Actionsなどからも同様の方法が取れるはずです。
まだまだ改善の余地は多々ありますので今後も継続して改善していきます。

また、今回は紹介しませんでしたがデプロイの自動化の前段階としてE2Eテストの導入といったより安心して作業できる環境づくりなども行いました。
これらの取り組みは機会があれば改めてご紹介させていただきます。

以上です。

参考文献

WireGuardについて

ユーザースペースでのWireGuard実装関連

その他