Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 96 additions & 2 deletions .github/workflows/docker-image-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ jobs:
docker-build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for Workload Identity Federation
contents: write # Required for uploading release assets
id-token: write # Required for Workload Identity Federation and OIDC signing
packages: write # Required for GHCR
attestations: write # Required for artifact attestations
security-events: write # Required for uploading SARIF to Security tab
steps:
- name: Checkout
uses: actions/checkout@v5
Expand Down Expand Up @@ -116,7 +118,12 @@ jobs:
username: oauth2accesstoken
password: ${{ steps.auth.outputs.access_token }}

- name: Set SOURCE_DATE_EPOCH for reproducible builds
id: source-date
run: echo "epoch=$(git log -1 --format=%ct)" >> $GITHUB_OUTPUT

- name: build and push docker image
id: build
uses: docker/build-push-action@v6
with:
context: .
Expand All @@ -127,3 +134,90 @@ jobs:
cache-to: type=gha,mode=max
# Build multi-platform only for production (main/tags), AMD64 only for PRs
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
# Pass SOURCE_DATE_EPOCH for reproducible builds
build-args: |
SOURCE_DATE_EPOCH=${{ steps.source-date.outputs.epoch }}

# =============================================================
# EU Cyber Resilience Act (CRA) Compliance - SBOM & Security
# =============================================================

- name: Generate SBOM (CycloneDX format)
uses: anchore/sbom-action@v0
if: github.event_name != 'pull_request'
with:
image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
format: cyclonedx-json
output-file: sbom.cyclonedx.json
artifact-name: sbom-cyclonedx

- name: Generate SBOM (SPDX format for compliance)
uses: anchore/sbom-action@v0
if: github.event_name != 'pull_request'
with:
image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
artifact-name: sbom-spdx

- name: Scan SBOM for vulnerabilities
id: scan
uses: anchore/scan-action@v6
if: github.event_name != 'pull_request'
with:
sbom: sbom.cyclonedx.json
fail-build: false
severity-cutoff: critical
output-format: sarif

- name: Upload SARIF to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: github.event_name != 'pull_request' && always()
with:
sarif_file: ${{ steps.scan.outputs.sarif }}

- name: Upload SBOM to release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
sbom.cyclonedx.json
sbom.spdx.json

# =============================================================
# Supply Chain Security - Attestations & Signing (SLSA L2)
# =============================================================

- name: Attest build provenance (GHCR)
uses: actions/attest-build-provenance@v2
if: github.event_name != 'pull_request'
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true

- name: Attest SBOM (GHCR)
uses: actions/attest-sbom@v2
if: github.event_name != 'pull_request'
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
sbom-path: sbom.spdx.json
push-to-registry: true

- name: Install Cosign
uses: sigstore/cosign-installer@v3
if: github.event_name != 'pull_request'

- name: Sign container image with Cosign (GHCR)
if: github.event_name != 'pull_request'
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}

- name: Sign container image with Cosign (Docker Hub)
if: github.event_name != 'pull_request' && secrets.DOCKERHUB_USERNAME != ''
continue-on-error: true # Don't fail if Docker Hub is not configured
run: |
cosign sign --yes \
docker.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# =============================================================
# EU Cyber Resilience Act (CRA) Compliance - Reproducible Build
# =============================================================
# This Dockerfile follows reproducibility best practices:
# - Uses lockfile for deterministic dependency installation
# - Supports SOURCE_DATE_EPOCH for reproducible timestamps
# - Multi-stage build to minimize final image size
# =============================================================

ARG BASEIMAGE
FROM ${BASEIMAGE:-"node:22.21.1-alpine"} AS builder

# Support reproducible builds with deterministic timestamps
ARG SOURCE_DATE_EPOCH
ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN npm i -g corepack && pnpm -v
Expand Down
237 changes: 237 additions & 0 deletions docs/eu-cra-compliance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# EU Cyber Resilience Act (CRA) Compliance

This document describes the supply chain security features implemented to comply with the EU Cyber Resilience Act (Regulation EU 2024/2847).

## Overview

The EU CRA requires manufacturers of products with digital elements to:
- Maintain a Software Bill of Materials (SBOM) in machine-readable format
- Cover at least top-level dependencies
- Provide SBOM to market surveillance authorities on request
- Report exploited vulnerabilities to ENISA

### Timeline

- **December 2024**: CRA entered into force
- **September 2026**: Vulnerability reporting to ENISA required
- **December 2027**: Full SBOM enforcement

### Penalties

Non-compliance can result in fines up to **EUR 15 million or 2.5% of global turnover**.

## Implemented Features

### 1. SBOM Generation

SBOMs are automatically generated for every Docker image build using [Anchore Syft](https://github.com/anchore/syft).

**Formats generated:**
- **CycloneDX** (primary) - Security-focused, excellent for vulnerability management
- **SPDX** (compliance) - ISO/IEC 5962:2021 standard

**Workflow steps:**
```yaml
- name: Generate SBOM (CycloneDX format)
uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
format: cyclonedx-json
output-file: sbom.cyclonedx.json
```

**Output locations:**
- GitHub workflow artifacts
- GitHub release assets (for tagged releases)

### 2. Vulnerability Scanning

Container images are scanned for vulnerabilities using [Anchore Grype](https://github.com/anchore/grype).

**Configuration:**
- Severity cutoff: `critical`
- Build failure: Disabled (warnings only, can be enabled)
- Output format: SARIF

### 3. Artifact Attestations (SLSA Level 2)

Build provenance attestations are generated using GitHub's native attestation features:

- **Build provenance**: Cryptographically signed proof of build origin
- **SBOM attestation**: Signed SBOM attached to the container image

These attestations provide SLSA v1.0 Build Level 2 compliance.

### 4. Container Image Signing

Images are signed using [Sigstore Cosign](https://github.com/sigstore/cosign) with keyless signing:

- Uses OIDC tokens from GitHub Actions
- Signatures stored in the container registry
- Logged in Sigstore transparency log (Rekor)

**Signed registries:**
- GitHub Container Registry (ghcr.io)
- Docker Hub (docker.io)

### 5. Reproducible Build Support

The Dockerfile supports reproducible builds through:

- `SOURCE_DATE_EPOCH` environment variable for deterministic timestamps
- `pnpm install --frozen-lockfile` for deterministic dependency installation
- Multi-stage builds for minimal final image

## Verification

### Verify GitHub Attestations

```bash
# Install GitHub CLI if not already installed
# https://cli.github.com/

# Verify build provenance
gh attestation verify \
oci://ghcr.io/eins78/hello-world-web@sha256:<digest> \
--owner eins78

# Download SBOM attestation
gh attestation download \
oci://ghcr.io/eins78/hello-world-web@sha256:<digest> \
--predicate-type https://spdx.dev/Document
```

### Verify Cosign Signatures

```bash
# Install cosign if not already installed
# https://docs.sigstore.dev/cosign/installation/

# Verify signature with identity constraints
cosign verify \
--certificate-identity-regexp "github.com/eins78/hello-world-web" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/eins78/hello-world-web@sha256:<digest>
```

### Download SBOM from Release

For tagged releases, SBOMs are attached as release assets:

```bash
# Download from GitHub release
gh release download v1.0.0 --pattern "sbom.*.json"

# Inspect CycloneDX SBOM
cat sbom.cyclonedx.json | jq '.components | length'

# Inspect SPDX SBOM
cat sbom.spdx.json | jq '.packages | length'
```

## Architecture

```
GitHub Actions Workflow
|
v
+-------------------+
| Build Image |
| (docker/build-push)|
+-------------------+
|
v
+-------------------+
| Generate SBOMs |
| (anchore/sbom) |
+-------------------+
|
v
+-------------------+
| Scan for Vulns |
| (anchore/scan) |
+-------------------+
|
v
+-------------------+
| Attestations |
| (actions/attest) |
+-------------------+
|
v
+-------------------+
| Sign Images |
| (sigstore/cosign) |
+-------------------+
|
v
+-------------------+
| Publish |
| GHCR, Docker Hub, |
| GAR |
+-------------------+
```

## Security Considerations

### What Provenance Proves

- The image was built from a specific commit
- The build occurred on GitHub Actions infrastructure
- The build used a specific workflow file
- The image hasn't been tampered with since signing

### What Provenance Does NOT Prove

- The source code is secure
- The dependencies are vulnerability-free
- The image is safe to run

Always combine provenance verification with:
- Vulnerability scanning
- Security audits
- Runtime security policies

## Future Improvements

### SLSA Level 3

To achieve SLSA Level 3, implement:
- Reusable workflows for hardened builds
- Isolated build environments
- Protected signing keys

### Policy Enforcement

Consider implementing:
- Kubernetes admission policies (Kyverno, Gatekeeper)
- Require signed images for deployment
- Enforce minimum SLSA level

### VEX (Vulnerability Exploitability eXchange)

Document which vulnerabilities are not exploitable in your context using VEX statements.

## Related Resources

- [EU Cyber Resilience Act](https://digital-strategy.ec.europa.eu/en/library/cyber-resilience-act)
- [SLSA Framework](https://slsa.dev/)
- [Sigstore Documentation](https://docs.sigstore.dev/)
- [CycloneDX Specification](https://cyclonedx.org/)
- [SPDX Specification](https://spdx.dev/)
- [GitHub Artifact Attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)

## Workflow File Reference

The implementation is in `.github/workflows/docker-image-publish.yml`.

Key sections (search for these comments in the workflow):
- `Set SOURCE_DATE_EPOCH` - Reproducible build timestamp from git commit
- `EU Cyber Resilience Act (CRA) Compliance` - SBOM generation and vulnerability scanning
- `Supply Chain Security - Attestations & Signing` - GitHub attestations and Cosign signing

The workflow includes:
1. SBOM generation in CycloneDX and SPDX formats
2. Vulnerability scanning with SARIF upload to GitHub Security tab
3. Build provenance and SBOM attestations
4. Keyless container signing with Cosign