Techfirm Cloud Architect Blog

テックファーム株式会社クラウドインフラグループのブログ

OpenSearchダッシュボードを多要素認証に

パスワード認証だけになっていませんか?
認証がユーザ名とパスワードだけで許される時代は終わり、多要素認証やパスキーの利用が当たり前になりました。
もちろん、インフラの管理ツール等についても同じです。

SIEM on Amazon OpenSearch Serviceは、ログを収集してOpenSearch Serviceで分析・可視化できるようしてくれます。
OpenSearchで参照できる情報は、CloudTrailの証跡をはじめとして、構成やセキュリティ設定を読み取れるセンシティブな情報です。それらを閲覧できる画面がパスワード認証だけなんて許されません。
そこで、OpenSearch Serviceの視覚化ツールであるOpenSearchダッシュボードに多要素認証を導入してみました。

今回は、Terraformを使ってOpenSearchダッシュボードに多要素認証を導入する方法の1つを紹介します。
(紹介しているのはOpenSearch Service単体の設定です。SIEM on Amazon OpenSearch Serviceの構成をTerraformで管理するには別に工夫が必要です)

OpenSearchダッシュボード

OpenSearchダッシュボードはOpenSearchが取り込んだデータを視覚化するツールです。

OpenSearchダッシュボードはAWSマネジメントコンソールからアクセスするものではなく、専用のURLからアクセスします。
つまり、AWSマネジメントコンソールとは認証が別になっています。

OpenSearchダッシュボードは内部ユーザデータベースで独自のユーザ管理と認証を行うことができます。
内部ユーザデータベースはOpenSearchのみで完結するため設定が簡単ですが、認証はパスワード認証のみです。
今回は多要素認証を導入したいため、機能として不十分となります。

OpenSearchダッシュボードは、内部ユーザデータベースの他にもSAML認証Cognito認証をサポートしています。
SAML認証では、IAM Identity Centerを利用することが可能です。IAM Identity Centerを利用している場合は、こちらでユーザを集中管理できます。

IAM Identity Center等を利用していない場合は、Amazon Cognitoと連携させることで比較的容易に多要素認証を含む細かい設定が可能です。

今回の設定概要

今回は、OpenSearchダッシュボードをCognito認証にすることで多要素認証を実現します。
大まかに、設定する内容は以下のようになります。

  1. Cognito IDプールの作成
  2. Cognitoユーザプールの作成
  3. OpenSearchにCognito連携の設定
  4. OpenSearchが自動的に作成したCognito設定のカスタマイズ

Cognito

Amazon Cognitoには、大きくユーザプールとIDプールがあります。
ユーザプールが実際のユーザを管理して認証する機能を提供し、IDプールは認証されたユーザにAWSの認証情報を提供するものです。

OpenSearchと連携させるためには両方作成する必要がありますが、ユーザプールとIDプールの連携はOpenSearch設定時に自動的に設定されます。

IDプールの設定

認証に関する設定等が数多くあるユーザプールと違い、IDプールは設定項目が少ないです。

本来ならIDプロバイダーの設定等が入るところですが、OpenSearch設定時に自動的に設定されるためここでの設定は不要です。

resource "aws_cognito_identity_pool" "aes_siem" {
  identity_pool_name               = "aes-siem-id-pool"

  # 認証していないユーザーの許可(OpenSearch設定時に上書き)
  allow_unauthenticated_identities = true

  lifecycle {
    # 自動設定される項目は管理から除外する
    ignore_changes = [
      allow_unauthenticated_identities,
      cognito_identity_providers,
    ]
  }
}

IDプロバイダーの設定等は後から追加されるためignore_changesで変更検知されないようにするのを忘れないでください。

ユーザプールの設定

Terraformのaws_cognito_user_poolリソースでユーザプールを作成します。
ユーザ管理方法やパスワードポリシー、パスワードリカバリ設定等、要件にあわせて適切な設定を行ってください。

また、Cognitoユーザプールにはドメインの設定(aws_cognito_user_pool_domainリソース)が必須になります。
こちらも環境にあわせて適切に設定してください。独自ドメインのFQDNではなくドメインプレフィックスを設定し、Cognitoドメインとすることも可能です。

resource "aws_cognito_user_pool" "aes_siem_users" {
  name                = "aes-siem-users"

  admin_create_user_config {
    allow_admin_create_user_only = true
    invite_message_template {
      email_subject = local.siem_cognito_invite_email_subject
      email_message = local.siem_cognito_invite_email_template
      sms_message   = local.siem_cognito_invite_sms_message
    }
  }
  auto_verified_attributes = [
    "email",
  ]
  email_configuration {
    email_sending_account = "COGNITO_DEFAULT"
  }
  account_recovery_setting {
    recovery_mechanism {
      name     = "verified_email"
      priority = 1
    }
  }
}
resource "aws_cognito_user_pool_domain" "aes_siem_users" {
  domain       = "tf-example-aes-siem"
  user_pool_id = aws_cognito_user_pool.aes_siem_users.id
}

IDプールと接続するためのアプリケーションクライアントの設定は不要です。 OpenSearchとの連携を設定した時に自動的に作成されます。

多要素認証

本記事の主体である多要素認証の設定はCognitoユーザプールに対して行います。
Cognitoがサポートする多要素認証は、電話番号へのSMSメッセージと時間ベースワンタイムパスワード(ソフトウェアトークン)です。

SMSメッセージを利用する場合は、各ユーザに電話番号(phone_number属性)の設定が必要です。
また、送信料金がかかる他、SNSの使用量制限の緩和等も必要です。
ワンタイムパスワードよりも考慮事項は多くなります。

時間ベースワンタイムパスワードは、ユーザがトークンを発行するアプリ(仮想MFAデバイス)を導入する必要があります。 (執筆時点でハードウェアトークンには対応していないようです)
しかし、AWS側の設定は簡単ですし考慮が必要な点もほとんどありません。

ワンタイムパスワードのみ有効にした多要素認証の設定例は以下のようになります。

resource "aws_cognito_user_pool" "aes_siem_users" {
  ……他の設定は省略……

  mfa_configuration          = "ON"
  software_token_mfa_configuration {
    enabled = true
  }
}

mfa_configurationONOPTIONALOFFのいずれかを設定できます。
多要素認証を必須にする場合はONを設定してください。

software_token_mfa_configurationブロックは有効か無効かの設定のみです。

認証画面やソフトウェアトークンの設定画面もCognitoに用意されているため、これだけで多要素認証を有効化できます。

ロールの設定

OpenSearchへのアクセスコントロールは以下のようなものがあります。

Cognito認証の場合、認証されたユーザに対して対応するIAMロールの認証情報を発行します。
この時、アイデンティティベースのポリシーはIAMロールに紐付けられたポリシーのことを指します。
きめ細かなアクセスコントロールでは、OpenSearch内部のロールとIAMロールを紐づけます。この設定はOpenSearchダッシュボード内で設定可能です。
OpenSearchのアクセスコントロール設定方法の詳細はAWSの各種ドキュメントを参照してください。

OpenSearchへのアクセスコントロールの設定をするために、ユーザごとにどのIAMロールを割り当てるか設定が必要です。
Cognitoユーザに対してIAMロールを選択する方法は以下の3つから選択できます。

  1. デフォルトの認証されたロールを使用
  2. ルールを使用してロールを選択
  3. トークンでpreferred_roleクレームを持つロールを選択

1はもっとも簡単な設定ですが、すべてのユーザが同じIAMロールに設定されてしまいます。
2では、IDプール上で条件によるロールの選択を行います。IDトークンに含まれるクレームの値によって選択することが可能です。
3では、ユーザプールのグループによってロールの選択を行います。ユーザプール側の設定でIAMロールの選択を行うことになります。

2と3を利用する場合は、OpenSearchとCognitoを連携した後に設定が必要です。そちらは後述します。
3のユーザグループを利用する方が、連携後に必要な設定内容が単純でIaCで管理しやすいかと思います。

ここでは、3のユーザグループを利用する時のユーザプールの設定を解説します。

まず、ログインしたユーザに割り当てるためのIAMロールを作成します。
IAMロールの信頼関係で、IDプールからのAssumeRoleWithWebIdentityを許可している必要があります。
Conditionで特定のIDプールからのみ許可することも可能です。

以下はIAMロールを作成する例です。
ロールにポリシーを割り当てるところは省略していますので、OpenSearchのAPIへのアクセスを許可したポリシーを割り当ててください。

locals {
  siem_cognito_user_roles = {
    admin = {}
    read  = {}
  }
}
resource "aws_iam_role" "aes_siem_cognito_user" {
  for_each = local.siem_cognito_user_roles

  name                = "aes-siem-user-role-${each.key}"
  assume_role_policy  = data.aws_iam_policy_document.aes_siem_cognito_user_assumerole.json
}
data "aws_iam_policy_document" "aes_siem_cognito_user_assumerole" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]
    principals {
      type        = "Federated"
      identifiers = ["cognito-identity.amazonaws.com"]
    }
    condition {
      test     = "StringEquals"
      variable = "cognito-identity.amazonaws.com:aud"
      values   = [aws_cognito_identity_pool.aes_siem_identities.id]
    }
    condition {
      test     = "ForAnyValue:StringLike"
      variable = "cognito-identity.amazonaws.com:amr"
      values   = ["authenticated"]
    }
  }
}

きめ細かなアクセスコントロールを有効にする場合は、初期設定等を行うためのマスターユーザ(特権ユーザ)を設定します。
必ずマスターユーザ用のIAMロールを作成してください。

次に、ユーザプールにグループを作成し、グループに対してIAMロールを割り当てます。

locals{
  siem_cognito_groups = {
    admin  = { precedence=10, role=aws_iam_role.aes_siem_cognito_user["admin"].arn }
    reader = { precedence=20, role=aws_iam_role.aes_siem_cognito_user["read"].arn }
  }
}

resource "aws_cognito_user_group" "aes_siem_user_groups" {
  for_each = local.siem_cognito_groups

  name         = each.key
  user_pool_id = aws_cognito_user_pool.aes_siem_users.id
  precedence   = lookup(each.value, "precedence", 50)
  role_arn     = lookup(each.value, "role", null)
}

この時、優先順位(precedence)の設定には注意が必要です。
ユーザは複数のグループに所属できますが、cognito:preferred_roleクレームに適用されるIAMロールはもっともprecedenceの値が小さいグループのものになります。
(この例の場合、adminとreaderの両方のグループに所属したユーザはprecedenceの値が小さいadminグループのIAMロールでアクセスすることになります)
しかし、2つ以上の同じ優先順位で異なるIAMロールを割り当てられたグループに所属している場合はcognito:preferred_roleクレームは設定されないとあります。
この場合、適切にIAMロールが割り当てられずアクセスコントロールが正常に機能しません。
複数のグループに所属させる必要がある場合は、優先順位が同じ値にならないように注意してください。

ここまででロールの設定に関するユーザプールの準備は完了です。
実際にユーザを作成するときに適切なグループに所属するように設定を行ってください。

接続元制限

OpenSearchをVPCに作成した場合(VPCドメインの場合)はセキュリティグループの設定により適切な接続元の設定が可能です。
OpenSearchをパブリックドメインとして作成した場合も、ドメインアクセスポリシー(リソースベースのポリシー)やIAMロールのポリシーで接続元IPアドレスの制限を行うことができます。
ただし、これらの制限はOpenSearchに対するものであってCognitoの認証画面は別になっています。

OpenSearchとCognitoを連携した後にOpenSearchダッシュボードへアクセスすると、Cognitoユーザプールの認証画面へリダイレクトされます。
パブリックドメインの場合、OpenSearchで接続元制限をかけていてもCognitoの認証画面へのリダイレクトとCognitoの認証は接続元に関係無くできてしまいます。

CognitoユーザプールにはWAFを設定できるため、そちらでCognito認証画面の接続元制限をかけることができます。

以下はWAFの設定例ですが、IPセットの作成等は省略しているので必要に応じて適切な設定にしてください。

resource "aws_wafv2_web_acl" "aes_siem_cognito" {
  name        = "aes-siem-cognito"
  scope       = "REGIONAL"

  default_action {
    allow { }
  }

  # 特定URLは他の検査は行わない
  rule {
    name     = "bypass"
    priority = 5
    action {
      allow { }
    }
    statement {
      regex_match_statement {
        regex_string = "/token"
        field_to_match {
          uri_path {}
        }
        text_transformation {
          priority = 1
          type     = "URL_DECODE"
        }
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = false
      metric_name                = "bypass"
      sampled_requests_enabled   = true
    }
  }

  # 特定IPアドレス以外はブロック
  rule {
    name     = "iprestrict"
    priority = 10
    action {
      block{
        custom_response {
          response_code = 403
        }
      }
    }
    statement {
      not_statement {
        statement {
          ip_set_reference_statement {
            arn = aws_wafv2_ip_set.aes_siem_cognito_allow.arn
          }
        }
      }
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "iprestrict"
      sampled_requests_enabled   = true
    }
  }
}

# Cognitoへ割り当て
resource "aws_wafv2_web_acl_association" "aes_siem_users" {
  resource_arn = aws_cognito_user_pool.aes_siem_users.arn
  web_acl_arn  = aws_wafv2_web_acl.aes_siem_cognito.arn
}

このWAF ACLには以下の2つのルールのみが含まれています。

ルール名 内容
bypass 特定のURL(/token)のものは以降のルール(iprestrict)を評価せず許可します
iprestrict IPセットに登録されてない接続からのアクセスをブロックして403レスポンスを返します

最初に試したときはiprestrictのルールのみでしたが、OpenSearchダッシュボードは正常に動作しなくなってしまいました。
その時にWAFのログを確認すると、User-AgentがAWSElasticsearch/tokenへのアクセスをブロックしています。
そこで、最初のルール(bypass)を追加してアクセスを許可することで動作を確認することができました。
本番利用する場合は、他のルールも勘案して適切に設定してください。

OpenSearchとCognitoの連携

CognitoのユーザプールとIDプールを用意したら、OpenSearchにCognito認証の設定を行います。

連携設定

まず、OpenSearchがCognitoへアクセスするためのIAMロールを用意します。
ロールには、AmazonOpenSearchServiceCognitoAccessというAWS管理ポリシーを設定します。

data "aws_iam_policy_document" "aes_siem_cognito_access_assumerole" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["es.amazonaws.com","opensearchservice.amazonaws.com"]
    }
  }
}
resource "aws_iam_role" "aes_siem_cognito_access" {
  name                = "aes-siem-opensearch-cognito-access"
  assume_role_policy  = data.aws_iam_policy_document.aes_siem_cognito_access_assumerole.json
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonOpenSearchServiceCognitoAccess"
  ]
}

ここで設定する権限には、Cognitoユーザプールのアプリクライアントを管理するための権限等も含まれています。

次にCognito認証の設定です。
以下の例では、きめ細かなアクセスコントロールも有効化しています。

resource "aws_opensearch_domain" "aes_siem" {
  ……他の設定は省略……

  cognito_options {
    enabled          = true
    user_pool_id     = aws_cognito_user_pool.aes_siem_users.id
    identity_pool_id = aws_cognito_identity_pool.aes_siem_identities.id
    role_arn         = aws_iam_role.aes_siem_cognito_access.arn
  }
  advanced_security_options {
    enabled                        = true
    anonymous_auth_enabled         = false
    internal_user_database_enabled = false
    master_user_options {
      master_user_arn = aws_iam_role.aes_siem_cognito_user["admin"].arn
    }
  }
}

cognito_optionsブロックでCognitoとの連携を設定します。
Cognito連携の有効化(enabled)、CognitoユーザプールのID(user_pool_id)とIDプールのID(identity_pool_id)、上で作成したCognito連携用のIAMロールのARN(role_arn)を指定します。
これにより、Cognitoユーザプールにアプリクライアント、IDプールにIDプロバイダーが自動的に作成されてCognito認証が有効化されます。

advanced_security_optionsブロックはきめ細かいアクセスコントロールのための設定です。
ここでは、同機能の有効化(enabled)、匿名ユーザの無効化(anonymous_auth_enabled)、内部ユーザデータベースの無効化(internal_user_database_enabled)を設定しています。
また、master_user_optionブロックのmaster_user_arnで『マスターユーザ用に設定したIAMロール』を指定します。

以上でOpenSearchとCognitoを連携するための設定は完了です。
OpenSearchの設定変更は時間がかかるため反映時は認証情報のセッション切れ等にはお気を付けください。

連携後の設定

CognitoユーザプールのアプリクライアントとIDプールのIDプロバイダーはOpenSearchにより自動的に作成されます。
そのため、アプリクライアントの設定とIDプロバイダーの設定について変更が必要な場合はOpenSearch連携後に対応が必要です。
アプリクライアントにはトークンの有効期限が含まれますし、IDプロバイダーの設定にはIAMロールの選択方法の設定が含まれます。

とくに、IDプールのIDプロバイダーの設定は多くの場合で変更する必要があるかと思います。
初期の状態ではIAMロールの選択がデフォルトのロールとなっているため、すべてのユーザが同じ権限でアクセスするようになります。
また、IDプールにデフォルトのロールが設定されていない場合はOpenSearchダッシュボードへアクセスできなくなってしまいます。

アプリクライアントやIDプロバイダーの設定はOpenSearchにより自動的に設定されるため、Terraform管理下にはありません。
アプリクライアントのIDを調べてTerraformテンプレートに反映してインポートしたり、手動で設定変更する必要があります。

以下は、IDプールのIDプロバイダーの設定を一律で変更するPythonスクリプトの例です。
IAMロールをトークンのpreferred_roleクレームのものを利用するように設定します。

import sys
import boto3

client_cognito_id = boto3.client('cognito-identity')

def set_all_cognito_idp_roles(identity_pool, roles={'Type':'Token', 'AmbiguousRoleResolution':'Deny'}):
    # Cognitoプロバイダが無いものは処理対象外
    if 'CognitoIdentityProviders' not in identity_pool:
        return
    # 現行のロール設定情報
    roles_info = client_cognito_id.get_identity_pool_roles(IdentityPoolId=identity_pool['IdentityPoolId'])
    del roles_info['ResponseMetadata']
    if 'RoleMappings' not in roles_info:
        roles_info['RoleMappings'] = {}
    # 設定の追加/上書き    
    for id_provider in identity_pool['CognitoIdentityProviders']:
        role_key = '{}:{}'.format(id_provider['ProviderName'], id_provider['ClientId'])
        roles_info['RoleMappings'][role_key] = roles
    # 設定の反映
    if 'Roles' not in roles_info:
        roles_info['Roles'] = {}
    client_cognito_id.set_identity_pool_roles(**roles_info)

if __name__ == '__main__':
    identity_pool_id = sys.argv[1]
    identity_pool_info = client_cognito_id.describe_identity_pool(IdentityPoolId=identity_pool_id)
    del identity_pool_info['ResponseMetadata']
    set_all_cognito_idp_roles(identity_pool_info)

テンプレートがTerraformの実行環境に依存してしまいますが、Terraformからスクリプトを実行することも可能です。

resource "terraform_data" "aes_siem_cognito_settings" {
  triggers_replace = {
    cognito_user_pool = aws_cognito_user_pool.aes_siem_users.id
    cognito_identity  = aws_cognito_identity_pool.aes_siem_identities.id
    opensearch        = aws_opensearch_domain.aes_siem.domain_id
  }

  provisioner "local-exec" {
    command = "/usr/bin/python3 ${path.module}/tools/cognito_identity_settings.py '${aws_cognito_identity_pool.aes_siem_identities.id}'"
  }
}

また、Terraform実行環境に影響がないようにLambda関数として実装して実行することも可能かと思います。

最後に

今回は、OpenSearchダッシュボードへのアクセスで多要素認証を行うためにCognito認証の設定を行いました。
Cognitoユーザプールのアプリクライアントの設定をOpenSearchが自動的に行ってくれるのは楽ではありますが、Terraform等のIaCツールでは管理しにくく感じます。

OpenSearchダッシュボードで多要素認証を行うための1つの方法として参考になれば幸いです。