Introduction
Setting up IAM Roles Anywhere is the "gold standard" for bringing the power of IAM Roles to external servers (On-premises, Azure, GCP) without needing permanent, risky Access Keys.
This mechanism relies on PKI (Public Key Infrastructure): You use a digital certificate to prove your identity to AWS, and in return, AWS provides you with temporary, short-lived credentials.
Prerequisites
Before we begin, you should have a basic understanding of IAM Identity Center and how to set up AWS Access Keys/Secrets, as this guide builds upon those concepts.
Quick Recap
If you have already set up a profile and logged in via SSO, you would typically use that profile in a NestJS source code like this:
import {Injectable} from '@nestjs/common'
import {getSignedUrl} from '@aws-sdk/s3-request-presigner'
import {ConfigService} from '@nestjs/config'
import {
S3Client,
ListObjectsV2Command,
GetObjectCommand,
} from '@aws-sdk/client-s3'
import {fromIni} from '@aws-sdk/credential-providers'
@Injectable()
export class S3Service {
private s3Client: S3Client
private bucket = ''
private profile = ''
constructor(private configService: ConfigService) {
const region = this.configService.get<string>('REGION') || ''
this.profile = this.configService.get<string>('PROFILE') || ''
this.bucket = this.configService.get<string>('BUCKET') || ''
this.s3Client = new S3Client({
region,
credentials: fromIni({profile: this.profile}),
})
}
async listFiles() {
const command = new ListObjectsV2Command({
Bucket: this.bucket,
})
const {Contents} = await this.s3Client.send(command)
if (!Contents) return []
const fileList = await Promise.all(
Contents.map(async file => {
const getCommand = new GetObjectCommand({
Bucket: this.bucket,
Key: file.Key,
})
return {
fileName: file.Key,
size: file.Size,
lastModified: file.LastModified,
viewUrl: await getSignedUrl(this.s3Client, getCommand, {
expiresIn: 3600,
}),
}
})
)
return fileList
}
}
Explanation:
- fromIni: This allows you to use a local profile defined in your .env file (ensure you have performed an SSO login first to obtain the token).
- listFiles: This function simply lists all files within the specified S3 Bucket.
The Problem: While this method is secure, there is a catch: the sessionDuration. If it is set to "PT2H", the user is forced to log in again every 2 hours. This isn't feasible for Production environments where applications must run 24/7 without manual intervention. IAM Roles Anywhere is the solution to this problem.
The 3-Phase Process
- Phase 1: Create certificates using OpenSSL (Free).
- Phase 2: Use AWS CDK to build the Roles Anywhere infrastructure.
- Phase 3: Configure your Server (Azure/GCP/On-prem) and NestJS.
Step-by-Step Implementation
1. Generate Certificates
First, create a rootCA.conf file on your local machine:
[ req ]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no
[ req_distinguished_name ]
CN = MyRootCA
[ v3_ca ]
basicConstraints = critical, CA:TRUE
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
Run these commands to generate the Root CA (rootCA.key and rootCA.pem):
$ openssl genrsa -out rootCA.key 2048
$ openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.pem -config rootCA.conf
Verify that the rootCA.pem file meets AWS requirements:
$ openssl x509 -in rootCA.pem -text -noout | grep -A 1 "Basic Constraints"
X509v3 Basic Constraints: critical
CA:TRUE
Now, create a client.conf file:
[ v3_client ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
Generate the Client Certificate (client.key and client.pem):
$ openssl genrsa -out client.key 2048
$ openssl req -new -key client.key -out client.csr -subj "/CN=nestjs-external-server"
$ openssl x509 -req -in client.csr \
-CA rootCA.pem -CAkey rootCA.key \
-CAcreateserial -out client.pem \
-days 365 -sha256 \
-extfile client.conf -extensions v3_client
Certificate request self-signature ok
subject=CN=nestjs-external-server
Verify the link between the Root CA and Client certificate:
$ openssl verify -CAfile rootCA.pem client.pem
client.pem: OK
Note: These commands are for Linux/macOS. Use Git Bash if you are on Windows.
In the end, the following files will be created:
2. AWS CDK Infrastructure
Create lib/role-anywhere-stack.ts:
import * as cdk from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
import * as rolesanywhere from "aws-cdk-lib/aws-rolesanywhere";
import * as fs from "fs";
export class RolesAnywhereStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const rootCaCert = fs.readFileSync("./path-to-file/rootCA.pem", "utf8").trim();
const trustAnchor = new rolesanywhere.CfnTrustAnchor(
this,
"MyTrustAnchor",
{
name: "NestJS-External-Anchor",
source: {
sourceData: { x509CertificateData: rootCaCert },
sourceType: "CERTIFICATE_BUNDLE",
},
enabled: true,
},
);
const nestjsRole = new iam.Role(this, "NestjsExternalRole", {
assumedBy: new iam.ServicePrincipal("rolesanywhere.amazonaws.com", {
conditions: {
StringEquals: {
"aws:SourceArn": trustAnchor.attrTrustAnchorArn,
},
},
}),
});
nestjsRole.assumeRolePolicy?.addStatements(
new iam.PolicyStatement({
actions: ["sts:TagSession", "sts:SetSourceIdentity"],
principals: [new iam.ServicePrincipal("rolesanywhere.amazonaws.com")],
effect: iam.Effect.ALLOW,
conditions: {
StringEquals: {
"aws:SourceArn": trustAnchor.attrTrustAnchorArn,
},
},
}),
);
nestjsRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonS3FullAccess"),
);
const profile = new rolesanywhere.CfnProfile(this, "NestjsProfile", {
name: "NestjsExternalProfile",
roleArns: [nestjsRole.roleArn],
enabled: true,
});
new cdk.CfnOutput(this, "TrustAnchorArn", {
value: trustAnchor.attrTrustAnchorArn,
description:
"The Trust Anchor ARN to be entered in the config file on the server",
});
new cdk.CfnOutput(this, "ProfileArn", {
value: profile.attrProfileArn,
});
new cdk.CfnOutput(this, "RoleArn", {
value: nestjsRole.roleArn,
});
}
}
Explanation:
- rootCaCert: This specifies the file path to your rootCA.pem file.
- nestjsRole.addManagedPolicy: This grants the IAM role full access permissions to Amazon S3.
Update bin/aws-cdk.ts and deploy. Your terminal output will provide the ARNs needed for the next step.
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib/core";
import { RolesAnywhereStack } from "../lib/role-anywhere-stack";
const app = new cdk.App();
new RolesAnywhereStack(app, "RolesAnywhereStack");
After deploy the result look like this
RolesAnywhereStack
✨ Deployment time: 41.33s
Outputs:
RolesAnywhereStack.ProfileArn = <RolesAnywhereStack.ProfileArn>
RolesAnywhereStack.RoleArn = <RolesAnywhereStack.RoleArn>
RolesAnywhereStack.TrustAnchorArn = <RolesAnywhereStack.TrustAnchorArn>
✨ Total time: 46.98s
You could check Role Anywhere already deploy to AWS
3. Server Configuration
Download the aws_signing_helper for your OS from the official repository.
Test the connection: replace the values for trust-anchor-arn, profile-arn, and role-arn with the corresponding outputs displayed after the AWS CDK deployment.
$ path-to\aws_signing_helper credential-process --certificate path-to\client.pem --private-key path-to\client.key --trust-anchor-arn {trust-anchor-arn} --profile-arn {rofile-arn} --role-arn {role-arn}
{
"Version": 1,
"AccessKeyId": "AccessKeyId",
"SecretAccessKey": "SecretAccessKey",
"SessionToken": "SessionToken",
"Expiration": "2026-02-01T12:07:20Z"
}
If successful, update your ~/.aws/config file with new profile:
[profile roles-anywhere]
credential_process = path-to\aws_signing_helper credential-process --certificate path-to\client.pem --private-key path-to\client.key --trust-anchor-arn {trust-anchor-arn} --profile-arn {rofile-arn} --role-arn {role-arn}
Finally, point your NestJS .env to the new profile:
PROFILE = my-dev-profile # local profile
PROFILE = roles-anywhere # role anywhere profile
Why is this better?
- Maximum Security: No permanent Access Keys are stored on the server. If client.pem is leaked, the attacker still needs client.key. You can also disable the Trust Anchor in AWS to instantly cut off access.
- Zero Cost: By using x509CertificateData in CDK, you don't need to pay for an AWS Private CA.
- Always Online: The credential_process in the AWS config file is automatically called by the SDK whenever keys expire, ensuring your application runs 24/7 without interruption.
Happy coding!
See more articles here.
Comments
Post a Comment