Closed rbracewell closed 1 year ago
Thanks for raising the question @rbracewell 👍 .
Adding the relevant slack thread for additional details - https://steampipe.slack.com/archives/C01UECB59A7/p1673974501135479
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?
'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.'
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.
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.
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.
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';
The revised query returned the expected results this time. The policy correctly detected kms:CallerAccount and marked as ok rather than alarm
Thanks @rbracewell for testing the updated query suggested by @bigdatasourav 👍.
The feedback has been extremely helpful in nailing down the query output 🎉 .
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?