npmサプライチェーン攻撃によるAWS権限悪用リスクに備え、CI/CDパイプラインを見直してみた
近年、npmなどのサプライチェーン攻撃は、公的機関が繰り返し注意喚起を行うレベルのリスクになっています。
- 2025年9月: Shai-Huludワーム — 500以上のnpmパッケージを侵害した自己複製型マルウェア
- 2026年3月: Axiosパッケージ侵害 — 北朝鮮国家アクターがメンテナーのnpm資格情報を乗っ取り、RATを配布
さらに、npmパッケージ侵害を起点にGitHub PATを窃取し、OIDC連携経由で72時間以内にAWS管理者権限に到達したとされる事例も報じられています。
UNC6426 Hackers Exploit NPM Package to Gain AWS Admin Access in 72 Hours
CodeBuildのビルド中に実行されるプロセスは、CodeBuildサービスロールの一時認証情報を利用できます。依存パッケージのインストール時に悪性コードが実行された場合、そのロールに許可されたAWS APIが攻撃面になります。このリスクに備え、CI/CDパイプラインのIAM権限を見直してみました。
結果として、既存構成を大きく変えず、IAMポリシーの差分を小さく保ったまま、被害が大きくなりやすい経路の是正やガードレール追加ができました。
パイプライン構成とアセスメント
対象のパイプラインはCodePipelineで、GitHubへのpushをトリガーに起動します。
Source ──→ Build ──→ SecurityCheck ──→ Deploy ──→ PostDeployTest
(GitHub) (ARM64) (ECRスキャン) (CFn) (E2Eテスト)
| ステージ | 実行環境 | IAM権限 |
|---|---|---|
| Build | CodeBuild ARM_CONTAINER | ECR push + SSM + PowerUserAccess |
| SecurityCheck | Step Functions (EXPRESS) | ECRスキャン結果参照のみ |
| Deploy | CodeBuild | CFn更新 + ECS + IAM(限定的) |
| PostDeployTest | Lambda (VPC内) | CodePipeline結果通知のみ |
ビルドとデプロイでロールは分離されており、通常のパイプライン実行上はBuildステージとDeployステージの責務は分かれていました。一方で、BuildロールにPowerUserAccessが残っていたこと、またAssumeRole可能なロールが存在する場合は横展開の余地があることから、Buildロール側の権限見直しが必要でした。
既に守れていた点
- ビルドとデプロイのロール分離
iam:PassRoleのResource限定(特定プレフィックスのみ)- CloudFormation操作が特定スタック限定
- ECRイメージスキャン(既知脆弱性の検出用途)
- デプロイステージはCodeBuildプロジェクト側で定義したbuildspecでCFnパラメータ渡しを行う構成(リポジトリ内buildspecには非依存、npm installやdocker buildのような依存パッケージ実行もなし)
足りなかった点
- 永続化・証跡破壊につながる操作へのDenyポリシーなし
- ビルド用ロールの
sts:AssumeRole制限なし - BuildロールにPowerUserAccessが残存(パイプライン化前の一体構成時代からの引き継ぎ。ステージ分離後は不要だった)
対応1: 高リスク操作のDeny(初期対応)
パイプラインを壊さないよう、正規操作に影響する可能性が低い改善から実施しました。
ビルド用ロールにsts:AssumeRole Deny
当アカウントではBuildロールにPowerUserAccessマネージドポリシーを利用していました。PowerUserAccessはIAMやOrganizations、Account系などの一部を除き、多くのAWSサービス操作を許可する強い権限です。同一アカウント内にBuildロールからAssumeRole可能なロール(CDK関連など)が存在していたため、Buildロール(CodeBuildサービスロール)にsts:AssumeRoleの明示Denyを追加し、ビルドコンテナ内から別ロールへの横展開リスクを下げました。
なお、今回のDenyはCodeBuildサービスロールに対する対策です。CodePipelineロール、CloudFormation実行ロール、ECSタスクロールなど、他の実行主体の権限を直接制限するものではありません。
buildspecを調査し、BuildステージではAssumeRoleを使っていないことを確認しました。
ビルドで行っている処理(buildspecより):
1. ECR Public からベースイメージpull → public、認証不要
2. docker build → ローカル処理
3. 同一アカウントのECRにpush → ecr:GetAuthorizationToken(追加のAssumeRole不要)
4. SSM PutParameter → 同一アカウント直接呼び出し(追加のAssumeRole不要)
5. S3 sync → 同一アカウント直接呼び出し(追加のAssumeRole不要)
全て同一アカウント内で完結しており、クロスアカウント操作がないため、sts:AssumeRoleをDenyしても正規ビルドには影響しないと判断しました。
予防措置としてのDeny
sts:AssumeRoleに加えて、正規用途に不要であることが明確で、侵害時の被害が大きい操作もまとめてDenyしました。DenyはAllowに優先されるため、将来のAllow追加に備えた保険にもなります。
なお、PowerUserAccessにはiam:*は含まれないため、iam:CreateAccessKey等のDenyは現時点では実効性はありません。ただし将来ポリシーが変更された場合や、別のAllow追加があった場合の多層防御として追加しています。
- 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: '*'
狭めた攻撃経路
- 悪性npmパッケージ実行→Build環境突破→別ロールへ横展開 →
sts:AssumeRoleを明示Deny ✅ - Build環境からのアクセスキー作成→永続化 → 現時点ではIAM権限がないため不可。将来のAllow追加に備えて
iam:CreateAccessKey等を明示Deny ✅ - 証跡破壊→検知回避 → CloudTrail/GuardDutyの主要な停止・削除・無効化操作をDeny ✅
- Build環境突破時のAWSリソース広範囲アクセス → PowerUserAccess撤去により攻撃面を大幅縮小 ✅(次節で実施)
検討したDenyアクションと採用判断
以下はBuildロールに対して検討したDenyアクションです。
| アクション | 目的 | 判断 | 理由 |
|---|---|---|---|
sts:AssumeRole |
AssumeRoleによる別ロールへの横展開防止 | ✅ 採用 | buildspecで未使用を確認済み |
iam:CreateAccessKey |
永続化 | ✅ 採用 | CI/CDでアクセスキー発行する正規用途はない |
iam:CreateUser |
永続化 | ✅ 採用 | 同上 |
cloudtrail:StopLogging / guardduty:DeleteDetector 等 |
証跡破壊 | ✅ 採用 | 攻撃の検知・追跡が妨害されることを防止 |
iam:AttachRolePolicy(Admin級) |
権限昇格 | ⏳ 今後 | 条件付きDenyの設計確認が必要 |
iam:CreatePolicyVersion |
権限昇格 | ⏳ 今後 | SetDefaultPolicyVersionも含め検討が必要 |
iam:PassRole |
権限委任 | ❌ 不採用 | Buildロールでの正規利用と他ステージへの影響確認が必要なため、今回は対象外 |
対応2: PowerUserAccessの撤去(権限縮小)
初期対応でDenyによるガードレールを設置した後、そもそもPowerUserAccess自体が必要なのかを見直しました。
背景
BuildロールのPowerUserAccessは、1つのbuildspecでビルドからデプロイまで一気に実行していた初期構成の名残でした。現在はステージ分離によりBuildロールにDeploy権限は不要ですが、PowerUserAccessだけがそのまま残り続けていました。
調査手順
- buildspecで処理内容を確認 — ECR push、SSM Parameter Store、S3 sync(
_next/static配信用)が実行されていることを特定。S3のアクセス先はbuildspec内のaws s3 syncコマンドに明示されていたため、バケット名・パスを直接確認できました - CodeBuildのビルド履歴から実績のある日時を特定 — CodeBuild APIで直近のビルド実行日時を取得
- IAM Access Advisorでサービス単位の利用状況を確認 — ECRの利用を確認。Access Advisorはサービス単位の補助情報であり、APIアクション単位・リソース単位の判断には向かないため、S3依存はこれだけでは判断しませんでした
- CloudTrailでビルドセッション名ベースの管理イベントを確認 — ECR(push系)、SSM(Get/PutParameter)、CloudWatch Logs(CreateLogStream)を特定。S3オブジェクト操作はデータイベントを有効化していないと確認できないため、buildspecを併用しました
CloudTrail・Access Advisorではbuildspecで確認済みのS3アクセスを確認できませんでしたが、buildspec上でアクセス先が明確だったため、S3権限を明示追加する判断ができました。
是正内容
CodeBuildServiceRoleに対して以下を実施しました。ECR push、SSM Parameter Store、CloudWatch Logsに必要な権限はCodeBuildBasePolicyで既に許可していたため、PowerUserAccess撤去に伴う追加差分はS3権限のみでした。
削除:
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/PowerUserAccess' # 削除
追加(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/*'
検証結果
CloudFormationでスタック更新後、パイプラインを手動実行しました。
- ビルド(docker build + ECR push): ✅ 成功
- SSM PutParameter: ✅ 成功
- S3 sync(
_next/staticアップロード、約13MiB/280ファイル): ✅ 成功 - デプロイ(後続ステージ): ✅ 成功
IAMを見直したビルドステージを含め、全ステージの正常動作を確認できました。
まとめ
今回はビルドとデプロイのロール分離が既にされていた構成のおかげで、Buildロールに高リスク操作のDenyを追加するだけで、主要な横展開・永続化・証跡破壊経路にガードレールを追加できました。さらにPowerUserAccessを撤去し、確認できた正規処理に必要な権限へ縮小できました。Denyによる防御とPowerUserAccess撤去による権限縮小を、既存パイプラインを壊さず実施できた事例です。
一方で、ECSタスクロール経由のアクセスや、正規デプロイ経路を悪用した不正コンテナの配置、CloudFormation実行ロール経由のIAM変更といった経路は残っており、今後の課題として検討していきたいと思います。今回の対応は侵害されたビルド環境からの被害範囲を小さくするための対策であり、依存パッケージ自体の検証やlockfile運用、ビルド時のネットワーク制御などを置き換えるものではありません。
なお、本記事はCodePipeline + CodeBuildの構成を対象としていますが、GitHub Actions環境でもOIDCロールのsub条件厳格化やステージごとの専用ロール付与など、同様のアプローチは応用できます。まずはbuildspecやCloudTrailで影響範囲を確認した上で、高リスク操作のDenyによる防御から検討してみてください。







