見出し画像

SquidのForward Proxyで、コンテキストパスのWhitelistアクセス制限をする

最近のトレンドはSASEでのセキュリティ確保ですが、既存のEnterpriseシステムなどでは、まだまだ閉鎖網のシステムも多いです。
久しぶりにSquidでForward Proxyサーバを構築し、ドメインだけでなくコンテキストパスでもアクセス先URLをWhitelistで制限したい、という案件を扱いました。
「https通信でコンテキストパスのWhitelist制限」という組み合わせは、Web上で、断片的な情報しか見つけられたなかったので、ここでまとめておきます。



まとめ

結論から言うと、「https通信でコンテキストパスのWhitelist制限」を実現するには、以下の対応が必要です。

  1. SSLインターセプトの設定(サーバ側)

  2. サーバー証明書のインポート(クライアント側)

  3. CONNECTメソッドの許可とコンテキストパスの許可(サーバ側)


環境構成

  • Redhat Enterprise Linux 8.6

  • Squid 4.15


1.SSLインターセプトの設定

Squidには、dstdomainやurl_regex、urlpath_regexなどでフィルタリング条件を指定できますが、冒頭の「https通信でコンテキストパスのWhitelist制限」という要件に対しては、動作しません。
https通信では、通信内容が暗号化されており、通信元のIPアドレス、通信先のドメインは暗号化されていないヘッダに含まれているので、この情報を使ったフィルタリングは可能です。しかし、コンテキストパスやリクエストパラメータなどの情報は暗号化されており、通信を中継するProxyサーバでは暗号化された中身を見ることができないためです。

じゃあどうするのかというと、「Proxyサーバで、一度、暗号通信を複合化して、フィルタリングした後、許可した通信だけ、再度暗号化して宛先に送信する」ことになります。
なお、お気づきの通り、これは通信盗聴っぽい話なので、閉鎖網の中でやってください。

1-1. squid.confの設定

まずは、Squidのsquid.confで以下の設定を行います。

### squid.conf

# 1. SSL Intercept
acl intermediate_fetching transaction_initiator certificate-fetching
http_access allow intermediate_fetching


# ここにフィルタリング条件等の設定を後で記載
# 暫定的に全通信許可
http_access allow all
# ここにフィルタリング条件等の設定を後で記載


# 2. Squid listens to port
http_port 8888 tcpkeepalive=60,30,3 ssl-bump generate-host-certificates=on dynamic_cert_mem_cache_size=20MB tls-cert=/etc/squid/bump.crt tls-key=/etc/squid/bump.key cipher=HIGH:MEDIUM:!LOW:!RC4:!SEED:!IDEA:!MD5:!EXP:!PSK:!DSS options=NO_TLSv1,NO_SSLv3,SINGLE_DH_USE,SINGLE_ECDH_USE tls-dh=prime256v1:/etc/squid/bump_dhparam.pem

# 3. access log format
logformat jcombined "%{%Y/%m/%d %H:%M:%S}tl.%03tu" %>a %>st %<a %<st %>rP %mt "%rm %>ru HTTP/%rv" %>Hs "%{Referer}>h" "%Ss:%Sh"
access_log daemon:/var/log/squid/access.log jcombined

# 4. SSL Intercept
sslcrtd_program /usr/lib64/squid/security_file_certgen -s /var/lib/squid/ssl_db -M 20MB
sslproxy_cert_error allow all
always_direct allow all
ssl_bump stare all

$$
\begin{array}{|l|l|l|} \hline
1. SSL Intercept&acl\ intermediate\_fetching …&"中間TLS証明書をFetchするトランザクション"という条件に"intermediate\_fetching"という名前を付ける\\
&http\_access\ allow\ intermediate\_fetching&"intermediate\_fetching"のアクセスを許可(つまり証明書の中間Interceptを許可する)\\ \hline
2. Squid listens to port&http\_port\ 8888\ tcpkeepalive=60,30,3\ ssl-bump…&ProxyがListenするポートを指定、かつ、通信の暗号化をDecryptしてFetchし、Proxyのサーバー証明書で再Encrypt\\ \hline
3. access log format&logformat\ jcombined…&access.logの出力formatを指定(8要素目の\%rm: Request Methodと、9要素目の\%>ru: full Request URLがポイント)\\
&access\_log\ daemon…&access.logの出力パスと上記formatでの出力を指定\\ \hline
4. SSL Intercept&sslcrtd\_program…&Proxyの動的証明書を生成するProgramとして付属のsecurity\_file\_certgenを使用し、証明書をssl\_dbディレクトリにキャッシュする\\
&sslproxy\_cert\_error…&全ての証明書検証エラーを許容する(元サイトの証明書を差し替えているので、検証エラーになるため)\\
&ssl\_bump…&"2. Squid listens to port"で指定した"ssl-bump"の動作指定(stareはDecryptする)\\ \hline
\end{array}
$$

1.2. サーバー証明書の作成

さて、ここまでの設定で、squid.confには"http_access allow all"(暫定的に全通信を許可)を設定していますので、クライアント側から外部Internetに対してアクセスできるのですが、Webブラウザで「証明書の情報が合わないよ」という警告が出ます。なんだったら最近のWebブラウザでは、警告の先にアクセスできないようになっていたりもします。途中のPROXYサーバでSSLインターセプトなんて盗聴っぽいことをやっているので当然です。
しかし、「このSSLインターセプトは、クライアント側も合意の元でやってるんですよ」という設定をしてやることで、Webブラウザで警告なくアクセスできるようになります。
まず、このセクションでPROXYサーバの証明書(自己署名証明書)を発行し、その証明書を次セクションでクライアント側にインポートしてやればよいです。

0.SELinuxがOFFであることを確認

[root@servers ~]$ getenforce
Disabled

1.証明書と鍵ファイルの作成

[root@server ~]$ cd /etc/squid
[root@server squid]$ openssl genrsa -out bump.key 4096
.................++++
...............................................++++
e is 65537 (0x010001)
[root@server squid]$
[root@server squid]$ openssl req -new -key bump.key -out bump.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:JP
State or Province Name (full name) []:
Locality Name (eg, city) [Default City]:
Organization Name (eg, company) [Default Company Ltd]:RANAMICUS
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server''s hostname) []:SQUID
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
[root@server squid]$
[root@server squid]$ openssl x509 -req -days 365 -in bump.csr -signkey bump.key -out bump.crt
Signature ok
subject=C = JP, L = Default City, O = RANAMICUS, CN = SQUID
Getting Private key
[root@server squid]$ 
[root@server squid]$ chown squid:squid /etc/squid/bump.*
[root@server squid]$ chmod 400 /etc/squid/bump.*
[root@server squid]$ openssl dhparam -outform PEM -out /etc/squid/bump_dhparam.pem 2048
Generating DH parameters, 2048 bit long safe prime, generator 2
This is going to take a long time
..................
[root@server squid]$ 

これで、"/etc/squid"ディレクトリの下に、"bump.crt"、"bump.csr"、"bump.key"、"bump_dhparam.pem"の4つのファイルが作成されます。
この内、"bump.crt"は次セクションでクライアント側にインポートする証明書ファイルなので、ダウンロードしてクライアント側にも配置しておきましょう。
なお、ここで作成した証明書は有効期間365日です。変えたい場合は、"openssl x509 -req -days"のところの値を365から変更してください。

2.証明書キャッシュDBの作成

上記で作成したサーバー証明書をルート証明として、一緒に作成された鍵ファイルを使って、前セクション「1-1. squid.confの設定」の"2. Squid listens to port"のところの設定で、アクセス先サイトごとの動的な証明書を作成し、再暗号化をしています。
そして、"4. SSL Intercept"のところの設定で、動的な証明書をキャッシュしているのですが、そのキャッシュ先を作成します。

[root@server squid]$ mkdir -p /var/lib/squid
[root@server squid]$ /usr/lib64/squid/security_file_certgen -c -s /var/lib/squid/ssl_db -M 20MB
Initialization SSL db...
Done
[root@server squid]$ chown -R squid:squid /var/lib/squid

squid.confの編集と、サーバー証明書の作成が終わったら、最後にサービスを再起動しましょう。

[root@server ~]$ systemctl stop squid
[root@server ~]$ systemctl start squid

2.サーバー証明書のインポート

前セクション「1.2. サーバー証明書の作成」で作成した"bump.crt"ファイルを、信頼された証明書としてクライアント側にインポートしましょう。
Windows11のクライアントを前提とします。

Windowsキーの検索欄に、"certmgr.msc"と入力
certmgr画面の"信頼されたルート証明機関">"証明書"を右クリックし、"すべてのタスク">"インポート"を選択
証明書インポートウィザードで"次へ"
ファイル名に証明書ファイル(bump.crt)のパスを入力
確認画面で"完了"を選択
「自己証明書だけど大丈夫ですか?」と警告が出ますが「はい」を選択
certmgr画面の証明書の一覧に、"SQUID"の証明書が追加されます

3.CONNECTメソッドの許可とコンテキストパスの許可

ここまでの設定を行うと、Squidのアクセスログ(squid.confの"3. access log format"で設定した/var/log/access.log)には、CONNECT以外にGETやPUTの通信ログも出力されていると思います。
コンテキストパスでフィルタリングをするには、ログに出力されているリクエストURLのパスでフィルタ条件を設定していくことになります。
ただし、https通信の仕様を考慮した設定が必要になりますので、access.logを見てみましょう。

"2024/04/01 10:35:37.771" xx.xx.xx.1 258 yy.yy.yy.222 0 443 - "CONNECT sub1.sample_site.com:443 HTTP/1.1" 200 "-" "NONE:HIER_DIRECT"
"2024/04/01 10:35:37.944" xx.xx.xx.1 722 yy.yy.yy.222 1359 443 text/html "GET https://sub1.sample_site.com/functionA/home HTTP/1.1" 200 "-" "TCP_MISS:HIER_DIRECT"

ログを見るとわかりますが、httpsの通信は、以下のように2段階になっています。

  1. まず、宛先ドメインに対してCONNECT

  2. 次に、リクエストURLに対してGETやPUT

上記1のCONNECTは、ドメインに対して行われるので、パスはありません(access.logでも、リクエストURLの部分がドメインのみの表示になっています)。
よって、コンテキストパスのWhitelistだけを許可していると、
(パスが /AAA or /BBB を含んでいたら)vs (パスなし)
というような比較になり、条件に合わないため、最初のCONNECTが拒否されます。そして、続くGETやPUTの操作も行われません。
と言うことで、次のようにフィルタ条件を設定しましょう。

### squid.conf

## 条件項目
# 通信元IP CIDR条件
acl source_ip src xx.xx.xx.0/24

# 通信ポート条件(このセクションは既定と思ってください)
acl SSL_ports port 443
acl Safe_ports port 80          # http
acl Safe_ports port 443         # https

# httpメソッド条件
acl CONNECT method CONNECT      # CONNECTメソッド

# ドメイン条件
acl Safe_domains dstdomain .sub1.sample_site.com

# パス条件
acl Safe_path_samplesite urlpath_regex -i /functionA/
acl Safe_path_samplesite urlpath_regex -i /functionB/


## 許可設定
# 利用ポートの制限(このセクションは既定と思ってください)
http_access deny !Safe_ports           # Safe_ports(443 or 80)以外の通信を拒否
http_access deny CONNECT !SSL_ports    # SSL_ports(443)以外のCONNECT操作を拒否

# CONNECT許可
http_access allow Safe_domains CONNECT source_ip   # [1]ドメインがSafe_domains AND CONNECTメソッド AND 通信元IPがsource_ipに含まれる

# パスの制限
http_access allow Safe_domains Safe_path_samplesite source_ip   # [2]ドメインがSafe_domains AND Safe_path_samplesiteのどれかがパスに含まれる AND 通信元IPがsource_ipに含まれる

# その他
http_access deny all   # 上記許可設定に合致しなかった、その他すべての通信を拒否

上記squid.confの[1]のところで、アクセス先ドメインへのCONNECTを許可しており、その後の[2]のところで、WhitelistのコンテキストパスへのGETやPUT操作を許可しています。
(ちなみに上のconfでは、source_ipという条件も加えて、通信元IP CIDRも制限しています)

この設定で以下のようなサイトにアクセスした場合のアクセス可否を示しておきます。

$$
\begin{array}{ l|l|l }
sample\_site.com&NG& \\
  ├ sub1.sample\_site.com&OK&Safe\_domainsで許可\\
  │   ├ sub1.sample\_site.com/functionA/home&OK&Safe\_path\_samplesiteで許可  \\
  │   ├ sub1.sample\_site.com/functionA/list&OK&Safe\_path\_samplesiteで許可  \\
  │   ├ sub1.sample\_site.com/functionB/home&OK&Safe\_path\_samplesiteで許可  \\
  │   ├ sub1.sample\_site.com/functionC/home&NG& \\
  │   └ sub1.sample\_site.com/functionC/functionA/home&OK&Safe\_path\_samplesiteで許可 \\
  └ sub2.sample\_site.com&NG& \\
       └ sub2.sample\_site.com/functionA/home&NG& \\
\end{array}
$$

ちなみに、パス条件のurlpath_regexの指定は正規表現なので、以下のようなパターン表現も可能です

  • "/functionA/"から始まるパスだったら → ^/functionA/

  • "/list"で終わるパスだったら → /list$

  • "/functionA/list"に完全一致するパスだったら → ^/functionA/list$


おまけ

個人情報や金融情報などの機密情報を扱うシステムでも、AWSなどのPublic Cloudに移行する流れになっています。
そんな流れの中、機能上、または、運用上の都合で、Cloudのドメインを内部から通信許可していることもあるかと思いますが、システム構成やCloud側の仕様によっては…
良からぬことを考える内部の人が、システム内で個人情報を収集し、同じPublic Cloudを自分で契約して、自分のCloudアカウントにアクセスして、収集した情報をアップロード、なんてこともあるかもしれません。
内部統制のフィルタの1つとして、今しばらくは使うこともあるでしょう。

おまけでsquid.confの全文も掲載しておきます。

squid.conf全文

# SSL Intercept
acl intermediate_fetching transaction_initiator certificate-fetching
http_access allow intermediate_fetching


#
# Recommended minimum configuration:
#

# Example rule allowing access from your local networks.
# Adapt to list your (internal) IP networks from where browsing
# should be allowed
acl source_ip src xx.xx.xx.0/24

# port rule
acl SSL_ports port 443
acl Safe_ports port 80          # http
acl Safe_ports port 443         # https

# http method rule
acl CONNECT method CONNECT


#
# Recommended minimum Access Permission configuration:
#
# Deny requests to certain unsafe ports
http_access deny !Safe_ports

# Deny CONNECT to other than secure SSL ports
http_access deny CONNECT !SSL_ports

# Only allow cachemgr access from localhost
# http_access allow localhost manager
http_access deny manager

# We strongly recommend the following be uncommented to protect innocent
# web applications running on the proxy server who think the only
# one who can access services on "localhost" is a local user
#http_access deny to_localhost

#
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
#

# URL Filtering rule
acl Safe_domains dstdomain .sub1.sample_site.com
acl Safe_path_samplesite urlpath_regex -i /functionA/
acl Safe_path_samplesite urlpath_regex -i /functionB/

# Example rule allowing access from your local networks.
# Adapt localnet in the ACL section to list your (internal) IP networks
# from where browsing should be allowed
http_access allow Safe_domains CONNECT source_ip
http_access allow Safe_domains Safe_path_samplesite source_ip

# And finally deny all other access to this proxy
http_access deny all


# Squid normally listens to port 3128
http_port 8888 tcpkeepalive=60,30,3 ssl-bump generate-host-certificates=on dynamic_cert_mem_cache_size=20MB tls-cert=/etc/squid/bump.crt tls-key=/etc/squid/bump.key cipher=HIGH:MEDIUM:!LOW:!RC4:!SEED:!IDEA:!MD5:!EXP:!PSK:!DSS options=NO_TLSv1,NO_SSLv3,SINGLE_DH_USE,SINGLE_ECDH_USE tls-dh=prime256v1:/etc/squid/bump_dhparam.pem

# Uncomment and adjust the following to add a disk cache directory.
#cache_dir ufs /var/spool/squid 100 16 256
acl NOCACHE src all    # Disable Cache
cache deny NOCACHE     # Disable Cache

# Leave coredumps in the first cache dir
coredump_dir /var/spool/squid

#
# Add any of your own refresh_pattern entries above these.
#
# refresh_pattern ^ftp:           1440    20%     10080
# refresh_pattern -i (/cgi-bin/|\?) 0     0%      0
# refresh_pattern .               0       20%     4320


# Minimizing information returned to clients
acl errors http_status 400-599
deny_info TCP_RESET errors
http_reply_access deny errors

reply_header_access X-Squid-Error deny all
reply_header_access X-Cache deny all
reply_header_access X-Cache-Lookup deny all
reply_header_access Server deny all
reply_header_access Via deny all
reply_header_access Vary deny all
reply_header_access Mime-Version deny all
reply_header_access Content-Language deny all


# access log format
logformat jcombined "%{%Y/%m/%d %H:%M:%S}tl.%03tu" %>a %>st %<a %<st %>rP %mt "%rm %>ru HTTP/%rv" %>Hs "%{Referer}>h" "%Ss:%Sh"
access_log daemon:/var/log/squid/access.log jcombined


# SSL Intercept
sslcrtd_program /usr/lib64/squid/security_file_certgen -s /var/lib/squid/ssl_db -M 20MB
sslproxy_cert_error allow all
# sslproxy_flags DONT_VERIFY_PEER
always_direct allow all
ssl_bump stare all

この記事が気に入ったらサポートをしてみませんか?