電通総研 テックブログ

電通総研が運営する技術ブログ

認可以外にも使えるぞ Amazon Verified Permissions

こんにちは。コーポレート本部 サイバーセキュリティ推進部の耿です。

以前 Amazon Verified Permissions についての 記事を書きましたが、アプリケーションの認可以外にもこのサービスを利用できるのではないかと思い、汎用のポリシー判定エンジンとして使ってみました。具体的なユースケースでお話ししたいと思います。

社内クラウド SOC 運用上の課題

Verified Permissions の話に繋げる前に、まずは普段の業務で発生している具体的な課題についてお話します。

電通総研ではクラウド環境を組織的にセキュアに保つために、クラウド SOC による複数案件アカウントの集中監視を行っております。

こちらのオペレーションの一つに、案件アカウントが GuardDuty アラートを検知した場合、クラウド SOC でも内容を確認して案件担当に確認を依頼するというものがあります。(以前のブログ「Security Hub の社内展開を支えるツールたち」でもご説明しています。)

SOCのオペレーション

ただし、GuardDuty がアラートとして検知した挙動が、案件アカウントの正当な挙動であって実際には問題がない場合が多くあります。
正当な挙動が繰り返されれば、そのうち GuardDuty が学習してアラートを出さなくなることもありますが、頻繁に行われない操作などの場合はいつまでも GuardDuty にアラートとして扱われる場合もありました(GuardDuty のアラート検知ロジックはブラックボックスなので、どうすればアラート扱いされなくなるかは分かりません)。このように「正当」と分かりきっている挙動の場合は、煩わしさを解消するためにクラウド SOC から案件担当への連絡をスキップしたいことがあります。

例として、案件 A では以下のような条件のアラートは正当な操作なので、クラウド SOC からの確認依頼はスキップする運用としたいです。(以下に当てはまらないアラートは、通常通り確認依頼を行いたいです)

<通知スキップの条件>
・アカウントID: 111122223333
・GuardDutyの検出タイプ: TTPs/Defense Evasion/DefenseEvasion:EC2-UnusualDNSResolver
・対象EC2インスタンス: arn:aws:ec2:ap-northeast-1:111122223333:instance/i-0123456789
・リモートIPアドレス: 1.2.3.4/32 または 1.2.3.5/32

また案件 B では、以下のような条件のアラートは正当な操作なので、確認依頼はスキップする運用としたいです。

<通知スキップの条件>
・アカウントID: 444455556666
・GuardDutyの検出タイプ: TTPs/Exfiltration:S3-AnomalousBehavior
・操作対象のS3バケット名: 「my-log-bucket-」から始まるバケット名
・発生時間帯: 毎月1日の 2:00AM ~ 6:00AM

案件ごとに異なる条件が多数ある状況で、アラートがスキップ対象かどうかをいかに素早く、正確に判定するかが課題となっていました。条件が増えてくると人手で突き合わせるのは間違えやすいですし、時間がかかってしまいます。GuardDuty のアラートを受け取る Lambda 関数にロジックを書いてスキップ判断をすることがまず思いつくやり方ですが、条件によってスキップと判定するためのフィールドが全く異なる上、AND 条件と OR 条件が入り混じっていたり、文字列の部分一致条件があったり、時間帯のように値の範囲による判定条件があったり、判定ロジックの実装や判定条件データの保持方法には工夫が必要だと感じました。

そこで Verified Permissions が登場します。Verified Permissions を汎用的なポリシー判定エンジンとして利用すれば、判定ロジックの実装の手間を省けるのではないかと考えました。

AWS は Verified Permissions を認可サービスとして打ち出している現状、このような使い方を推奨しているわけではありません。実験的に「このように使ってみた」という記事としてご認識ください。
※汎用的なポリシー判定エンジンとしては OSS の Open Policy Agent (OPA) が有名ですね。以前の柴田さんの記事で詳しく説明されています。
Policy as Codeを実現する Open Policy Agent / Rego の紹介
OPA に対して、AWS マネージドサービスである Verified Permissions はポリシーを作成するだけで使えるのが便利です。

Verified Permissions を汎用ポリシー判定エンジンとして使う

以前の記事で記載した通り、 Verified Permissions は特定条件における認可結果を ALLOW / DENY として返してくれますが、その後の扱いは完全にアプリケーションの責務となります。逆に言うとアプリケーション側に自由度があり、 ALLOW / DENY は好きなように解釈して良いわけです。そこで ALLOW は「アラート通知スキップ」、 DENY はスキップせずに「通知」、として扱うことを考えます。

Verified Permissionsの結果を利用する

そして Verified Permissions に登録するポリシーを「アラート通知をスキップする条件(およびロジック)」として扱うことで、条件の判定を完全に Verified Permissions にお任せし、その結果を便利に利用することができます。

Verified Permissionsの結果を利用する

Cedar ポリシー

以上のユースケースを実現するためのCedar 言語による Verified Permissions のポリシー例を書いてみます。実際に使う場合は、公式サイトのチュートリアルなどで Cedar 言語の動きを一通り理解しておくことをおすすめします。

Verified Permissions のポリシーにはアプリケーションの認可を想定したマネージドサービスなので、 principalactionresource をポリシーの中で明示できます。ただし今回は任意のフィールドで判定をさせたいため、principalactionresource は全許可し、代わりに context で全て判定させてみます。

スキーマ定義

スキーマ定義は次のようにしました。

{
  "GuardDutyApp": {
    "commonTypes": {
      "ContextType": {
        "type": "Record",
        "attributes": {
          "accountId": {
            "type": "String",
            "required": false
          },
          "findingType": {
            "type": "String",
            "required": false
          },
          "ec2InstanceArn": {
            "type": "String",
            "required": false
          },
          "s3BucketName": {
            "type": "String",
            "required": false
          },
          "remoteIpAddress": {
            "type": "String",
            "required": false
          },
          "date": {
            "type": "Long",
            "required": false
          },
          "hour": {
            "type": "Long",
            "required": false
          },
          "minute": {
            "type": "Long",
            "required": false
          }
        }
      }
    },
    "entityTypes": {},
    "actions": {
      "check": {
        "appliesTo": {
          "context": {
            "type": "ContextType"
          }
        }
      }
    }
  }
}

commonTypescontext で利用する属性を記載しています(accountIdfindingTypeec2InstanceArns3BucketNameremoteIpAddressdatehourminute)。スキップ判定に利用する属性が増えたら適宜追加していきます。
check というアクションを定義し、定義した context を利用するように設定しています。今回は Verified Permission の呼び出しでは常に check というアクションを利用するようにします。
今回は principalresource は利用しないので、 entityTypes は空にしています。

ポリシーの例1

まずは上で述べた案件 A の判定条件:

<通知スキップの条件>
・アカウントID: 111122223333
・GuardDutyの検出タイプ: TTPs/Defense Evasion/DefenseEvasion:EC2-UnusualDNSResolver
・対象EC2インスタンス: arn:aws:ec2:ap-northeast-1:111122223333:instance/i-0123456789
・リモートIPアドレス: 1.2.3.4/32 または 1.2.3.5/32

これを Cedar ポリシーにしてみます:

permit (
  principal,
  action == GuardDutyApp::Action::"check",
  resource
)
when
{
  context has accountId &&
  context.accountId == "111122223333" &&
  context has findingType &&
  context.findingType == "TTPs/Defense Evasion/DefenseEvasion:EC2-UnusualDNSResolver" &&
  context has ec2InstanceArn &&
  context.ec2InstanceArn == "arn:aws:ec2:ap-northeast-1:111122223333:instance/i-0123456789" &&
  context has remoteIpAddress &&
  ["1.2.3.4", "1.2.3.5"].contains(context.remoteIpAddress)
};

判定に必要な属性のみを条件としています。属性はすべて「必須」とはしていないため、リクエストに含まれていない可能性があります。そのため has オペレータでまずは存在を確認しています。
また、OR 条件である「リモートIPアドレス」は、.contains() オペレータを利用しています。

has で属性の存在確認をしないと、必須ではない属性に関しては Cedar の VS Code 拡張利用時にエラーが表示されます。

VS Code拡張のエラー

ポリシーの例2

続いて案件 B の判定条件:

<通知スキップの条件>
・アカウントID: 444455556666
・GuardDutyの検出タイプ: TTPs/Exfiltration:S3-AnomalousBehavior
・操作対象のS3バケット名: 「my-log-bucket-」から始まるバケット名
・発生時間帯: 毎月1日の 2:00AM ~ 6:00AM

これを Cedar ポリシーにしてみます:

permit (
  principal,
  action == GuardDutyApp::Action::"check",
  resource
)
when
{
  context has accountId &&
  context.accountId == "444455556666" &&
  context has findingType &&
  context.findingType == "TTPs/Exfiltration:S3-AnomalousBehavior" &&
  context has s3BucketName &&
  context.s3BucketName like "my-log-bucket-*" &&
  context has date &&
  context.date == 1 &&
  context has hour &&
  context.hour >= 2 &&
  context.hour <= 6
};

バケット名の部分一致については、like オペレータでワイルドカードを利用できます。
「毎月1日の 2:00AM ~ 6:00AM」という範囲による判定条件は不等号 (>=<=)が利用できます。

評価結果の例

以上の2つのポリシーを作成したとき、実際に Verified Permissions にリクエストを送信した判定結果をいくつか示します。

  • 案件 A のポリシーに該当する条件、リモート IP アドレスが 1.2.3.4
コンテキスト属性
accountId "111122223333"
findingType "TTPs/Defense Evasion/DefenseEvasion:EC2-UnusualDNSResolver"
ec2InstanceArn "arn:aws:ec2:ap-northeast-1:111122223333:instance/i-0123456789"
remoteIpAddress "1.2.3.4"

結果: ALLOW

  • 案件 A のポリシーに該当する条件、リモート IP アドレスが 1.2.3.5
コンテキスト属性
accountId "111122223333"
findingType "TTPs/Defense Evasion/DefenseEvasion:EC2-UnusualDNSResolver"
ec2InstanceArn "arn:aws:ec2:ap-northeast-1:111122223333:instance/i-0123456789"
remoteIpAddress "1.2.3.5"

結果: ALLOW

  • 案件 A のポリシーに該当する条件、判定を左右しない追加属性がある:
コンテキスト属性
accountId "111122223333"
findingType "TTPs/Defense Evasion/DefenseEvasion:EC2-UnusualDNSResolver"
ec2InstanceArn "arn:aws:ec2:ap-northeast-1:111122223333:instance/i-0123456789"
remoteIpAddress "1.2.3.4"
date 10
hour 10

結果: ALLOW

  • 案件 A のポリシーに近いが、リモート IP アドレスが通知スキップ条件に該当しない:
コンテキスト属性
accountId "111122223333"
findingType "TTPs/Defense Evasion/DefenseEvasion:EC2-UnusualDNSResolver"
ec2InstanceArn "arn:aws:ec2:ap-northeast-1:111122223333:instance/i-0123456789"
remoteIpAddress "1.2.3.6"

結果: DENY

  • 案件 A のポリシーに近いが、リモート IP アドレス属性が不足:
コンテキスト属性
accountId "111122223333"
findingType "TTPs/Defense Evasion/DefenseEvasion:EC2-UnusualDNSResolver"
ec2InstanceArn "arn:aws:ec2:ap-northeast-1:111122223333:instance/i-0123456789"

結果: DENY

  • 案件 B のポリシーに該当する条件、S3 バケット名が my-log-bucket-1
コンテキスト属性
accountId "444455556666"
findingType "TTPs/Exfiltration:S3-AnomalousBehavior"
s3BucketName "my-log-bucket-1"
date 1
hour 2

結果: ALLOW

  • 案件 B のポリシーに該当する条件、S3 バケット名が my-log-bucket-abcdefg
コンテキスト属性
accountId "444455556666"
findingType "TTPs/Exfiltration:S3-AnomalousBehavior"
s3BucketName "my-log-bucket-abcdefg"
date 1
hour 2

結果: ALLOW

  • 案件 B のポリシーに近いが、S3 バケット名が通知スキップ条件に該当しない:
コンテキスト属性
accountId "444455556666"
findingType "TTPs/Exfiltration:S3-AnomalousBehavior"
s3BucketName "other-bucket"
date 1
hour 2

結果: DENY

  • 案件 B のポリシーに近いが、発生時間が通知スキップ条件に該当しない:
コンテキスト属性
accountId "444455556666"
findingType "TTPs/Exfiltration:S3-AnomalousBehavior"
s3BucketName "my-log-bucket-1"
date 1
hour 7

結果: DENY

  • 案件 B のポリシーに近いが、発生時間が数値型ではなく文字列型:
コンテキスト属性
accountId "444455556666"
findingType "TTPs/Exfiltration:S3-AnomalousBehavior"
s3BucketName "my-log-bucket-1"
date 1
hour "2"

結果: DENY

SDK で Verified Permission を呼びだす例

AWS SDK for JavaScript を利用して Verified Permission を呼びだすコード例を書いておきます。
(受け取った GuardDuty のアラートから該当するフィールドのデータを取り出す部分は省略しています。)

import { VerifiedPermissionsClient, IsAuthorizedCommand } from "@aws-sdk/client-verifiedpermissions";

const client = new VerifiedPermissionsClient({ region: "ap-northeast-1" });

// GuardDuty アラートから内容を取りだす(省略)
const accountId: string = // アカウントID
const findingType: string = // GuardDuty 検出タイプ
const ec2InstanceArn: string = // 対象 EC2 インスタンス ARN
const s3BucketName: string = // 対象 S3 バケット名
const remoteIpAddress: string = // リモートIPアドレス
const date: number = // 検出日時(日付)
const hour: number = // 検出日時(時間)
const minute: number = // 検出日時(分)

const command = new IsAuthorizedCommand({
  // マネジメントコンソールから確認できる Verified Permissions のポリシーストア ID
  policyStoreId: "DUMMYSTOREID",
  // アクションは常に check
  action: { actionType: "GuardDutyApp::Action", actionId: "check" },
  // コンテキストに属性を追加
  context: {
    contextMap: {
      accountId: { string: accountId },
      findingType: { string: findingType },
      ec2InstanceArn: { string: ec2InstanceArn },
      s3BucketName: { string: s3BucketName },
      remoteIpAddress: { string: remoteIpAddress },
      date: { long: date },
      hour: { long: hour },
      minute: { long: minute },
    },
  },
});

client
  .send(command)
  .then((result) => {
    console.log(result.decision); // 判定結果
    console.log(JSON.stringify(result.determiningPolicies)); // 該当したポリシー

    if (result.decision === "ALLOW") {
      // 通知スキップ時の処理
    } else {
      // 通知時の処理
    }
  })
  .catch((e: unknown) => {
    // コマンドエラー時の処理
  });

Verified Permissions のサービスクオータ

Verified Permissions の利用に際してはサービスクオータを念頭に置いておくと良いです。ドキュメントには次のように記載されています。

単一リソースに関するすべてのポリシーの合計サイズは、200,000 バイトを超えることはできません。

1つのポリシーストアに作成できるポリシーの数にはクオータは設定されていないようですが、ポリシーサイズにはクオータがあります。ポリシーがリソース(resource)を指定している場合、同じリソースを指定している全てのポリシーの合計サイズは 200,000 バイトが上限とのことです。今回の使い方では resource を指定していませんが、サポートに問い合わせたところ resource を指定していない全てのポリシーの合計サイズの上限が 200,000 バイトになるそうです。またポリシー内に記載しているコメントや、改行文字、空白文字もサイズとしてカウントされるそうです。ポリシー数が多くなることが予想される場合には留意しておきましょう。

ポリシーのサイズを節約したい場合は、以前の記事の通り CDK でポリシーの管理やデプロイを行い、デプロイ時に CDK のコードでポリシーファイル内のコメント行を削除するなどの処理を入れても良いかもしれません。

また、Cedar のベストプラクティスによると、可能であれば ポリシーで resource を指定するとパフォーマンスメリットがあるとのことです。今回はどのような条件でスキップが判定されるのかわからない想定のため resource を指定せずに利用しましたが、例えば常にアカウント ID が判定条件に使われることが分かっているのであれば、context ではなく resource にアカウント ID を指定しても良いかもしれません。判定時にパフォーマンスの向上が期待でき、 resource も分散するのでサービスクオータに引っかかりにくくなります。

まとめ

Verified Permissions を汎用ポリシー判定エンジンとして利用し、GuardDuty アラートを社内案件に通知するかどうかの条件判定に使ってみました。Cedar 言語の表現力のおかげで、複雑な判定ロジックを自前で実装せずに済み、ポリシーを作成するだけで利用できるのが嬉しいところです。工夫をすれば認可以外でも Verified Permissions を便利に利用できるケースはあると感じました。

執筆:@kou.kinyo、レビュー:@kano.nanami
Shodoで執筆されました