一つのCloudFrontで複数ドメインのアクセスをAWS WAFで制限する

2021.05.10

はじめに

CloudFrontでは、複数のドメインを設定してリクエストを処理することができますが、ドメイン毎にアクセス制限、例えばIP制限を行う場面でどのように設定するのかやってみました。 IP制限は、親和性が高いAWS WAFでやってみます。

構成

構成は以下の通りです。1つのCloudFrontで3つドメインのリクエストを処理できるようにします。 Originは、IP制限を簡易的に確認するだけの構成なのでS3バケットを指定します。 ドメインを用意(例としてexample.domain)してPublicHostedZoneを作成しておきます。 証明書はAmazon Certicicate Managerで作成します。

設定(下準備)

まずは、ACMの証明書では、Zone Apexとワイルドカード証明書を発行し、CloudFrontリソースにアタッチします。検証では3つのドメインでアクセスできるようにワイルドカードで発行します。

次にS3バケットを作成します。簡易的にindex.htmladmin/index.html を配置しておきます。 次にCloudFrontリソースを作成します。Originは、S3バケットを指定し、OAIでバケットアクセスを制限します。 Alternate Domain Names(CNAMEs) では、Zone Apexとワイルドカードを入力します。ACM同様にワイルドカードで設定することでサブドメインの増減、変更の対応が不要となります。

最後にRoute53で3つのDNSレコードを作成します。

  • example.domain:ALIAS
  • test.example.domain:CNAME
  • test1.example.domain:CNAME

設定(AWS WAF)

下準備が終わったのでCLoudFrontリソースにAWS WAFを設定していきます。 概要は以下の通りです。

  • 共通
    • URIPathにadminを含む Deny
  • example.domain
    • index.html Allow
  • test.example.domain
    • index.html Allow
    • /admin/index.html 特定IPのみAllow
  • test1.example.domain
    • index.html 特定IPのみAllow
    • /admin/index.html 特定IPのみAllow

まずは、 IP sets で特定IPのリストを作成します。

次にRuleGroupを作成します。 1つ目は、URIPathにadminを含む場合は拒否するルールです。

{
  "Name": "adminblock",
  "Priority": 0,
  "Statement": {
    "ByteMatchStatement": {
      "SearchString": "admin",
      "FieldToMatch": {
        "UriPath": {}
      },
      "TextTransformations": [
        {
          "Priority": 0,
          "Type": "LOWERCASE"
        }
      ],
      "PositionalConstraint": "CONTAINS"
    }
  },
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "adminblock"
  }
}

2つ目のRuleGroupは2つのルールを作成します。 1つ目のルールは、hostヘッダがtest.example.domainと一致し、URIPathにadminを含む特定のIPアドレスのリクエストを許可するルールです。

{
  "Name": "tenant1allow",
  "Priority": 0,
  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "ByteMatchStatement": {
            "SearchString": "test.example.domain",
            "FieldToMatch": {
              "SingleHeader": {
                "Name": "host"
              }
            },
            "TextTransformations": [
              {
                "Priority": 0,
                "Type": "LOWERCASE"
              }
            ],
            "PositionalConstraint": "EXACTLY"
          }
        },
        {
          "IPSetReferenceStatement": {
            "ARN": "arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/ipset/SpecificIPList/3ef7186f-0ecd-4dc0-bf76-xxxxxxxxxxxx"
          }
        },
        {
          "ByteMatchStatement": {
            "SearchString": "admin",
            "FieldToMatch": {
              "UriPath": {}
            },
            "TextTransformations": [
              {
                "Priority": 0,
                "Type": "LOWERCASE"
              }
            ],
            "PositionalConstraint": "CONTAINS"
          }
        }
      ]
    }
  },
  "Action": {
    "Allow": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "tenant1allow"
  }
}

2つ目のルールは、hostヘッダがtest.example.domainと一致し、URIPathにadminを含むリクエストを拒否するルールです。

{
  "Name": "tenant1block",
  "Priority": 1,
  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "ByteMatchStatement": {
            "SearchString": "test.example.domain",
            "FieldToMatch": {
              "SingleHeader": {
                "Name": "host"
              }
            },
            "TextTransformations": [
              {
                "Priority": 0,
                "Type": "LOWERCASE"
              }
            ],
            "PositionalConstraint": "EXACTLY"
          }
        },
        {
          "ByteMatchStatement": {
            "SearchString": "admin",
            "FieldToMatch": {
              "UriPath": {}
            },
            "TextTransformations": [
              {
                "Priority": 0,
                "Type": "LOWERCASE"
              }
            ],
            "PositionalConstraint": "CONTAINS"
          }
        }
      ]
    }
  },
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "tenant1block"
  }
}

複数のルールの設定する際に気をつける点は、ルールのPriorityです。RuleGroupの中にあるルールの先頭から順番に評価します。ここでは特定IPを許可するルールのPriorityを0、それ以外を拒否するルールのPriorityを1とします。 Web ACL でのルールとルールグループの処理順序

3つ目のRuleGroupも2つルールです。 1つ目のルールは、hostヘッダがtest1.example.domainと一致し、特定のIPアドレスのリクエストを許可するルールです。

{
  "Name": "tenant2allow",
  "Priority": 0,
  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "ByteMatchStatement": {
            "SearchString": "test1.example.domain",
            "FieldToMatch": {
              "SingleHeader": {
                "Name": "host"
              }
            },
            "TextTransformations": [
              {
                "Priority": 0,
                "Type": "LOWERCASE"
              }
            ],
            "PositionalConstraint": "EXACTLY"
          }
        },
        {
          "IPSetReferenceStatement": {
            "ARN": "arn:aws:wafv2:us-east-1:xxxxxxxxxxxx:global/ipset/SpecificIPList/3ef7186f-0ecd-4dc0-bf76-xxxxxxxxxxxx"
          }
        }
      ]
    }
  },
  "Action": {
    "Allow": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "tenant2allow"
  }
}

2つ目のルールは、hostヘッダがtest1.example.domainと一致するリクエストを拒否するルールです。

{
  "Name": "tenant2block",
  "Priority": 1,
  "Statement": {
    "ByteMatchStatement": {
      "SearchString": "test1.example.domain",
      "FieldToMatch": {
        "SingleHeader": {
          "Name": "host"
        }
      },
      "TextTransformations": [
        {
          "Priority": 0,
          "Type": "LOWERCASE"
        }
      ],
      "PositionalConstraint": "EXACTLY"
    }
  },
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "tenant2block"
  }
}

RuleGroupの作成は以上です。Global(CloudFront)のWeb ACLsを作成します。Associated AWS rewourcesには、作成したCloudFrontリソースを追加します。

Add my own rules add rule groupsでRuleを追加していきます。

作成した3つのRuleGroupを追加します。WebACLのDefault actionはAllowで良いです。

set rule priorityでadminblockの順番を最後尾にします。最初にするとadminを含むURIPathリクエストをブロックしてしまいます。その他のRuleGroupを先に評価してからadminblockを評価するように順番を設定します。

動作確認

異なるIPでアクセスしてみます。筆者の環境ではProxyの有無で動作確認しています(Proxy有りが許可するIPアドレス)。プロキシ環境下で無い場合は、 -x socks4://proxyserver:port は不要です。

example.domain

アクセスを制限するルールが無い為、正常にアクセスできます。

% curl https://example.domain
top page.
% curl -x socks4://proxyserver:port https://example.domain
top page.

URIPathにadminを含む

URIPathにadminが含まれる為、アクセスが拒否されます。

% curl https://example.domain/admin/index.html
<h1>403 ERROR</h1>
<h2>The request could not be satisfied.</h2>

<hr noshade="noshade" size="1px" />

Request blocked.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
<br clear="all" />If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
<br clear="all" />

<hr noshade="noshade" size="1px" />

<pre>Generated by cloudfront (CloudFront)
Request ID: mgWnYJCQ9FSXliwSmePsV7Vw962KrLzVO7fW7AW3jtOYrpg4dBdElg==
</pre>
<address> </address>%
% curl -x socks4://proxyserver:port https://example.domain/admin/index.html
<h1>403 ERROR</h1>
<h2>The request could not be satisfied.</h2>

<hr noshade="noshade" size="1px" />

Request blocked.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
<br clear="all" />If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
<br clear="all" />

<hr noshade="noshade" size="1px" />

<pre>Generated by cloudfront (CloudFront)
Request ID: 4Q5udwa9nEn8Aa-GPxCwBXbFQnKv0dITiSnccGnwcQo2aAoWKI9shw==
</pre>
<address> </address>%

test.example.domain

こちらもアクセスを制限するルールが無い為、正常にアクセスできます。

% curl https://test.example.domain
top page.
% curl -x socks4://proxyserver:port https://test.example.domain
top page.

test.example.domain(adminを含む)

今度は特定のIPだけがアクセスできます。

% curl https://test.example.domain/admin/index.html
<h1>403 ERROR</h1>
<h2>The request could not be satisfied.</h2>

<hr noshade="noshade" size="1px" />

Request blocked.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
<br clear="all" />If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
<br clear="all" />

<hr noshade="noshade" size="1px" />

<pre>Generated by cloudfront (CloudFront)
Request ID: jXsG3QF20bFBYaRi4t-6wQmj_ZpHCIrapKh09exUjbgGUlRlZzvk_A==
</pre>
<address> </address>
% curl -x socks4://proxyserver:port https://test.example.domain/admin/index.html
admin page.

test1.example.domain

こちらも特定のIPだけがアクセスできます。

% curl https://test1.example.domain
<h1>403 ERROR</h1>
<h2>The request could not be satisfied.</h2>

<hr noshade="noshade" size="1px" />

Request blocked.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
<br clear="all" />If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
<br clear="all" />

<hr noshade="noshade" size="1px" />

<pre>Generated by cloudfront (CloudFront)
Request ID: egSXkRk5s5Ingl-3q8VgmnvInW2fjIMoEpP8R2o7z0U2cJgZGBvQXA==
</pre>
<address> </address>
% curl https://test1.example.domain/admin/index.html
<h1>403 ERROR</h1>
<h2>The request could not be satisfied.</h2>

<hr noshade="noshade" size="1px" />

Request blocked.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
<br clear="all" />If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
<br clear="all" />

<hr noshade="noshade" size="1px" />

<pre>Generated by cloudfront (CloudFront)
Request ID: 9Vh9_5kTu9RRd6Nky_CUxX9YZ_Rf9rCh4SAtB5LicARXRyKODSejfw==
</pre>
<address> </address>
% curl -x socks4://proxyserver:port https://test1.example.domain
top page.
% curl -x socks4://proxyserver:port https://test1.example.domain/admin/index.html
admin page.

さいごに

マルチテナント構成の一案として利用されるであろう今回のような環境でどのようにアクセス制限できるのか検証してみました。AWS WAFで複数のルールを設定すると評価の優先順位があることに注意して設定してください。