Reviewing Our CI/CD Pipeline to Prepare for the Risk of AWS Privilege Abuse Through npm Supply Chain Attacks

Reviewing Our CI/CD Pipeline to Prepare for the Risk of AWS Privilege Abuse Through npm Supply Chain Attacks

To guard against AWS permission abuse from npm supply chain attacks, I added explicit Denies for sts:AssumeRole and evidence-tampering actions to the CodeBuild build role, removed PowerUserAccess, and narrowed it to permissions confirmed via CloudTrail and buildspec.
2026.05.16

This page has been translated by machine translation. View original

In recent years, supply chain attacks such as those targeting npm have become a level of risk that public agencies repeatedly issue warnings about.

  • September 2025: Shai-Hulud worm — a self-replicating malware that compromised more than 500 npm packages
  • March 2026: Axios package compromise — a North Korean state actor hijacked a maintainer's npm credentials and distributed a RAT

In addition, there have been reports of a case in which a GitHub PAT was stolen starting from an npm package compromise, and AWS administrator privileges were reportedly reached within 72 hours via OIDC federation.

UNC6426 Hackers Exploit NPM Package to Gain AWS Admin Access in 72 Hours

Processes executed during a CodeBuild build can use the temporary credentials of the CodeBuild service role. If malicious code is executed while installing dependency packages, the AWS APIs permitted to that role become the attack surface. To prepare for this risk, I reviewed the IAM permissions of our CI/CD pipeline.

As a result, without significantly changing the existing configuration and while keeping the IAM policy diff small, I was able to fix paths where damage is likely to grow and add guardrails.

Pipeline Configuration and Assessment

The target pipeline is a CodePipeline that starts with a push to GitHub as the trigger.

Source ──→ Build ──→ SecurityCheck ──→ Deploy ──→ PostDeployTest
(GitHub)   (ARM64)   (ECRスキャン)    (CFn)      (E2Eテスト)
Stage Execution environment IAM permissions
Build CodeBuild ARM_CONTAINER ECR push + SSM + PowerUserAccess
SecurityCheck Step Functions (EXPRESS) ECR scan result read-only
Deploy CodeBuild CFn update + ECS + IAM (limited)
PostDeployTest Lambda (in VPC) CodePipeline result notification only

Roles were separated between build and deploy, and during normal pipeline execution the responsibilities of the Build stage and Deploy stage were distinct. However, because PowerUserAccess remained on the Build role, and because there is room for lateral movement if an assumable role exists, the permissions of the Build role needed to be reviewed.

What Was Already Protected

  • Role separation between build and deploy
  • iam:PassRole restricted by Resource (specific prefixes only)
  • CloudFormation operations limited to specific stacks
  • ECR image scanning (for detecting known vulnerabilities)
  • The deploy stage passes CFn parameters using a buildspec defined on the CodeBuild project side (not dependent on the buildspec in the repository, and with no execution of dependency packages such as npm install or docker build)

What Was Lacking

  • No Deny policy for operations that lead to persistence or evidence destruction
  • No sts:AssumeRole restriction on the build role
  • PowerUserAccess remained on the Build role (inherited from the monolithic setup before the pipeline was introduced; it became unnecessary after the stages were separated)

Action 1: Deny High-Risk Operations (Initial Response)

To avoid breaking the pipeline, I started with improvements that were unlikely to affect legitimate operations.

Deny sts:AssumeRole on the Build Role

In this account, the PowerUserAccess managed policy was used for the Build role. PowerUserAccess is a strong permission that allows operations on many AWS services, except for some such as IAM, Organizations, and Account. Because roles that the Build role could assume (such as CDK-related ones) existed in the same account, I added an explicit Deny of sts:AssumeRole to the Build role (the CodeBuild service role) to reduce the risk of lateral movement from inside the build container to other roles.

Note that this Deny is a measure for the CodeBuild service role. It does not directly restrict the permissions of other principals such as the CodePipeline role, the CloudFormation execution role, or the ECS task role.

I investigated the buildspec and confirmed that AssumeRole is not used in the Build stage.

ビルドで行っている処理(buildspecより):
1. ECR Public からベースイメージpull → public、認証不要
2. docker build → ローカル処理
3. 同一アカウントのECRにpush → ecr:GetAuthorizationToken(追加のAssumeRole不要)
4. SSM PutParameter → 同一アカウント直接呼び出し(追加のAssumeRole不要)
5. S3 sync → 同一アカウント直接呼び出し(追加のAssumeRole不要)

Everything is completed within the same account and there are no cross-account operations, so I determined that denying sts:AssumeRole would not affect legitimate builds.

Deny as a Preventive Measure

In addition to sts:AssumeRole, I also denied, all together, operations that are clearly unnecessary for legitimate use and would cause great damage if compromised. Because Deny takes precedence over Allow, this also serves as insurance against future Allow additions.

Note that PowerUserAccess does not include iam:*, so Deny of iam:CreateAccessKey and the like is not effective at present. However, I added it as defense in depth in case the policy is changed in the future or another Allow is added.

- PolicyName: DenyHighRiskActionsForBuild
  PolicyDocument:
    Version: '2012-10-17'
    Statement:
      - Sid: DenyAssumeRole
        Effect: Deny
        Action:
          - sts:AssumeRole
        Resource: '*'
      - Sid: DenyCredentialCreation
        Effect: Deny
        Action:
          - iam:CreateAccessKey
          - iam:CreateUser
          - iam:CreateLoginProfile
        Resource: '*'
      - Sid: DenyTrailAndDetectionTampering
        Effect: Deny
        Action:
          - cloudtrail:StopLogging
          - cloudtrail:DeleteTrail
          - cloudtrail:UpdateTrail
          - guardduty:DeleteDetector
          - guardduty:UpdateDetector
        Resource: '*'

Attack Paths Narrowed

  • Malicious npm package execution → Build environment breakout → lateral movement to another role → explicit Deny of sts:AssumeRole ✅
  • Access key creation from the Build environment → persistence → not possible at present since there are no IAM permissions. Explicitly denied iam:CreateAccessKey and the like in preparation for future Allow additions ✅
  • Evidence destruction → detection evasion → denied the main stop, delete, and disable operations for CloudTrail/GuardDuty ✅
  • Broad access to AWS resources upon Build environment breakout → attack surface greatly reduced by removing PowerUserAccess ✅ (done in the next section)

Deny Actions Considered and Adoption Decisions

The following are the Deny actions I considered for the Build role.

Action Purpose Decision Reason
sts:AssumeRole Prevent lateral movement to other roles via AssumeRole ✅ Adopted Confirmed unused in the buildspec
iam:CreateAccessKey Persistence ✅ Adopted There is no legitimate use for issuing access keys in CI/CD
iam:CreateUser Persistence ✅ Adopted Same as above
cloudtrail:StopLogging / guardduty:DeleteDetector, etc. Evidence destruction ✅ Adopted Prevents detection and tracing of attacks from being obstructed
iam:AttachRolePolicy (Admin-level) Privilege escalation ⏳ Future Need to confirm the design of a conditional Deny
iam:CreatePolicyVersion Privilege escalation ⏳ Future Needs consideration including SetDefaultPolicyVersion
iam:PassRole Permission delegation ❌ Not adopted Excluded this time because legitimate use by the Build role and impact on other stages need to be confirmed

Action 2: Remove PowerUserAccess (Permission Reduction)

After setting up the Deny guardrails in the initial response, I reviewed whether PowerUserAccess itself was necessary in the first place.

Background

PowerUserAccess on the Build role was a remnant of the initial configuration, in which everything from build to deploy was executed in one go with a single buildspec. Now that the stages are separated, the Build role does not need Deploy permissions, but PowerUserAccess alone had remained as it was.

Investigation Steps

  1. Check the processing in the buildspec — Identified that ECR push, SSM Parameter Store, and S3 sync (for serving _next/static) are executed. Because the S3 access destination was explicitly specified in the aws s3 sync command in the buildspec, I could directly confirm the bucket name and path
  2. Identify dates and times with actual usage from CodeBuild build history — Retrieved recent build execution dates and times via the CodeBuild API
  3. Check usage per service with IAM Access Advisor — Confirmed use of ECR. Access Advisor is supplementary information per service and is not suited for decisions at the API action or resource level, so I did not judge S3 dependency from this alone
  4. Check management events based on the build session name in CloudTrail — Identified ECR (push-related), SSM (Get/PutParameter), and CloudWatch Logs (CreateLogStream). S3 object operations cannot be confirmed unless data events are enabled, so I used the buildspec in combination

CloudTrail and Access Advisor could not confirm the S3 access that had been verified in the buildspec, but since the access destination was clear in the buildspec, I was able to decide to explicitly add S3 permissions.

Remediation

I made the following changes to CodeBuildServiceRole. The permissions required for ECR push, SSM Parameter Store, and CloudWatch Logs were already allowed in CodeBuildBasePolicy, so the only additional diff accompanying the removal of PowerUserAccess was the S3 permissions.

Removed:

ManagedPolicyArns:
  - 'arn:aws:iam::aws:policy/PowerUserAccess'  # 削除

Added (appended to CodeBuildBasePolicy):

- Effect: Allow
  Action:
    - 's3:PutObject'
    - 's3:GetObject'
    - 's3:ListBucket'
    - 's3:DeleteObject'
  Resource:
    - 'arn:aws:s3:::my-static-assets-bucket'
    - 'arn:aws:s3:::my-static-assets-bucket/_next/static/*'

Verification Results

After updating the stack with CloudFormation, I ran the pipeline manually.

  • Build (docker build + ECR push): ✅ Success
  • SSM PutParameter: ✅ Success
  • S3 sync (_next/static upload, about 13MiB/280 files): ✅ Success
  • Deploy (subsequent stages): ✅ Success

I confirmed that all stages, including the build stage whose IAM I reviewed, operate normally.

Summary

Thanks to the configuration in which build and deploy roles were already separated, simply adding a Deny of high-risk operations to the Build role let me add guardrails to the main lateral movement, persistence, and evidence destruction paths. Furthermore, I removed PowerUserAccess and reduced the permissions to those needed for the legitimate processing I could confirm. This is a case where defense through Deny and permission reduction through the removal of PowerUserAccess were carried out without breaking the existing pipeline.

On the other hand, paths such as access via the ECS task role, placement of malicious containers by abusing the legitimate deploy path, and IAM changes via the CloudFormation execution role remain, and I would like to consider them as future work. The measures this time are intended to reduce the blast radius from a compromised build environment, and they do not replace verification of the dependency packages themselves, lockfile operations, or network control during builds.

This article targets a CodePipeline + CodeBuild configuration, but similar approaches can also be applied in GitHub Actions environments, such as tightening the sub condition of OIDC roles and assigning dedicated roles per stage. First, check the scope of impact with the buildspec and CloudTrail, and then consider starting with defense through Deny of high-risk operations.

Share this article

AWSのお困り事はクラスメソッドへ