# CI/CD Workflows Security Checklist

Security checks for GitHub Actions deployment pipelines. Extracted from the
Gemma server security research report (§07 CI/CD Deployment via GitHub Actions).

---

## Check: GitHub Actions pinned to SHA digest

- **Priority**: Important
- **What to look for**: Any `uses:` line in `.github/workflows/*.yml` that references a tag (e.g. `@v4`, `@main`) instead of a full SHA digest.
- **Pass condition**: Every third-party action is pinned to a full 40-character SHA commit digest with a comment noting the version, e.g. `actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7`.
- **Fail example**:
  ```yaml
  - uses: actions/checkout@v4
  - uses: docker/login-action@master
  ```
- **Remediation**: Replace tag references with the SHA digest of the tagged commit. Use `pin-github-action` or `Ratchet` to automate. Add a comment with the human-readable version.
  ```yaml
  # Before
  - uses: actions/checkout@v4

  # After
  - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
  ```
- **Source**: Security report §07

---

## Check: Environment protection rules with required reviewers

- **Priority**: Important
- **What to look for**: Workflow jobs that deploy to production do not reference a GitHub Environment, or the `production` environment has no required reviewers configured in GitHub repository settings.
- **Pass condition**: Deploy jobs reference `environment: production`, and the GitHub `production` environment is configured with at least one required reviewer and the "prevent self-review" option enabled.
- **Fail example**:
  ```yaml
  deploy:
    runs-on: ubuntu-latest
    # No environment: key — deploys without any gate
    steps:
      - run: ssh deploy@${{ secrets.VPS_HOST }} 'docker compose up -d'
  ```
- **Remediation**: Add `environment: production` to the deploy job, then configure the environment in GitHub → Settings → Environments → production → Required reviewers.
  ```yaml
  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: production   # Triggers protection rules
    steps:
      - run: ssh deploy@${{ secrets.VPS_HOST }} 'docker compose up -d'
  ```
- **Source**: Security report §07

---

## Check: Hardcoded secrets in workflow files

- **Priority**: Critical
- **What to look for**: Any literal credential, token, password, private key, or IP address written directly in a `.github/workflows/*.yml` file rather than being read from `${{ secrets.* }}`.
- **Pass condition**: All sensitive values are stored in GitHub Actions secrets and referenced as `${{ secrets.SECRET_NAME }}`. No plaintext credentials appear in workflow files.
- **Fail example**:
  ```yaml
  - name: Deploy
    env:
      DB_PASSWORD: "mysecretpassword123"
      SSH_KEY: "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC..."
    run: ssh deploy@192.168.1.100 'docker compose up -d'
  ```
- **Remediation**: Move all secrets to GitHub Actions repository or environment secrets. Reference them via `${{ secrets.* }}`. Never commit key material to the repository.
  ```yaml
  # Before
  env:
    DB_PASSWORD: "mysecretpassword123"

  # After
  env:
    DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
  ```
- **Source**: Security report §07

---

## Check: Deploy user has minimal permissions with ForceCommand

- **Priority**: Important
- **What to look for**: The SSH deploy key used by CI/CD grants unrestricted shell access, or the deploy user has broad sudo privileges. Look for the `authorized_keys` entry for the `deploy` user on the server — it should contain `command="..."` restrictions.
- **Pass condition**: The deploy user's `authorized_keys` entry uses `ForceCommand` to restrict execution to a single deploy script. The user has no `sudo` access or only tightly scoped `sudo` for specific commands like `docker compose`.
- **Fail example**:
  ```
  # /home/deploy/.ssh/authorized_keys — unrestricted
  ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... ci-deploy-key
  ```
- **Remediation**: Restrict the deploy key with `command=` and forwarding disabled in `authorized_keys`.
  ```
  # /home/deploy/.ssh/authorized_keys — restricted
  command="/home/deploy/deploy.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... ci-deploy-key
  ```
- **Source**: Security report §07

---

## Check: Dependabot configured for GitHub Actions

- **Priority**: Recommended
- **What to look for**: Absence of a `.github/dependabot.yml` file, or the file exists but does not include a `github-actions` package ecosystem entry.
- **Pass condition**: `.github/dependabot.yml` exists and includes an entry for the `github-actions` ecosystem so that SHA-pinned action updates are tracked automatically.
- **Fail example**:
  ```yaml
  # .github/dependabot.yml — missing github-actions entry
  version: 2
  updates:
    - package-ecosystem: "pip"
      directory: "/"
      schedule:
        interval: "weekly"
  ```
- **Remediation**: Add a `github-actions` entry to `dependabot.yml`.
  ```yaml
  version: 2
  updates:
    - package-ecosystem: "github-actions"
      directory: "/"
      schedule:
        interval: "weekly"
  ```
- **Source**: Security report §07

---

## Check: GHCR used for private Docker images

- **Priority**: Recommended
- **What to look for**: Workflow files that push images to Docker Hub (docker.io) or another public registry, or that pull images without authentication — exposing private application code in a public registry.
- **Pass condition**: Images are pushed to and pulled from `ghcr.io/${{ github.repository }}` using `${{ secrets.GITHUB_TOKEN }}` for authentication. The GHCR package is set to private in GitHub package settings.
- **Fail example**:
  ```yaml
  - uses: docker/login-action@...
    with:
      username: ${{ secrets.DOCKERHUB_USERNAME }}
      password: ${{ secrets.DOCKERHUB_TOKEN }}
  - run: docker push myorg/myapp:latest
  ```
- **Remediation**: Switch to GHCR with `GITHUB_TOKEN` — no additional secrets required.
  ```yaml
  - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
    with:
      registry: ghcr.io
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

  - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
    with:
      push: true
      tags: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
  ```
- **Source**: Security report §07

---

## Check: Build provenance attestation

- **Priority**: Nice-to-have
- **What to look for**: No `actions/attest-build-provenance` step in the build job. Without provenance, it is impossible to verify that a given container image was built from a specific commit by the official CI pipeline.
- **Pass condition**: The build job uses `actions/attest-build-provenance` (or SLSA generator) to publish a signed provenance attestation to the registry alongside the image.
- **Fail example**:
  ```yaml
  # build job has no attestation step
  - uses: docker/build-push-action@...
    id: push
    with:
      push: true
      tags: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
  # no attest step
  ```
- **Remediation**: Add the attestation step after `build-push-action` and grant `attestations: write` and `id-token: write` permissions.
  ```yaml
  permissions:
    attestations: write
    id-token: write

  - uses: actions/attest-build-provenance@v3
    with:
      subject-name: ghcr.io/${{ github.repository }}
      subject-digest: ${{ steps.push.outputs.digest }}
      push-to-registry: true
  ```
- **Source**: Security report §07

---

## Check: CODEOWNERS file for sensitive paths

- **Priority**: Recommended
- **What to look for**: No `.github/CODEOWNERS` file, or the file does not cover workflow files, infrastructure configs, and secrets-adjacent files. Without CODEOWNERS, any contributor can modify CI/CD pipelines without a designated review.
- **Pass condition**: A `.github/CODEOWNERS` file exists and assigns owners to `.github/workflows/`, deployment scripts, `docker-compose*.yml`, and any files containing secrets references.
- **Fail example**:
  ```
  # No .github/CODEOWNERS file exists
  ```
- **Remediation**: Create `.github/CODEOWNERS` and cover sensitive paths. Pair with branch protection rules requiring CODEOWNERS approval.
  ```
  # .github/CODEOWNERS
  .github/workflows/   @org/platform-team
  docker-compose*.yml  @org/platform-team
  Dockerfile*          @org/platform-team
  ```
- **Source**: Security report §07
