はじめまして。
エムスリーエンジニアリンググループSREチームの山本です。
先日来のリモートワーク促進の中、弊社でも多くの社員がオフィス外から接続するようになりました。 もちろん、VPNを利用すれば社内のサービスも利用できますが、VPNの使用量が一気に増えるとそちらの制限にかかります。 今回「VPNを可能な限り利用せず、なおかつセキュアに社内のサービスを利用してもらう」という課題に取り組みましたので、ここでその紹介をさせてください。
前提
- VPNは可能な限り使わずに作業を可能とする
- 社内の人間は数多くの社内サーバを利用しており、そのサービスはインターネット公開されていない
- 社員が利用するPCにはクライアント証明書は配布している
- 緊急事態宣言発令で一気にVPN利用者が増えそうなので、数日のうちにでもVPNレスでセキュアな接続を実現してほしい
もともと社内と社外は普通は別のネットワークですので「VPNを利用しなければ社内サーバに接続できない」という形は普通です。
しかし、リモートワークが盛んになった、特に急増したときには…どうなるでしょうか?
VPNはパンクしてしまいます。新規接続ができなくなったり、あるいはVPNの設定で短時間に接続を切断することを強制するなどといった手当を取ることで非常に不便になってしまいます。
これをなんとかする、というのが今回の話です。
方針
クライアント証明書による認証が可能ならば、一定以上のセキュリティレベルを保つことができます。 したがって、クライアント証明書を利用している場合にはインターネットに社内サービスを公開することを可能としても、セキュリティレベルの低下は少ないと判断しました。(もちろん、社内サービスの種類にも依存しますので、一概になんでも「クライアント証明書を持っていればOK」というものではありません。しかし、社内サービスでも重要な部分には認証が入りますし、それで十分だろうと判断できるものも存在したということです。)
クライアント証明書の問題点
しかし、クライアント証明書での認証を実施するとしても、各Webサーバごとにその設定を実施するとすれば非常に面倒です。
10個のサービスを公開するとすれば、10個のWebサーバについてインターネットからの接続の穴を開けた上で証明書検証を組み込むことになります。もう少し楽にできないのか、という話が出てきます。
また、社内サービスはHTTPによる提供が多く、さらにいえばドメインも存在しなかったりします。イメージ的には http://server1/service のような接続を実施しているものもあり、クライアント証明書以前にサーバ証明書も存在しません。HTTP接続ではクライアント証明書検証はできません。まず、HTTPSにするところから、ドメインを変えて証明書を組み込むところからという話になります。
一括でのSSL化・証明書検証
バックエンド側には全く変更を加えないで、なんとかクライアント証明書チェックはできないのか?
そこで考えたのが、特定のproxyを経由させ、クライアント証明書チェックを一括で行うという手法です。以下に図を示します。
- インターネットに公開したSquidを立ち上げ、ブラウザではそのSquidを経由するように設定してもらう
- Squidが名前解決を実施して接続をproxyするが、その接続先を一律で特定のサーバ(今回の場合127.0.0.1)に設定する(つまり、何を解決しろといわれても常に127.0.0.1を返す)
- Squidの接続先にnginxを設置し、nginxでSSLを終端し、証明書を検証する
- nginxは、真の接続先であるサーバにproxyする(HTTPであっても可)
このSquid/nginx同居のサーバは、社内サーバのひとつなので、社内の他のサーバへの接続許可がなされています。
以下にもう少し詳細な図を示します。
Squidを経由した際には、URLの名前解決はSquid自身が行いますので、このようなトリックを使うことですべての接続を特定のnginx-proxyに向けることが可能となります。nginxのところに「header書き換え」というものが入っていますが、これについては後で書かせてください。
ドメイン変換
ただ、そのようなproxyを作成しても、URLを不変にすることはできませんでした。
次のようなアナウンスを出しています。
http://server1/ は https://server1.example.com/ に変わりました。*1
http://server2/ は https://server2.example.com/ に変わりました。
http://office1/ は https://office1.example.com/ に変わりました。
http://office2/ は https://office2.example.com/ に変わりました。
http://test.example.com/ は https://test.example.com/ に変わりました。
2つのURL変更があります。
- 証明書を保持したドメイン名に変えてもらう(上の例では
*.example.com
ですが、実際に利用するドメインでは証明書を確保します) - すべてHTTPSにする
このドメイン変更による弊害は存在するのですが、それは後述いたします。
実際の設定
Squidの設定(抜粋)
squid.confでは以下のような設定があります。
dns_nameservers 127.0.0.1 acl whitelist dst 127.0.0.1/32
Squidはこの設定によって、127.0.0.1のネームサーバを利用します。 また、dst、つまり接続先を127.0.0.1に限っています。これがなければ、セキュリティ上の穴を作成してしまいます。
unboundの設定
127.0.0.1のネームサーバとしてunboundを立ち上げました。 このunboundでは次の設定(any-zone.conf)をincludeしました。
server: local-zone: "." redirect local-data: ". 3600 IN A 127.0.0.1"
何を聞かれても、127.0.0.1としか答えさせないということです。このようなことでもなければ書くことがなさそうです。
nginxの設定(クライアント証明書検証)
クライアント証明書検証のために以下の設定を記述しておきます。ここではcommon/ssl.confとしておきます。これを別の設定からincludeすることになります。
ssl_verify_client on; ssl_client_certificate ssl-root.crt; if ($ssl_client_s_dn !~ "OU=適切な検証すべき名前") { return 403; } ssl_session_cache shared:SSL:50m; ssl_session_timeout 1d; ssl_session_tickets off; ssl_protocols TLSv1.2; ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:略'; ssl_prefer_server_ciphers on;
nginxの設定(HTTPサーバに対するproxy)
http://*.example.com/
というサーバに対するproxy設定となります。
server { listen 443 ssl; server_name *.example.com; root /var/www/html; ssl_certificate certs/wildcard.example.com.crt; ssl_certificate_key certs/wildcard.example.com.key; include common/ssl.conf; # allow from squid allow 127.0.0.1; location / { proxy_connect_timeout 2s; proxy_send_timeout 60s; proxy_read_timeout 180s; proxy_buffers 8 32K; proxy_buffer_size 32k; client_max_body_size 10m; resolver 内部DNSサーバ; proxy_pass http://$http_host$request_uri; } }
ここで、proxy_passの先は http://$http_host$request_uri
としてあります。
もともと社内サービスはSSLを使っていないものも多いのですが、今回、この proxyを利用することで自動的にSSL化されます。
例えば http://XXX.example.com
というURLは、 proxyを利用することで自動的に https://XXX.example.com/
としてアクセス可能となります。
nginxの設定(個別対応)
しかし、もう少し個別に設定を進めたものもあります。実際のところ、全てが統一的に XXX.example.com
のようなドメインではありませんでした。それらについては、都度同様の設定を入れてあります。
また、もともとエンドポイントがHTTPSに対応しているものもあります。その場合にHTTPでproxyしてしまうと当然アクセスができません。
server { listen 443 ssl; server_name ssl.example.com ssl2.example.com; 上の設定と同様 location / { 上の設定と同様 proxy_pass https://$http_host$request_uri; } }
ssl.example.com
と ssl2.example.com
については、もともとSSL対応しているのでproxy_passの先をHTTPSにしておかばならないという個別設定です。
server { listen 443 ssl; server_name *.example.org; root /var/www/html; ssl_certificate certs/wildcard.example.org.crt; ssl_certificate_key certs/wildcard.example.org.key; 以下略 }
example.com
以外に example.org
も使っているということなら、*.example.org
の証明書を準備して設置することになります。
ブラウザのProxy設定
Proxyの設定を利用者の社員全員に案内することになりますが、今回の場合はProxy SwitchyOmegaというブラウザプラグインを案内するドキュメントを作成しました。 また、同時にpacファイル(proxy auto-config)の配布を実施し、特定のドメインに対してのみ今回のproxyを通すように設定しています。
作業開始からここまで、数日でたどり着くことができました。
その後発生した問題
運用開始直後からいくつかの問題が報告されましたので、それらについて紹介いたします。
ポート問題
後で確認した話では、実は80や443だけでなく、もっと他のポートでも社内向けサービスを提供していたという話でした。 例えば、 http://server1:8080/ http://server1:8081/ http://server1:8082/ でそれぞれのサービスを提供しているという形です。
それらはもはやどうしようもないので個別にproxyを実施する設定をnginxに追加しています。
# server1:8081 # http://server1:8081/ => https://server1-8081.example.com/ server { listen 443 ssl; server_name server1-8081.example.com; 略 location / { 略 proxy_pass http://server1:8081$request_uri; } }
上記のような設定をいくつも書いて配置することになりました。ポートはドメインに変換してしまった上でSSL化します。 しかし、逆に言えば、どのような接続先であろうと、設定ファイルを追加すれば認証proxyが作れるということなので自由度があるとも言えるかもしれません。
Hostヘッダ問題
接続先によっては「そもそも接続できないです。変な画面が出ます」という苦情も受けました。
調査したところ、これは勝手にドメインを変えたことに起因するものでした。今回の変更によってドメインの変更がないものはよいのですが、例えば上のhttp://server1:8081/
はこの変換の過程で https://server1-8081.example.com/
に変更してしまっています。
# server1:8081 # http://server1:8081/ => https://server1-8081.example.com/ server { listen 443 ssl; server_name server1-8081.example.com; 略 location / { 略 # set Host header explicitly to be accepted by the target web server.. proxy_set_header Host server1; proxy_pass http://server1:8081$request_uri; } }
個別設定ではありますが、nginxで proxy_set_header
を追加してもとのHostヘッダを明示的につけてしまうことにしました。
戻りヘッダ問題
勝手にドメインを変えたことによる弊害はまだありました。
対象の社内サービスサーバは、自分がドメインが変わったことなどは何も知りません。リンクのURLなどはもはやいかんともしがたいのですが、すぐに問題になったのが302などのLocationヘッダです。
- ユーザが https://server1.example.com/ にアクセスして、proxy経由で http://server1/ にアクセス
- server1は、(例えば)ログインを求めるなどして http://server1/login にリダイレクトを指示
- ユーザのブラウザは http://server1/login に接続できず撃沈
これもなんとかせねばなりません。設定を汚くしてでも、以下のような特殊設定を入れ込むことになりました。
# server1 # http://server1/ => https://server1.example.com/ server { listen 443 ssl; server_name server1.example.com; 略 location / { 略 # set Host header explicitly to be accepted by the target web server.. proxy_set_header Host server1; proxy_pass http://server1$request_uri; proxy_redirect http://server1/ https://server1.example.com/; } }
nginxの設定でさらに proxy_redirect
を追加します。これはLocationヘッダやRefreshヘッダを書き換えるものです。これによってアクセスできるドメイン(自分が変更したドメイン)へのリダイレクトに強制的に変更してしまいます。
ドメインを変更したことの齟齬をこのproxyで吸収してしまって、対象のサーバ自体には手を加えなくて済むわけですので、短期に一気に対応してしまうという目的の上ではマッチしていたと思います。
解決できなかった問題
ドメインそのものに依存したアプリケーションについてはさすがにどうしようもありませんでした。
また、全員がリモート勤務をしているわけでもありませんし、Webに限らない作業というものもあるため、VPNを使うこともあります。そうなると、次のような条件ごとに細かな差異が出てきます。
- proxyをオンにしているかどうか
- VPN/オフィスからの作業かどうか
proxyをオンにした上でオフィスから利用してもそれ自体は問題ありませんが、なにか問題が起きる際に「proxyはオンにしてますか?」と最初に確認する必要が出てきてしまいました。
セキュリティ上の注意
Squid自体はインターネットに公開しており、オープンに接続がなされます。したがって、何も考えずにこの構成で立ててしまうと、Squidを経由して社内のサーバ(ネットワーク的な制限やミドルウェアレベルでのIP制限)を突破されることがおきえます。
悪意のある人が http://very-important-information/
という社内サーバに接続したいと思ったときに、今回のSquidを経由して接続されることがあってはなりません。今回の場合、nginxのクライアント認証にセキュリティを託しているのでそこをスルーされることだけは防がねばなりません。
Squidの接続先を確実にnginx(クライアント証明書認証必須)に絞るということは必須要素であって、そのようなACLを記述しておく必要があります。もちろん、80番でのproxyを許可してしまうことも問題です。80番ではクライアント証明書認証がなされないので、80番はSquidで通さないか、あるいは通してもnginxですぐにHTTPSに対して308リダイレクトするようにしておくとよさそうです。
まとめ
- 一括で社内Webサービスにクライアント証明書認証をかけることができた
- 社内Webサービス側には手を入れずに済んだ
- 工数はそれほど必要なく、数日で大体うごくようになり、その後問題を解決しつつ運用できた
今回はリモートワークという話でしたが、それに限らず、IPが固定されない外部から社内サービスを使ってもらうためのゲートとして紹介させていただきました。
We're hiring
エムスリーではSRE エンジニアを大募集中です!少しでも興味を持っていただいた方は是非お声掛けください。
open.talentio.com https://open.talentio.com/1/c/m3-inc/requisitions/detail/15300open.talentio.com jobs.m3.com
*1:ドメインは例であり、実際のものではありません。他のドメインも同様です。