ALBで指定のセキュリティポリシー以外の使用を検出するAWS Configのカスタムルールを作成してみた

カスタムルールを使いこなそう
2023.11.14

ALBでTLS 1.0が含まれているSSLポリシーを使用していたら検出したい

こんにちは、のんピ(@non____97)です。

皆さんはALBでTLS 1.0が含まれているSSLポリシーを使用していたら検出したいなと思ったことはありますか? 私はあります。

ALBやNLBで使用可能なセキュリティポリシーは以下AWS公式ドキュメントにまとまっています。

セキュリティポリシーにはELBSecurityPolicy-2016-08などTLS 1.0やTLS 1.1など非推奨なTLSの使用を許可しているものもあります。

古いクライアントをサポートするためにあえて選択することもあると思いますが、間違って設定してしまうことも多いと思います。

そのような場合はAWS Configで検出したいところです。

残念ながらAWS ConfigのマネージドルールではALBやNLBのセキュリティポリシーをチェックすることはできません。elb-predefined-security-policy-ssl-checkという、いかにもな名前のマネージドルールがありますが、こちらはCLBしかチェックできません。

ということでカスタムルールを作成して対応します。

いきなりまとめ

  • 基本的にはカスタムポリシールールを使いたい
  • カスタムポリシールールでサポートしているリソースタイプはAWS Config Resource Schema GitHub Repositoryのresource-typesを確認しよう
  • AWS Config RDKで生成したLambda関数のテンプレートを活用すれば、ロジックを少し書くだけでカスタムLambdaルールの実装ができる

ALBのリスナーをチェックするカスタムルールはカスタムLambdaルールである必要がある

作成するカスタムルールはカスタムLambdaルールである必要があります。

AWS Config のカスタムルールは以下の 2 種類あります。

  1. カスタム Lambda ルール
  2. カスタムポリシールール

前者はその名の通り、リソースをチェックする Lambda 関数を用意して、その Lambda 関数の評価結果をベースに準拠/非準拠を判断します。

後者は「Guard」と呼ばれる独自のポリシー言語を利用してリソースの評価をするルールです。

基本的には「カスタムポリシールール」を使うべきだと考えます。

理由は以下の通りです。

  1. Lambda 関数を用意する必要がなくなる
    • 定期的なランタイムのバージョンアップや、IAM ロールの権限周りの設計から解放される
  2. Lambda 関数実行にかかるコストが減る

一方、カスタムポリシールールの懸念点として「サポートされていないリソースタイプがある」が挙げられます。

サポートされているリソースタイプ一覧はAWS Config Resource Schema GitHub Repositoryのresource-typesにまとまっています。

残念ながらALBやNLBのリスナーであるAWS::ElasticLoadBalancingV2::Listenerは2023/11/14時点ではサポートされていません。

そのため、今回はカスタムLambdaルールを作成します。

カスタムLambdaルールの作成

AWS Configのベストプラクティスを確認すると、「AWS Config RDKを使ってカスタムルールを作成する」と記載されています。

15.AWS Config Rule Development Kit (RDK) を使用して、カスタムルールを作成します。

RDK はオーサリングエクスペリエンスを簡素化し、さまざまなリソースタイプのルールを検証します。

AWS Config ベストプラクティス | Amazon Web Services ブログ

DevelopersIOにもAWS Config RDKを使ってカスタムLambdaルールを用意する方法が紹介されています。

AWS Config RDKを使ってLambda関数のテンプレートを生成します。

Lambda関数で行いたい処理は以下のとおりです。

  1. カスタムLambdaルールのパラメーターとして指定したセキュリティポリシーが存在するか
    • 存在しなければ終了
  2. カスタムLambdaルールトリガー時のconfiguration_itemからリスナーのプロトコルを取得し、サポートしているプロトコルに含まれるか
    • サポートしているプロトコルでなければNOT_APPLICABLE
  3. カスタムLambdaルールトリガー時のconfiguration_itemからリスナーのセキュリティポリシーを取得し、カスタムLambdaルールのパラメーターとして指定したセキュリティポリシーリスト内に含まれるか
    • 含まれていればCOMPLIANT
    • 含まれていればNON_COMPLIANT

Config Ruleのパラメーターはリストで指定できません。そのため、許可したいSSLポリシーはカンマ区切りで指定する想定です。

最終的なコードは以下のとおりです。テンプレートから変更した箇所はハイライトしておきます。

./lib/lambda/check_elbv2_ssl_policy/index.py

import json
import sys
import datetime
import boto3
import botocore
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

try:
    import liblogging
except ImportError:
    pass

##############
# Parameters #
##############

# Define the default resource to report to Config Rules
DEFAULT_RESOURCE_TYPE = "AWS::ElasticLoadBalancingV2::Listener"

# Set to True to get the lambda to assume the Role attached on the Config Service (useful for cross-account).
ASSUME_ROLE_MODE = False

# Other parameters (no change needed)
CONFIG_ROLE_TIMEOUT_SECONDS = 900

# Support protocol
SUPPORT_PROTOCOL=['HTTPS', 'TLS']

#############
# Main Code #
#############


def evaluate_compliance(event, configuration_item, valid_rule_parameters):
    """Form the evaluation(s) to be return to Config Rules

    Return either:
    None -- when no result needs to be displayed
    a string -- either COMPLIANT, NON_COMPLIANT or NOT_APPLICABLE
    a dictionary -- the evaluation dictionary, usually built by build_evaluation_from_config_item()
    a list of dictionary -- a list of evaluation dictionary , usually built by build_evaluation()

    Keyword arguments:
    event -- the event variable given in the lambda handler
    configuration_item -- the configurationItem dictionary in the invokingEvent
    valid_rule_parameters -- the output of the evaluate_parameters() representing validated parameters of the Config Rule

    Advanced Notes:
    1 -- if a resource is deleted and generate a configuration change with ResourceDeleted status, the Boilerplate code will put a NOT_APPLICABLE on this resource automatically.
    2 -- if a None or a list of dictionary is returned, the old evaluation(s) which are not returned in the new evaluation list are returned as NOT_APPLICABLE by the Boilerplate code
    3 -- if None or an empty string, list or dict is returned, the Boilerplate code will put a "shadow" evaluation to feedback that the evaluation took place properly
    """

    ###############################
    # Add your custom logic here. #
    ###############################

    protocol = configuration_item["configuration"]["Protocol"]

    logger.info('Listener arn : %s',configuration_item["configuration"]["ListenerArn"])
    logger.info('Listener protocol : %s', protocol)

    if not protocol in SUPPORT_PROTOCOL:
        logger.info('NOT_APPLICABLE')
        return build_evaluation_from_config_item(configuration_item, "NOT_APPLICABLE")

    ssl_policy = configuration_item["configuration"]["SslPolicy"]
    rule_ssl_policies = [x.strip() for x in valid_rule_parameters['sslPolicies'].split(',')]

    logger.info('Listener SSL policy : %s', ssl_policy)
    logger.info('Rule SSL policies : %s', rule_ssl_policies)

    if ssl_policy in rule_ssl_policies:
        logger.info('COMPLIANT')
        return build_evaluation_from_config_item(configuration_item, "COMPLIANT")
    else:
        logger.info('NON_COMPLIANT')
        return build_evaluation_from_config_item(configuration_item, "NON_COMPLIANT")

def evaluate_parameters(rule_parameters):
    """Evaluate the rule parameters dictionary validity. Raise a ValueError for invalid parameters.

    Return:
    anything suitable for the evaluate_compliance()

    Keyword arguments:
    rule_parameters -- the Key/Value dictionary of the Config Rules parameters
    """

    rule_ssl_policies = [x.strip() for x in rule_parameters['sslPolicies'].split(',')]

    client = boto3.client('elbv2')
    
    try :
        client.describe_ssl_policies(Names=rule_ssl_policies)
    except client.exceptions.ClientError as e:
        logger.info('Rule SSL policies : %s', rule_ssl_policies)
        logger.error(e)
        raise ValueError from e

    valid_rule_parameters = rule_parameters
    return valid_rule_parameters

.
.
(以下略)
.
.

Lambda関数とカスタムLambdaルールはAWS CDKでデプロイします。

使用したコードは以下リポジトリに保存しています。

動作確認

許可しているセキュリティポリシーを設定した場合

それでは動作確認をしてみます。

AWS CDKデプロイ後、ELBSecurityPolicy-TLS13-1-2-2021-06のセキュリティポリシーを設定したHTTPSリスナーを持つALBを作成します。

セキュリティポリシーがELBSecurityPolicy-TLS13-1-2-2021-06

こちらのALBを作成してしばらくすると、コンプライアンスが準拠であるリスナーを確認できました。

準拠されていることを確認

Lambda関数のログは以下のとおりです。COMPLIANTで判定したことが分かります。

INIT_START Runtime Version: python:3.11.v18	Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:20e91e114d5658aaf7ecc7e7bcec28f4aca1368a06e7faa048c90033a9de7f31
START RequestId: 0fca4c90-8c7c-47fd-ad96-43b959a7b2cb Version: $LATEST
[INFO]	2023-11-13T08:35:34.578Z	0fca4c90-8c7c-47fd-ad96-43b959a7b2cb	Found credentials in environment variables.
[INFO]	2023-11-13T08:35:35.919Z	0fca4c90-8c7c-47fd-ad96-43b959a7b2cb	Listener arn : arn:aws:elasticloadbalancing:us-east-1:<AWSアカウントID>:listener/app/alb/18cce6b2b289a938/5ecc9c6c361275dc
[INFO]	2023-11-13T08:35:35.919Z	0fca4c90-8c7c-47fd-ad96-43b959a7b2cb	Listener protocol : HTTPS
[INFO]	2023-11-13T08:35:35.919Z	0fca4c90-8c7c-47fd-ad96-43b959a7b2cb	Listener SSL policy : ELBSecurityPolicy-TLS13-1-2-2021-06
[INFO]	2023-11-13T08:35:35.919Z	0fca4c90-8c7c-47fd-ad96-43b959a7b2cb	Rule SSL policies : ['ELBSecurityPolicy-TLS13-1-2-2021-06', 'ELBSecurityPolicy-TLS13-1-2-Res-2021-06']
[INFO]	2023-11-13T08:35:35.919Z	0fca4c90-8c7c-47fd-ad96-43b959a7b2cb	COMPLIANT
END RequestId: 0fca4c90-8c7c-47fd-ad96-43b959a7b2cb
REPORT RequestId: 0fca4c90-8c7c-47fd-ad96-43b959a7b2cb	Duration: 1775.33 ms	Billed Duration: 1776 ms	Memory Size: 128 MB	Max Memory Used: 76 MB	Init Duration: 232.23 ms

参考までにAWS::ElasticLoadBalancingV2::ListenerのJSONは以下のとおりです。

{
  "version": "1.3",
  "accountId": "<AWSアカウントID>",
  "configurationItemCaptureTime": "2023-11-13T07:52:43.753Z",
  "configurationItemStatus": "ResourceDiscovered",
  "configurationStateId": "1699861963753",
  "configurationItemMD5Hash": "",
  "arn": "arn:aws:elasticloadbalancing:us-east-1:<AWSアカウントID>:listener/app/alb/18cce6b2b289a938/5ecc9c6c361275dc",
  "resourceType": "AWS::ElasticLoadBalancingV2::Listener",
  "resourceId": "arn:aws:elasticloadbalancing:us-east-1:<AWSアカウントID>:listener/app/alb/18cce6b2b289a938/5ecc9c6c361275dc",
  "awsRegion": "us-east-1",
  "availabilityZone": "Regional",
  "tags": {},
  "relatedEvents": [],
  "relationships": [],
  "configuration": {
    "SslPolicy": "ELBSecurityPolicy-TLS13-1-2-2021-06",
    "LoadBalancerArn": "arn:aws:elasticloadbalancing:us-east-1:<AWSアカウントID>:loadbalancer/app/alb/18cce6b2b289a938",
    "DefaultActions": [
      {
        "Type": "forward",
        "TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:<AWSアカウントID>:targetgroup/test-tg/151945e8b918f93d",
        "Order": 1,
        "ForwardConfig": {
          "TargetGroups": [
            {
              "TargetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:<AWSアカウントID>:targetgroup/test-tg/151945e8b918f93d",
              "Weight": 1
            }
          ],
          "TargetGroupStickinessConfig": {
            "Enabled": false,
            "DurationSeconds": 3600
          }
        }
      }
    ],
    "Port": 443,
    "Certificates": [
      {
        "CertificateArn": "arn:aws:acm:us-east-1:<AWSアカウントID>:certificate/f1941e30-42f8-41cc-8d96-c850b3bb86e8"
      }
    ],
    "Protocol": "HTTPS",
    "ListenerArn": "arn:aws:elasticloadbalancing:us-east-1:<AWSアカウントID>:listener/app/alb/18cce6b2b289a938/5ecc9c6c361275dc",
    "AlpnPolicy": []
  },
  "supplementaryConfiguration": {}
}

Config Ruleのリソース一覧に作成したリスナーが表示されない場合は、以下re:Postに従ってトラブルシューティングします。

私の場合はAWS::ElasticLoadBalancingV2::ListenerをAWS Configのレコード設定で有効化していないことにより、時間を溶かしてしまいました。

許可していないセキュリティポリシーを設定した場合

次に、許可していないセキュリティポリシーを設定した場合の動作を確認します。

リスナーのセキュリティポリシーをELBSecurityPolicy-2016-08に変更します。

セキュリティポリシーをELBSecurityPolicy-2016-08に変更

すると、カスタムLambdaルールにてリスナーが非準拠状態になりました。

非準拠

Lambda関数のログは以下のとおりです。NON_COMPLIANTで判定したことが分かります。

INIT_START Runtime Version: python:3.11.v18	Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:20e91e114d5658aaf7ecc7e7bcec28f4aca1368a06e7faa048c90033a9de7f31
START RequestId: 4ff56904-f08c-49fb-a043-73e280bcffc2 Version: $LATEST
[INFO]	2023-11-13T09:48:27.153Z	4ff56904-f08c-49fb-a043-73e280bcffc2	Found credentials in environment variables.
[INFO]	2023-11-13T09:48:28.655Z	4ff56904-f08c-49fb-a043-73e280bcffc2	Listener arn : arn:aws:elasticloadbalancing:us-east-1:<AWSアカウントID>:listener/app/alb/18cce6b2b289a938/5ecc9c6c361275dc
[INFO]	2023-11-13T09:48:28.655Z	4ff56904-f08c-49fb-a043-73e280bcffc2	Listener protocol : HTTPS
[INFO]	2023-11-13T09:48:28.730Z	4ff56904-f08c-49fb-a043-73e280bcffc2	Listener SSL policy : ELBSecurityPolicy-2016-08
[INFO]	2023-11-13T09:48:28.730Z	4ff56904-f08c-49fb-a043-73e280bcffc2	Rule SSL policies : ['ELBSecurityPolicy-TLS13-1-2-2021-06', 'ELBSecurityPolicy-TLS13-1-2-Res-2021-06']
[INFO]	2023-11-13T09:48:28.731Z	4ff56904-f08c-49fb-a043-73e280bcffc2	NON_COMPLIANT
END RequestId: 4ff56904-f08c-49fb-a043-73e280bcffc2
REPORT RequestId: 4ff56904-f08c-49fb-a043-73e280bcffc2	Duration: 1953.81 ms	Billed Duration: 1954 ms	Memory Size: 128 MB	Max Memory Used: 76 MB	Init Duration: 251.05 ms

セキュリティポリシーが設定できないプロトコルの場合

続いて、セキュリティポリシーが設定できないプロトコルの場合の動作を確認します。

ALBにHTTPリスナーを追加します。

HTTPリスナーの追加

リスナーを追加して数分待ちましたが、追加したリスナーのコンプライアンスの判定はされていません。

コンプライアンスの状態が設定れされていないリスナーがあることを確認

Lambda関数のログは以下のとおりです。NON_COMPLIANTで判定したことが分かります。

START RequestId: a4cc46a1-fc1b-4b0c-82e3-b678c42d7d37 Version: $LATEST
[INFO]	2023-11-13T10:01:20.328Z	a4cc46a1-fc1b-4b0c-82e3-b678c42d7d37	Listener arn : arn:aws:elasticloadbalancing:us-east-1:<AWSアカウントID>:listener/app/alb/18cce6b2b289a938/e737c4f21e402091
[INFO]	2023-11-13T10:01:20.328Z	a4cc46a1-fc1b-4b0c-82e3-b678c42d7d37	Listener protocol : HTTP
[INFO]	2023-11-13T10:01:20.328Z	a4cc46a1-fc1b-4b0c-82e3-b678c42d7d37	NOT_APPLICABLE
END RequestId: a4cc46a1-fc1b-4b0c-82e3-b678c42d7d37
REPORT RequestId: a4cc46a1-fc1b-4b0c-82e3-b678c42d7d37	Duration: 549.33 ms	Billed Duration: 550 ms	Memory Size: 128 MB	Max Memory Used: 77 MB

許可するセキュリティポリシーリストにELBがサポートしていないセキュリティポリシーが含まれている場合

最後に、許可するセキュリティポリシーリストにELBがサポートしていないセキュリティポリシーが含まれている場合の動作を確認します。

カスタムLambdaルールのパラメーターsslPoliciesの値をELBSecurityPolicy-TLS13-1-2-2021-06, ELBSecurityPolicy-TLS13-1-2-Res-2021-06からELBSecurityPolicy-TLS13-1-2-2021-06, ELBSecurityPolicy-TLS13-1-2-Res-2021-06_に変更します。

sslPoliciesの値を変更

設定変更後、カスタムLambdaルールの再評価を行います。

特に既存リスナーのコンプライアンスの状態に変化はありませんでした。

Lambda関数のログは以下のとおりです。パラメーターの値が適していないとのことでエラーになっていますね。

INIT_START Runtime Version: python:3.11.v18	Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:20e91e114d5658aaf7ecc7e7bcec28f4aca1368a06e7faa048c90033a9de7f31
START RequestId: 5dd3a786-ba96-4fd9-9d75-949af6f13525 Version: $LATEST
[INFO]	2023-11-14T07:24:31.215Z	5dd3a786-ba96-4fd9-9d75-949af6f13525	Found credentials in environment variables.
[INFO]	2023-11-14T07:24:36.524Z	5dd3a786-ba96-4fd9-9d75-949af6f13525	Rule SSL policies : ['ELBSecurityPolicy-TLS13-1-2-2021-06', 'ELBSecurityPolicy-TLS13-1-2-Res-2021-06_']
[ERROR]	2023-11-14T07:24:36.524Z	5dd3a786-ba96-4fd9-9d75-949af6f13525	An error occurred (ServiceUnavailable) when calling the DescribeSSLPolicies operation (reached max retries: 4): Internal failure
{'internalErrorMessage': 'Parameter value is invalid', 'internalErrorDetails': 'An ValueError was raised during the validation of the Parameter value', 'customerErrorMessage': '', 'customerErrorCode': 'InvalidParameterValueException'}
END RequestId: 5dd3a786-ba96-4fd9-9d75-949af6f13525
REPORT RequestId: 5dd3a786-ba96-4fd9-9d75-949af6f13525	Duration: 5498.76 ms	Billed Duration: 5499 ms	Memory Size: 128 MB	Max Memory Used: 75 MB	Init Duration: 241.72 ms

カスタムルールを使いこなそう

ALBで指定のセキュリティポリシー以外を使用した場合を検出するAWS Configのカスタムルールを作成してみました。

NLBの場合もTLSリスナーで動作することを確認しています。

基本的にはマネージドルールを使いたいところですが、サポートされていないリソースについてはカスタムルールを使っていきましょう。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!