Minimize risk using the Principle of Least Privilege and AWS IAM permissions
Developer Advocate, CircleCI
Modern CI/CD pipelines require secure access to cloud resources, but traditional approaches using long-lived access keys create significant security risks. When service accounts are compromised, overly permissive credentials can lead to data breaches, unauthorized resource access, and substantial financial damage. The solution lies in implementing the Principle of Least Privilege combined with modern authentication methods.
The Principle of Least Privilege means granting only the minimum permissions necessary to perform a specific function. Instead of using static AWS access keys, this tutorial demonstrates how to implement secure, keyless authentication between CircleCI and AWS using OpenID Connect (OIDC). This approach eliminates the risks associated with storing long-lived credentials while maintaining precise control over resource access.
In this tutorial, you’ll build a secure CI/CD pipeline that deploys a Node.js application to AWS S3 using OIDC authentication. The pipeline will have minimal, targeted permissions to access only a specific folder in your S3 bucket, demonstrating practical implementation of least privilege principles.
Prerequisites
Before starting this tutorial, ensure you have:
- An AWS account with administrative access to create IAM roles and policies
- A CircleCI account (sign up for free)
- A GitHub account for hosting the demo project
- Basic familiarity with AWS IAM concepts (roles, policies, and permissions)
- Experience with CI/CD pipelines and YAML configuration files
- Understanding of OIDC authentication principles
Demo project setup
The demo project for this tutorial is a simple Node.js application that builds static assets and deploys them to AWS S3. You can find the complete project in this GitHub repository.
To follow along, fork the repository to your GitHub account. The project structure includes:
src/- Application source codedist/- Built assets (generated during CI).circleci/config.yml- CircleCI pipeline configurationpackage.json- Node.js dependencies and build scripts
If you prefer to create your own project, you’ll need a basic Node.js application with a build process that generates static files for deployment.
AWS S3 bucket setup
First, create an S3 bucket to host your deployed application. Follow these steps in the AWS Console:
- Navigate to the S3 Console
- Click Create bucket
- Choose a globally unique bucket name (e.g.,
my-circleci-app-deploy-bucket-12345) - Select your preferred AWS region
- Leave default settings for now and click Create bucket
For this tutorial, we’ll deploy builds to a builds/ folder within your bucket. This folder will be created automatically when the first deployment runs.
Note: Make a note of your bucket name and region. You will need these values later for the IAM policy and CircleCI configuration.
Configure AWS OIDC identity provider
Modern security best practices recommend using OpenID Connect (OIDC) instead of long-lived access keys. OIDC allows CircleCI to assume AWS roles temporarily without storing permanent credentials.
Set up the OIDC identity provider
- In the AWS Console, navigate to IAM > Identity providers
- Click Add provider
- Select OpenID Connect as the provider type
- Enter the following values:
- Provider URL:
https://oidc.circleci.com/org/YOUR_ORG_ID - Audience:
YOUR_ORG_ID
- Provider URL:
Find your CircleCI Organization ID:
- Go to your CircleCI Organization Settings.
- Copy the Organization ID from the Overview page
- Replace
YOUR_ORG_IDin the URLs with your actual organization ID. - Back in the AWS Console, click Get thumbprint to automatically retrieve the thumbprint. (This might not be needed).
- Click Add provider.
The OIDC provider is now configured and ready to authenticate CircleCI requests.
Create a least-privilege IAM policy
Now you’ll create a custom IAM policy that grants only the minimum permissions needed for deployment. This policy follows the Principle of Least Privilege by restricting access to specific S3 operations on a targeted resource path.
Create the custom policy
- In the AWS Console, navigate to IAM > Policies
- Click Create policy
- Switch to the JSON tab
- Replace the default policy with this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetBucketLocation", "s3:ListBucket"],
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME",
"Condition": {
"StringLike": {
"s3:prefix": "builds/*"
}
}
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/builds/*"
}
]
}
Note: Replace YOUR_BUCKET_NAME with your own S3 bucket name in both resource ARNs.
- Click Next: Tags (add tags if desired)
- Click Next: Review
- Enter policy details:
- Name:
CircleCI-S3-Builds-LeastPrivilege - Description:
Minimal permissions for CircleCI to deploy to S3 builds folder using OIDC
- Name:
- Click Create policy
Understanding the policy
This policy implements least privilege using:
- Bucket-level permissions allows listing and location queries only for objects with the
builds/prefix - Object-level permissionsgrants put, get, and delete access exclusively to objects in the
builds/path - No wildcard actions specifies exact actions instead of using
"Action": "*" - Conditional access
StringLikecondition further restricts bucket operations to the intended path
Create IAM role for CircleCI
Instead of creating IAM users with access keys, you’ll create an IAM role that CircleCI can assume using OIDC. This approach eliminates the need to store long-lived credentials.
Create the IAM role
- In the AWS Console, navigate to IAM > Roles
- Click Create role
- Select Web identity as the trusted entity type
- Configure the web identity:
- Identity provider: Select the OIDC provider you created earlier
- Audience: Enter your CircleCI organization ID
- Click Next
Configure the trust policy
The trust policy determines which CircleCI contexts can assume this role. You can restrict access to specific projects or contexts for additional security.
In the trust policy editor, use this template:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/oidc.circleci.com/org/YOUR_ORG_ID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.circleci.com/org/YOUR_ORG_ID:aud": "YOUR_ORG_ID"
},
"StringLike": {
"oidc.circleci.com/org/YOUR_ORG_ID:sub": "org/YOUR_ORG_ID/project/YOUR_PROJECT_ID/user/*"
}
}
}
]
}
Replace the placeholders with your own information:
YOUR_ACCOUNT_ID: Your AWS account ID (12-digit number)YOUR_ORG_ID: Your CircleCI organization IDYOUR_PROJECT_ID: Your CircleCI project ID (found in project settings)
- Click Next to attach permissions policies
- Search for and select
CircleCI-S3-Builds-LeastPrivilege(the policy you created earlier) - Click Next
- Enter role details:
- Role name:
CircleCI-S3-Deploy-Role - Description:
OIDC role for CircleCI S3 deployments with least privilege
- Role name:
- Click Create role
Note: Make a note of the role ARN from the role summary page. You’ll need this for the CircleCI configuration later on in the tutorial.
CircleCI pipeline configuration
The pipeline configuration demonstrates secure OIDC authentication with least-privilege AWS access. The demo project already includes this configuration in .circleci/config.yml:
version: 2.1
orbs:
aws-cli: circleci/aws-cli@5.1.1
workflows:
deploy-to-s3:
jobs:
- build-and-test
- deploy:
requires:
- build-and-test
filters:
branches:
only: main
jobs:
build-and-test:
docker:
- image: cimg/node:18.20
steps:
- checkout
- run:
name: Install dependencies
command: npm install
- run:
name: Run tests
command: npm test
- run:
name: Build application
command: npm run build
- persist_to_workspace:
root: .
paths:
- dist/
deploy:
docker:
- image: cimg/node:18.20
steps:
- attach_workspace:
at: .
- aws-cli/setup:
role_arn: $AWS_ROLE_ARN
region: $AWS_DEFAULT_REGION
role_session_name: "CircleCI-Deploy-Session"
- run:
name: Verify AWS CLI setup
command: aws sts get-caller-identity
- run:
name: Deploy to S3
command: |
aws s3 sync dist/ s3://$S3_BUCKET_NAME/builds/ --delete --exact-timestamps
echo "Deployment completed successfully to s3://$S3_BUCKET_NAME/builds/"
Configuration breakdown
OIDC Authentication:
- aws-cli/setup:
role_arn: $AWS_ROLE_ARN
region: $AWS_DEFAULT_REGION
role_session_name: "CircleCI-Deploy-Session"
This step uses CircleCI’s OIDC token to assume the AWS role without requiring stored credentials. The role ARN and region are provided through environment variables.
AWS CLI verification:
aws sts get-caller-identity
This command verifies that the OIDC authentication worked correctly by showing which role was assumed.
Least-privilege S3 deployment:
aws s3 sync dist/ s3://$S3_BUCKET_NAME/builds/ --delete --exact-timestamps
The deployment syncs built files to the builds/ folder in your S3 bucket, matching the restricted permissions in your IAM policy.
Workspace persistence:
- persist_to_workspace:
root: .
paths:
- dist/
Build artifacts are securely passed between jobs without exposing them to unauthorized access.
Push to GitHub and set up CircleCI project
Commit and push your code to GitHub:
git add .
git commit -m "Add secure OIDC deployment configuration"
git push origin main
Set up project in CircleCI
Go to CircleCI and log into your account. Go to the Projects section in the sidebar and find your repository; it should match the name you used when creating the GitHub repo.

Click Set Up Project and enter main as the branch name that contains your .circleci/config.yml file. Then click Set Up Project.

The pipeline will start running, but it will fail because the required environment variables haven’t been configured yet.

This is expected behavior and demonstrates the security of our setup.
Configure environment variables
Now you’ll add the environment variables needed for OIDC authentication. In your CircleCI project, click Project Settings in the top-right corner. Select Environment Variables from the sidebar. Click Add Environment Variable and add three variables:
AWS_ROLE_ARNshould be set to the ARN of the IAM role you created earlier (something likearn:aws:iam::123456789012:role/CircleCI-S3-Deploy-Role).AWS_DEFAULT_REGIONshould match your S3 bucket’s region (us-east-1).S3_BUCKET_NAMEshould be your own S3 bucket name.
These variables enable OIDC authentication without storing permanent AWS credentials. This is a significant security improvement over traditional access keys.

Test the pipeline
With the environment variables configured, return to your CircleCI project dashboard and click Rerun workflow from start. This time, watch the pipeline execute successfully. The Build job will install dependencies, run tests, and build the application. The Deploy job uses OIDC to assume the AWS role and deploys to the builds/ folder.

Verify deployment
To confirm everything worked, check your S3 bucket by going to the S3 Console. Open your bucket and go to the builds/ folder. There will be a timestamped directory containing your deployed files.

Conclusion
You’ve successfully implemented a secure CI/CD pipeline that follows the Principle of Least Privilege using OIDC authentication. This approach eliminates the security risks associated with traditional access keys while maintaining precise control over AWS resource access.
The key improvements over legacy approaches include:
- Enhanced security: No long-lived credentials to manage or compromise
- Better compliance: Easier auditing and access management
- Reduced maintenance: Automatic token lifecycle management
- Granular control: Precise permissions for specific resources and actions
By implementing these practices, you’ve significantly reduced your attack surface while building a foundation for secure, scalable CI/CD operations. These patterns can be extended to other AWS services and CI/CD use cases while maintaining the same security principles.