turbot / steampipe-mod-aws-perimeter

Is your AWS perimeter secure? Use Powerpipe and Steampipe to check your AWS accounts for public resources, resources shared with untrusted accounts, insecure network configurations and more.
https://hub.powerpipe.io/mods/turbot/aws_perimeter
Apache License 2.0
105 stars 6 forks source link

KMS check does not take into account KMS specific conditionals #23

Closed rbracewell closed 1 year ago

rbracewell commented 1 year ago

Running the AWS perimeter mod control aws_perimeter.control.kms_key_policy_prohibit_public_access reports alerts against an AWS generated KMS policy. AWS documentation calls out the default kms policy created for CloudTrail trails. AWS creates a condition based policy that leverages a condition that uses kms:CallerAccount. The perimeter mod check only takes into account aws:SourceOwner, aws:SourceAccount, aws:PrincipalOrgID, aws:PrincipalAccount, aws:PrincipalArn and aws:SourceArn.

Should kms:CallerAccount not be a candidate for the where clause when analysing KMS resources?

misraved commented 1 year ago

Thanks for raising the question @rbracewell 👍 .

Adding the relevant slack thread for additional details - https://steampipe.slack.com/archives/C01UECB59A7/p1673974501135479

misraved commented 1 year ago

Going through the documentation provided by AWS, I think we should add a condition to check for kms:CallerAccount since setting this parameter to * can result in the key becoming public.

@khushboo9024 @rajlearner17 could you please provide some more insight on this issue?

github-actions[bot] commented 1 year ago

'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.'

github-actions[bot] commented 1 year ago

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days.

bigdatasourav commented 1 year ago

Hey @rbracewell, sorry for the delayed response.

The kms:CallerAccount condition is often used to enforce access control and limit the usage of KMS keys to specific AWS accounts. By including this condition in a key policy, we can ensure that only authorized AWS accounts can perform operations on the KMS key, such as encrypting or decrypting data. I have created a KMS key in CloudTrail, and the below policies are created by default. I have updated the kms:CallerAccount value to '*' in one of the below policies.

-- alias/test-key

select jsonb_pretty(policy) from aws_kms_key where id = '01d1934c-af89-4cf9-8ed7-b6b49c0a85e3'
+---------------------------------------------------------------------------------------------------------------+
| jsonb_pretty                                                                                                  |
+---------------------------------------------------------------------------------------------------------------+
| {                                                                                                             |
|     "Id": "Key policy created by CloudTrail",                                                                 |
|     "Version": "2012-10-17",                                                                                  |
|     "Statement": [                                                                                            |
|         {                                                                                                     |
|             "Sid": "Enable IAM User Permissions",                                                             |
|             "Action": "kms:*",                                                                                |
|             "Effect": "Allow",                                                                                |
|             "Resource": "*",                                                                                  |
|             "Principal": {                                                                                    |
|                 "AWS": [                                                                                      |
|                     "arn:aws:sts::123456789:assumed-role/superuser/sourav@turbot.com-by2Uc93YihDeurZGWks", |
|                     "arn:aws:iam::123456789:root"                                                          |
|                 ]                                                                                             |
|             }                                                                                                 |
|         },                                                                                                    |
|         {                                                                                                     |
|             "Sid": "Allow CloudTrail to encrypt logs",                                                        |
|             "Action": "kms:GenerateDataKey*",                                                                 |
|             "Effect": "Allow",                                                                                |
|             "Resource": "*",                                                                                  |
|             "Condition": {                                                                                    |
|                 "StringLike": {                                                                               |
|                     "kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:123456789:trail/*"   |
|                 },                                                                                            |
|                 "StringEquals": {                                                                             |
|                     "AWS:SourceArn": "arn:aws:cloudtrail:us-east-1:123456789:trail/test-trail"             |
|                 }                                                                                             |
|             },                                                                                                |
|             "Principal": {                                                                                    |
|                 "Service": "cloudtrail.amazonaws.com"                                                         |
|             }                                                                                                 |
|         },                                                                                                    |
|         {                                                                                                     |
|             "Sid": "Allow CloudTrail to describe key",                                                        |
|             "Action": "kms:DescribeKey",                                                                      |
|             "Effect": "Allow",                                                                                |
|             "Resource": "*",                                                                                  |
|             "Principal": {                                                                                    |
|                 "Service": "cloudtrail.amazonaws.com"                                                         |
|             }                                                                                                 |
|         },                                                                                                    |
|         {                                                                                                     |
|             "Sid": "Allow principals in the account to decrypt log files",                                    |
|             "Action": [                                                                                       |
|                 "kms:Decrypt",                                                                                |
|                 "kms:ReEncryptFrom"                                                                           |
|             ],                                                                                                |
|             "Effect": "Allow",                                                                                |
|             "Resource": "*",                                                                                  |
|             "Condition": {                                                                                    |
|                 "StringLike": {                                                                               |
|                     "kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:123456789:trail/*"
                      "kms:CallerAccount": "*"  // I have edited the policy here |
|                 },                                                                                            |                                                                                          |
|             },                                                                                                |
|             "Principal": {                                                                                    |
|                 "AWS": "*"                                                                                    |
|             }                                                                                                 |
|         },                                                                                                    |
|         {                                                                                                     |
|             "Sid": "Allow alias creation during setup",                                                       |
|             "Action": "kms:CreateAlias",                                                                      |
|             "Effect": "Allow",                                                                                |
|             "Resource": "*",                                                                                  |
|             "Condition": {                                                                                    |
|                 "StringEquals": {                                                                             |
|                     "kms:ViaService": "ec2.us-east-1.amazonaws.com",                                          |
|                     "kms:CallerAccount": "123456789"                                                       |
|                 }                                                                                             |
|             },                                                                                                |
|             "Principal": {                                                                                    |
|                 "AWS": "*"                                                                                    |
|             }                                                                                                 |
|         },                                                                                                    |
|         {                                                                                                     |
|             "Sid": "Enable cross account log decryption",                                                     |
|             "Action": [                                                                                       |
|                 "kms:Decrypt",                                                                                |
|                 "kms:ReEncryptFrom"                                                                           |
|             ],                                                                                                |
|             "Effect": "Allow",                                                                                |
|             "Resource": "*",                                                                                  |
|             "Condition": {                                                                                    |
|                 "StringLike": {                                                                               |
|                     "kms:EncryptionContext:aws:cloudtrail:arn": "arn:aws:cloudtrail:*:123456789:trail/*"   |
|                 },                                                                                            |
|                 "StringEquals": {                                                                             |
|                     "kms:CallerAccount": "123456789"                                                       |
|                 }                                                                                             |
|             },                                                                                                |
|             "Principal": {                                                                                    |
|                 "AWS": "*"                                                                                    |
|             }                                                                                                 |
|         }                                                                                                     |
|     ]                                                                                                         |
| }                                                                                                             |
+---------------------------------------------------------------------------------------------------------------+

Here is the updated the mod control query - (got the expected output)

 with wildcard_action_policies as (
      select
        arn,
        count(*) as statements_num
      from
        aws_kms_key,
        jsonb_array_elements(policy_std -> 'Statement') as s
      where
        s ->> 'Effect' = 'Allow'
        -- kms:CallerAccount.   // check for  CallerAccount
        and s -> 'Condition' -> 'StringLike' -> 'kms:calleraccount' ? '*'
        -- aws:SourceOwner
        and s -> 'Condition' -> 'StringEquals' -> 'aws:sourceowner' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:sourceowner' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:sourceowner' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:sourceowner' ? '*'
        )
        -- aws:SourceAccount
        and s -> 'Condition' -> 'StringEquals' -> 'aws:sourceaccount' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:sourceaccount' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:sourceaccount' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:sourceaccount' ? '*'
        )
        -- aws:PrincipalOrgID
        and s -> 'Condition' -> 'StringEquals' -> 'aws:principalorgid' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:principalorgid' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:principalorgid' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:principalorgid' ? '*'
        )
        -- aws:PrincipalAccount
        and s -> 'Condition' -> 'StringEquals' -> 'aws:principalaccount' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:principalaccount' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:principalaccount' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:principalaccount' ? '*'
        )
        -- aws:PrincipalArn
        and s -> 'Condition' -> 'StringEquals' -> 'aws:principalarn' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:principalarn' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:principalarn' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:principalarn' ? '*'
        )
        and (
          s -> 'Condition' -> 'ArnEquals' -> 'aws:principalarn' is null
          or s -> 'Condition' -> 'ArnEquals' -> 'aws:principalarn' ? '*'
        )
        and (
          s -> 'Condition' -> 'ArnLike' -> 'aws:principalarn' is null
          or s -> 'Condition' -> 'ArnLike' -> 'aws:principalarn' ? '*'
        )
        -- aws:SourceArn
        and s -> 'Condition' -> 'StringEquals' -> 'aws:sourcearn' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:sourcearn' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:sourcearn' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:sourcearn' ? '*'
        )
        and (
          s -> 'Condition' -> 'ArnEquals' -> 'aws:sourcearn' is null
          or s -> 'Condition' -> 'ArnEquals' -> 'aws:sourcearn' ? '*'
        )
        and (
          s -> 'Condition' -> 'ArnLike' -> 'aws:sourcearn' is null
          or s -> 'Condition' -> 'ArnLike' -> 'aws:sourcearn' ? '*'
        )
        and (
          s -> 'Principal' -> 'AWS' = '["*"]'
          or s ->> 'Principal' = '*'
        )
      group by
        arn
    )
    select
      r.arn as resource,
      case
        when p.arn is null then 'ok'
        else 'alarm'
      end as status,
      case
        when p.arn is null then title || ' policy does not allow public access.'
        else title || ' policy contains ' || coalesce(p.statements_num, 0) ||
        ' statement(s) that allow public access.'
      end as reason
    from
      aws_kms_key as r
      left join wildcard_action_policies as p on p.arn = r.arn
    where
      key_manager = 'CUSTOMER';
+------------------------------------------------------------------------------+--------+---------------------------------------------------------------------------+
| resource                                                                     | status | reason                                                                    |
+------------------------------------------------------------------------------+--------+---------------------------------------------------------------------------+
| arn:aws:kms:ap-south-1:123456789:key/e6ce31c8-5f6f-45ce-bec4-ee7354a4757d | ok     | alias/intg-test policy does not allow public access.                      |
| arn:aws:kms:us-east-1:123456789:key/01d1934c-af89-4cf9-8ed7-b6b49c0a85e3  | alarm  | alias/test-key policy contains 1 statement(s) that allow public access.   |
| arn:aws:kms:ap-south-1:123456789:key/a1dbc718-e60a-416e-8ab5-3a935043eeae | ok     | alias/test2 policy does not allow public access.                          |
+------------------------------------------------------------------------------+--------+---------------------------------------------------------------------------+

Could you please check the above query and let us know if it solves the problem?

@misraved @rajlearner17 Please share your views regarding the updated query.

rbracewell commented 1 year ago

When running the existing query from the documentation I get the following results ok: 192 alarm: 8

Running the revised query from above I get the following results ok: 200 alarm: 0

This is not what I expected. Of those 8 in alarm only one of them uses CallerAccount so I would still have expected the alarm count to be 7 with the new query.

bigdatasourav commented 1 year ago

Hey @rbracewell, thanks for the quick check. Could you please try the below one? I hope this will work as it was expected. If it does not work, please share one policy structure(without sensitive info) where CallerAccount is not used.

 with wildcard_action_policies as (
      select
        arn,
        count(*) as statements_num
      from
        aws_kms_key,
        jsonb_array_elements(policy_std -> 'Statement') as s
      where
        s ->> 'Effect' = 'Allow'
        -- kms:CallerAccount.   // check for  CallerAccount
       and s -> 'Condition' -> 'StringEquals' -> 'kms:calleraccount' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'kms:calleraccount' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'kms:calleraccount' is null
          or s -> 'Condition' -> 'StringLike' -> 'kms:calleraccount' ? '*'
        )
        -- aws:SourceOwner
        and s -> 'Condition' -> 'StringEquals' -> 'aws:sourceowner' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:sourceowner' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:sourceowner' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:sourceowner' ? '*'
        )
        -- aws:SourceAccount
        and s -> 'Condition' -> 'StringEquals' -> 'aws:sourceaccount' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:sourceaccount' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:sourceaccount' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:sourceaccount' ? '*'
        )
        -- aws:PrincipalOrgID
        and s -> 'Condition' -> 'StringEquals' -> 'aws:principalorgid' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:principalorgid' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:principalorgid' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:principalorgid' ? '*'
        )
        -- aws:PrincipalAccount
        and s -> 'Condition' -> 'StringEquals' -> 'aws:principalaccount' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:principalaccount' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:principalaccount' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:principalaccount' ? '*'
        )
        -- aws:PrincipalArn
        and s -> 'Condition' -> 'StringEquals' -> 'aws:principalarn' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:principalarn' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:principalarn' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:principalarn' ? '*'
        )
        and (
          s -> 'Condition' -> 'ArnEquals' -> 'aws:principalarn' is null
          or s -> 'Condition' -> 'ArnEquals' -> 'aws:principalarn' ? '*'
        )
        and (
          s -> 'Condition' -> 'ArnLike' -> 'aws:principalarn' is null
          or s -> 'Condition' -> 'ArnLike' -> 'aws:principalarn' ? '*'
        )
        -- aws:SourceArn
        and s -> 'Condition' -> 'StringEquals' -> 'aws:sourcearn' is null
        and s -> 'Condition' -> 'StringEqualsIgnoreCase' -> 'aws:sourcearn' is null
        and (
          s -> 'Condition' -> 'StringLike' -> 'aws:sourcearn' is null
          or s -> 'Condition' -> 'StringLike' -> 'aws:sourcearn' ? '*'
        )
        and (
          s -> 'Condition' -> 'ArnEquals' -> 'aws:sourcearn' is null
          or s -> 'Condition' -> 'ArnEquals' -> 'aws:sourcearn' ? '*'
        )
        and (
          s -> 'Condition' -> 'ArnLike' -> 'aws:sourcearn' is null
          or s -> 'Condition' -> 'ArnLike' -> 'aws:sourcearn' ? '*'
        )
        and (
          s -> 'Principal' -> 'AWS' = '["*"]'
          or s ->> 'Principal' = '*'
        )
      group by
        arn
    )
    select
      r.arn as resource,
      case
        when p.arn is null then 'ok'
        else 'alarm'
      end as status,
      case
        when p.arn is null then title || ' policy does not allow public access.'
        else title || ' policy contains ' || coalesce(p.statements_num, 0) ||
        ' statement(s) that allow public access.'
      end as reason
    from
      aws_kms_key as r
      left join wildcard_action_policies as p on p.arn = r.arn
    where
      key_manager = 'CUSTOMER';
rbracewell commented 1 year ago

The revised query returned the expected results this time. The policy correctly detected kms:CallerAccount and marked as ok rather than alarm

misraved commented 1 year ago

Thanks @rbracewell for testing the updated query suggested by @bigdatasourav 👍.

The feedback has been extremely helpful in nailing down the query output 🎉 .