Sunday, December 20, 2015

Launching EC2 Instance with an IAM Role – Part 1 of 2




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!

function SSMCreateKeypair (
        [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
    }
}


You can find the code under “AWS/SSM” folder at https://github.com/padisetty/Samples.

Explore & Enjoy!
/Siva

No comments: