
2026/07/09 23:57
内部サービス向けの TLS 証明書:正しく設定する方法
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
本テキストは、予約済みトップレベルドメイン(TLD)や自己署名証明書を用いた内部 HTTP サービスの代替として、「スプリット・ホライズン DNS」を採用する安全なアプローチを提唱する。このアーキテクチャでは、クライアントが外部からアクセスするか、NetBird のカスタムゾーンによって管理される VPN を経由して接続かに応じて、単一の公開ドメインを異なる IP アドレスに解決させる。これにより、自己署名証明書ではなく、Let's Encrypt などの信頼された公開 CA(認証局)の使用が可能になる。重要なセキュリティ構成要素として、内部システムに至る前に全てのパブリックインターネットトラフィックを厳格にブロックする Web アプリケーションファイアウォール(WAF)が設定されている。本構成では WAF を nginx にて実装し、VPN インターフェースにバインドしている。信頼された CA の導入と内部サービスへの露出の回避、およびメインサーバーでのポート 80 コンフィグの不要を実現するため、
acme.sh を使用し、--standalone フラグで http-01 チャレンジャーを介して証明書を発行する方式が採用されている。自動化は、証明書を更新するための日次 cron ジョブと、新しい証明書を nginx コンフィグに同期する bash スクリプト(root 権限不要での動作を実現するために setcap を活用)によって実現されている。この構成により、Subject Alternative Names (SANs) を用いて複数のサブドメインを単一の証明書下で保護することが可能になる。本文
スプリットホライゾン DNS と WAF を用いた、内部サービス向けの安全な TLS 設定手法
クリックベイト臭い内容かもしれませんが、重要な技術的なテーマです。以下に「なぜこれが正解なのか」を説明し、実装手順を解説します。
コンテキスト:HTTP サービスのホスト例
外部サービスと内部サービスを両方ホストするサーバーの例を考えます。
- 内部サービス: VPN 接続が必要(例:Grafana)。
- アクセス方法:
- ICANN で予約されたプライベート TLD を使う(例:
).internal - 所有するパブリックドメインを使う(例:
)tuxnet.dev
- ICANN で予約されたプライベート TLD を使う(例:
ここでは、内部 IP アドレス
10.0.1.10 でアクセス可能な Grafana を使用して解説します。
検討すべき選択肢と課題
① プライベート TLD(.internal
)を使用する方法
.internal- 実装: DNS レコードを作成し、ドメインを内部 IP に解決する。
- メリット: チュートリアルやドキュメントが非常に多い。
- デメリット:
- HTTP クライアントに自己署名証明書を検証させなければならない。
- 利用者に TLS エラーを無視させる提案が必要になる(非推奨)。
② パブリックドメイン(tuxnet.dev
)とスプリットホライゾン DNS を使用する方法
tuxnet.dev- 実装:
- パブリック DNS リゾルバからはパブリック IP に解決。
- VPN 接続クライアントからは内部 IP に解決。
- メリット:
- **パブリック CA(Let's Encrypt や ZeroSSL)**を使用できるため、信頼性の高い証明書を発行可能。
- デメリット:
- **WAF(Web アプリケーションファイアウォール)**の導入が必要。
結論: 全クライアントに自己署名証明書を読み込ませる手法よりも、WAF を集中管理する手法の方が実装が容易で安全です。
構成要素と要件
以下のような構成を実装します:
- VPN リゾルバ: NetBird(スプリットホライゾン DNS の管理を担当)
- ACME クライアント: acme.sh(証明書の発行・自動更新)
- リバースプロキシ/WAF: Nginx
注:ここでは NetBird のカスタムゾーン機能を活用し、サーバー側がパブリックリゾルバを使用するように制御します。サーバー自体は例外として内部 IP に解決されます。
ACME.sh で証明書を発行する
http-01 チャALLENGE を採用して、以下のコマンドで証明書を取得します:
acme.sh --issue -d grafana.tuxnet.dev --server letsencrypt --standalone
- スタンドアロンのモード (
):--standalone- Nginx がポート 80 に常時リスニングする必要はありません。
- acme.sh だけがポート 80 を一時的に活用して課題を完了します。
Nginx の設定
以下の設定ファイルを使用します(※
our-server.netbird.cloud は VPN ネットワークインターフェースまたは IP アドレスへ置き換えてください)。
upstream grafana { server localhost:3000; } map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen our-server.netbird.cloud:443 ssl; server_name grafana.tuxnet.dev; http2 on; ssl_certificate /etc/ssl/certs/grafana.tuxnet.dev.crt; ssl_certificate_key /etc/ssl/private/grafana.tuxnet.dev.key; access_log /var/log/nginx/grafana.tuxnet.dev.access.log main; error_log /var/log/nginx/grafana.tuxnet.dev.error.log warn; location / { proxy_pass http://grafana; proxy_set_header Host $host; } # Grafana のライブ WebSocket 接続の代理化 location /api/live/ { proxy_pass http://grafana; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; } }
重要な設定ポイント
:listen our-server.netbird.cloud:443 ssl;- サーバーのVPN ネットワークインターフェースにバインドしています。
- これにより、パブリックインターネットから送信される
へのトラフィックはすべて拒否(ブロック)されます。grafana.tuxnet.dev
これにより、スプリットホライゾン DNS と WAF の2 つのセキュリティ層が構築され、回避が困難な防御ラインとなります。
証明書の自動更新設定(Cron Job)
acme.sh には
--cron フラグがあり、証明書の自動更新を制御できます。
Nginx の設定にある証明書ファイルを更新し、サービス再起動する必要があります。
以下のスクリプトを日次 cron ジョブとして実行します:
main() { refresh_certs sync_api_tuxnet_dev_certs sync_internal_tuxnet_dev_certs reload_nginx } refresh_certs() { setcap CAP_NET_BIND_SERVICE=+ep /usr/bin/socat1 sudo -u acmesh /home/acmesh/acme.sh/acme.sh --cron setcap -r /usr/bin/socat1 } sync_api_tuxnet_dev_certs() { local green=$(get_checksum "$API_SRC_KEY") local blue=$(get_checksum "$API_DST_KEY") local key_allowed_group=www-data if [[ "$green" != "$blue" ]]; then sync_certs "$API_SRC_KEY" "$API_DST_KEY" "$API_SRC_CERT" "$API_DST_CERT" "$key_allowed_group" fi } sync_internal_tuxnet_dev_certs() { local green=$(get_checksum "$INTERNAL_SRC_KEY") local blue=$(get_checksum "$INTERNAL_DST_KEY") local key_allowed_group=www-data if [[ "$green" != "$blue" ]]; then sync_certs "$INTERNAL_SRC_KEY" "$INTERNAL_DST_KEY" "$INTERNAL_SRC_CERT" "$INTERNAL_DST_CERT" "$key_allowed_group" fi } reload_nginx() { nginx -t systemctl reload nginx } get_checksum() { sha256sum "$1" | cut -d' ' -f1 } sync_certs() { local src_key="$1" local dst_key="$2" local src_cert="$3" local dst_cert="$4" local key_allowed_group="$5" cp -v "$src_key" "$dst_key" logger "synced $dst_key" cp -v "$src_cert" "$dst_cert" logger "synced $dst_cert" chown root:${key_allowed_group} "$dst_key" chown root:ssl-cert "$dst_cert" chmod 640 "$dst_key" "$dst_cert" } main "$@"
補足:setcap CAP_NET_BIND_SERVICE=+ep
について
setcap CAP_NET_BIND_SERVICE=+ep- acme.sh はスタンドアロンモードでポート 80 を使用しますが、root ユーザーとして実行したくない場合があります。
- ポート 80 は「特権ポート」のため通常はルート以外がバインドできません。
- この Cap がそれを可能にします。
これで、サービスが内部・外部を問わず安全な TLS 通信が可能になります。
ボーナス:SANs と CNAME を使用した拡張運用
複数の内部サービス(Grafana、Analytics など)がある場合、個別の証明書を作成するのは手間です。 その代わりに TLS SAN (Subject Alternative Name) を活用します。
ワイルドカード証明書 vs. TLS SAN
- ワイルドカード: セキュリティ懸念から避けるのが無難です。
- SAN 付き単一証明書: CN とともに、複数のサブドメインを指定できます。
これにより、以下の DNS レコード構成で 1 つの証明書を使用可能です:
# A レコード internal.tuxnet.dev -> 10.0.1.x (内部 IP) # CNAME レコード grafana.tuxnet.dev -> internal.tuxnet.dev analytics.tuxnet.dev -> internal.tuxnet.dev
SAN を含む証明書の発行例
1 つの ACME コマンドで複数のドメインを指定します:
acme.sh --issue -d internal.tuxnet.dev -d grafana.tuxnet.dev -d analytics.tuxnet.dev \ --server letsencrypt --standalone
確認結果:
echo | openssl s_client -connect internal.tuxnet.dev:443 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName subject=CN=internal.tuxnet.dev X509v3 Subject Alternative Name: DNS:analytics.tuxnet.dev, DNS:grafana.tuxnet.dev, DNS:internal.tuxnet.dev
Nginx 設定では、異なる
server ブロック間でこの同じ証明書ファイルを共有・再利用します。
まとめ
- スプリットホライゾン DNSにより、内部/外部の IP を適切に分離。
- **WAF(リバースプロキシ)**で不要なトラフィックをフィルタリングし、セキュリティ層を追加。
- ACME プロトコルを活用して、自動化された無料証明書管理を実現。
この構成により、ダウンストリームの HTTP クライアントに影響を与えずに、内部サービスに対して安全な TLS 環境を構築できます。すべて無料です!