Chapter 09

安全实践:OIDC、权限与漏洞扫描

无密钥云部署、最小权限原则、代码安全扫描——构建安全可信的 CI/CD 流水线

GITHUB_TOKEN 最小权限

默认权限与安全限制

GITHUB_TOKEN 是 GitHub Actions 自动提供的临时令牌,用于与 GitHub API 交互。默认情况下,它具有仓库的读写权限——这比大多数 Job 需要的权限要宽泛。遵循最小权限原则,应在 Workflow 和 Job 级别精确声明所需权限。

# 在 Workflow 顶层声明默认权限(推荐设为只读)
permissions:
  contents: read   # 全局只读

jobs:
  read-only-job:
    runs-on: ubuntu-latest
    # 继承 workflow 级别的 contents: read
    steps:
      - uses: actions/checkout@v4

  deploy-job:
    runs-on: ubuntu-latest
    # 覆盖:这个 Job 需要额外权限
    permissions:
      contents: read
      packages: write    # 推送到 GHCR
      id-token: write    # OIDC 认证
    steps:
      - run: echo "有权限推送镜像"

  pr-comment-job:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write   # 在 PR 中写评论
      issues: write
    steps:
      - run: echo "有权限写 PR 评论"

权限参考表

权限名称read 允许write 允许
contents读取仓库内容推送代码、创建 Release
packages下载包发布包到 GHCR
pull-requests读取 PR 信息创建/修改 PR、添加评论
issues读取 Issue创建/修改 Issue
security-events读取安全警报上传 SARIF 扫描结果
id-token-获取 OIDC JWT(云部署)
checks读取 Check Run创建 Check Run

OIDC:无密钥云部署

为什么 OIDC 比 Access Key 更安全?

传统方式需要将云平台的长期访问密钥(AWS Access Key、GCP Service Account Key)存储为 GitHub Secrets。这些密钥一旦泄露,攻击者可以长期使用。OIDC(OpenID Connect)允许 GitHub Actions 通过临时令牌向云平台证明身份,无需存储任何长期密钥

# AWS OIDC 配置(需要先在 AWS IAM 中配置 OIDC Provider)
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # 必须!用于获取 OIDC 令牌
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: 配置 AWS 凭证(OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          # IAM Role ARN(在 AWS 中创建并信任 GitHub OIDC Provider)
          role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
          # 精确控制:只允许特定仓库和分支使用此 Role
          aws-region: us-east-1

      # 现在可以直接使用 AWS CLI,无需任何密钥
      - run: aws s3 ls

AWS IAM OIDC 配置(Terraform)

# 在 AWS 中创建 OIDC Provider
aws iam create-open-id-connect-provider \
  --url "https://token.actions.githubusercontent.com" \
  --client-id-list "sts.amazonaws.com" \
  --thumbprint-list "6938fd4d98bab03faadb97b34396831e3780aea1"

# 创建 IAM Role(限定只有特定仓库/分支可以 Assume)
# Trust Policy 中的条件:
# "token.actions.githubusercontent.com:sub":
#   "repo:your-org/your-repo:ref:refs/heads/main"

CodeQL 代码安全扫描

name: CodeQL Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 0 * * 1'   # 每周一凌晨扫描

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      actions: read
      contents: read

    strategy:
      matrix:
        language: [javascript, python, go]
        # 支持:cpp, csharp, go, java, javascript, python, ruby, swift

    steps:
      - uses: actions/checkout@v4

      - name: 初始化 CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: ${{ matrix.language }}
          # 使用安全扩展查询集
          queries: security-extended

      - name: 自动构建(CodeQL 需要)
        uses: github/codeql-action/autobuild@v3

      - name: 执行 CodeQL 分析
        uses: github/codeql-action/analyze@v3
        with:
          category: "/language:${{ matrix.language }}"

Trivy 容器漏洞扫描

jobs:
  trivy-scan:
    runs-on: ubuntu-latest
    permissions:
      security-events: write

    steps:
      - uses: actions/checkout@v4

      - name: 构建镜像
        run: docker build -t my-app:${{ github.sha }} .

      # 扫描容器镜像
      - name: Trivy 漏洞扫描
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: my-app:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          exit-code: 1        # 发现高危漏洞时 CI 失败

      # 上传扫描结果到 GitHub Security Tab
      - name: 上传 Trivy 结果
        uses: github/codeql-action/upload-sarif@v3
        if: always()   # 即使 trivy 扫描失败也上传结果
        with:
          sarif_file: trivy-results.sarif

  # 同时扫描代码依赖(SCA)
  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Trivy 文件系统扫描
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: .
          format: table
          severity: CRITICAL,HIGH

依赖安全:Dependabot 自动更新

# .github/dependabot.yml — 配置自动依赖更新
version: 2
updates:
  # npm 依赖更新
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
      day: monday
      time: "09:00"
    # 将安全更新与常规更新分组(减少 PR 数量)
    groups:
      production-dependencies:
        dependency-type: production
      development-dependencies:
        dependency-type: development
    # 忽略主版本更新(可能有破坏性变更)
    ignore:
      - dependency-name: "*"
        update-types: ["version-update:semver-major"]

  # GitHub Actions 版本更新(包括固定 SHA 的更新)
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
    # 自动合并补丁和小版本更新
    reviewers:
      - "platform-team"
供应链安全:固定 Action 版本到 SHA

不要使用 uses: actions/checkout@main@v4 标签(标签可以被恶意覆盖)。应固定到具体 commit SHA:uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683(即 v4.2.2 对应的 SHA)。配合 Dependabot 自动更新这些 SHA,既安全又省心。这是防止供应链攻击的关键实践。

本章小结

本章核心要点