This guide explains how to configure GitHub Actions to run the complete CI/CD pipeline for SkyLink.
- Overview
- Prerequisites
- Repository Secrets Configuration
- Pipeline Configuration
- Cosign Setup (Supply Chain Security)
- Branch Protection Rules
- Troubleshooting
The CI/CD pipeline includes the following stages:
| Stage | Jobs | Description |
|---|---|---|
| Lint | ruff, black, bandit | Code quality and security linting |
| Test | pytest | Unit tests with coverage (min 75%) |
| Build | docker build | Build and push Docker images to GHCR |
| Scan | trivy, gitleaks, pip-audit, openapi | Security scanning |
| SBOM | cyclonedx | Software Bill of Materials |
| DAST | ZAP | Dynamic Application Security Testing |
| Sign | cosign | Image signing with Sigstore (keyless) |
Before setting up CI/CD, ensure you have:
- GitHub repository created
- GitHub Container Registry (GHCR) enabled (automatic with GitHub)
- RSA keys generated for JWT authentication
- (Optional) WeatherAPI key for weather service
Note: No Cosign keys are required - the pipeline uses keyless signing with Sigstore OIDC.
Go to: Settings → Secrets and variables → Actions → Secrets
| Secret Name | Description | How to Generate |
|---|---|---|
PRIVATE_KEY_PEM |
RSA private key for JWT signing | See Generate RSA Keys |
PUBLIC_KEY_PEM |
RSA public key for JWT verification | See Generate RSA Keys |
| Secret Name | Description | How to Generate |
|---|---|---|
WEATHER_API_KEY |
WeatherAPI.com API key | https://www.weatherapi.com/ |
GOOGLE_CLIENT_ID |
Google OAuth client ID | Google Cloud Console |
GOOGLE_CLIENT_SECRET |
Google OAuth client secret | Google Cloud Console |
ENCRYPTION_KEY |
32-byte hex key for token encryption | openssl rand -hex 32 |
Note:
COSIGN_*secrets are NOT required - the pipeline uses Sigstore keyless signing.
# Generate RSA key pair
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
# Display keys (copy entire content including BEGIN/END lines)
cat private.pem # → PRIVATE_KEY_PEM
cat public.pem # → PUBLIC_KEY_PEMImportant: When adding to GitHub Secrets:
- Copy the entire content including
-----BEGIN...-----and-----END...-----lines - Do NOT base64 encode (GitHub handles this automatically)
The pipeline is defined in .github/workflows/ci.yml.
- Image tagging: Uses short SHA (
abc1234) for consistent tagging across jobs - Keyless signing: Uses Sigstore OIDC - no keys to manage
- Fail-safe scans: Trivy and ZAP use
continue-on-error: trueto not block the pipeline - GHCR integration: Uses GitHub Container Registry with automatic authentication
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ lint │──▶│ test │──▶│ build │──▶│ trivy │──▶│ sign │
└───────┘ └───────┘ └───────┘ └───────┘ └───────┘
│ │ │ │ │
│ │ │ │ ▼
│ │ │ │ ┌─────────┐
│ │ │ └─────▶│ attest │
│ │ │ └─────────┘
│ │ │
│ │ └──────────▶┌───────┐
│ │ │ zap │
│ │ └───────┘
│ │
│ └──────────────────────▶┌───────┐
│ │ sbom │
│ └───────┘
│
└──────────────────────────────────▶┌─────────┐
│gitleaks │
└─────────┘
The build job exports an image_tag output containing the short SHA:
outputs:
image_tag: ${{ steps.short_sha.outputs.sha }}This tag is used by downstream jobs (trivy, zap, sign, attest) to reference the correct image.
| Tag | Example | Description |
|---|---|---|
| SHA | abc1234 |
Short commit SHA |
| Branch | master |
Branch name |
| Latest | latest |
Only on default branch |
| Version | 1.0.0 |
Only on release tags |
The pipeline uses Sigstore keyless signing by default - no setup required!
- GitHub Actions provides an OIDC token
- Sigstore's Fulcio CA issues a short-lived certificate
- The image is signed with this certificate
- Signature is recorded in Rekor transparency log
# Verify with keyless (Sigstore)
cosign verify \
--certificate-identity-regexp="https://github.com/laugiov/security-by-design" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/laugiov/security-by-design:latest
# Verify SBOM attestation
cosign verify-attestation \
--certificate-identity-regexp="https://github.com/laugiov/security-by-design" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--type cyclonedx \
ghcr.io/laugiov/security-by-design:latestIf you prefer key-based signing, you can modify the workflow:
# Generate Cosign key pair
cosign generate-key-pair
# Add to GitHub Secrets:
# COSIGN_PRIVATE_KEY = content of cosign.key
# COSIGN_PASSWORD = password you enteredThen modify the sign job in .github/workflows/ci.yml:
- name: Sign image with key
env:
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
cosign sign --yes --key env://COSIGN_PRIVATE_KEY \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag }}Go to: Settings → Branches → Add rule
Configure for master (or main):
-
Require a pull request before merging
- Require approvals: 1
- Dismiss stale pull request approvals when new commits are pushed
-
Require status checks to pass before merging
- Require branches to be up to date before merging
- Required checks:
Lint & Security CheckUnit TestsSecret ScanningDependency Audit
-
Require conversation resolution before merging
-
Do not allow bypassing the above settings
Cause: Secret not configured or incorrect format.
Solution:
# Verify key format
cat private.pem | head -1
# Should output: -----BEGIN PRIVATE KEY-----
# Add to GitHub Secrets with exact content (no extra spaces/newlines)Cause: Test coverage threshold not met.
Solution:
- Add more tests
- Or temporarily lower threshold in
pyproject.toml:[tool.pytest.ini_options] addopts = "--cov-fail-under=70"
Cause: GitHub token doesn't have package write permission.
Solution: Ensure workflow has:
permissions:
packages: writeCause: Image not found or SARIF file not generated.
Solution: The current workflow handles this with:
docker pullbefore scanningexit-code: '0'to not fail on vulnerabilitiescontinue-on-error: trueon the job
Cause: Image tag mismatch between build and scan jobs.
Solution: The workflow now uses needs.build.outputs.image_tag to ensure consistent tagging. Make sure:
# In ZAP job
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag }}Cause: Missing OIDC permissions.
Solution: Ensure the sign job has:
permissions:
id-token: write # Required for keyless signing
packages: write # Required to push signature-
Enable debug logging:
env: ACTIONS_STEP_DEBUG: true
-
Check image exists:
- name: List images run: | docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag }} docker images
-
View container logs on failure:
- name: Debug on failure if: failure() run: docker logs skylink
- Create repository on GitHub
- Generate RSA keys:
openssl genrsa -out private.pem 2048 - Add
PRIVATE_KEY_PEMsecret - Add
PUBLIC_KEY_PEMsecret - Push code to trigger first pipeline
- Configure branch protection rules
- (Optional) Set up Codecov for coverage reports
No Cosign setup required - keyless signing works automatically!
- Never commit secrets to the repository
- Use protected secrets for production keys
- Rotate RSA keys every 90 days
- Review Dependabot alerts regularly
- Enable GitHub Advanced Security if available
- Use signed commits (GPG or SSH)
- DEMO.md - Manual testing guide
- TECHNICAL_DOCUMENTATION.md - Architecture details
- GITLAB_CI_SETUP.md - GitLab CI alternative
- CONTRIBUTING.md - Contribution guidelines
Last updated: December 2025