mirror of
https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git
synced 2026-11-03 01:13:24 +00:00
fix(ci): harden workflow token scope and download integrity (#39)
Security audit of .github/workflows found a script-injection vector in the release workflow, over-broad token grants on non-publishing jobs, and downloaded tooling/manifests executed without integrity checks. - release: pass the tag/dispatch input through env instead of interpolating it into the validation script; default workflow permissions to contents:read - build-main/build-gateway: grant write scopes only to the build job - _build-image/codeql: stop persisting checkout credentials - ci/release: verify actionlint, hadolint and git-cliff against pinned SHA-256s - ci-manifests/e2e: verify upstream CRD manifests against versions.env pins - e2e: redact token material before uploading cluster diagnostics - docs: add workflow-security.md with controls, audit record and residual repository-setting recommendations Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
This commit is contained in:
committed by
GitHub
parent
4c073eef71
commit
2f959e6afb
2
.github/workflows/_build-image.yaml
vendored
2
.github/workflows/_build-image.yaml
vendored
@@ -53,6 +53,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare environment
|
||||
env:
|
||||
|
||||
4
.github/workflows/build-gateway.yaml
vendored
4
.github/workflows/build-gateway.yaml
vendored
@@ -12,11 +12,9 @@ on:
|
||||
- .github/workflows/build-gateway.yaml
|
||||
- .github/workflows/_build-image.yaml
|
||||
|
||||
# Write scopes are granted to the build job only; every other job stays read-only.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
concurrency:
|
||||
group: build-gateway-${{ github.ref }}
|
||||
|
||||
4
.github/workflows/build-main.yaml
vendored
4
.github/workflows/build-main.yaml
vendored
@@ -13,11 +13,9 @@ on:
|
||||
- .github/workflows/build-main.yaml
|
||||
- .github/workflows/_build-image.yaml
|
||||
|
||||
# Write scopes are granted to the build job only; every other job stays read-only.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
concurrency:
|
||||
group: build-main-${{ github.ref }}
|
||||
|
||||
11
.github/workflows/ci-manifests.yaml
vendored
11
.github/workflows/ci-manifests.yaml
vendored
@@ -46,12 +46,19 @@ jobs:
|
||||
- name: Install agent-sandbox and Gateway API CRDs
|
||||
run: |
|
||||
set -euo pipefail
|
||||
kubectl apply -f "https://github.com/kubernetes-sigs/agent-sandbox/releases/download/$AGENT_SANDBOX_VERSION/sandbox-with-extensions.yaml"
|
||||
# Download, verify the pinned digest from versions.env, then apply.
|
||||
curl -fsSL --proto '=https' -o /tmp/agent-sandbox.yaml \
|
||||
"https://github.com/kubernetes-sigs/agent-sandbox/releases/download/$AGENT_SANDBOX_VERSION/sandbox-with-extensions.yaml"
|
||||
printf '%s %s\n' "$AGENT_SANDBOX_MANIFEST_SHA256" /tmp/agent-sandbox.yaml | sha256sum -c -
|
||||
kubectl apply -f /tmp/agent-sandbox.yaml
|
||||
kubectl wait --for=condition=Established crd/sandboxtemplates.extensions.agents.x-k8s.io --timeout=180s
|
||||
kubectl wait --for=condition=Established crd/sandboxwarmpools.extensions.agents.x-k8s.io --timeout=180s
|
||||
kubectl wait --for=condition=Established crd/sandboxclaims.extensions.agents.x-k8s.io --timeout=180s
|
||||
kubectl wait --for=condition=Established crd/sandboxes.agents.x-k8s.io --timeout=180s
|
||||
kubectl apply -f "https://github.com/kubernetes-sigs/gateway-api/releases/download/$GATEWAY_API_VERSION/standard-install.yaml"
|
||||
curl -fsSL --proto '=https' -o /tmp/gateway-api.yaml \
|
||||
"https://github.com/kubernetes-sigs/gateway-api/releases/download/$GATEWAY_API_VERSION/standard-install.yaml"
|
||||
printf '%s %s\n' "$GATEWAY_API_MANIFEST_SHA256" /tmp/gateway-api.yaml | sha256sum -c -
|
||||
kubectl apply -f /tmp/gateway-api.yaml
|
||||
kubectl wait --for=condition=Established crd/gateways.gateway.networking.k8s.io --timeout=180s
|
||||
kubectl wait --for=condition=Established crd/httproutes.gateway.networking.k8s.io --timeout=180s
|
||||
- name: Set up Python
|
||||
|
||||
16
.github/workflows/ci.yaml
vendored
16
.github/workflows/ci.yaml
vendored
@@ -37,19 +37,29 @@ jobs:
|
||||
- name: Install actionlint
|
||||
env:
|
||||
ACTIONLINT_VERSION: v1.7.12
|
||||
# SHA-256 of actionlint_1.7.12_linux_amd64.tar.gz, published in the
|
||||
# release's actionlint_1.7.12_checksums.txt. Update it with the version.
|
||||
ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${ACTIONLINT_VERSION#v}"
|
||||
curl -fsSL -o /tmp/actionlint.tar.gz "https://github.com/rhysd/actionlint/releases/download/$ACTIONLINT_VERSION/actionlint_${version}_linux_amd64.tar.gz"
|
||||
tar -xzf /tmp/actionlint.tar.gz -C /tmp actionlint
|
||||
asset="actionlint_${version}_linux_amd64.tar.gz"
|
||||
curl -fsSL --proto '=https' -o "/tmp/$asset" \
|
||||
"https://github.com/rhysd/actionlint/releases/download/$ACTIONLINT_VERSION/$asset"
|
||||
printf '%s %s\n' "$ACTIONLINT_SHA256" "/tmp/$asset" | sha256sum -c -
|
||||
tar -xzf "/tmp/$asset" -C /tmp actionlint
|
||||
install -m 0755 /tmp/actionlint /usr/local/bin/actionlint
|
||||
actionlint -version
|
||||
- name: Install hadolint
|
||||
env:
|
||||
HADOLINT_VERSION: v2.12.0
|
||||
# SHA-256 of hadolint-Linux-x86_64, published next to the download.
|
||||
HADOLINT_SHA256: 56de6d5e5ec427e17b74fa48d51271c7fc0d61244bf5c90e828aab8362d55010
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL -o /tmp/hadolint "https://github.com/hadolint/hadolint/releases/download/$HADOLINT_VERSION/hadolint-Linux-x86_64"
|
||||
curl -fsSL --proto '=https' -o /tmp/hadolint \
|
||||
"https://github.com/hadolint/hadolint/releases/download/$HADOLINT_VERSION/hadolint-Linux-x86_64"
|
||||
printf '%s %s\n' "$HADOLINT_SHA256" /tmp/hadolint | sha256sum -c -
|
||||
install -m 0755 /tmp/hadolint /usr/local/bin/hadolint
|
||||
hadolint --version
|
||||
- name: Repository checks
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -58,6 +58,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
|
||||
9
.github/workflows/e2e-kind.yaml
vendored
9
.github/workflows/e2e-kind.yaml
vendored
@@ -40,7 +40,10 @@ jobs:
|
||||
- name: Install agent-sandbox
|
||||
run: |
|
||||
set -euo pipefail
|
||||
kubectl apply -f "https://github.com/kubernetes-sigs/agent-sandbox/releases/download/$AGENT_SANDBOX_VERSION/sandbox-with-extensions.yaml"
|
||||
curl -fsSL --proto '=https' -o /tmp/agent-sandbox.yaml \
|
||||
"https://github.com/kubernetes-sigs/agent-sandbox/releases/download/$AGENT_SANDBOX_VERSION/sandbox-with-extensions.yaml"
|
||||
printf '%s %s\n' "$AGENT_SANDBOX_MANIFEST_SHA256" /tmp/agent-sandbox.yaml | sha256sum -c -
|
||||
kubectl apply -f /tmp/agent-sandbox.yaml
|
||||
kubectl -n agent-sandbox-system rollout status deployment --timeout=180s
|
||||
kubectl wait --for=condition=Established crd/sandboxtemplates.extensions.agents.x-k8s.io --timeout=180s
|
||||
kubectl wait --for=condition=Established crd/sandboxwarmpools.extensions.agents.x-k8s.io --timeout=180s
|
||||
@@ -66,7 +69,9 @@ jobs:
|
||||
echo "== events =="
|
||||
kubectl get events -n dsh-demo --sort-by=.lastTimestamp 2>&1 || true
|
||||
echo "== gateway logs =="
|
||||
kubectl -n dsh-demo logs deploy/dsh-gateway --tail=200 2>&1 || true
|
||||
# Redact any token material before this file becomes an artifact.
|
||||
kubectl -n dsh-demo logs deploy/dsh-gateway --tail=200 2>&1 \
|
||||
| sed -E 's/(token=)[^&"[:space:]]+/\1[REDACTED]/g' || true
|
||||
} > /tmp/e2e-cluster.txt
|
||||
- name: Upload cluster state
|
||||
if: always()
|
||||
|
||||
23
.github/workflows/release.yaml
vendored
23
.github/workflows/release.yaml
vendored
@@ -17,11 +17,10 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
# Read-only by default; the build and publish jobs add the write scopes they
|
||||
# need as job-level grants.
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
@@ -51,13 +50,17 @@ jobs:
|
||||
- name: Validate tag against versions.env
|
||||
id: meta
|
||||
shell: bash
|
||||
env:
|
||||
# Passed through the environment, never interpolated into the script:
|
||||
# a tag name or dispatch input can contain shell metacharacters.
|
||||
REQUESTED_TAG: ${{ inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source versions.env
|
||||
set +a
|
||||
tag="${{ inputs.tag || github.ref_name }}"
|
||||
tag="$REQUESTED_TAG"
|
||||
identity="$DSH_VERSION"
|
||||
[ -z "$RELEASE_REVISION" ] || identity="$DSH_VERSION-$RELEASE_REVISION"
|
||||
if [ "$tag" != "$identity" ]; then
|
||||
@@ -205,11 +208,17 @@ jobs:
|
||||
- name: Install git-cliff
|
||||
env:
|
||||
GIT_CLIFF_VERSION: v2.14.2
|
||||
# SHA-256 of git-cliff-2.14.2-x86_64-unknown-linux-gnu.tar.gz; upstream
|
||||
# publishes the matching SHA-512 next to the asset. Update with the version.
|
||||
GIT_CLIFF_SHA256: 24f397c733add5390fdceee3a2088588ab0d5f944ce00d34cb7029b888cf2db4
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${GIT_CLIFF_VERSION#v}"
|
||||
curl -fsSL -o /tmp/git-cliff.tar.gz "https://github.com/orhun/git-cliff/releases/download/$GIT_CLIFF_VERSION/git-cliff-$version-x86_64-unknown-linux-gnu.tar.gz"
|
||||
tar -xzf /tmp/git-cliff.tar.gz -C /tmp
|
||||
asset="git-cliff-${version}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
curl -fsSL --proto '=https' -o "/tmp/$asset" \
|
||||
"https://github.com/orhun/git-cliff/releases/download/$GIT_CLIFF_VERSION/$asset"
|
||||
printf '%s %s\n' "$GIT_CLIFF_SHA256" "/tmp/$asset" | sha256sum -c -
|
||||
tar -xzf "/tmp/$asset" -C /tmp
|
||||
install -m 0755 "/tmp/git-cliff-$version/git-cliff" /usr/local/bin/git-cliff
|
||||
git-cliff --version
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ gets a DSH Web container running before anything else.
|
||||
| [examples.md](examples.md) | The example contract: required layout, manifest rules, exposure variants, components and image naming, scripts, docs, and how to add a new example. |
|
||||
| [gitflow.md](gitflow.md) | Branches, protected `main`, squash merges, Conventional PR titles, required CI checks, release branches, hotfixes and the one-time GitHub settings checklist. |
|
||||
| [versioning.md](versioning.md) | Release identity (`DSH ref` + optional `-rN`), the current identity `dsh-v0.2.1-rc.5`, why identities are immutable, channels, compatibility matrix, bumping pins, the release checklist, rollback, git-cliff and digest pinning. |
|
||||
| [workflow-security.md](workflow-security.md) | GitHub Actions security posture: least-privilege tokens, pinned actions, untrusted-input handling, download integrity checks, artifact hygiene, the latest audit record and residual repository-setting recommendations. |
|
||||
|
||||
## Project skills
|
||||
|
||||
|
||||
69
docs/workflow-security.md
Normal file
69
docs/workflow-security.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Workflow security (GitHub Actions)
|
||||
|
||||
The CI/CD surface lives entirely in [`.github/workflows/`](../.github/workflows/)
|
||||
and runs on GitHub-hosted runners. This page describes the controls those
|
||||
workflows enforce and records the most recent audit. They complement the threat
|
||||
notes in [SECURITY.md](../SECURITY.md) and the release procedure in
|
||||
[versioning.md](versioning.md).
|
||||
|
||||
## Controls
|
||||
|
||||
| Control | Where | What it does |
|
||||
| --- | --- | --- |
|
||||
| Least-privilege tokens | Every workflow | A top-level `permissions:` block defaults to read-only; `packages: write`, `contents: write`, `id-token: write` and `attestations: write` are granted only to the jobs that publish images, attestations or releases. |
|
||||
| Pinned actions | Every `uses:` | Third-party actions and reusable workflows are pinned to a full 40-character commit SHA (the version is kept in the trailing comment). Dependabot updates the pins weekly. The only unpinned reference is the in-repository `./.github/workflows/_build-image.yaml`. |
|
||||
| No privileged untrusted triggers | Trigger blocks | No workflow uses `pull_request_target` or `workflow_run`. PR-triggered workflows run untrusted code with a read-only token and no secrets. |
|
||||
| Untrusted input is data | `release.yaml` | The release tag (from a tag push or `workflow_dispatch` input) is passed through `env:` and read as a shell variable; it is never interpolated into a `run:` script, so metacharacters in a tag name cannot execute. |
|
||||
| Checkout credential hygiene | Checkout steps | `persist-credentials: false` is set wherever the job does not need to push. The changelog job keeps credentials because it pushes a branch and opens a PR with `GITHUB_TOKEN`. |
|
||||
| Download integrity | `ci.yaml`, `release.yaml` | actionlint, hadolint and git-cliff are downloaded with `curl --proto '=https'` and verified against a pinned SHA-256 with `sha256sum -c` before execution. |
|
||||
| Cluster manifest integrity | `ci-manifests.yaml`, `e2e-kind.yaml` | The agent-sandbox and Gateway API release manifests are downloaded and checked against the `*_MANIFEST_SHA256` pins in [versions.env](../versions.env) before `kubectl apply`. |
|
||||
| Log and artifact hygiene | `ci-image.yaml`, `e2e-kind.yaml` | Pod logs are truncated and token material is redacted before logs are printed or diagnostic files are uploaded as artifacts. |
|
||||
| Supply-chain monitoring | Repository settings | Dependabot (GitHub Actions, pip, Docker), CodeQL advanced setup and the dependency-review action run on every applicable change. CODEOWNERS covers the whole repository, including `.github/workflows/`. |
|
||||
|
||||
## Maintaining the pins
|
||||
|
||||
- The tool versions and their SHA-256 digests live together in the install step
|
||||
(`ACTIONLINT_SHA256`, `HADOLINT_SHA256`, `GIT_CLIFF_SHA256`); update both
|
||||
values when bumping a version.
|
||||
- Control-plane versions are in `versions.env`; update
|
||||
`AGENT_SANDBOX_MANIFEST_SHA256` or `GATEWAY_API_MANIFEST_SHA256` together with
|
||||
the corresponding version, otherwise CI fails the digest check by design.
|
||||
- GitHub Actions pins are updated by Dependabot; never replace a SHA with a tag.
|
||||
|
||||
## Audit record (2026-10-30)
|
||||
|
||||
Scope: all 11 workflows under `.github/workflows/`, plus `.github/CODEOWNERS`
|
||||
and `.github/dependabot.yml`. Findings are grouped by the categories in
|
||||
GitHub's [Secure use reference](https://docs.github.com/en/actions/reference/security/secure-use).
|
||||
|
||||
| # | Category | File | Severity | Finding | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | Script injection | `release.yaml` | High | Tag name / dispatch input interpolated into a privileged `run:` block | Fixed: passed through `env` |
|
||||
| 2 | Token permissions | `release.yaml` | Medium | `meta` and `tests` inherited workflow-level write scopes | Fixed: workflow default is `contents: read` |
|
||||
| 3 | Token permissions | `build-main.yaml`, `build-gateway.yaml` | Low | `meta` inherited `packages`/`id-token`/`attestations` write | Fixed: workflow default is `contents: read` |
|
||||
| 4 | Checkout | `_build-image.yaml` | Low | Checkout persisted the token in `.git/config` | Fixed: `persist-credentials: false` |
|
||||
| 5 | Checkout | `codeql.yml` | Low | Checkout persisted the token in `.git/config` | Fixed: `persist-credentials: false` |
|
||||
| 6 | Supply chain | `ci.yaml` | Medium | actionlint and hadolint were executed without a checksum | Fixed: pinned SHA-256 verified before install |
|
||||
| 7 | Supply chain | `release.yaml` | Medium | git-cliff was executed without a checksum in the release job | Fixed: pinned SHA-256 verified before install |
|
||||
| 8 | Supply chain | `ci-manifests.yaml`, `e2e-kind.yaml` | Medium | Upstream CRD/controller manifests applied without integrity checks | Fixed: pinned SHA-256 in `versions.env` |
|
||||
| 9 | Secret hygiene | `e2e-kind.yaml` | Low | Raw gateway logs uploaded in a 7-day artifact | Fixed: `token=` values redacted |
|
||||
| 10 | Repository controls | Repository settings | Low | No environment approval gate on publishing; single CODEOWNER | Recommendation: see below |
|
||||
|
||||
Checked and clean: no `pull_request_target`/`workflow_run`, no self-hosted
|
||||
runners, all third-party actions pinned, PR workflows read-only, no secrets in
|
||||
workflow files, no secret interpolation into scripts.
|
||||
|
||||
## Residual recommendations (repository settings)
|
||||
|
||||
These cannot be enforced from workflow files alone:
|
||||
|
||||
- Add a protected `release` environment with required reviewers and attach it to
|
||||
the `build-main`, `build-gateway` and `publish-release` jobs. This requires the
|
||||
environment to exist in repository settings first, so it is intentionally not
|
||||
wired into the workflow yet.
|
||||
- Keep "Allow GitHub Actions to create and approve pull requests" limited to
|
||||
creating PRs (the changelog job needs create, not approve).
|
||||
- Require review from Code Owners for `.github/workflows/` and add a second
|
||||
owner so workflow changes are never single-person.
|
||||
- Optionally run OpenSSF Scorecard on a schedule for continuous checks on these
|
||||
controls.
|
||||
@@ -16,8 +16,12 @@ PNPM_VERSION=11.28.5
|
||||
RELEASE_REVISION=
|
||||
|
||||
# Control-plane versions used by the examples and their manifests.
|
||||
# The matching SHA-256s pin the release manifests that CI downloads and
|
||||
# applies; update both the version and the digest when bumping either pin.
|
||||
AGENT_SANDBOX_VERSION=v1.0.9
|
||||
AGENT_SANDBOX_MANIFEST_SHA256=4bf9ee5558e9a663f91cfa5963e96f2591bfaa1dcf79f03bfab65bc5d65cc699
|
||||
GATEWAY_API_VERSION=v1.2.1
|
||||
GATEWAY_API_MANIFEST_SHA256=97598bf6ab3b33b9b5c5432bdd24de091e4e9c3aa0575ebb0710a2a19cd64d64
|
||||
OAUTH2_PROXY_IMAGE=quay.io/oauth2-proxy/oauth2-proxy:v7.15.5
|
||||
|
||||
# Registry layout: the main image is the repository itself, and every example
|
||||
|
||||
Reference in New Issue
Block a user