Skip to main content

Securing the Connection from NodeJS App on EKS to S3

ยท 4 min read
Abdulmalik
AppSec Engineer

You have your app deployed on an EC2 instance via nodes on EKS and this app needs to access/interact with files stored in an Amazon S3 bucket.

Originally, you would want to create an s3 IAM user with minimal policies to access the S3, now you have more responsibilities, having to worry about the secret keys, making sure it doesn't leak, making sure even if someone has access, there is a log or monitoring process to see if the key has been compromised.

But why bear all these responsibilities when your app can still do all it needs without the credential keys and just fall back to using IAM Role Access?

If you are working with EKS, it's pretty easy to set up IRSA (IAM Roles for Service Accounts), this way you can provision and rotate the IAM temporary credentials (called a Web Identity) which the Kubernetes ServiceAccount you've mounted into your node pods can use to call AWS APIs.

Prerequisiteโ€‹

  • Terraform provisioned EKS Cluster

So Let's jump into it;

Setting Up EKS IRSA for your NodeJs Podsโ€‹

๐Ÿ‘‰ Step 1: You want to set up your S3 Access Policyโ€‹

You can declare how you streamline your policy here,

keylessnodes3.tf
resource "aws_iam_policy" "s3eksnodejs-access" {
name = "s3eksnodejs"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3NodeJSEKS",
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::YOURS3BUCKETNAME",
"arn:aws:s3:::YOURS3BUCKETNAME/*"
]
}
]
}
EOF
}

๐Ÿ‘‰ Step 2: Creating EKS IRSA,โ€‹

here, you will create the EKS IRSA by attaching the policy you created above alongside your EKS Open ID Connect, your nodeJS app namespace and the ServiceAccount you want to attach to your app pod.

keylessnodes3.tf
module "s3eksnodejs_role" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"

role_name = "s3eksnodejs"

role_policy_arns = {
policy = aws_iam_policy.s3eksnodejs.arn
}

oidc_providers = {
ex = {
provider_arn = module.eks.oidc_provider_arn //YOUR EKS OIDC ARN
namespace_service_accounts = ["YOURAPPNAMESPACENAME:s3eksnodejs"]
}
}
}

๐Ÿ‘‰ Step 3: Creating your ServiceAccountโ€‹

You create your ServiceAccount with an annotation of the IAM Role you created above

keylessnodes3.tf
resource "kubernetes_service_account_v1" "s3eksnodejs" {
metadata {
name = "s3eksnodejs"
namespace = "default"
annotations = {
"eks.amazonaws.com/role-arn" = module.s3eksnodejs_role.iam_role_arn
}
}
}

Prepping your app and deploying to EKSโ€‹

๐Ÿ‘‰ Step 4: Prep your code and remove the credentialsโ€‹

You can start by removing your codes that call for AWS credential keys.

Here is a sample of the usage with the aws-sdk/client-s3 v3. listing objects in your s3 using credential keys.

app.js
const { S3Client, ListObjectsV2Command } = require("@aws-sdk/client-s3");

const s3Client = new S3Client({
region: 'AWSREGION',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});

// Function to list objects (images) in an S3 bucket
const listObjectsInBucket = async (bucketName) => {
const params = {
Bucket: bucketName
};

try {
const data = await s3Client.send(new ListObjectsV2Command(params));
if (data.Contents.length > 0) {
console.log("Objects in the bucket:");
data.Contents.forEach(obj => {
console.log(obj.Key);
});
} else {
console.log("The bucket is empty.");
}
} catch (error) {
console.error("Error listing objects:", error);
}
};

// Define your S3 bucket name
const bucketName = 'YOURS3BUCKETNAME';

listObjectsInBucket(bucketName);

so what will your code look like when you remove the credentials you are calling from env?

nocredsapp.js
const { S3Client, ListObjectsV2Command } = require("@aws-sdk/client-s3");

const s3Client = new S3Client({
region: 'AWSREGION',
});

// Function to list objects (images) in an S3 bucket
const listObjectsInBucket = async (bucketName) => {
const params = {
Bucket: bucketName
};

try {
const data = await s3Client.send(new ListObjectsV2Command(params));
if (data.Contents.length > 0) {
console.log("Objects in the bucket:");
data.Contents.forEach(obj => {
console.log(obj.Key);
});
} else {
console.log("The bucket is empty.");
}
} catch (error) {
console.error("Error listing objects:", error);
}
};

// Define your S3 bucket name
const bucketName = 'YOURS3BUCKETNAME';

listObjectsInBucket(bucketName);

That's it, now you are left with the responsibility of making sure your running pod for the app has the ServiceAccount with the IAM role mounted.

Doing that isnt hard, in your existing YAML file, just add serviceAccountName: s3eksnodejs to the spec, just like the below example

deploy.yaml
    spec:
serviceAccountName: s3eksnodejs
containers:
- image: busybox

Well, that's it, folks! I hope you find this piece insightful and helpful.

Till next time โœŒ๏ธ

Referencesโ€‹


Comments