Skip to content

Commit

Permalink
Instance profile mitigations for AWS
Browse files Browse the repository at this point in the history
* Issue 341 - Instance profile mitigations for AWS

* Undo comment out

* Added in some loggin messages

* PR suggestions
  • Loading branch information
ramo-j authored Aug 6, 2021
1 parent f10086d commit 71b33ea
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 2 deletions.
41 changes: 40 additions & 1 deletion libcloudforensics/providers/aws/forensics.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,8 @@ def InstanceNetworkQuarantine(
instance.
Args:
instance_id (str): : The id (i-xxxxxx) of the virtual machine.
zone (str): AWS Availability Zone the instance is in.
instance_id (str): The id (i-xxxxxx) of the virtual machine.
exempted_src_subnets (List[str]): List of subnets that will be permitted
Raises:
Expand All @@ -487,10 +488,48 @@ def InstanceNetworkQuarantine(
try:
aws_account = account.AWSAccount(zone)
vpc = aws_account.ec2.GetInstanceById(instance_id).vpc
logger.info('Creating isolation security group')
sg_id = \
aws_account.ec2.CreateIsolationSecurityGroup(vpc, exempted_src_subnets)
logger.info('Replacing attached security groups with isolation group')
aws_account.ec2.SetInstanceSecurityGroup(instance_id, sg_id)
except errors.ResourceNotFoundError as exception:
raise errors.ResourceNotFoundError(
'Cannot qurantine non-existent instance {0:s}: {1!s}'.format(instance_id,
exception), __name__) from exception

def InstanceProfileMitigator(
zone: str,
instance_id: str,
revoke_existing: bool = False
) -> None:
"""Remove an instance profile attachment from an instance.
Also, optionally revoke existing issued tokens for the profile.
Args:
zone (str): AWS Availability Zone the instance is in.
instance_id (str): The id (i-xxxxxx) of the virtual machine.
revoke_existing (bool): True to revoke existing tokens for the profile's
role. False otherwise.
Raises:
ResourceNotFoundError: If the instance cannot be found, or does not have a
profile attachment.
"""
logger.info('Finding profile attachment')
aws_account = account.AWSAccount(zone)
assoc_id, profile = aws_account.ec2.GetInstanceProfileAttachment(instance_id)

if not profile or not assoc_id:
raise errors.ResourceNotFoundError(
'Instance not found or does not have a profile attachment: {0:s}'.
format(instance_id), __name__)

logger.info('Removing profile attachment')
aws_account.ec2.DisassociateInstanceProfile(assoc_id)

if revoke_existing:
logger.info('Invalidating old tokens')
role_name = profile.split('/')[1]
aws_account.iam.RevokeOldSessionsForRole(role_name)
51 changes: 51 additions & 0 deletions libcloudforensics/providers/aws/internal/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,57 @@ def SetInstanceSecurityGroup(
'Could not modify instance attributes: {0!s}'.format(
exception), __name__) from exception

def GetInstanceProfileAttachment(
self,
instance_id: str) -> Tuple[str, str]:
"""Get the instance profile attached to the instance, if any.
Args:
instance_id (str): The instance ID (i-xxxxxx)
Returns:
Tuple[str, str]: A tuple containing the association id of the profile
attachment, and the Arn of the Instance profile attached. Both are None
if no profile is attached, or the instance cannot be found.
"""
try:
client = self.aws_account.ClientApi(common.EC2_SERVICE)
response = client.describe_iam_instance_profile_associations(
Filters=[{
'Name': 'instance-id',
'Values': [instance_id]
}])
print(response)
if not response['IamInstanceProfileAssociations']:
return '', ''
return response['IamInstanceProfileAssociations'][0]['AssociationId'],\
response['IamInstanceProfileAssociations'][0]['IamInstanceProfile']\
['Arn']
except client.exceptions.ClientError as exception:
raise errors.ResourceNotFoundError(
'Instance does not exist: {0!s}'.format(
exception), __name__) from exception

def DisassociateInstanceProfile(
self,
assoc_id: str) -> None:
"""Remove an instance profile attachment from an instance.
Args:
assoc_id (str): The instance profile association ID. Can be retrieved via
ec2.GetInstanceProfileAttachment.
Raises:
ResourceNotFoundError: If the assoc_id cannot be found.
"""
try:
client = self.aws_account.ClientApi(common.EC2_SERVICE)
client.disassociate_iam_instance_profile(AssociationId=assoc_id)
except client.exceptions.ClientError as exception:
raise errors.ResourceNotFoundError(
'AssociationID {0:s} not found: {1!s}'.format(assoc_id,
exception), __name__) from exception

def GetSnapshotInfo(
self,
snapshot_id: str,
Expand Down
32 changes: 31 additions & 1 deletion libcloudforensics/providers/aws/internal/iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
# limitations under the License.
"""AWS IAM Functionality"""

import datetime
import json
import os

from typing import TYPE_CHECKING, Optional, Tuple
from libcloudforensics import errors
from libcloudforensics import logging_utils
Expand All @@ -37,6 +38,9 @@
# Policy doc to allow EC2 to assume the role. Necessary for instance profiles
EC2_ASSUME_ROLE_POLICY_DOC = 'ec2_assume_role_policy.json'

# Policy to deny all session tokens generated after a date
IAM_DENY_ALL_AFTER_TOKEN_ISSUE_DATE = 'revoke_old_sessions.json'


class IAM:
"""Class that represents AWS IAM services"""
Expand Down Expand Up @@ -292,6 +296,32 @@ def DetachInstanceProfileFromRole(self, role_name: str, profile_name: str) \
pass
# It doesn't matter if this fails.

def RevokeOldSessionsForRole(self, role_name: str) -> None:
"""Revoke old session tokens for a role.
This is acheived by adding an inline policy to the role, Deny *:* on the
condition of TokenIssueTime.
Args:
role_name (str): The role name to act on.
"""
now = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.000Z")
policy = json.loads(ReadPolicyDoc(IAM_DENY_ALL_AFTER_TOKEN_ISSUE_DATE))
policy['Statement'][0]['Condition']['DateLessThan']['aws:TokenIssueTime']\
= now
policy = json.dumps(policy)

try:
self.client.put_role_policy(
RoleName=role_name,
PolicyName='RevokeOldSessions',
PolicyDocument=policy
)
except self.client.exceptions.ClientError as exception:
raise errors.ResourceNotFoundError(
'Could not add inline policy to IAM role {0:s}: {1!s}'.format(
role_name, exception), __name__) from exception

def ReadPolicyDoc(filename: str) -> str:
"""Read and return the IAM policy doc at filename.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"DateLessThan": {
"aws:TokenIssueTime": "DATE"
}
}
}
]
}
10 changes: 10 additions & 0 deletions tools/aws_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,13 @@ def InstanceNetworkQuarantine(args: 'argparse.Namespace') -> None:
return
forensics.InstanceNetworkQuarantine(args.zone,
args.instance_id, exempted_src_subnets)

def InstanceProfileMitigator(args: 'argparse.Namespace') -> None:
"""Remove an instance profile attachment from an instance. Also, optionally
revoke existing issued tokens for the profile.
Args:
args (argparse.Namespace): Arguments from ArgumentParser.
"""

forensics.InstanceProfileMitigator(args.zone, args.instance_id, args.revoke)
10 changes: 10 additions & 0 deletions tools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
'deleteinstance': aws_cli.DeleteInstance,
'gcstos3': aws_cli.GCSToS3,
'imageebssnapshottos3': aws_cli.ImageEBSSnapshotToS3,
'instanceprofilemitigator': aws_cli.InstanceProfileMitigator,
'listdisks': aws_cli.ListVolumes,
'listimages': aws_cli.ListImages,
'listinstances': aws_cli.ListInstances,
Expand Down Expand Up @@ -255,6 +256,15 @@ def Main() -> None:
('--exempted_src_subnets', 'Comma separated list of source '
'subnets to exempt from ingress firewall rules.', None)
])
AddParser('aws', aws_subparsers, 'instanceprofilemitigator',
'Remove an instance profile from an instance, and optionally '
'revoke all previously issued temporary credentials.',
args=[
('instance_id', 'ID (i-xxxxxx) of the instance to quarantine.',
None),
('--revoke', 'Revoke existing temporary creds for the instance'
' profile.', False)
])

# Azure parser options
az_parser.add_argument('default_resource_group_name',
Expand Down

0 comments on commit 71b33ea

Please sign in to comment.