CloudFront Functionsで実現するCIDRベースのアクセス制御

## はじめに
特定のIP範囲のみにWebサービスやツールへアクセスを制限したい場合、一般的にはWAFでIP制御を行えますが、コストや運用の複雑化が課題となることもあります。
そこで、AWS CloudFront Functions を利用して、CIDRベースのアクセス制御を軽量かつ低コストで実現する方法を紹介します。
これによりセキュリティ強化とコスト削減とを同時に実現することを目指します。

## 本記事で扱う内容
- アクセス制限をするために使用可能なサービスの比較
- CloudFront Functions でアクセス制限をするための実装例
- CloudFront Functions のデプロイ方法

## CloudFront Functionsと他サービスとの比較

**WAF (Web Application Firewall):**
- L7制御可能で細かなルール定義ができる
- ただしコスト増大・ルール運用が複雑化しがち

**Lambda@Edge:**
- CloudFrontでNode.jsやPythonを使用した複雑なロジックの実行が可能
- しかしデプロイや実行コストがCloudFront Functionsより重い

**CloudFront Functions:**
- 軽量なJavaScript実行環境
- 低コスト・低レイテンシ
- デプロイ/更新が容易
- CIDR判定などのシンプルなロジックに向く

## アクセス制御の仕組み

CloudFront Functionsを使用すれば、リクエストがオリジンに到達する前のViewer Requestなどのフェーズでアクセス制限を実行できます。

1. `event.viewer.ip` からクライアントIPを取得
2. 許可するCIDR一覧とクライアントIPを比較
3. 一致すればリクエスト通過、不一致なら403Forbiddenレスポンスを返却

![Screenshot 2024-12-14 at 0.05.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245686/ac11838a-8590-ee1a-45db-8c77e40ada10.png)


## 実装例
以下はCIDR判定を行うサンプルコードです。

event.viewer.ipでCloudFrontにアクセスしてきたクライアントのIPアドレスを取得します。このclientIPが後ほどCIDR範囲に含まれるかチェックされます。(Cloudfront-js-2.0から使用可能)

```javascript
var request = event.request;
var clientIP = event.viewer.ip;
```

Inputは許可されるCIDR一覧をカンマ区切りで記述した文字列です。split(',')で配列化します。

```javascript
var allowedCIDRsInput = '203.0.113.0/24,198.51.100.0/24';
var allowedCIDRs = allowedCIDRsInput.split(',');
```

与えられたipとcidrを基に、ipがcidr範囲内に属するか判定します。

```javascript
function isIPInCIDR(ip, cidr) {
    var cidrParts = cidr.split('/');
    var baseIP = cidrParts[0];
    var maskBits = parseInt(cidrParts[1], 10);
...
}
```

関数を用いてIPv4アドレスを32ビット整数に変換します。

```javascript
function ipToInt(ip) {
    var parts = ip.split('.');
    if (parts.length !== 4) {
        return null;
    }
    return (
        (parseInt(parts[0], 10) << 24) |
        (parseInt(parts[1], 10) << 16) |
        (parseInt(parts[2], 10) << 8) |
        parseInt(parts[3], 10)
    ) >>> 0;
}
```

ネットワーク部とホスト部を分けるためのビットマスクを計算します。32ビット整数にしたクライアントIPとCIDRのネットワーク部を比較してクライアントIPがCIDRの範囲内にあることを判定します。

```javascript
var mask = ~(Math.pow(2, 32 - maskBits) - 1);
var ipInt = ipToInt(ip);
var baseInt = ipToInt(baseIP);

if (ipInt === null || baseInt === null) {
    return false;
}

return (ipInt & mask) === (baseInt & mask);
```

許可CIDRが複数ある場合は上記の処理をループしてクライアントIPがいずれかのCIDR内にあるか確認します。一つでも範囲内と判定された場合はリクエストをそのまま返してオリジンに進めます。

```javascript
for (var i = 0; i < allowedCIDRs.length; i++) {
    if (isIPInCIDR(clientIP, allowedCIDRs[i])) {
        return request;
    }
}
```

どのCIDRにも当てはまらなかった場合は403 Forbiddenを返します。これにより、オリジンサーバーに到達せず、アクセスが遮断されます。

```javascript
return {
    statusCode: 403,
    statusDescription: 'Forbidden',
    headers: {
        'content-type': { value: 'text/html; charset=UTF-8' }
    },
    body: '<h1>403 Forbidden</h1><p>Access denied.</p>'
};
```

---
**コードの全体**

```javascript
function handler(event) {
    var request = event.request;
    var clientIP = event.viewer.ip;
    
    var allowedCIDRsInput = '203.0.113.0/24,198.51.100.0/24';
    var allowedCIDRs = allowedCIDRsInput.split(',');
    
    function isIPInCIDR(ip, cidr) {
        var cidrParts = cidr.split('/');
        var baseIP = cidrParts[0];
        var maskBits = parseInt(cidrParts[1], 10);
    
        function ipToInt(ip) {
            var parts = ip.split('.');
            if (parts.length !== 4) {
                return null;
            }
            return (
                (parseInt(parts[0], 10) << 24) |
                (parseInt(parts[1], 10) << 16) |
                (parseInt(parts[2], 10) << 8) |
                parseInt(parts[3], 10)
            ) >>> 0;
        }
    
        var mask = ~(Math.pow(2, 32 - maskBits) - 1);
        var ipInt = ipToInt(ip);
        var baseInt = ipToInt(baseIP);
    
        if (ipInt === null || baseInt === null) {
            return false;
        }
    
        return (ipInt & mask) === (baseInt & mask);
    }
    
    for (var i = 0; i < allowedCIDRs.length; i++) {
        if (isIPInCIDR(clientIP, allowedCIDRs[i])) {
            return request;
        }
    }
    
    return {
        statusCode: 403,
        statusDescription: 'Forbidden',
        headers: {
            'content-type': { value: 'text/html; charset=UTF-8' }
        },
        body: '<h1>403 Forbidden</h1><p>Access denied.</p>'
    };
}
```

## CloudFront Functionsへの適用手順

### Functionsの作成：

1. CloudFrontのコンソールから「関数」を選択
2. 「関数を作成」をクリック
3. 「名前」と「ランタイム」を設定し「関数を作成」をクリック

![Screenshot 2024-12-14 at 14.58.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245686/4decd84f-c05b-ee1a-4af9-b58ad276c4e7.png)

### 関数の設定：
1. 作成した関数をクリックすると詳細画面が表示される
2. 「構築」　→「関数コード」から関数を設定する。
3. 「変更を保存」をクリックし保存

![Screenshot 2024-12-14 at 14.59.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245686/d9c39224-1117-e446-09dd-2f7a2826a3ae.png)


### Distributionへの割当：

1. 「発行」から「関連付けを追加」をクリック
2. 「ディストリビューション」, 「イベントタイプ」, 「キャッシュビヘイビア」を適切に設定し「関連付けを追加」をクリック
3. 「関数を発行」をクリック

![Screenshot 2024-12-14 at 14.59.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245686/e210d8b8-965b-1b76-afca-9220eb075c97.png)


## まとめ
CloudFront Functionsを活用することで、CIDRベースのアクセス制御がスムーズに行えます。
これにより、低コスト・高速なセキュリティ対策を実現可能です。


## 参考資料・リンク
- [CloudFront FunctionsでIP制限を試してみた](https://dev.classmethod.jp/articles/cloudfront-functions-ip-control/)
- [CloudFront Functions と Lambda@Edge の違い](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/edge-functions-choosing.html)
- [リクエストに True-Client-IP ヘッダーを追加する](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/example-function-add-true-client-ip-header.html)