Defenders - caller based EC2 security with CDK

This content is more than 4 years old and the cloud moves fast so some information may be slightly out of date.

Defenders: Caller based EC2 security

The risk with security credentials is that they get exposed an are being used elsewhere. What if we could prevent that the are being used elsewhere. The idea from the article of William Bengston from netflix was: Dynamically locking credentials to the environment.

This implementation of this idea is much more simple with the cdk. So, let’s defend ourselves!

Our story here is the battle of the defenders (tm). They are DC comix superheroes and they have some special attributes - more on this later…

But first we need a battleground. We will code the Resources with the CDK in node style. More on the cdk in the blog


We build a vpc as battleground. As a first line of defense, we want to secure our battleground. So a security group is included, which first determines the current public ip of our workstation and limit ssh access to the EC2 instances to this public ip.

First line of defense - Get rid of ‘’

The public IP from our workstation is read via AWS Checkip and the put into a SecurityGroup:


import * as request from "request-promise-native";

export async function GetLocalIp() : Promise<string> {
    var clientIp: string;
    const baseUrl = '';

    var options = {
        uri: baseUrl,

    var result = await request.get(options);
    if(result.indexOf(",") > -1) {
        var arr = result.split(",");
        result = arr[1]

    clientIp = result.trim()+"/32";

    return clientIp;


In this node code snippet we make a simple request to the checkip site from aws. Sometimes you get two ips (don’t ask me why). In this case we drop the first one.


const defVpc = new Vpc(this, 'defendersVPC', {});
this.vpc = defVpc;
const clientIp = GetLocalIp();

clientIp.then((ip) => {

    const sg = new SecurityGroup(this, "defenderDemo",{
        vpc: defVpc,
        securityGroupName: "SSH incoming",
        description:  "SSH Incoming on current public ip",
        allowAllOutbound: true,
    Tag.add(sg, "Name", "dynamicIncomingSSHClient");

    sg.addIngressRule(Peer.ipv4(ip), Port.tcp(22), "Ssh incoming")

    this.sshIncomingSecurityGroup = sg;

In this code snippet we create a one-line-vpc (and I love the one-line-vpc! ) and attach the security group to it.

In creating this SecurityGroup for my work from a mobile office we madee sure that there is no - SSH-open-to-the-world incoming line in the security group.

First line of defense

With this being done, we create our first superhero instance: Meet Matt.

Matt with limited security superpower and second line of defense

There is “the DareDevil” Matt (oops, now you know his secret identity). Matt has some limited superpowers, so on the screen he mostly gets serious damage.

Matt is the codename for our mildly secured credentials. Let’s build Matt:

Use limited ssh access security group of the battleground

    const mattSGId = Fn.importValue('dynamicIncomingSSHClient');
    const mattSG = SecurityGroup.fromSecurityGroupId(this, "Matt Security Group", mattSGId);

Battleground exports the security group id as dynamicIncomingSSHClient - we import the id and use the security group.

Matt gets some rights

Matt now gets some rights, with the AWS managed policy AmazonEC2ReadOnlyAccess:

  const instanceRole = new Role(this, 'mattinstancerole',
        assumedBy: new ServicePrincipal(''),
        managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ReadOnlyAccess')],

Matt will spawn in the battleground vpc:

const matt = new Instance(this, "matt", {
      machineImage: linuxImageId,
      instanceType:  iType,
      role: instanceRole,
      vpc: vpcStack.vpc,
      securityGroup: mattSG,
      vpcSubnets: {subnetType: SubnetType.PUBLIC },

Now we have an EC2 instance with a role, which allows to list other EC2 instances. In production you should narrow the resources down to specific instances.

First line of defense

With EC2 Instance Connect i just type:

mssh i-007e6bd0aecd25f71

where i-007e6bd0aecd25f71 is the instance ID of. In the terminal i test whether Matt is really able to list instances:

[ec2-user@ip-10-0-49-237 ~]$ aws ec2 describe-instances --region eu-central-1 | head -n2
    "Reservations": [

It works! But is Matt able to keep his secrets for himself…? Meet Jessica Jones from the “Defenders”.

Jessica vs. Matt: 1:0

Jessica Jones is a detective, owner of “Alias investigation”. She is used to sneak into everything, so she manages to get onto the Matt instance.

Now she steals the keys! How to do that?

On Matts EC2 instance the keys are stored in the metadata. She gets the keys with:


Where matt-mattinstanceroleFB806583-1R0IS3ZTZG4ZB is the role name.

As an output you get something like:

  "Code" : "Success",
  "LastUpdated" : "2019-09-29T14:05:24Z",
  "Type" : "AWS-HMAC",
  "SecretAccessKey" : "eb36OnQb9P7+No2rn8tbYm4lur4sNjtxAYfFjrKz",
  "Token" : "AgoJb3JpZ2luX2VjEM7//////////wEaDGV1LWNlbnRyYWwtMSJGMEQCIB8ZibWslHCgHrHgXQWsYX+a/hbpvHUAMBGn+LS/siKnAiBdIAz7roTIuRdSHZNDpjIpmYYSmOZ+nEJkFIxlQo6KZiqBBAin//////////8BEAAaDDY2OTQ1MzQwMzMwNSIMlFHDg9lf8oCVtI3TKtUDVWk9swjngH873C1uovh3EBF8/RK2M5vwx60cwZhKp7STku3vci276soQdz8bIxexnZ6a3UtNcrEXePVdJu8u74rY+JEZmNHMK1h3JSoNN5LEXdQZgB/fGETn1q8RPOw7iAx3bFMov790SsCbm5UXTSzy9pyZ0+LgWt0YUUZKYCvAcbXVuR4XYWwq7mQXZjugFJv08NJaFlmSyCJSk9veOO+K2R5Gc2LVE9sc0x9A5D78QbCB7SJEQHyXH8AN4Jpl0tGl852BPPGW0KrD6OYUzDsYTiIyGMaiN1bDI+w2eedx0PInfEvu0tubB3eiPIbB1YmMKUwVuKJ+QgMWix3QSWjFdqFASzAA2klYhO2uQmmqi0UQR7CVme0ex4s4xqCMWqnGYtbo9gy5ULf0elWAoSrw5KQjzB1iqiIeQGXhcuMsoO1fjkpFcIJJTohwc6UYGYtwrEoSAoYe+5CxkL2y/0UN26DxEeandr/aA7MviOZ20mUGJypkR2dhhqsfRtomdr8OqQn/iLh/H6z6oJzPH6+W/U8E8v3h3a+rvvX70yFnWc4nQGppBaBgTzFbeWhTnZmGIdiZLGDI6XJ/t/4fVzyoSFWvCDb7bYXnRtfdysgfzS3UEDCB9MLsBTq1AUcgEBYGTSX5tOCDYSpBAeY2FXSIN6GBRF1wgwVk3ybKiJWnHePOg6W2cZ7MiQVIXJFGMAXf5KGY5JhpvuJxLafBhu45MIPMaJ7xKriDI7pNDLQ6GBp9XUsW8TjAtjkx942mlE53qg7X8tQzTPAExoPDa1KYdP6EwuR1RwmbwJ0hr8ly53oIXQJm7uoltft67RxBXAK9W+z6lVhqb71P0fecF50AS8nbv6hfZlexwrfNhQxTAzM=",
  "Expiration" : "2019-09-29T20:09:18Z"

Jessica takes the Access Key, the secret and the token. The expiration says the key is only valid until 20:09. Its 16:16 So she got a few hours to use the key!

She jumps onto her own instance

mssh ec2-user@i-07553190f25334a3d

And tries the EC2 describe instances:

[ec2-user@ip-10-0-51-109 ~]$ aws ec2 describe-instances --region eu-central-1

An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation.

So at this moment she is not allowed. Now she exports the stolen secrets into the environment:

export AWS_DEFAULT_REGION=eu-central-1
export AWS_SECRET_ACCESS_KEY=eb36OnQb9P7+No2rn8tbYm4lur4sNjtxAYfFjrKz

By doing that she as assumed Matts identity!

She proves that by asking the token service:

[ec2-user@ip-10-0-51-109 ~]$ aws sts get-caller-identity
    "Account": "1112223334445",
    "UserId": "AROAZXXUIISUTBRLFHQYW:i-05a7bb3a3883106f0",
    "Arn": "arn:aws:sts::1112223334445:assumed-role/matt-mattinstanceroleFB806583-1R0IS3ZTZG4ZB"

(The outputs are not really the same from above - that is just because i did this multiple times)

Now the moment of suspense. Is Jessica now allowed to list the instances with the stolen identity?

[ec2-user@ip-10-0-51-109 ~]$ aws ec2 describe-instances --region eu-central-1 | head -n2
    "Reservations": [

Yes. She now has succesfully stolen the keys. Thats not good! Or as in the Capitol One breach (see Appendix) - This is really bad.

Welcome on the stage, the Luke, Luke (Cage).

Luke with third line of defense

Luke Cage alias Powerman is undestructable. Nobody gets through his security. But he also has a role and also has credentials, which could be exposed. His secret superpower here is the caller based security. Where Bengston used lambdas injecting security after the instance has started, we just refer to the instance and add a policy to the role, which is refered by the instance profile:

  const callerBasedPolicy = new Policy(this, "callerBasedPolicy");
  const callerBasedPolicyStatement = new PolicyStatement({
    actions: ['*'],
    effect: Effect.DENY,
    resources: ['*'],

  callerBasedPolicyStatement.addCondition('NotIpAddress', {
    "aws:SourceIp" : [


With this policy a call to any service (actions: ['*']) is denied, if it is not coming from Lukes ip (luke.instancePublicIp).

Full caller based defense

You may not add the caller based statement directly to the Luke-Profile. When Luke starts, the policy have to be there. When you create the caller bases policy, the ip of Luke must be there. To avoid the circular dependency, we just add a Policy to the role later, after the instance has started.

And this statement says:

If the call does not come from Lukes IP Address - do not allow it.

Because it is a DENY effect, it has more power than any ALLOW.

Jessica vs. Luke 0:1

Ok, Jessica manages to steal Lukes secrets - watch the series if you want to know how. But as she tries to use the secrets, a surprise is waiting for her:

[ec2-user@ip-10-0-51-109 ~]$ aws sts get-caller-identity
    "Account": "1112223334445",
    "UserId": "AROAZXXUIISUTBRLFHQYW:i-05a7bb3a3883106f0",
    "Arn": "arn:aws:sts::1112223334445:assumed-role/luke-lukeinstanceroleFB806583-1R0IS3ZTZG4ZB/i-05a7bb3a3883106f0"
[ec2-user@ip-10-0-51-109 ~]$ aws ec2 describe-instances --region eu-central-1 | head -n2

An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation.

She has managed to steal Lukes identity, but because the call comes from her ip, she gets no access.


The first line of defense always is the security group with least privileges. I showed you an easy way to automate this. On account level you could/should add NACL as well. (Chemist joke: To salt the food )

The second line of defense here is the policy of the IAM identity you use. And as you surely are aware - with EC2 instances or other AWS services you only use assumed roles, never never static access keys with locally stored credentials.

Here i have showed you how to easily add a third line of defense: Attaching the “where do you come from” question to the policy. So an attacker may get hold of the credentials, but may not use them. Usually this is tricky when you do not automate it. But with the CDK it has become much easier!

Hidden 3 1/2 line of defense: By generating unique names for roles you make it harder to guess the role name. This comes for free with the cdk!



The Technical Side of the Capitol One AWS Security Breach


Dynamically locking credentials to the environment.


Photo by Touann Gatouillat Vergos on Unsplash

Similar Posts You Might Enjoy

AWS Client VPN - Access your Virtual Private Cloud

One of the most unknown options to access a VPC is Client VPN. Nearly all customers I am talking to are using a Bastion Host or similar to access services within their VPC. But what about direct access without any jumps in between? After reading this blog, you can create your own Client VPN. - by Patrick Schaumburg

Assessing compliance with AWS Audit Manager

Introduction As in traditional IT infrastructures, firms in regulated industries such as banks or energy providers have strict security requirements to comply with when using public cloud providers as well. However, cloud adoption is often driven by application development teams that are striving for increased speed and agility to launch new features in their application, but don’t care too much about those regulatory requirements. That makes it particularly important for IT governance functions to have effective tools to evaluate compliance with the aforementioned standards and gather evidence that can be provided to their internal or external auditors. - by Benjamin Wagner

Start Guessing Capacity - Benchmark EC2 Instances

Stop guessing capacity! - Start calculating. If you migrate an older server to the AWS Cloud using EC2 instances, the prefered way is to start with a good guess and then rightsize with CloudWatch metric data. But sometimes you’ve got no clue, where to start. And: Did you think all AWS vCPUs are created equal? No, not at all. The compute power of different instance types is - yes - different. - by Gernot Glawe