Fixing AWS ExpiredTokenException in CI/CD Pipelines

intermediateโ˜๏ธ AWS2026-07-11| Linux/macOS, AWS CLI v2, Boto3 (Python), GitHub Actions, GitLab CI/CD, Jenkins

Error Message

An error occurred (ExpiredTokenException) when calling the ListBuckets operation: The security token included in the request is expired
#aws#sts#iam#boto3#devops#ci-cd

The ErrorYour CI/CD pipeline is running smoothly until, suddenly, it isn't. Everything halts, and your logs spit out a frustrating message:

An error occurred (ExpiredTokenException) when calling the ListBuckets operation: The security token included in the request is expired

This usually happens right in the middle of a long Terraform apply or a large S3 file upload. It means the temporary keys you generated have reached their end of life.

Why This HappensWhen you call sts assume-role, AWS hands you a set of temporary credentials. These aren't permanent like an IAM User's secret key. They consist of an Access Key ID, a Secret Access Key, and a Session Token that eventually dies.

By default, these tokens expire in exactly 1 hour (3600 seconds). If your deployment script takes 61 minutes, the final 10% of your tasks will fail. This timeout also triggers if you're caching environment variables from an old build step instead of generating fresh ones for the current job.

Step-by-Step Fixes### 1. Extend the Session DurationYou can request a longer lifespan when assuming the role. AWS allows sessions to last up to 12 hours (43,200 seconds). However, you must update two places for this to work.

Update your AWS CLI command:

# Request a 4-hour session (14400 seconds)
CREDENTIALS=$(aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/MyCiCdRole \
  --role-session-name "CiCdSession" \
  --duration-seconds 14400)

Update the IAM Role: Even if you ask for 12 hours, AWS will cap you at 1 hour unless the role itself allows more. Navigate to IAM > Roles > [Your Role Name]. In the Settings tab, find "Maximum CLI/API session duration." Click Edit and set it to 4 hours or more.

2. Use Refreshable Credentials in Python (Boto3)Standard Boto3 sessions don't automatically renew temporary tokens. If you have a Python script processing thousands of images over several hours, it will crash once the initial token expires. You can solve this by using botocore's RefreshableCredentials.

import boto3
from botocore.credentials import RefreshableCredentials
from botocore.session import get_session

def get_refreshable_session():
    sts_client = boto3.client('sts')
    
    def refresh():
        # This function runs automatically before the token expires
        params = {
            "RoleArn": "arn:aws:iam::123456789012:role/MyRole",
            "RoleSessionName": "refreshable-session",
            "DurationSeconds": 3600
        }
        res = sts_client.assume_role(**params)
        creds = res['Credentials']
        return {
            "access_key": creds['AccessKeyId'],
            "secret_key": creds['SecretAccessKey'],
            "token": creds['SessionToken'],
            "expiry_time": creds['Expiration'].isoformat()
        }

    # advisory_refresh_timeout: refresh 10 mins before actual expiry
    session_creds = RefreshableCredentials.create_from_metadata_generator(
        metadata_generator=refresh,
        refresh_timeout=300,
        advisory_refresh_timeout=600
    )
    
    bc_session = get_session()
    bc_session._credentials = session_creds
    return boto3.Session(botocore_session=bc_session)

3. Modernize Your GitHub Actions WorkflowStop manually parsing JSON from the CLI to export AWS_SESSION_TOKEN. It's brittle and hard to maintain. Instead, use OpenID Connect (OIDC) with the official AWS action. This method handles the token exchange securely and allows you to set the duration easily.

- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/MyRole
    aws-region: us-east-1
    role-duration-seconds: 7200 # 2-hour window

How to Verify Your SessionTo see if your credentials are active, run aws sts get-caller-identity. However, this won't show you the expiration time. To check when your session actually dies, look at the Expiration field in the JSON response when you first ran assume-role, or check your active configuration:

aws configure list

Pro-Tips for Stable Pipelines

  • Mind the Role Limit: If you request --duration-seconds 36000 but the IAM Role is set to 3600, the command will fail immediately with an error.
  • Ditch IAM User Keys: Use OIDC for GitHub or GitLab. It eliminates the need for long-lived secrets and manages session windows more gracefully.
  • Optimize, Don't Just Extend: If a job takes 3 hours, check for bottlenecks. Sometimes a 10-minute code optimization is better than a 10-hour token.
  • Stay Secure: When configuring trust policies for these roles, use a Password Generator to create a unique External ID for added security against confused deputy attacks.

Related Error Notes