AWS CDK - How to pass Access Key Secret and Secret Key Id as Env Param to Container - amazon-iam

I'm using CDK to build our infra on AWS. I create my IAM User for my microservices to talk to AWS Services under the defined policies. My issue is I cannot get aws secret key and id and then, pass as Env variable to my container. See below:
First, I create my IAM user which will be used by my microservices.
const user = new User(this, "user", {
userName: `${myAppEnv}-api-iam-user`,
});
Second, I'm trying to create Access Key.
const accessKey = new CfnAccessKey(this, "ApiUserAccessKey", {
userName: user.userName,
});
const accessKeyId = new CfnOutput(this, "AccessKeyId", {
value: accessKey.ref,
});
const accessKeySecret = new CfnOutput(this, "SecretAccessKeyId", {
value: accessKey.attrSecretAccessKey,
});
Next, I want to pass it as an env variable.
const apiContainer = apiTaskDefinition.addContainer(containerId, {
image: apiImage,
environment: {
APP_ENV: myAppEnv,
AWS_ACCESS_KEY_ID: awsAccessKeyId.value || ":(",
AWS_SECRET_ACCESS_KEY: awsSecretAccessKey.value || ":(",
NOTIFICATIONS_TABLE_ARN: notificationsTableDynamoDBArn,
NOTIFICATIONS_QUEUE_URL: notificationsQueueUrl,
},
cpu: 256,
memoryLimitMiB: 512,
logging: new AwsLogDriver({ streamPrefix: `${myAppEnv}-ec-api` }),
});
When my CDK deploy finishes successfully, I see the below printed out :/
Outputs:
staging-ecstack.AccessKeyId = SOMETHING
staging-ecstack.SecretAccessKeyId = SOMETHINGsy12X21xSSOMETHING2X2
Do you have any idea how I can achieve this?

Generally speaking, creating an IAM user isn't the way to go here - you're better off using an IAM role. With the CDK it will create a taskRole for you automatically when you instantiate the taskDefinition construct. You can assign permissions to other constructs in your stack using various grant* methods as described here:
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef');
const container = taskDefinition.addContainer('web', {
image: ecs.ContainerImage.fromRegistry("apps/myapp"),
memoryLimitMiB: 256,
});
// Grant this task role access to use other resources
myDynamodbTable.grantReadWriteData(taskDefinition.taskRole);
mySnsTopic.grantPublish(taskDefinition.taskRole);

In short, find the answer in my blogpost here:
https://blog.michaelfecher.com/i-tell-you-a-secret-provide-database-credentials-to-an-ecs-fargate-task-in-aws-cdk
To explain a bit more detailled on your issue:
Avoid custom created IAM Roles and use the generated ones by CDK. They are aligned and using the least-privilege principle. In my blog post, the manual IAM role creation isn't necessary. Yes, I know... I need to update that. ;)
Use the grant* method of the corresponding resource, e.g. taskDefinition.
This will create a policy statement for the generated role of 1)
Don't use environment variables for secrets.
There are a lot of resources on the web, which tell you why.
One is this here: https://security.stackexchange.com/questions/197784/is-it-unsafe-to-use-environmental-variables-for-secret-data
Especially, when working with ECS, make use of the secrets argument in the task definition (see the blog post).
It seems, that you're passing the token instead of the actual secret value.
That doesn't work. The token is resolved during synth / Cloud Formation generation.
You won't need the CfnOutput. Use the direct Stack -> Stack passing of fields.
The CfnOutput is really sth., which you should avoid, when having all Stacks under control in one CDK application.
That only makes sense, if you want to share between CDK applications, which are separated deployments and repositories.
If sth. is unclear, don't hesitate asking questions.

Related

AWS CDK ECS Task Definition Without Task Role

In AWS CDK v2 the ECS TaskDefinition L2 construct has an optional property TaskRole if not specified CDK default behavior is to create a task role. However I do not want a task role set for this resource, it is not actually required in AWS - the Task Definition can function without this property. How can i manage that in CDK? I can't see any way to unset that task role or not have it generated in the first place. Do I need to step back to the L1 construct for this? My configuration:
taskDefinition := awsecs.NewEc2TaskDefinition(stack, jsii.String(deploymentEnv+service.Tag+"TaskDef"), &awsecs.Ec2TaskDefinitionProps{
Family: jsii.String(deploymentEnv + service.Tag),
NetworkMode: awsecs.NetworkMode_BRIDGE,
//TaskRole: what can i do here to fix this
Volumes: &[]*awsecs.Volume{
&efs_shared_volume,
},
})
In the CDK, it's necessary because the L2 construct implements the Grantable interface, and its methods depend on the existence of the role. Technically, you can override almost any property on any node which would allow you to get this effect, but that may result in difficult to track errors down the road.
Additionally, if no role is specified for a task definition, your tasks inherit permissions from the EC2 instance role in the cluster, which is almost certainly not a behavior you want. If that is the behavior you want, you're better off explicitly defining the role to be the same as the role used in the EC2 cluster.
Alternatively, if your intention is to make your tasks have no permissions, your best bet is to either stick with the default behavior or explicitly define a role with no attached policies then (optionally) pass the object returned by the .withoutPolicyUpdates on the role object to prevent it from being updated by grants.
const role = new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
description: 'Empty ECS task role with no permissions',
});
// ...
taskDefinition := awsecs.NewEc2TaskDefinition(stack, jsii.String(deploymentEnv+service.Tag+"TaskDef"), &awsecs.Ec2TaskDefinitionProps{
// ...
TaskRole: role.withoutPolicyUpdates(),
// ...
},
})
You can remove arbitrary child constructs by ID, using the tryRemoveChild escape hatch method:
// remove the role
taskDefinition.Node().TryRemoveChild(jsii.String("TaskRole"))
// remove the reference to the role
t := taskDefinition.Node().DefaultChild().(awsecs.CfnTaskDefinition)
t.AddPropertyDeletionOverride(jsii.String("TaskRoleArn"))
The trick is identifying the construct ID. You sometimes need to look for it in the source code.

CDK cloud9 - How to attach preconstructed instance profile to Cloud9 instance iam role in cdk?

I created cloud9 instance and vpc environment via cdk. Also with role permissions and instance profile, how do i attach that at the end via cdk too?
Currently there seem to be no in built parameters about setting iam role in Ec2Environment
Can't achieve this automatically too if i use CloudFormation, so i am thinking this is not available yet?
I know i can use custom resource or create a lambda to achieve that, but was thinking it's just a bit too much to just to use to attach an instance profile
My code:
const c9IamRole = new iam.Role(this, 'C9IamRole', {
roleName: 'cloud9-admin-access-role',
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess'),
]
});
const c9InstanceProfile = new iam.CfnInstanceProfile(this, 'C9InstanceProfile', {
roles: [c9IamRole.roleName],
});
// create a cloud9 ec2 environment in a new VPC
const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 3 });
const c9Env = new cloud9.Ec2Environment(this, 'Cloud9Env', {
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
});
IAM role that i want to attach the instance profile (at the created cloud9 ec2 instance page)
Anything using a Cfn prefixed method is an L1 construct. They do not have the hooks necessary to automatically apply them to other constructs (l2 and l3 - the higher level objects) - they are bare bones, just basically a translation from your code to cfn template snippet.
if iam.CfnInstanceProfile does not have a l2 or l3 version (as of this answer it does not seem to, but the CDK team is always updating) then you'll have to manually attach it using other cfn methods.
Also, the cloud9 library is (as of this writing) still Experimental, which is a good indication that it wont have all the things it needs - It does not seem to have any property for attaching a role. You might be able to manually (again using cfn escape hatch methods) attach a role.
You might try instead applying the roles to a User/Group and giving them permission to access the cloud9, rather than attaching the role to cloud9 and give allowance to various Identities - it may be easier with current CDK constructs.

AWS CDK and two EKS clusters sharing the same IAM Roles

This is probably not unique to these exact components, but this is where I have encountered this problem:
I am standing up multiple EKS clusters with CDK and they all need to be able to assume certain IAM roles with RBAC to do AWS-y things. For instance:
var AllowExternalDNSUpdatesRole = new Role(
this,
"AllowExternalDNSUpdatesRole",
new RoleProps
{
Description = "Route53 External DNS Role",
InlinePolicies = new Dictionary<string, PolicyDocument>
{
["AllowExternalDNSUpdates"] = externalDnsPolicy
},
RoleName = "AllowExternalDNSUpdatesRole",
AssumedBy = new FederatedPrincipal(Cluster.OpenIdConnectProvider.OpenIdConnectProviderArn, new Dictionary<string, object>
{
["StringLike"] = ExternalDnsCondition,
}, "sts:AssumeRoleWithWebIdentity"),
}
);
I'm giving it a RoleName so I can reference it in a sane way in the Kubernetes yaml files. At the time I'm creating the role, I need to be able to create a FederatedPrincipal referring to the Cluster's OIDC provider so I can drop it in AssumedBy. I can't create the same named role when I stand up the second or nth cluster. It bombs spectacularly.
Ideally I would create these kinds of roles in their own IAM-only stack, and then attach the FederatedPrincipal to the created roles at EKS Cluster creation time. I have tried to figure out how to do that. When these clusters get built up and torn down they would just add and remove themselves from the AssumedBy part of the role. I would love a clue to help me figure that out.
Beyond that the only other thing I can think of to do is to create roles-per-cluster and then have to modify the YAML to refer to the uniquely-named generated roles. That is less than ideal. I'm trying to avoid having to maintain per-cluster yaml files for Kubernetes.
I'm game for other strategies too....

How can I connect GitHub actions with AWS deployments without using a secret key?

I'd like to be able to use GitHub Actions to be able to deploy resources with AWS, but without using a hard-coded user.
I know that it's possible to create an IAM user with a fixed credential, and that can be exported to GitHub Secrets, but this means if the key ever leaks I have a large problem on my hands, and rotating such keys are challenging if forgotten.
Is there any way that I can enable a password-less authentication flow for deploying code to AWS?
Yes, it is possible now that GitHub have released their Open ID Connector for use with GitHub Actions. You can configure the Open ID Connector as an Identity Provider in AWS, and then use that for an access point to any role(s) that you wish to enable. You can then configure the action to use the credentials acquired for the duration of the job, and when the job is complete, the credentials are automatically revoked.
To set this up in AWS, you need to create an Open Identity Connect Provider using the instructions at AWS or using a Terraform file similar to the following:
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = [
// original value "sigstore",
"sts.amazonaws.com", // Used by aws-actions/configure-aws-credentials
]
thumbprint_list = [
// original value "a031c46782e6e6c662c2c87c76da9aa62ccabd8e",
"6938fd4d98bab03faadb97b34396831e3780aea1",
]
}
The client id list is the 'audience' which is used to access this content -- you can vary that, provided that you vary it in all the right places. The thumbprint is a hash/certificate of the Open ID Connector, and 6938...aea1 is the current one used by GitHub Actions -- you can calculate/verify the value by following AWS' instructions. The thumbprint_list can hold up to 5 values, so newer versions can be appended when they are being made available ahead of time while continuing to use older ones.
If you're interested in where this magic value came from, you can find out at How can I calculate the thumbprint of an OpenID Connect server?
Once you have enabled the identity provider, you can use it to create one or more custom roles (replacing with :
data "aws_caller_identity" "current" {}
resource "aws_iam_role" "github_alblue" {
name = "GitHubAlBlue"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Action = "sts:AssumeRoleWithWebIdentity"
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
}
Condition = {
StringLike = {
"token.actions.githubusercontent.com:aud" : ["sts.amazonaws.com" ],
"token.actions.githubusercontent.com:sub" : "repo:alblue/*"
}
}
}]
})
}
You can create as many different roles as you need, and even split them up by audience (e.g. 'production', 'dev'). Provided that the OpenID connector's audience is trusted by the account then you're good to go. (You can use this to ensure that the OpenID Connector in a Dev account doesn't trust the roles in a production account and vice-versa.) You can have, for example, a read-only role for performing terraform validate and then another role for terraform apply.
The subject is passed from GitHub, but looks like:
repo:<organization>/<repository>:ref:refs/heads/<branch>
There may be different formats that come out later. You could have an action/role specifically for PRs if you use :ref:refs/pulls/* for example, and have another role for :ref:refs/heads/production/*.
The final step is getting your GitHub Actions configured to use the token that comes back from the AWS/OpenID Connect:
Standard Way
jobs:
terraform-validate:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout#v2
- name: Configure AWS credentials from Test account
uses: aws-actions/configure-aws-credentials#master
with:
role-to-assume: arn:aws:iam::<accountid>:role/GitHubAlBlue
aws-region: us-east-1
- name: Display Identity
run: aws sts get-caller-identity
Manual Way
What's actually happening under the covers is something like this:
jobs:
terraform-validate:
runs-on: ubuntu-latest
env:
AWS_WEB_IDENTITY_TOKEN_FILE: .git/aws.web.identity.token.file
AWS_DEFAULT_REGION: eu-west-2
AWS_ROLE_ARN: arn:aws:iam::<accountid>:role/GitHubAlBlue
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout#v2
- name: Configure AWS
run: |
sleep 3 # Need to have a delay to acquire this
curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" \
| jq -r '.value' > $AWS_WEB_IDENTITY_TOKEN_FILE
aws sts get-caller-identity
You need to ensure that your AWS_ROLE_ARN is the same as defined in your AWS account, and that the audience is the same as accepted by the OpenID Connect and the Role name as well.
Essentially, there's a race condition between the job starting, and the token's validity which doesn't come in until after GitHub has confirmed the job has started; if the size of the AWS_WEB_IDENITY_TOKEN_FILE is less than 10 chars, it's probably an error and sleep/spinning will get you the value afterwards.
The name of the AWS_WEB_IDENTITY_TOKEN_FILE doesn't really matter, so long as it's consistent. If you're using docker containers, then storing it in e.g. /tmp will mean that it's not available in any running containers. If you put it under .git in the workspace, then not only will git ignore it (if you're doing any hash calculations) but it will also be present in any other docker run actions that you do later on.
You might want to configure your role so that the validity of the period used is limited; once you have the web token it's valid until the end of the job, but the token requested has a lifetime of 15 minutes, so it's possible for a longer-running job to expose that.
It's likely that GitHub will have a blog post on how to configure/use this in the near future. The above information was inspired by https://awsteele.com/blog/2021/09/15/aws-federation-comes-to-github-actions.html, who has some examples in CloudFormation templates if that's your preferred thing.
Update GitHub (accidentally) changed their thumbprint and the example has been updated. See for more information. The new thumbprint is 6938fd4d98bab03faadb97b34396831e3780aea1 but it's possible to have multiple thumbprints in the IAM OpenID connector.
Github recently updated their certifcate chain and the thumprint has changed from the one mentioned above a031c46782e6e6c662c2c87c76da9aa62ccabd8e to 6938fd4d98bab03faadb97b34396831e3780aea1
Github Issue: https://github.com/aws-actions/configure-aws-credentials/issues/357
For some reason I keep getting this with the provided answer of AlBlue:
"message":"Can't issue ID_TOKEN for audience 'githubactions'."
Deploying the stack provided in this post, does work however.

AWS CDK: how to deploy resources to different accounts?

Is it possible to deploy resources to two different AWS accounts using CDK?
As a simple example, imagine 2 resources that are linked (2 different IAM roles, perhaps), that need to be deployed to accounts accountA and accountB. They are linked, so their lifecycles should be tied together (i.e. they are created and destroyed at the same time and I shouldn't have to run two deployment actions). I'd like to be able to specify at deployment time where the resources go, not at synthesis time.
This requirement is easily achieved using Terraform (by means of two different provisioner definitions). How do I achieve this using CDK? Can this be achieved within a single stack, or is the CDK model that of one stack per target account? What does a simple example look like?
Yes, this is possible. you need to pass environment config object to stack props.
From the docs:
Each Stack instance in your AWS CDK app is explicitly or implicitly
associated with an environment (env). An environment is the target AWS
account and AWS Region into which the stack is intended to be
deployed.
Usage:
const envEU = { account: '2383838383', region: 'eu-west-1' };
const envUSA = { account: '8373873873', region: 'us-west-2' };
new MyFirstStack(app, 'first-stack-us', { env: envUSA, encryption: false });
new MyFirstStack(app, 'first-stack-eu', { env: envEU, encryption: true });
More info here.
In case you want to deploy 2 different resources within the same stack to 2 different accounts, this is not supported yet.
You would need to create different stack for each resource and pass the environment object accordingly.