diff --git a/.github/WORKFLOW_SECURITY.md b/.github/WORKFLOW_SECURITY.md new file mode 100644 index 000000000000..bf84eb0f1bef --- /dev/null +++ b/.github/WORKFLOW_SECURITY.md @@ -0,0 +1,220 @@ +# GitHub Actions Workflow Security Guide + +## Overview + +This document describes the security practices implemented in the GitHub Actions workflows for this repository to protect private data and sensitive information. + +## Security Measures Implemented + +### 1. API Key Protection + +**Location**: `.github/workflows/etherscan-apiv2.yml` + +**Measures**: +- ✅ API keys are constructed in subshells with command echoing disabled (`set +x`) +- ✅ Secrets are masked using `::add-mask::` command at the start of jobs +- ✅ Shell debugging is explicitly disabled to prevent command echoing +- ✅ All API requests use HTTPS +- ✅ Temporary files are cleaned up immediately after use +- ✅ URL construction happens in isolated subshells to minimize exposure risk + +**Why**: While Etherscan requires API keys as URL parameters (not headers), we can still protect them by: +1. Constructing URLs in subshells with `set +x` to disable command echoing +2. Masking secrets at the job level using `::add-mask::` +3. Avoiding storage of URLs containing secrets in environment variables that could be logged + +### 2. GitHub Actions Runtime Token Protection + +**Location**: `.github/actions/configure-docker/action.yml` + +**Measures**: +- ✅ `ACTIONS_RUNTIME_TOKEN` is explicitly masked using `core.setSecret()` +- ✅ Token is only exported for Docker buildkit cache operations +- ✅ Token is scoped to workflow run and expires automatically + +**Why**: The runtime token is sensitive and should not appear in logs, even though it's automatically scoped and expires. + +### 3. Git Credential Management + +**Location**: `.github/workflows/self-hosted-runner-setup.yml` + +**Measures**: +- ✅ Git credentials are cleared after each workflow run +- ✅ Credential helpers are explicitly unset +- ✅ No persistent credential storage + +**Why**: Self-hosted runners may persist data between runs, so credentials must be explicitly cleared. + +### 4. Secret Scanning + +**Locations**: +- `.github/workflows/bitcoin-ownership-announcement.yml` (lines 248-294) +- `.github/workflows/wiki-management.yml` (lines 47-83) +- `.github/workflows/deploy-website.yml` (lines 81-105) + +**Measures**: +- ✅ Automated scanning for common secret patterns +- ✅ Pattern matching for private keys, passwords, API keys +- ✅ Validation fails if potential secrets are detected +- ✅ Third-party security scanning tools (TruffleHog, Trivy) + +**Why**: Multiple layers of defense prevent accidental secret commits. + +### 5. Environment Variable Sanitization + +**Location**: `.github/workflows/self-hosted-runner-setup.yml` (line 139) + +**Measures**: +- ✅ Only safe environment variables are displayed +- ✅ Variables containing tokens, secrets, passwords are filtered out +- ✅ Explicit allow-list approach for environment display + +**Why**: Environment variables may contain sensitive data and should not be dumped to logs. + +### 6. Checkout Security + +**Locations**: Multiple workflows + +**Measures**: +- ✅ `persist-credentials: false` used in sensitive operations +- ✅ Clean checkouts to prevent cross-contamination +- ✅ Limited token scopes where possible + +**Why**: Persisting credentials can lead to accidental exposure in subsequent steps. + +### 7. Workspace Cleanup + +**Location**: `.github/workflows/self-hosted-runner-setup.yml` (lines 142-152, 191-198) + +**Measures**: +- ✅ Temporary files are removed after each run +- ✅ Private keys (`.pem`, `.key`) are explicitly deleted +- ✅ Environment files (`.env*`) are removed +- ✅ Git credential files are cleared + +**Why**: Self-hosted runners persist data, so cleanup is essential to prevent data leakage. + +## Secret Types Protected + +### Never Logged or Exposed: +1. **API Keys** (Etherscan, etc.) +2. **GitHub Tokens** (GITHUB_TOKEN, ACTIONS_RUNTIME_TOKEN) +3. **Private Keys** (SSH, GPG, cryptocurrency) +4. **Passwords and Credentials** +5. **Seed Phrases and Mnemonics** +6. **Personal Access Tokens** + +### Public Data (Safe to Log): +1. **ENS Names** (e.g., kushmanmb.eth) +2. **Public Blockchain Addresses** (after resolution) +3. **Repository Information** (commit SHAs, branch names) +4. **Workflow Run IDs** +5. **Public Configuration Values** + +## Best Practices for Contributors + +### When Adding New Workflows: + +1. **Prefer** header-based authentication when the API supports it + ```yaml + # ✅ BEST (if API supports it) + curl -H "Authorization: Bearer ${{ secrets.API_KEY }}" "https://api.example.com/data" + + # ✅ ACCEPTABLE (if API requires URL parameters) + # Use subshell with disabled echoing + ( + set +x 2>/dev/null + curl "https://api.example.com/data?apikey=${{ secrets.API_KEY }}" -o output.json + ) + + # ❌ BAD - Direct construction that could be logged + curl "https://api.example.com/data?apikey=${{ secrets.API_KEY }}" + ``` + +2. **Always** mask secrets at the start of jobs + ```yaml + - name: Security - Mask secrets + run: | + set +x # Disable shell debugging + echo "::add-mask::${{ secrets.MY_SECRET }}" + ``` + +3. **Never** echo or log secrets + ```yaml + # ❌ BAD + - run: echo "API Key is ${{ secrets.API_KEY }}" + + # ✅ GOOD + - run: echo "API Key configured successfully" + ``` + +4. **Use** subshells for API calls that require secrets in URLs + ```bash + # ✅ GOOD - Subshell with disabled echoing + ( + set +x 2>/dev/null # Disable debugging + curl "https://api.example.com/data?apikey=${{ secrets.API_KEY }}" -o output.json + ) + ``` + +5. **Disable** shell debugging in sensitive operations + ```bash + set +x # Disable debugging + # sensitive operations here + ``` + +6. **Clear** credentials and temporary files after use + ```bash + git config --unset-all credential.helper + rm -rf .git/credentials + ``` + +7. **Use** `persist-credentials: false` when checking out code + ```yaml + - uses: actions/checkout@v6 + with: + persist-credentials: false + ``` + +### When Reviewing Pull Requests: + +1. Check for hardcoded secrets or credentials +2. Verify API keys are passed via headers, not URLs +3. Ensure shell debugging is disabled for sensitive operations +4. Confirm credentials are cleared after use +5. Look for accidental secret logging (`echo`, `print`, etc.) + +## Security Checklist for New Workflows + +- [ ] Secrets are passed via HTTP headers, not URL parameters +- [ ] Secrets are masked using `::add-mask::` at job start +- [ ] Shell debugging is disabled (`set +x`) +- [ ] No secrets are echoed or logged +- [ ] Git credentials are cleared after use (if applicable) +- [ ] `persist-credentials: false` is used for checkout (if applicable) +- [ ] Workspace is cleaned up after job completion (for self-hosted) +- [ ] Security scanning is included (if handling user content) +- [ ] Timeouts are set to prevent runaway jobs + +## Reporting Security Issues + +If you discover a security vulnerability in our workflows: + +1. **Do NOT** open a public issue +2. Follow the process in [SECURITY.md](../SECURITY.md) +3. Include details about the vulnerability +4. Suggest a fix if possible + +## References + +- [GitHub Actions Security Best Practices](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) +- [Using Secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) +- [Security Hardening for Self-Hosted Runners](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#hardening-for-self-hosted-runners) + +## Last Updated + +This document was last updated: 2026-02-15 + +--- + +**Note**: This is a living document. As new security measures are implemented or best practices evolve, this document will be updated accordingly. diff --git a/.github/actions/configure-docker/action.yml b/.github/actions/configure-docker/action.yml index 5c9531d35925..7ffd2508780f 100644 --- a/.github/actions/configure-docker/action.yml +++ b/.github/actions/configure-docker/action.yml @@ -1,5 +1,7 @@ name: 'Configure Docker' description: 'Set up Docker build driver and configure build cache args' +# Security: This action handles GitHub Actions cache tokens (ACTIONS_RUNTIME_TOKEN) +# which are automatically masked to prevent exposure in logs. inputs: cache-provider: description: 'gha or cirrus cache provider' @@ -28,6 +30,10 @@ runs: script: | Object.keys(process.env).forEach(function (key) { if (key.startsWith('ACTIONS_')) { + // Security: Mask the runtime token to prevent accidental exposure in logs + if (key === 'ACTIONS_RUNTIME_TOKEN') { + core.setSecret(process.env[key]); + } core.info(`Exporting ${key}`); core.exportVariable(key, process.env[key]); } diff --git a/.github/workflows/etherscan-apiv2.yml b/.github/workflows/etherscan-apiv2.yml index 9d8a3fa8f2f9..34db9369ede6 100644 --- a/.github/workflows/etherscan-apiv2.yml +++ b/.github/workflows/etherscan-apiv2.yml @@ -15,6 +15,16 @@ # This workflow is part of the kushmanmb-org/bitcoin repository # and is maintained by the repository owner and authorized contributors. # ═══════════════════════════════════════════════════════════════════ +# +# SECURITY NOTICE: +# This workflow handles sensitive API keys and follows security best practices: +# - API keys are constructed in subshells with command echoing disabled (set +x) +# - Secrets are masked using GitHub Actions ::add-mask:: command +# - Shell debugging (set -x) is explicitly disabled at job start +# - All API requests use HTTPS +# - No sensitive data is logged or exposed in workflow outputs +# - Temporary files with API responses are cleaned up immediately +# ═══════════════════════════════════════════════════════════════════ name: Etherscan API Integration (kushmanmb.eth) @@ -67,6 +77,16 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 + - name: Security - Disable shell debugging and mask secrets + run: | + # Ensure shell debugging is disabled to prevent secret exposure + set +x + # Mask API key if accidentally echoed (additional protection) + if [ -n "${{ secrets.ETHERSCAN_API_KEY }}" ]; then + echo "::add-mask::${{ secrets.ETHERSCAN_API_KEY }}" + fi + echo "✓ Security measures applied" + - name: Setup environment run: | echo "WORKFLOW_RUN_TIME=$(date -u +%Y-%m-%d-%H-%M-%S)" >> $GITHUB_ENV @@ -104,11 +124,18 @@ jobs: # Use Etherscan API to resolve ENS name echo "Attempting ENS resolution via Etherscan API V2..." - # Use Etherscan's ENS lookup API V2 with error handling - ENS_LOOKUP_URL="https://api.etherscan.io/v2/api?chainid=1&module=ens&action=getaddress&name=${ENS_NAME}&apikey=${{ secrets.ETHERSCAN_API_KEY }}" + # Security: Build the URL carefully to prevent API key exposure + # Etherscan requires apikey as URL parameter, so we must prevent logging + # Use set +x and build URL in a subshell to minimize exposure risk + ( + set +x 2>/dev/null # Disable command echoing + ENS_LOOKUP_URL="https://api.etherscan.io/v2/api?chainid=1&module=ens&action=getaddress&name=${ENS_NAME}&apikey=${{ secrets.ETHERSCAN_API_KEY }}" + + # Fetch with error handling - URL construction happens in subshell + curl -s -f "${ENS_LOOKUP_URL}" -o /tmp/ens_response.json + ) - # Fetch with error handling - if ! API_RESPONSE=$(curl -s -f "${ENS_LOOKUP_URL}"); then + if [ ! -f /tmp/ens_response.json ]; then echo "⚠️ Failed to connect to Etherscan API" echo "ENS_NAME=${ENS_NAME}" >> $GITHUB_ENV echo "ENS_RESOLVED=false" >> $GITHUB_ENV @@ -117,6 +144,8 @@ jobs: fi # Extract address with error handling + API_RESPONSE=$(cat /tmp/ens_response.json) + rm -f /tmp/ens_response.json RESOLVED_ADDRESS=$(echo "$API_RESPONSE" | jq -r '.result // empty' 2>/dev/null || echo "") # Check if we got a valid address (simplified condition) @@ -229,10 +258,13 @@ jobs: # Build API URL - All endpoints now use V2 API api_url="${BASE_URL}?chainid=1&module=${MODULE}&action=${ACTION}${PARAMS}" - # Make request with API key from secret - # Key is only exposed during curl execution, not in logs - curl -s "${api_url}&apikey=${{ secrets.ETHERSCAN_API_KEY }}" \ - -o data/etherscan/latest.json || { + # Security: Make request with API key as URL parameter (Etherscan requirement) + # Use subshell with disabled command echoing to prevent API key logging + ( + set +x 2>/dev/null # Disable command echoing + full_url="${api_url}&apikey=${{ secrets.ETHERSCAN_API_KEY }}" + curl -s "${full_url}" -o data/etherscan/latest.json + ) || { echo '{"status":"0","message":"API request failed"}' \ > data/etherscan/latest.json }