TutorialsLast Updated Oct 29, 20258 min read

Minimize risk using the Principle of Least Privilege and AWS IAM permissions

Angel Rivera

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 code
  • dist/ - Built assets (generated during CI)
  • .circleci/config.yml - CircleCI pipeline configuration
  • package.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

  1. In the AWS Console, navigate to IAM > Identity providers
  2. Click Add provider
  3. Select OpenID Connect as the provider type
  4. Enter the following values:
    • Provider URL: https://oidc.circleci.com/org/YOUR_ORG_ID
    • Audience: YOUR_ORG_ID

Find your CircleCI Organization ID:

  1. Go to your CircleCI Organization Settings.
  2. Copy the Organization ID from the Overview page
  3. Replace YOUR_ORG_ID in the URLs with your actual organization ID.
  4. Back in the AWS Console, click Get thumbprint to automatically retrieve the thumbprint. (This might not be needed).
  5. 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

  1. In the AWS Console, navigate to IAM > Policies
  2. Click Create policy
  3. Switch to the JSON tab
  4. 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.

  1. Click Next: Tags (add tags if desired)
  2. Click Next: Review
  3. Enter policy details:
    • Name: CircleCI-S3-Builds-LeastPrivilege
    • Description: Minimal permissions for CircleCI to deploy to S3 builds folder using OIDC
  4. 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 StringLike condition 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

  1. In the AWS Console, navigate to IAM > Roles
  2. Click Create role
  3. Select Web identity as the trusted entity type
  4. Configure the web identity:
    • Identity provider: Select the OIDC provider you created earlier
    • Audience: Enter your CircleCI organization ID
  5. 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 ID
  • YOUR_PROJECT_ID: Your CircleCI project ID (found in project settings)
  1. Click Next to attach permissions policies
  2. Search for and select CircleCI-S3-Builds-LeastPrivilege (the policy you created earlier)
  3. Click Next
  4. Enter role details:
    • Role name: CircleCI-S3-Deploy-Role
    • Description: OIDC role for CircleCI S3 deployments with least privilege
  5. 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.

Search for your project in CircleCI Projects

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

Select project branch

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

Failed pipeline

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_ARN should be set to the ARN of the IAM role you created earlier (something like arn:aws:iam::123456789012:role/CircleCI-S3-Deploy-Role).
  • AWS_DEFAULT_REGION should match your S3 bucket’s region (us-east-1).
  • S3_BUCKET_NAME should 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.

Environment variables

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.

Deployment success

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.

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.