2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ラズパイで、パスワードマネージャとDNS型広告ブロッカーサーバを立ててみる

Last updated at Posted at 2025-08-28

はじめに・動機

家に転がってた4年くらい前に買ったラズパイ互換のなんか(libre computer)が有ったので、VaultwardenとDNS広告ブロッカーを立ててみようと思いました。
最近、Click Fixなんかも多いので悪性ドメインに不用意にアクセスしたくないですし、マルバタイジングを組み合わせたClick Fixとかも防ぎたいっすからね。
あと、フィッシングに引っかかる可能性もあるので、フィッシング耐性のあるパスキーを使いたい。
Vaultwardenでパスキーの管理とかも全部まるっとやりたい。(Googleに登録してもいいけど、自分自身でホストしておくのもいいっすよね、まぁ趣味の問題です)

それではやっていきましょう。

全体のコンセプト

  • クラウドっぽさ:家からも、インターネット越しからも自宅にあることを意識せず使えること
  • 電気代以外無料であること:運用コストは小さく。ドメイン名は極力取りたくない
  • 耐障害性:すべてDockerコンテナにまとめて、環境が最悪壊れても復旧コストを最小にする
  • TLS必須:管理画面の通信は自宅内だからといってHTTPで妥協しない
  • 攻撃面の最小化:外部公開サービスを限定して、さらにサービスとしてもログインにパスキーを必須とする

技術的制約

IPoEでの接続(事実上のCGNAT)なので従来の着信型の公開は不可能です。
これはリバーストンネル型VPN(Tailscaleなど)を使うことで克服します。

やること

自宅サーバ環境に パスワードマネージャ(Vaultwarden)広告ブロッカー(Pi-hole) を導入します。
あとは、Pi-holeの管理コンソールはローカルからアクセスしたいので、ローカルCAを立てた上でクライアントマシンはローカルCAを信頼するようにしておきます。
外部公開はTailscale Funnelを使ってVaultwardenのみを外部に公開します。

作るものを箇条書きでまとめると

  • Vaultwarden パスワードマネージャを構築する
  • Pi-Hole DNS型広告ブロッカー兼ローカル名解決用DNSを構成する
  • nginx でPi-Hole管理画面のTLS終端を担うリバースプロキシを構成する
  • Step CA によるPi-Hole管理画面の証明書を発行する(ローカル名の証明書はLets’encryptで発行できないため)
  • Tailscale Funnel を利用したグローバルへの公開(Vaultwarden)

環境

項目 内容
ホスト Raspberry Pi 4 (ARM64)
OS Raspbian
コンテナ実行環境 Docker CE
VPN Tailscale
証明書管理 Step CA

ディレクトリの全体構成

server
├── .env
├── compose.yml
├── etc
│   ├── dnsmasq.d
│   └── pihole
├── logs
├── nginx
│   ├── certs
│   └── conf.d
├── scripts
│   ├── ca-setup.sh
│   ├── cert-renew-and-reload.sh
│   └── create-cert.sh
├── step-ca
└── vw-data

1. Vaultwarden のクロスビルド

Raspberry Pi 向けに Docker イメージを作成する手順です。

# QEMU 準備
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

# buildx ビルダー作成
docker buildx create --name vwbuilder --use
docker buildx inspect --bootstrap

# ソース取得
git clone https://github.com/dani-garcia/vaultwarden.git
cd vaultwarden
VERSION=1.34.3
git switch -d $VERSION

# ARM64 向けビルド
docker buildx build \
  --platform linux/arm64 \
  -t vaultwarden_arm64:$VERSION \
  --load .

生成したイメージを保存して、Raspberry Pi に転送し、イメージをロードして利用します。

Vaultwardenの運用方針・セキュリティ

外部公開するので以下の点にだけ注意すること

  • 新規ユーザ登録を無効化する
    • 使うユーザは自分自身のみなので、新規追加する必要がない
    • ユーザ追加するときはFunnelを無効化してから
    • compose.ymlからこのあたりの設定をできるように構成する
  • MFAはPasskey必須とする
    • フィッシング攻撃によるパスワードマネージャのパスワードの露呈を防ぐため
  • TOTPは使わない
    • リアルタイムフィッシングに脆弱であるため
    • やむを得ない場合には許容するが、基本運用としてはPasskeyを使う

2. Docker Composeの構成

docker compose でまとめて管理します。

3. Step CA での証明書管理

.envのサンプル

compose.ymlとCA初期化・運用スクリプトと共用です。

.env

.env
# CA 設定
CA_DNSNAME=local-ca.local
CA_NAME=LocalCA
# ディレクトリ(スクリプトの場所を基準に補完)
STEP_CA_DIR=step-ca
CERT_DIR=nginx/certs
# サーバ情報
SERVERS="server.local"
O=Local
OU=Local
# Step CLI / CA
STEP_CA_URL=https://$CA_DNSNAME:8443
STEP_CLI_IMAGE=smallstep/step-cli:latest
STEP_CA_IMAGE=smallstep/step-ca:latest

CAの初期化とサイト毎に証明書を作成するスクリプト

CAの初期化

環境依存の内容は compose.yml と同じディレクトリの .env に書いてください。

ca-setup.sh

ca-setup.sh
#!/bin/bash
set -eu

# スクリプトの存在するディレクトリ
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd ../ && pwd)"

# .env の読み込み
source "$SCRIPT_DIR/.env"

# 相対パスを補完
STEP_CA_DIR="$SCRIPT_DIR/$STEP_CA_DIR"
CERT_DIR="$SCRIPT_DIR/$CERT_DIR"
PASSWORD_FILE="$STEP_CA_DIR/secrets/password"

mkdir -p "$STEP_CA_DIR/secrets"
docker compose down stepca || true
# 自動パスワード生成
openssl rand -base64 32 > "$PASSWORD_FILE"
chmod 600 "$PASSWORD_FILE"

# Step CA を非対話で初期化
docker run --rm \
  -v "$STEP_CA_DIR":/home/step \
  "$STEP_CA_IMAGE" \
  step ca init \
    --deployment-type="standalone" \
    --name "$CA_NAME" \
    --dns "$CA_DNSNAME" \
    --address ":443" \
    --provisioner "admin@$CA_DNSNAME" \
    --password-file /home/step/secrets/password

サイト毎の証明書作成

create-cert.sh

create-cert.sh
#!/bin/bash
set -eu

# スクリプトの存在するディレクトリ
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd ../ && pwd)"

# .env の読み込み
source "$SCRIPT_DIR/.env"

# 相対パスを補完
STEP_CA_DIR="$SCRIPT_DIR/$STEP_CA_DIR"
CERT_DIR="$SCRIPT_DIR/$CERT_DIR"
PASSWORD_FILE="$STEP_CA_DIR/secrets/password"

mkdir -p "$CERT_DIR"
docker compose up stepca -d
for SERVER in $SERVERS; do
  echo "=== request cert for: $SERVER ==="

  # CSR / キー作成
  docker run --rm -v "$CERT_DIR":/certs alpine:3.18 \
    sh -c "apk add --no-cache openssl >/dev/null 2>&1 && \
           openssl req -new -newkey ec:<(openssl ecparam -name secp521r1) -nodes \
             -keyout /certs/$SERVER.key \
             -out /certs/$SERVER.csr \
             -subj '/C=JP/O=$O/OU=$OU/CN=$SERVER' \
           && chmod 600 /certs/$SERVER.key && chmod 644 /certs/$SERVER.csr"

  # CSR を CA に送信して署名
  docker run --rm \
    -v "$STEP_CA_DIR":/home/step:ro \
    -v "$CERT_DIR":/certs \
    --network host \
    "$STEP_CLI_IMAGE" \
    step ca sign /certs/$SERVER.csr /certs/$SERVER.crt \
      --ca-url "$STEP_CA_URL" \
      --root /home/step/certs/root_ca.crt \
      --password-file /home/step/secrets/password \
      --not-after 24h \
      --force
done

# nginxのコンテナを潰して作り直す、再起動でもOKです
docker compose down nginx;docker compose up nginx -d

最後、定期的にTLS証明書を自動更新するスクリプト

crontab -e でこれを実行するようにしておくと、12時間ごとに証明書が更新されます。
* */12 * * * /path/to/server/scripts/cert-renew-and-reload.sh > /dev/null 2>&1

cert-renew-and-reload.sh

cert-renew-and-reload.sh
#!/bin/bash
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && cd ../ && pwd)"
source "$SCRIPT_DIR/.env"

# 相対パスを絶対パスに補完
STEP_CA_DIR="$SCRIPT_DIR/$STEP_CA_DIR"
CERT_DIR="$SCRIPT_DIR/$CERT_DIR"
PASSWORD_FILE="$STEP_CA_DIR/secrets/password"

mkdir -p "$CERT_DIR"
changed=false

for SERVER in $SERVERS; do
  echo "=== request cert for: $SERVER ==="

  CRT_FILE="$CERT_DIR/$SERVER.crt"
  KEY_FILE="$CERT_DIR/$SERVER.key"
  TMP_CRT="$CERT_DIR/.${SERVER}.crt.tmp"
  TMP_KEY="$CERT_DIR/.${SERVER}.key.tmp"

  docker run --rm \
    -v "$STEP_CA_DIR":/home/step:ro \
    -v "$CERT_DIR":/certs \
    --network host \
    "$STEP_CLI_IMAGE" \
    step ca certificate "$SERVER" "/certs/.${SERVER}.crt.tmp" "/certs/.${SERVER}.key.tmp"  \
      --ca-url "$STEP_CA_URL" \
      --root /home/step/certs/root_ca.crt \
      --not-after 24h \
      --password-file /home/step/secrets/password || {
        echo "warning: certificate request failed for $SERVER"
        continue
      }

  chmod 644 "$TMP_CRT" || true
  chmod 600 "$TMP_KEY" || true
  mv -f "$TMP_CRT" "$CRT_FILE"
  mv -f "$TMP_KEY" "$KEY_FILE"

  echo "issued/updated: $CRT_FILE $KEY_FILE"
  changed=true
done

if [ "$changed" = true ]; then
  echo "reloading nginx container..."
  docker exec nginx nginx -s reload || {
    echo "warning: nginx reload failed"
  }
else
  echo "no certificate changes; skip reload"
fi

nginxの設定ファイル

pihole.conf
# nginx が $connection_upgrade を理解するように map を追加
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name server.local;

    ssl_certificate     /etc/nginx/certs/server.local.crt;
    ssl_certificate_key /etc/nginx/certs/server.local.key;

    # Docker の内部DNS を使う(コンテナ内では 127.0.0.11 が Docker DNS)
    resolver 127.0.0.11 valid=30s;

    # upstream を変数で指定すると起動時に解決しない(リクエスト時に解決される)
    set $upstream_host "pihole:80";

    location / {
        proxy_pass http://$upstream_host;
        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 $scheme;

        # WebSocket 等が必要なら以下も
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

4. Tailscale Funnel での公開

Tailscale Funnel を使って VaultwardenをHTTPSで公開します。

tailscale funnel --bg --https=443 http://127.0.0.1:8080/

デーモン化(自動起動)

/usr/local/bin/enable-funnel.sh を用意し、systemd ユニットで起動時に Funnel を有効化できます。

enable-funnel.sh
#!/bin/bash

# 最大5回まで tailscaled とバックエンドの起動を待つ
for i in {1..5}; do
    if systemctl is-active --quiet tailscaled; then
        # バックエンドが応答可能か確認
        if curl -s http://127.0.0.1:8080/ > /dev/null; then
            # Funnel 設定を適用
            tailscale funnel --https=443 http://127.0.0.1:8080/
            exit 0
        else
            echo "Backend not responding yet, retrying in 3s..."
            sleep 3
        fi
    else
        echo "tailscaled not active yet, retrying in 3s..."
        sleep 3
    fi
done

echo "Failed to enable funnel after retries"
exit 1
/etc/systemd/system/tailscale-funnel.service
[Unit]
Description=Enable Tailscale Funnel on boot
After=network-online.target tailscaled.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/enable-funnel.sh
RemainAfterExit=true

[Install]
WantedBy=multi-user.target

5. インターネットからPi-HoleにDNS解決させる方法

Vaultwardenはインターネットから使えるようになりました。同じようにPi-holeもインターネットから使いたいです。
ただしDNSをインターネットに公開すると以下のようなリスクがあります。

  • 公開キャッシュサーバになることでDNSキャッシュ汚染攻撃を受ける恐れがある
  • DNSアンプ攻撃(リフレクション攻撃)に利用される
  • ゾーン転送の不正利用(AXFR機能の悪用)
    など

オープンリゾルバは上記のようにリスキーです。ではどうするかという話でコンセプトに移ります。

DNSの実装コンセプト

Tailscale(VPN)でスプリットトンネリングを実装します。DNS解決のみをVPN経由でクエリを送信し、それ以外の通信は直接インターネットに流す方式です。

Tailscaleの設定

  1. Tailscaleの管理画面のDNS設定内の「MagicDNS」を有効にする(デフォルトで有効なはずですが、念のため確認してください)
  2. sudo tailscale ipを確認する
  3. Tailscaleの管理画面のDNS設定内の「Global nameservers」に2で確認したIPアドレスを書く

Tailscale VPNにスマホを参加させて、スプリットトンネルを構成します。
DNSクエリのみをVPNに通過させて、解決させてそれ以外のトラフィックは直接インターネットに出すことでVPN側に不要なトラフィックを発生させません。もう一つのメリットとして通信パケットのカプセル化が必要以上にされないと思うので(諸説)通信(量|料)の節約にもつながるかも。
広告ブロックのみを主眼に置いた構成です。

Pi-Holeの設定

Pi-Hole > SYSTEM > Settings > DNS(DNS Settings画面) で作業をします

DNS Settings

設定モードをExpertモードにしてください(Basicモードだと設定できません)
Interface settings > Potentially dangerous optionsブロックの
Permit all origins を選択します。(まぁファイアウォールで制御していれば問題なしです)

これで、TailscaleからのDNSリクエストをPi-holeで処理できます。

まとめ

  • Vaultwarden を ARM 向けにクロスビルドし、Raspberry Pi で稼働させる
  • Pi-hole を組み合わせて悪性広告をブロックする
  • Step CA によるローカル証明書管理で HTTPS を内部的に完結する
  • Tailscale Funnel でVaultwardenのみを外部公開する

この構成により、自宅内外からセキュアに利用できるDNS広告ブロッカーとパスワードマネージャ環境が構築できました。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?