AWS
offers a mechanism to control access in a granular and unified way using IAM
policies. Often a script or an app running in the instance need to make AWS
rest calls, which requires AWS security credentials. The question is, where
will the app get these credentials from? Saved somewhere within the instance?
This is a big no, as it runs the risk of exposing long lived static
credentials. Why? Since it is hard to implement a secure infrastructure to
rotate these stored credentials and dynamically available to the app, most of
them end up leaving these credentials unchanged (or change infrequently). Fortunately,
AWS provides a nice mechanism to automatically manage these credentials and
rotates them periodically every few hours. This mechanism can be leveraged by creating a
role and assigning the role to the instance. The app running in the instance
can retrieve these dynamic credentials and use them to make the AWS calls. The role defines the level of access. This blog discusses helper
functions to launch EC2 instance with an IAM role and configure security groups.
These functions are implemented in an idempotent manner (e.g.) If the role is
already created then, it does nothing.
Create IAM Role (SSMCreateRole Helper)
IAM role is a secure mechanism to delegate access to users,
applications, or services. The role defines both who can assume the permissions
and what these permissions are. These are defined as two separate JSON policy
documents. When an IAM role is associated with the EC2 instance, EC2 makes
temporary credentials available to the instance, and periodically rotates them.
The applications running inside the instance can retrieve and use these temporary
credentials. The role creation for EC2 is a multistep process
a) Create
the role with policy document that defines who can assume this role.
b) Write
the permission policy document to grant specific permissions.
c) Create
the instance profile. Role cannot be directly associated with the instance, but
only through instance profile. The
profile container provides extra level of indirection. That is why a profile is
created first and then the role is added to it. As a convention, profile name
is the same as the role name. AWS Console follows the same convention as well.
d) Add
role to the instance profile
e) Finally,
associate instance profile to the instance as part of the launch.
Policy document defines “who” has access. In this case, permissions
are granted to the agent running inside the EC2 instance (ec2config).
$assumePolicy
= @"
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"",
"Effect":"Allow",
"Principal":{"Service":"ec2.amazonaws.com"},
"Action":"sts:AssumeRole"
}
]
}
"@
Below JSON document defines which API actions and resources, the
application or user is allowed after assuming the role. The ssm:*,
ec2messages:* APIs listed below are needed for ec2config to interact with the
SSM service. The ds:CreateComputer is needed for auto domain join, logs:* and
cloudwatch:* are needed for the CloudWatch plugin, s3:* is for storing Run
Command output in s3.
Please follow the security best practices in granting minimal
permissions needed. (e.g.) If you are not using CloudWatch, remove that part.
Likewise, restrict to specific resources instead of “*”.
#Define “what” access (specific APIs and resource)
$policy
= @"
{
"Version":
"2012-10-17",
"Statement": [
{
"Effect":
"Allow",
"Action":
[
"cloudwatch:PutMetricData",
"ds:CreateComputer",
"ec2messages:*",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
"logs:PutLogEvents",
"s3:PutObject",
"s3:GetObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts",
"s3:ListBucketMultipartUploads",
"ssm:DescribeAssociation",
"ssm:ListAssociations",
"ssm:GetDocument",
"ssm:UpdateAssociationStatus",
"ssm:UpdateInstanceInformation",
"ec2:DescribeInstanceStatus"
],
"Resource": "*"
}
]
}
"@
Below script performs the above defined four steps to create a role
and add it to the instance profile.
function
SSMCreateRole ([string]$RoleName = 'ssm-demo-role')
{
#Skips if the
role is already present
if (Get-IAMRoles | ? {$_.RoleName -eq
$RoleName}) {
Write-Verbose
"Skipping as role ($RoleName) is
already present."
return
}
$assumePolicy
= @"
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"",
"Effect":"Allow",
"Principal":{"Service":"ec2.amazonaws.com"},
"Action":"sts:AssumeRole"
}
]
}
"@
# Define
which API actions and resources the application can use
# after
assuming the role
$policy
= @"
{
"Version":
"2012-10-17",
"Statement": [
{
"Effect":
"Allow",
"Action":
[
"cloudwatch:PutMetricData",
"ds:CreateComputer",
"ec2messages:*",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
"logs:PutLogEvents",
"s3:PutObject",
"s3:GetObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts",
"s3:ListBucketMultipartUploads",
"ssm:DescribeAssociation",
"ssm:ListAssociations",
"ssm:GetDocument",
"ssm:UpdateAssociationStatus",
"ssm:UpdateInstanceInformation",
"ec2:DescribeInstanceStatus"
],
"Resource": "*"
}
]
}
"@
#step a -
Create the role and specify who can assume
$null = New-IAMRole -RoleName $RoleName
`
-AssumeRolePolicyDocument
$assumePolicy
#step b -
write the role policy
Write-IAMRolePolicy
-RoleName $RoleName
`
-PolicyDocument
$policy -PolicyName
'ssm'
#step c -
Create instance profile
$null = New-IAMInstanceProfile
-InstanceProfileName $RoleName
#step d - Add
the role to the profile
Add-IAMRoleToInstanceProfile
-InstanceProfileName $RoleName
`
-RoleName
$RoleName
Write-Verbose
"Role $RoleName created"
}
Remove IAM Role (SSMRemoveRole Helper)
This function cleans up all the objects created by SSMCreateRole.
This takes name of the role as input, removes the role from the instance
profile and deletes instance profile. Then, it deletes the policy associated
with the role, finally it deletes the role itself.
function
SSMRemoveRole ([string]$RoleName = 'ssm-demo-role')
{
if (!(Get-IAMRoles | ? {$_.RoleName -eq $RoleName}))
{
Write-Verbose
"Skipping as role ($RoleName) not
found"
return
}
#Remove the
instance role and IAM Role
Remove-IAMRoleFromInstanceProfile
-InstanceProfileName $RoleName
`
-RoleName
$RoleName -Force
Remove-IAMInstanceProfile
$RoleName -Force
Remove-IAMRolePolicy
$RoleName ssm
-Force
Remove-IAMRole
$RoleName -Force
Write-Verbose
"Role $RoleName removed"
}
SSMCreateSecurityGroup and SSMRemoveSecurityGroup
A security group acts as a virtual firewall that controls the
traffic for one or more instances. AWS offers layers of security for defense in
depth and security group is one among them. Security best practice is to allow
only the expected traffic and from known sources. The script below allows
network traffic only from one source, where the script is executed. For
example, if you launch the EC2 instance from your home laptop, the security
group is configured to allow traffic only from the public IP address associated
with your home router. It uses http://checkip.amazonaws.com/ to get
the source IP address as seen by AWS. The source IP has to be a public IP
address. Private IP address behind the NAT or proxy server will not work. The code
below retrieves the source IP address and creates a security group to allow
RDP, PS remoting, port 80, ICMP traffic from this source IP. Change this
function to suit your needs.
function
SSMCreateSecurityGroup ([string]$SecurityGroupName
= 'ssm-demo-sg')
{
if (Get-EC2SecurityGroup |
? { $_.GroupName -eq
$securityGroupName }) {
Write-Verbose
"Skipping as SecurityGroup $securityGroupName
already present."
return;
}
#Security
group and the instance should be in the same network (VPC)
$securityGroupId
= New-EC2SecurityGroup
$securityGroupName -Description "SSM Demo" -VpcId
$subnet.VpcId
Write-Verbose
"Security Group $securityGroupName created"
$bytes
= (Invoke-WebRequest
'http://checkip.amazonaws.com/').Content
$SourceIPRange
= @(([System.Text.Encoding]::Ascii.GetString($bytes).Trim() + "/32"))
Write-Verbose
"$sourceIPRange retreived from checkip.amazonaws.com"
$fireWallPermissions
= @(
@{IpProtocol =
'tcp'; FromPort =
3389; ToPort =
3389; IpRanges =
$SourceIPRange},
@{IpProtocol =
'tcp'; FromPort =
5985; ToPort =
5986; IpRanges =
$SourceIPRange},
@{IpProtocol =
'tcp'; FromPort =
80; ToPort =
80; IpRanges =
$SourceIPRange},
@{IpProtocol =
'icmp'; FromPort =
-1; ToPort =
-1; IpRanges =
$SourceIPRange}
)
Grant-EC2SecurityGroupIngress
-GroupId $securityGroupId
`
-IpPermissions
$fireWallPermissions
Write-Verbose
'Granted permissions for ports 3389, 80, 5985'
}
function
SSMRemoveSecurityGroup ([string]$SecurityGroupName
= 'ssm-demo-sg')
{
$securityGroupId
= (Get-EC2SecurityGroup
| `
? { $_.GroupName
-eq $securityGroupName
}).GroupId
if ($securityGroupId) {
SSMWait
{(Remove-EC2SecurityGroup $securityGroupId -Force)
-eq $null}
`
'Delete
Security Group' 150
Write-Verbose
"Security Group $securityGroupName removed"
} else
{
Write-Verbose
"Skipping as SecurityGroup $securityGroupName
not found"
}
}
SSMCreateKeypair and SSMRemoveKeypair
EC2 key pair is based on private key and public key. The key pair
is used to encrypt password in Windows and ssh access on Linux. EC2 generates
random password and stores in an encrypted fashion, using the public key
associated with key name defined during the instance launch. EC2 does not save
the unencrypted password and also does not have access to private key. Since the
EC2 customer exclusively has access to the private key, only the customer can
decrypt. Creating a key pair is simple. The private key associated with the key
pair can only be retrieved at the time of creating. If the private key is lost,
then the password cannot be retrieved. Script to create a key pair and retrieve
the associated private key is shown below. The retrieved private key is saved in
the file name specified. Safe guarding this private key is very important from
security point of view and once lost, it is gone!
[string]$KeyName = 'ssm-demo-key',
[string]$KeyFile = "c:\keys\$keyName.$((Get-DefaultAWSRegion).Region).pem"
)
{
if (Get-EC2KeyPair
| ?
{ $_.KeyName
-eq $keyName
}) {
Write-Verbose
"Skipping as keypair ($keyName) already
present."
return
}
Write-Verbose
"Create keypair=$keypair, keyfile=$keyfile"
$keypair
= New-EC2KeyPair
-KeyName $keyName
"$($keypair.KeyMaterial)" | Out-File -encoding ascii -filepath
$keyfile
}
function
SSMRemoveKeypair (
[string]$KeyName = 'ssm-demo-key',
[string]$KeyFile = "c:\keys\$keyName.$((Get-DefaultAWSRegion).Region).pem"
)
{
#delete
keypair
del $keyfile -ea 0
Remove-EC2KeyPair
-KeyName $keyName
-Force
Write-Verbose
"Removed keypair=$keypair, keyfile=$keyfile"
}
Create Instances with an IAM Role and Security Group
Now that we have created all the dependent resources like IAM role,
security group, and key pair, the instance can be created.
function
SSMCreateInstance (
[string]$ImageName =
'WINDOWS_2012R2_BASE',
[string]$SecurityGroupName =
'ssm-demo-sg',
[string]$InstanceType =
'm4.large',
[string]$Tag = 'ssm-demo',
[string]$KeyName = 'ssm-demo-key',
[string]$KeyFile = "c:\keys\$keyName.$((Get-DefaultAWSRegion).Region).pem",
[string]$RoleName = 'ssm-demo-role'
)
{
$filter1
= @{Name='tag:Name';Value=$Tag}
$filter2
= @{Name='instance-state-name';Values=@('running','pending','stopped')}
$instance
= Get-EC2Instance
-Filter @($filter1, $filter2)
if ($instance) {
$instanceId
= $instance.Instances[0].InstanceId
Write-Verbose
"Skipping instance $instanceId
creation, already present"
$instanceId
return
}
$securityGroupId
= (Get-EC2SecurityGroup
| `
? { $_.GroupName
-eq $SecurityGroupName
}).GroupId
if (! $securityGroupId)
{
throw
"Security Group $SecurityGroupName not
found"
}
#Get the
latest R2 base image
$image
= Get-EC2ImageByName
$ImageName
#User Data to
enable PowerShell remoting on port 80
#User data
must be passed in as 64bit encoding.
$userdata
= @"
Enable-NetFirewallRule
FPS-ICMP4-ERQ-In
Set-NetFirewallRule
-Name WINRM-HTTP-In-TCP-PUBLIC -RemoteAddress Any
New-NetFirewallRule
-Name "WinRM80" -DisplayName "WinRM80" -Protocol TCP
-LocalPort 80
Set-Item
WSMan:\localhost\Service\EnableCompatibilityHttpListener -Value true
"@
$utf8 = [System.Text.Encoding]::UTF8.GetBytes($userdata)
$userdataBase64Encoded
= [System.Convert]::ToBase64String($utf8)
#Launch EC2
Instance with the role, firewall group created
# and on the
right subnet
$instance
= (New-EC2Instance
-ImageId $image.ImageId `
-InstanceProfile_Id
$RoleName `
-AssociatePublicIp
$true `
-SecurityGroupId
$securityGroupId `
-KeyName
$keyName `
-UserData
$userdataBase64Encoded `
-InstanceType
$InstanceType).Instances[0]
#Wait to
retrieve password
$cmd = {
$password
= Get-EC2PasswordData
-InstanceId $instance.InstanceId `
-PemFile
$keyfile -Decrypt
$password
-ne $null
}
SSMWait $cmd 'Password
Generation' 600
$password
= Get-EC2PasswordData
-InstanceId $instance.InstanceId `
-PemFile
$keyfile -Decrypt
$securepassword
= ConvertTo-SecureString
$Password -AsPlainText
-Force
$creds
= New-Object
System.Management.Automation.PSCredential
("Administrator", $securepassword)
#update the
instance to get the public IP Address
$instance
= (Get-EC2Instance
$instance.InstanceId).Instances[0]
#Wait for
remote PS connection
$cmd = {
icm $instance.PublicIpAddress
{dir c:\}
-Credential $creds
-Port 80
}
SSMWait $cmd 'Remote
Connection' 450
New-EC2Tag -ResourceId $instance.InstanceId -Tag
@{Key='Name';
Value=$Tag}
$instance.InstanceId
}
function
SSMRemoveInstance (
[string]$Tag = 'ssm-demo',
[string]$KeyName = 'ssm-demo-key',
[string]$KeyFile = "c:\keys\$keyName.$((Get-DefaultAWSRegion).Region).pem"
)
{
$filter1
= @{Name='tag:Name';Value=$Tag}
$filter2
= @{Name='instance-state-name';Values=@('running','pending','stopped')}
$instance
= Get-EC2Instance
-Filter @($filter1, $filter2)
if ($instance) {
$instanceId
= $instance.Instances[0].InstanceId
$null
= Stop-EC2Instance
-Instance $instanceId
-Force -Terminate
Write-Verbose
"Terminated instance $instanceId"
} else
{
Write-Verbose
"Skipping as instance with name=$Tag not found"
}
}
Instance creation is an async operation (i.e.) New-EC2Instance API
just initiates the instance creation process, and we need to periodically poll
in a loop for completion. We will use “Wait” function explained in a previous blog
located here. Waiting
for instance creation is achieved in two steps. First one is to poll for
password generation. Once the password is generated, keep trying until the
remote connection is established successfully. Establishing remote connection
is used as a proxy check for instance readiness.
#Wait function
function
SSMWait (
[ScriptBlock] $Cmd,
[string] $Message,
[int] $RetrySeconds,
[int] $SleepTimeInMilliSeconds = 5000)
{
$_msg = "Waiting for $Message to
succeed"
$_t1 = Get-Date
Write-Verbose
"$_msg in $RetrySeconds seconds"
while ($true)
{
$_t2
= Get-Date
try
{
$_result
= &
$Cmd 2>$_null | select -Last 1
if
($? -and
$_result)
{
Write-Verbose("Succeeded $Message in " + `
"$_([int]($_t2-$_t1).TotalSeconds)
Seconds, Result=$_result")
break;
}
}
catch
{
}
$_t
= [int]($_t2 - $_t1).TotalSeconds
if
($_t -gt
$RetrySeconds)
{
throw
"Timeout - $Message after $RetrySeconds seconds, " + `
"Current
result=$_result"
break
}
Write-Verbose
"$_msg ($_t/$RetrySeconds) Seconds."
Sleep -Milliseconds $SleepTimeInMilliSeconds
}
}
Explore
& Enjoy!
/Siva
No comments:
Post a Comment