ALBのパスベースルーティングによるアクセス制御運用を楽にするスクリプトを作成した

この記事ではMacOS、python3.6を使用しています。

##スクリプトを作成した動機

ALB＋EC2の構成で運用している中で、phpやwordpressを使っていないにも関わらずアプリケーションログに/index.phpや/wp-adminのような明らかに不正アクセスを狙ったリクエストがアプリケーションレイヤーまで到達していることを発見したことが事の発端です。
もっと上位のレイヤーで防ぎたいと考え、ALBのパスベースルーティングを使用して、任意のアクセスパスのみターゲットグループにルーティングを許可し、その他はdefaultルールで遮断する、ホワイトリスト形式で運用しようという話になりました。
ただ、日々の運用タスクの比率を減らそうと自動化や効率化を図っている中でこれ以上運用タスクの時間を増やしたくない、APIが追加になるたびにコンソールでぽちぽちALBのアクセスパスを許可するのが面倒、CLIで設定するにしてもALBのarnなどを取ってくる必要もありこれまた面倒、これらの思いからスクリプト化して運用を楽にしようというところからスクリプトを作成する運びとなりました。

##ディレクトリ構成
スクリプトの実行環境のディレクトリ構成は以下になります。

```
┗━ setup_elb_rules       ＃ スクリプトのルートディレクトリ
　　┣━ project
　　┃    ┣━ sushi            
　　┃    ┃   ┗━ target_path.csv    ＃ 各ALB毎に許可するアクセスパスを設定
　　┃    ┗━ yakiniku         
　　┃        ┗━ target_path.csv    ＃ 各ALB毎に許可するアクセスパスを設定
　　┣━ elb_config.json          ＃ 各ALB、listenerのarnを設定
　　┗━ setup_elb_rules.py       ＃ 設定スクリプト
```
各ファイルについて順に詳しく説明していきます。

##projectディレクトリ
projectディレクトリの配下には各ALBが識別できるproject名、その配下のtarget_path.csvにアクセスを許可するパスを記載していきます。
アクセスパスを記載する際の注意点としてはパスパラメータやクエリパラメータが入る場合はワイルドカード（*）で指定する必要があります。

```json:target_path.csv
/maguro
/maguro/zuke
/maguro/aburi*
/maguro/*/toro
```

##elb_config.jsonファイル
elb_config.jsonにはALB、及びlistenerのarnを記載していきます。
当該環境ではproduction、staging、developmentの環境があり、それぞれAWSアカウントを分かれているため、以下のような設定になっております。
このスクリプトの実行環境を用意する上でここが一番の頑張りどころです。

```json:elb_config_json
{
    "production": {
        "sushi": {
            "http_listener": "arn:aws:elasticloadbalancing:eu-west-2:111111111111:listener/app/awseb-AWSEB-XXXXXXXXXXXX/1234567890/1234567890",
            "https_listener": "arn:aws:elasticloadbalancing:eu-west-2:111111111111:listener/app/awseb-AWSEB-YYYYYYYYYYYY/1234567890/1234567890",
            "target_group": "arn:aws:elasticloadbalancing:eu-west-2:111111111111:listener/app/awseb-AWSEB-ZZZZZZZZZZZ/1234567890/1234567890"
        },
        "yakiniku": {
            "http_listener": "arn:aws:elasticloadbalancing:eu-west-2:111111111111:listener/app/awseb-AWSEB-AAAAAAAAAAAA/1234567890/1234567890",
            "https_listener": "arn:aws:elasticloadbalancing:eu-west-2:111111111111:listener/app/awseb-AWSEB-BBBBBBBBBBBB/1234567890/1234567890",
            "target_group": "arn:aws:elasticloadbalancing:eu-west-2:111111111111:listener/app/awseb-AWSEB-CCCCCCCCCCCC/1234567890/1234567890"
        },
    },
    "staging": {
        "sushi": {
            "http_listener": "arn:aws:elasticloadbalancing:eu-west-2:222222222222:listener/app/awseb-AWSEB-XXXXXXXXXXXX/1234567890/1234567890",
            "https_listener": "arn:aws:elasticloadbalancing:eu-west-2:222222222222:listener/app/awseb-AWSEB-YYYYYYYYYYYY/1234567890/1234567890",
            "target_group": "arn:aws:elasticloadbalancing:eu-west-2:222222222222:listener/app/awseb-AWSEB-ZZZZZZZZZZZ/1234567890/1234567890"
        },
        "yakiniku": {
            "http_listener": "arn:aws:elasticloadbalancing:eu-west-2:222222222222:listener/app/awseb-AWSEB-AAAAAAAAAAAA/1234567890/1234567890",
            "https_listener": "arn:aws:elasticloadbalancing:eu-west-2:222222222222:listener/app/awseb-AWSEB-BBBBBBBBBBBB/1234567890/1234567890",
            "target_group": "arn:aws:elasticloadbalancing:eu-west-2:222222222222:listener/app/awseb-AWSEB-CCCCCCCCCCCC/1234567890/1234567890"
        }
    },
    "development": {
        "sushi": {
            "http_listener": "arn:aws:elasticloadbalancing:eu-west-2:333333333333:listener/app/awseb-AWSEB-XXXXXXXXXXXX/1234567890/1234567890",
            "https_listener": "arn:aws:elasticloadbalancing:eu-west-2:333333333333:listener/app/awseb-AWSEB-YYYYYYYYYYYY/1234567890/1234567890",
            "target_group": "arn:aws:elasticloadbalancing:eu-west-2:333333333333:listener/app/awseb-AWSEB-ZZZZZZZZZZZ/1234567890/1234567890"
        },
        "yakiniku": {
            "http_listener": "arn:aws:elasticloadbalancing:eu-west-2:333333333333:listener/app/awseb-AWSEB-AAAAAAAAAAAA/1234567890/1234567890",
            "https_listener": "arn:aws:elasticloadbalancing:eu-west-2:333333333333:listener/app/awseb-AWSEB-BBBBBBBBBBBB/1234567890/1234567890",
            "target_group": "arn:aws:elasticloadbalancing:eu-west-2:333333333333:listener/app/awseb-AWSEB-CCCCCCCCCCCC/1234567890/1234567890"
        }
    }
}
```

##setup_elb_rules.py

setup_elb_rules.pyの内容は以下になります。
設定対象のパスが設定済みかのチェックをいれており、設定済みのパスはスキップする挙動になるため、既存のALBに許可したいパスを追加する時も、上記のtarget_path.csvに追加対象のパスを追記してスクリプトを実行すれば作業完了となります。

```python:setup_elb_rules.py
import os
import sys
import csv
import json
import click
import boto3
from boto3.session import Session
from logging import getLogger, INFO, basicConfig

logger = getLogger(__name__)

class ElbConfig():
    def __init__(self, client, project, elb_config, target_listener, base_path):
        self.client = client
        self.project_name = project
        self.base_path = base_path
        self.target_group = elb_config['target_group']
        if target_listener == 'http':
            self.elb_listener = elb_config['http_listener']
        elif target_listener == 'https':
            self.elb_listener = elb_config['https_listener']
        self.elb_rules = self.client.describe_rules(
            ListenerArn=self.elb_listener
            )
        self.priority = self._get_last_priority()


    def _get_last_priority(self):
        priority_list = [ int(i['Priority']) for i in self.elb_rules['Rules'] if i['Priority'] != 'default' ]
        return max(priority_list) if priority_list else 0


    def _update_priority(self):
        if self.priority is None:
            self.priority = 10
        else:
            self.priority += 10


    def _check_configured_rules(self):
        all_rules_list = [ i['Conditions'][0]['Values'][0] for i in self.elb_rules['Rules'] if i['Priority'] != 'default' ]
        return all_rules_list


    def create_elb_rules(self):
        configured_rules_list = self._check_configured_rules()
        file_path = self.base_path  + "/project/" + self.project_name + '/' + 'target_path.csv'
        with open(file_path, 'r') as f:
            reader = csv.reader(f)
            for target_path in reader:
                if target_path[0] in configured_rules_list:
                    logger.info(f"To skip because of the configured path {target_path[0]}")
                    continue
                else:
                    self._update_priority()
                    response = self.client.create_rule(
                        Actions=[
                            {
                                'TargetGroupArn': self.target_group,
                                'Type': 'forward',
                            },
                        ],
                        Conditions=[
                            {
                                'Field': 'path-pattern',
                                'Values': [target_path[0]],
                            },
                        ],
                        ListenerArn=self.elb_listener,
                        Priority=self.priority,
                    )
                    logger.info(f"create rules {response['Rules'][0]['Conditions']}, priority {response['Rules'][0]['Priority']}")


def _get_elb_config(env_name, project_name, base_path):
    config_file = base_path  + "/" + 'elb_config.json'
    with open(config_file, 'r') as f:
        elb_config_dict = json.load(f)
    return elb_config_dict[env_name][project_name]


def get_elb_session(profile_name):
    session = Session(profile_name=profile_name)
    credentials = session.get_credentials()
    return boto3.client(
        'elbv2',
        aws_access_key_id=credentials.access_key,
        aws_secret_access_key=credentials.secret_key)


@click.command()
@click.option('--profile')
@click.option('--env')
@click.option('--target_listener', default='https')
@click.option('--project')
def main(profile, env, target_listener, project):
    basicConfig(level=INFO)
    base_path = os.path.dirname(os.path.abspath(sys.argv[0]))
    client = get_elb_session(profile)
    target_elb_config = _get_elb_config(env, project, base_path)
    ElbConfig(client, project, target_elb_config, target_listener, base_path).create_elb_rules()


if __name__ == '__main__':
    main()
```

##実行コマンド例
以下の場合のコマンド例になります。
環境名：development
プロファイル名：development
対象リスナー：https
プロジェクト名：sushi

```python

$ python3 setup_elb_rules.py --profile=development --env=development --target_listener=https --project=sushi
```

##最後に
以上、ALBのパス制御スクリプトになります。
環境に合わせて設定して頂く事である程度は汎用的に使えると思いますので、よければ使ってみてください。


##参考資料
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elbv2.html