datahaven/.github/workflows/task-docker-release.yml
Steve Degosserie 746fce9328
security: 🛡️ Harden GitHub Actions workflows (#349)
## Summary

This PR addresses several security vulnerabilities and applies hardening
measures to the GitHub Actions workflows:

- **Replace `secrets: inherit` with explicit secret passing** - Prevents
unnecessary exposure of all repository secrets to called workflows
- **Add SHA256 checksum verification for downloaded binaries** -
Protects against supply chain attacks via compromised upstream releases
- **Add GitHub Environment protections for release workflows** -
Requires approval before publishing to Docker Hub or creating releases
- **Add explicit minimal permissions to all workflows** - Follows
principle of least privilege, removes unnecessary `packages: write` from
CI.yml

## Changes by Category

### 1. Explicit Secret Passing
| Workflow | Before | After |
|----------|--------|-------|
| CI.yml → docker-build-ci | `secrets: inherit` | No secrets
(GITHUB_TOKEN is automatic) |
| CI.yml → docker-build-release | `secrets: inherit` | Explicit
`DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` |
| CI.yml → e2e-tests | `secrets: inherit` | No secrets (GITHUB_TOKEN is
automatic) |

### 2. Binary Checksum Verification
| Workflow | Binary | SHA256 |
|----------|--------|--------|
| task-rust-lint.yml | taplo 0.8.1 | `c62baa73c9d7c1572...` |
| task-e2e.yml | kurtosis 1.11.99 | `5e88e98c1b255362...` |

### 3. Environment Protections
| Workflow | Job | Environment |
|----------|-----|-------------|
| task-docker-release.yml | build-test-push | `production` |
| task-publish-binary.yml | publish-draft-release | `releases` |
| task-publish-binary.yml | docker-release-candidate | `production` |
| task-publish-runtime.yml | publish-draft-release | `releases` |

### 4. Explicit Permissions
All 14 workflow files now have explicit `permissions:` blocks with
minimal required access.

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
2025-12-12 09:52:50 +00:00

163 lines
5.7 KiB
YAML

name: Docker Build & Publish (Release)
on:
workflow_dispatch:
inputs:
label:
description: "Label for the Docker image"
required: true
type: string
branch:
description: "Branch to checkout and build"
required: true
type: string
fast_runtime:
description: "Enable fast runtime features"
required: false
type: boolean
default: false
workflow_call:
secrets:
DOCKERHUB_USERNAME:
description: "Docker Hub username"
required: true
DOCKERHUB_TOKEN:
description: "Docker Hub access token"
required: true
outputs:
image-tag:
description: "The tag portion of the docker image (without registry)"
value: "${{ jobs.build-test-push.outputs.image-tag }}"
permissions:
contents: read
packages: write
concurrency:
group: docker-build-release-${{ github.ref }}
cancel-in-progress: true
jobs:
build-test-push:
runs-on: ubuntu-latest
# Require approval before publishing to Docker Hub
environment: production
outputs:
image-tag: ${{ steps.extract_tag.outputs.image-tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch || github.ref }}
- uses: ./.github/workflows/actions/cleanup-runner
# --- Docker metadata ---
- name: Docker meta (dispatch)
if: github.event_name == 'workflow_dispatch'
id: meta-dispatch
uses: docker/metadata-action@v5
with:
images: datahavenxyz/datahaven
flavor: |
latest=false
tags: |
type=raw,value=${{ github.event.inputs.label }}
- name: Docker meta (CI - main push)
if: github.event_name != 'workflow_dispatch'
id: meta-ci
uses: docker/metadata-action@v5
with:
images: datahavenxyz/datahaven
flavor: |
latest=true
tags: |
type=raw,value=latest
type=sha,format=short,prefix=sha-
- name: Extract tag for job output
id: extract_tag
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
FULL_TAG=$(echo '${{ steps.meta-dispatch.outputs.json }}' | jq -r '.tags[-1]')
else
FULL_TAG=$(echo '${{ steps.meta-ci.outputs.json }}' | jq -r '.tags[-1]')
fi
TAG_ONLY=$(echo "$FULL_TAG" | sed 's|.*:||')
echo "image-tag=$TAG_ONLY" >> $GITHUB_OUTPUT
echo "image-name=datahavenxyz/datahaven:$TAG_ONLY" >> $GITHUB_OUTPUT
# --- Cargo cache for full builds ---
- name: Set up cargo cache
uses: actions/cache@v4
id: cache
with:
path: |
**/cargo-registry
**/cargo-git
key: cache-mount-${{ hashFiles('./docker/datahaven-build.Dockerfile') }}-${{ hashFiles('./operator/Cargo.lock') }}-${{hashFiles('./operator/runtime/**/*.rs','./operator/pallets/**/*.rs', './operator/node/**/*.rs')}}
restore-keys: |
cache-mount-${{ hashFiles('./docker/datahaven-build.Dockerfile') }}-${{ hashFiles('./operator/Cargo.lock') }}
cache-mount-${{ hashFiles('./docker/datahaven-build.Dockerfile') }}
cache-mount-
- name: Inject cache into docker
uses: reproducible-containers/buildkit-cache-dance@v3.1.0
with:
cache-map: |
{
"cargo-registry": { "target": "/usr/local/cargo/registry" },
"cargo-git": { "target": "/usr/local/cargo/git" }
}
skip-extraction: ${{ steps.cache.outputs.cache-hit }}
# --- Build and push Docker image ---
- name: Build and push Docker image
uses: ./.github/workflow-templates/publish-docker
with:
dockerfile: ./docker/datahaven-build.Dockerfile
context: ./operator
registry: docker.io
registry_username: ${{ secrets.DOCKERHUB_USERNAME }}
registry_password: ${{ secrets.DOCKERHUB_TOKEN }}
image_tags: ${{ steps.meta-dispatch.outputs.tags || steps.meta-ci.outputs.tags }}
image_title: "DataHaven Node - Release"
image_description: "Release build of DataHaven blockchain node"
cache_scope: datahaven-release-build
build_args: |
FAST_RUNTIME=${{ github.event.inputs.fast_runtime == 'true' && 'TRUE' || 'FALSE' }}
# --- Smoke tests ---
- name: Pull and test node --help
run: |
docker pull ${{ steps.extract_tag.outputs.image-name }}
docker run --rm ${{ steps.extract_tag.outputs.image-name }} --help
- name: Integration test (dev chain starts)
run: |
docker run --rm -d -p 9944:9944 --name local-dh-node \
${{ steps.extract_tag.outputs.image-name }} --dev --unsafe-rpc-external
- name: Wait for node to be healthy and test
run: |
echo "Waiting for node to start..."
for i in {1..30}; do # Retry for 30 * 5s = 150 seconds
if curl --fail --location 'http://127.0.0.1:9944' \
--header 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"system_chain","params":[]}' ; then
echo "Node is healthy!"
docker logs local-dh-node --tail 100
exit 0
fi
echo "Attempt $i: Node not ready yet, sleeping 5s..."
sleep 5
done
echo "Node failed to start or respond in time."
docker logs local-dh-node --tail 100
exit 1
- name: Cleanup integration test container
if: always()
run: docker rm -f local-dh-node