【初心者向け】プロキシ と CORS についてまとめてみました。(図解)

はじめに

こんにちは、ディーネットの山中です。
今回はプロキシと CORS についてまとめてみました。
プロキシ編、CORS 編の2本立てで、座学 → 検証の流れとなります。

プロキシとは

まずはプロキシ編です。

プロキシ(フォワードプロキシ)とは

クライアントからのリクエストをプロキシが受け取り、
クライアントに代わってWebサーバへのリクエストを行ってくれます。

これによるメリットは以下となります。

  • Webフィルタリングを行う事ができる
    • 会社から表示したくないサイト(賭け事に関するサイトなど)にアクセスできないようにできる
  • クライアント認証を行う事ができる
    • 許可したユーザー以外には使用させないようにできる
    • また、問題が起こった際にログからどのクライアントから問題が起こったのか特定しやすくなる
  • クライアントの身元を隠せる
  • プロキシサーバがキャッシュしている場合はアクセスが速くなる
    フォワードプロキシ
    フォワードプロキシ

リバースプロキシとは

クライアントからのリクエストをリバースプロキシが受け取り、
Webサーバに変わってクライアントへのレスポンスを行ってくれます。

これによるメリットは以下となります。

  • Webサーバ側の身元を隠すことができる
  • リバースプロキシの背後にWebサーバが複数ある場合は、負荷分散ができる
    リバースプロキシ
    リバースプロキシ

フォワードプロキシとリバースプロキシの違い

  • フォワードプロキシ

    • Webブラウザの代わりにWebサーバへのリクエストを行ってくれます。
  • リバースプロキシ

    • Webサーバの代わりにWebブラウザにレスポンスを行ってくれます。

参考
「フォワードプロキシ」と「リバースプロキシ」の違い

以上、プロキシ編の座学でした。

プロキシ検証

ここからは実際の挙動について検証を行っていきます。

検証したい動き

リバースプロキシ 検証
リバースプロキシ 検証

こちらの挙動を実際にリバースプロキシを設定してみて確認してみます。

  1. リバースプロキシ 設定
  2. リバースプロキシ 動作確認

検証環境

・使用AMI
 ・AlmaLinux OS 8 (x86_64)
  ・ami-027abaaf97b9d3301

・インスタンスタイプ(web01,web02
 ・t3.small

・セキュリティグループ
 ・web01 ( www.denet-test.com )
  ・22 を 0.0.0.0/0 で解放
  ・80 を 0.0.0.0/0 で解放
 ・web02 ( app.denet-test.com )
  ・22 を 0.0.0.0/0 で解放
  ・80 を 0.0.0.0/0 で解放

・ドメイン ( denet-test.com ) については未取得なので、
 ローカルPCの /etc/hosts ファイルにて
 web01のIP と www.denet-test.com を
 web02のIP と app.denet-test.com を紐付けています

web01,web02共に
・Apacheインストール済み
・80番ポートの受付状態確認済み
・起動、自動起動設定済み
・ドキュメントルート配下に適当な index.html を配置後、IPでの初期ブラウザアクセス確認済み
・/etc/httpd/conf/httpd.conf には ServerName のみ記述
 ・wwwサーバ
  ServerName www.denet-test.com
 ・appサーバ
  ServerName app.denet-test.com

ローカルPCの /etc/hosts ファイルについて

リバースプロキシ 設定

最初にリバースプロキシの動作に必要なモジュールの有効化を確認します。
以下2つは必須となります。

[root@www ~]# httpd -M | egrep "proxy_module|proxy_http_module"
 proxy_module (shared)
 proxy_http_module (shared)

ロードバランス行いたい場合はこちらも(今回は不要)

[root@www ~]# httpd -M | egrep "proxy_balancer_module|lbmethod_byrequests_module"
 lbmethod_byrequests_module (shared)
 proxy_balancer_module (shared)

有効化されてないモジュールがあった場合は、
こちらのファイルの対象モジュールのコメントアウトを外して再起動か再読み込みをしていただければと思います。

/etc/httpd/conf.modules.d/00-proxy.conf

モジュールの確認ができましたら
wwwサーバにリバースプロキシの設定を行っていきます。
conf.d/ 配下に専用に conf ファイルを分けて作成するか、httpd.conf の最下行に以下を記述していきます。

ProxyPreserveHost Off
ProxyPass /app http://app.denet-test.com/
ProxyPassReverse /app http://app.denet-test.com/

ProxyPreserveHost
こちらを On にするとフォワードプロキシとして機能します。
踏み台として悪用される可能性があるのでフォワードプロキシとして動作させる時は
セキュリティ面を十分に気を付けてください。

ProxyPass
上記の設定の場合 http://www.denet-test.com/app へのリクエストを app.denet-test.com に転送します。
「/」を指定する事でドキュメントルートの指定となります。

ProxyPassReverse
プロキシ先 → リバースプロキシ → クライアントの戻りの Location レスポンスヘッダを修正してくれます。
動きとしては、プロキシ先から Location レスポンスヘッダが返ってきたときに
再度プロキシ先へリダイレクトさせずにクライアントに返すように修正してくれます。
基本的には ProxyPass と同じように指定します。

ヘッダーとは

設定が終わったら内容を反映させます。

[root@www ~]# apachectl configtest
[root@www ~]# systemctl status httpd
[root@www ~]# systemctl restart httpd
[root@www ~]# systemctl status httpd

上記設定ファイルでプロキシ先を app.denet-test.com に設定していますが、
denet-test.com のドメイン取得もDNS登録も行っていないので、
このままだと wwwサーバから www.denet-test.com と app.denet-test.com を見つける事ができません。

なので、ローカルPCの /etc/hosts に登録したように
wwwサーバの方の /etc/hosts にもそれぞれのドメインと IP を登録して見つけられるようにしておきます。

[root@www ~]# vi /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
54.178.196.1 www.denet-test.com
52.193.27.200 app.denet-test.com

hosts 設定後、wwwサーバにて curl コマンドを叩いて、
www.denet-test.com と app.denet-test.com それぞれにアクセスできれば hosts ファイルへの登録はOKです。

[root@www ~]# curl -v http://app.denet-test.com
* Rebuilt URL to: http://app.denet-test.com/
*   Trying 52.193.27.200...
* TCP_NODELAY set
* Connected to app.denet-test.com (52.193.27.200) port 80 (#0)
> GET / HTTP/1.1
> Host: app.denet-test.com
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK

~略~
[root@www ~]# curl -v http://www.denet-test.com
* Rebuilt URL to: http://www.denet-test.com/
*   Trying 54.178.196.1...
* TCP_NODELAY set
* Connected to www.denet-test.com (54.178.196.1) port 80 (#0)
> GET / HTTP/1.1
> Host: www.denet-test.com
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK

~略~

appサーバへのリダイレクトを分かりやすくしたいので、appサーバで index.html を編集します。

[root@app ~]# vi /var/www/html/index.html

編集内容にはドメインでも書いておきます。

app.denet-test.com

これで www → app へのリバースプロキシ設定が完了しました。

リバースプロキシ 動作確認

ブラウザからのアクセスで動作確認していきます。
アクセスが wwwサーバから appサーバにリダイレクトされ、appサーバの index.html が表示されている事を確認できればOKです。
※ Service Unavailable 503エラーが出た場合は SELinux の無効化 → reboot を試していただければと思います。

http://www.denet-test.com/app

curl コマンドでも確認してみます。
ステータスコードが200 & appサーバの index.html の内容を取得できている事が確認できます。

[root@www ~]# curl -v http://www.denet-test.com/app
*   Trying 54.178.196.1...
* TCP_NODELAY set
* Connected to www.denet-test.com (54.178.196.1) port 80 (#0)
> GET /app HTTP/1.1
> Host: www.denet-test.com
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK

~略~

app.denet-test.com
* Connection #0 to host www.denet-test.com left intact

以上、リバースプロキシの動作確認でした。

CORSの検証ではリバースプロキシ使わないので一旦コメントアウトしておきます。

#ProxyPreserveHost Off
#ProxyPass /app http://app.denet-test.com/
#ProxyPassReverse /app http://app.denet-test.com/

設定反映

[root@www ~]# apachectl configtest
[root@www ~]# systemctl status httpd
[root@www ~]# systemctl restart httpd
[root@www ~]# systemctl status httpd

リバースプロキシが動作せず、appサーバにリダイレクトされない事を確認します。
appサーバにリダイレクトされずに wwwサーバ内の /app を覗きに行こうとして何もないので 404 が出ています。

[root@www ~]# curl -v http://www.denet-test.com/app
*   Trying 54.178.196.1...
* TCP_NODELAY set
* Connected to www.denet-test.com (54.178.196.1) port 80 (#0)
> GET /app HTTP/1.1
> Host: www.denet-test.com
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 404 Not Found

CORS とは

続いては CORS 編です。

CORS とは Cross-Origin Resource Sharing の略称で、
ウェブブラウザが同一オリジン (※後述) 以外のリソースへのアクセスを制御するセキュリティメカニズムです。

例えば、前提としてユーザーからのアクセスがあった際に
www.example.com と app.example.com の2つのオリジンから画像を取得してページを表示するウェブサイトがあるとします。(イメージ図と平行して見てもらえばと思います)

この時、www.example.com から
「同一オリジン」である www.example.com への画像取得リクエストは許可されますが、
「異なるオリジン」である app.example.com への画像取得リクエストについては許可されないといった状態になります。

これは、ブラウザが「同一オリジンポリシー」(※後述) に基づいてリクエスト先が「同一オリジン」であるかを判断し、異なるオリジンであった場合は、そのリクエストを制限している為です。( CORS制約 )

この異なるオリジンへのリクエストを制約を緩めて可能にするのが CORS の許可設定となります。

CORS イメージ図_
CORS イメージ図_

参考
Cross-Origin Resource Sharing (CORS)

同一オリジンポリシーについて

以下の3つを比較し対象が同一オリジンであるかを判断するポリシーとなります。

・スキーム、プロトコル ( http, https )
・ドメイン ( www.example.com )
・ポート ( 80, 8080等 )

例えば、比較元が http://www.example.com:80 の場合は以下のように判断します。

http://www.example.com/dir2/
→ パスが違うだけなので同一ドメイン

https://www.example.com
→ スキームが違うので異なるオリジン

http://www.example.com:81
→ ポートが違うので異なるオリジン

http://app.example.com
→ ドメインが違うので異なるオリジン

参考
Same-origin policy

脆弱性と対策について

CORS の設定を緩めすぎると、悪意のあるサーバに攻撃されてしまう脆弱性があります。

例えば、 app.example.com の CORS 設定を "全てのオリジンからのアクセスを許可" にしたとします。
すると、悪意のあるサーバ ( warui.example.com ) から、リクエスト先の ( app.example.com ) へのアクセスが可能になってしまい、意図しない投稿や削除が行われたり、個人情報の漏洩に繋がってしまいます。

脆弱性
脆弱性

そのため、CORS にはこれらを防止するための保護戦略として「プリフライトリクエスト」があります。

プリフライトリクエストとは

やり取りを行う際、リクエスト先が異なるオリジンの場合、
最初に OPTIONS メソッドを使った HTTP ヘッダーのみの「プリフライトリクエスト」をリクエスト先に送ります。

この「プリフライトリクエスト」に対して、リクエスト先からの許可が下り、正常なレスポンスが返ってきたら、ブラウザはそのドメインへのリクエストを行うようになります。

プリフライトリクエスト
プリフライトリクエスト

以下の場合、リクエスト先 ( app.example.com ) では http://www.example.com からのアクセスのみを許可している状態なので、
warui.example.com からのアクセスを防ぐ事ができます。

アクセス側から送信されるプリフライトリクエスト

# リクエストを行うオリジン
Origin:http://www.example.com

# 許可してほしいメソッド
Access-Control-Request-Method:POST

# 許可してほしいリクエストヘッダー
Access-Control-Request-Headers:content-type

リクエスト先 許可するものを設定

# 指定したオリジンを許可、ここにワイルドカード「*」を指定すると全許可
Access-Control-Allow-Origin:http://www.example.com

# 指定したメソッドを許可
Access-Control-Allow-Methods:POST, GET, OPTIONS

# 指定したリクエストヘッダーを許可
Access-Control-Allow-Headers:Content-type

以上、プリフライトリクエストについてでした。

CORS編の座学はここまでとなります。
次は検証を行っていきます。

CORS 検証

ここからは実際の挙動について検証を行っていきます。

検証したい動き

www.denet-test.com から異なるオリジンである app.denet-test.com へのファイル取得を行っていきます。

CORS 検証_
CORS 検証_

以下の順で検証行っていきます。

  1. CORS 設定前の動作(リクエストが通らず CORS エラーが出る想定)
  2. CORS 設定後の動作(リクエストが通ってファイル取得ができる想定)

CORS 設定前の動作

CORS 設定前の動作確認を行っていきます。
リクエスト先へのアクセスは拒否される想定です。

wwwサーバにて index.html を編集します。

[root@www ~]# vi /var/www/html/index.html

以下のように記述します。
ここの部分で取得先を指定しています。

fetch('http://app.denet-test.com/app.jpg')
fetch('http://app.denet-test.com/app.json')

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>CORS動作確認</title>
</head>
<body>
  <h1>画像とJSONファイルの取得</h1>

  <img src="" id="image" alt="画像">
  <pre id="json"></pre>

  <script>
    // 画像の取得
    fetch('http://app.denet-test.com/app.jpg')
      .then(response => response.blob())
      .then(blob => {
        const imageUrl = URL.createObjectURL(blob);
        document.getElementById('image').src = imageUrl;
      })
      .catch(error => {
        console.error('画像の取得エラー:', error);
      });

    // JSONファイルの取得
    fetch('http://app.denet-test.com/app.json')
      .then(response => response.json())
      .then(data => {
        document.getElementById('json').textContent = JSON.stringify(data, null, 2);
      })
      .catch(error => {
        console.error('JSONファイルの取得エラー:', error);
      });
  </script>
</body>
</html>

appサーバにて wwwサーバに取得させる jpg と json ファイルを用意してドキュメントルート配下に置きます。
TeraTerm であればドラッグ&ドロップで送信先に /home/ec2-user を指定で jpg ファイルのアップロードができるかと思います。
※踏み台サーバを経由していたり、TeraTerm から直接サーバにログインしていない場合はアップロードできないみたいです。

json ファイルはこちらを使用します。

app.json

{
  "name": "John Doe",
  "age": 30,
  "email": "johndoe@example.com"
}

app側 ドキュメントルート配下はこのようになります。

[root@app ~]# ll /var/www/html/
total 544
-rw-r--r--  1 ec2-user ec2-user 546999 Jun  5 05:49 app.jpg
-rw-r--r--  1 root     root         73 Jun  6 09:57 app.json
-rw-r--r--. 1 root     root         16 Jun  6 07:33 index.html

www側

[root@www ~]# ll /var/www/html/
total 4
-rw-r--r-- 1 root root 936 Jun  6 09:53 index.html

これで準備が整いました。
ブラウザからのアクセスで CORS 設定前のファイル取得動作について確認していきます。
wwwサーバにアクセス → appサーバから画像を取得しようとする → CORS制約により拒否、の流れになります。

http://www.denet-test.com

以下のように CORS 制約によってブラウザにて CORS エラーの確認ができるかと思います。

CORSエラー
CORSエラー

CORS 設定後の動作

CORS 設定後の動作確認を行っていきます。
今回はリクエストが通って appサーバに置かれてある jpg と json ファイルが取得できる想定です。

appサーバにて
CORS に必要なモジュールを確認します。

[root@app ~]# httpd -M | grep headers_module
 headers_module (shared)

続いて appサーバにて
conf ファイルを分けて作成するか、httpd.conf 最下行に以下の記述を行います。

Header set Access-Control-Allow-Origin "http://www.denet-test.com"
Header set Access-Control-Allow-Methods "POST, GET, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type"

オリジン は http://www.denet-test.com を許可
メソッド は POST, GET, OPTIONS を許可
ヘッダー は Content-Type を許可

設定を反映させます。

[root@app ~]# apachectl configtest
[root@app ~]# systemctl status httpd
[root@app ~]# systemctl restart httpd
[root@app ~]# systemctl status httpd

動作確認を行っていきます。

curl コマンドを使用して wwwサーバからプリフライトリクエストを行い、
先程 appサーバに設定した内容がレスポンスヘッダーとして返ってくるか確認します。

OPTIONS メソッドで
Origin ヘッダーに http://www.denet-test.com
Access-Control-Request-Method ヘッダーに GET
Access-Control-Request-Headers ヘッダーに content-type
をセットして http://app.denet-test.com にプリフライトリクエストを行います。

[root@www ~]# curl -v -X OPTIONS -H "Origin: http://www.denet-test.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: content-type" http://app.denet-test.com/
*   Trying 52.193.27.200...
* TCP_NODELAY set
* Connected to app.denet-test.com (52.193.27.200) port 80 (#0)
> OPTIONS / HTTP/1.1
> Host: app.denet-test.com
> User-Agent: curl/7.61.1
> Accept: */*
> Origin: http://www.denet-test.com
> Access-Control-Request-Method: GET
> Access-Control-Request-Headers: content-type
>
< HTTP/1.1 200 OK
< Date: Wed, 07 Jun 2023 08:55:01 GMT
< Server: Apache/2.4.37 (AlmaLinux)
< Allow: OPTIONS,HEAD,GET,POST,TRACE
< Access-Control-Allow-Origin: http://www.denet-test.com
< Access-Control-Allow-Methods: POST, GET, OPTIONS
< Access-Control-Allow-Headers: Content-Type
< Content-Length: 0
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host app.denet-test.com left intact

以下部分で追加したレスポンスヘッダーが返ってきている事が確認できます。

< Access-Control-Allow-Origin: http://www.denet-test.com
< Access-Control-Allow-Methods: POST, GET, OPTIONS
< Access-Control-Allow-Headers: Content-Type

設定も問題なさそうなのでもう一度ブラウザからアクセスしてみます。

http://www.denet-test.com

先程は CORS エラーが出ていましたが、
今回はリクエストが成功して jpg, json ファイルの取得ができているかと思います。

CORS成功
CORS成功

ちなみに、ローカルPCの /etc/hosts に
www.denet-test.comのIP と warui.denet-test.com を紐づけて同様にブラウザからアクセスを試みた所、
プロトコルとポートは一致していますがドメインが違うので異なるオリジンと判断され
CORS エラーが発生しました。
「プリフライトリクエストとは」で記載した通りの結果となりました。

warui.denet-test.comからのリクエスト結果
warui.denet-test.comからのリクエスト結果

以上で検証が完了しました。

おわりに

お疲れ様でした、以上がプロキシと CORS についてのまとめとなります。

この記事がエンジニア初学者の参考になれば幸いです。
最後まで読んでいただきありがとうございました。

よければこちらも見ていただけると幸いです。
【STAR★FLAP 出発進行!】各駅停車「自動パタパタ展示」行き ~始発駅:企画編~
【STAR★FLAP 出発進行!】各駅停車「自動パタパタ展示」行き ~2駅目:デザイン編~
【STAR★FLAP 出発進行!】各駅停車「自動パタパタ展示」行き ~3駅目:組み込み編~
【STAR★FLAP 出発進行!】各駅停車「自動パタパタ展示」行き ~4駅目:ジオラマ編~

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA