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
109 changes: 109 additions & 0 deletions .github/workflows/secret-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: Secret Scan

on:
push:
branches:
- main
- 'release/**'
pull_request:
branches:
- main
- 'release/**'

permissions:
contents: read
pull-requests: write

jobs:
gitleaks-history-scan:
name: Full Git history secret scan
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Run GitLeaks full history scan
id: gitleaks-scan
continue-on-error: true
uses: docker://zricethezav/gitleaks:latest
with:
args: detect --source . --config .gitleaks.toml --baseline .gitleaks.baseline.toml --report-format json --report-path gitleaks-report.json --verbose

- name: Upload GitLeaks report
if: always()
uses: actions/upload-artifact@v4
with:
name: gitleaks-report
path: gitleaks-report.json

- name: Notify Slack on secret detection
if: steps.gitleaks-scan.outcome == 'failure' && env.SLACK_WEBHOOK_URL != ''
env:
SLACK_WEBHOOK_URL: ${{ secrets.SECRET_SCAN_SLACK_WEBHOOK_URL }}
REPO: ${{ github.repository }}
REF: ${{ github.ref }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
if [ -z "$SLACK_WEBHOOK_URL" ]; then
echo "SLACK_WEBHOOK_URL is not set; skipping Slack notification."
exit 0
fi

payload=$(jq -n --arg text "GitLeaks detected potential secrets in \`$REPO\` on \`$REF\`. <${RUN_URL}|View the workflow run>." '{text: $text}')
curl -sS -X POST -H 'Content-Type: application/json' --data "$payload" "$SLACK_WEBHOOK_URL"

- name: Extract leaked Stellar secrets
if: steps.gitleaks-scan.outcome == 'failure'
id: stellar-leak
run: |
leak_count=$(jq '[.[] | select(.rule_id == "stellar-secret-key")] | length' gitleaks-report.json)
echo "leak_count=$leak_count" >> "$GITHUB_OUTPUT"
if [ "$leak_count" -gt 0 ]; then
leaked_key=$(jq -r '[.[] | select(.rule_id == "stellar-secret-key")][0].strings[0]' gitleaks-report.json)
echo "leaked_key=$leaked_key" >> "$GITHUB_OUTPUT"
fi

- name: Revoke leaked Stellar key
if: steps.stellar-leak.outputs.leak_count != '' && steps.stellar-leak.outputs.leak_count != '0'
env:
STELLAR_HORIZON_URL: ${{ secrets.STELLAR_HORIZON_URL }}
STELLAR_ROTATION_SIGNER_SECRET: ${{ secrets.STELLAR_ROTATION_SIGNER_SECRET }}
run: |
npm ci --ignore-scripts
bash scripts/revoke-stellar-key.sh --leaked-key "${{ steps.stellar-leak.outputs.leaked_key }}"

- name: Comment on PR with remediation steps
if: steps.gitleaks-scan.outcome == 'failure' && github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.pull_request.number }}
body: |
⚠️ **Secret scan detected potential sensitive data in this PR.**

Please remove the exposed secret from the branch and rewrite history before merging.

Recommended remediation steps:
1. Identify the leaked file(s) in the GitLeaks report.
2. Remove the secret from the file and commit the fix.
3. Rewrite history to remove the secret from earlier commits using `git filter-repo` or `git filter-branch`.
- Example with `git filter-repo`:
```bash
git filter-repo --path <file> --invert-paths
```
- Example with `git filter-branch`:
```bash
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch <file>' --prune-empty --tag-name-filter cat -- --all
```
4. Force-push the cleaned branch and create a new PR.

If this detection includes a Stellar secret key, the CI workflow will attempt automated key rotation if configured.

- name: Fail build when secrets are detected
if: steps.gitleaks-scan.outcome == 'failure'
run: |
echo "GitLeaks detected secrets in repository history. Failing job."
exit 1
10 changes: 10 additions & 0 deletions .gitleaks.baseline.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
title = "SubTrackr GitLeaks baseline"
description = "Allowlist for known false positives and approved non-secret patterns."

# Add known false positives here to keep GitLeaks scans actionable.
# Example:
# [[allowlist]]
# description = "Known false positive sample data in documentation"
# regex = '''EXAMPLE_BASE64_PLACEHOLDER'''
# paths = ["docs/SECURITY.md"]

36 changes: 36 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
title = "SubTrackr GitLeaks configuration"
description = "Detect Stellar secrets, API keys, JWTs, private keys, and high-entropy data."

[[rules]]
id = "stellar-secret-key"
description = "Stellar secret key starting with S and 56 characters"
regex = '''S[A-Z2-7]{55}'''
tags = ["stellar", "secret"]

[[rules]]
id = "stripe-secret-key"
description = "Stripe API secret key"
regex = '''sk_(live|test)_[0-9A-Za-z]{24,}'''
tags = ["api-key", "stripe"]

[[rules]]
id = "jwt-token"
description = "JSON Web Token"
regex = '''eyJ[A-Za-z0-9_-]{10,}.[A-Za-z0-9_-]{10,}.[A-Za-z0-9_-]{10,}'''
tags = ["jwt", "token"]
entropy = 4.5
entropyAlgorithm = "shannon"

[[rules]]
id = "pem-private-key"
description = "PEM private key block"
regex = '''-----BEGIN (RSA|EC|OPENSSH|PRIVATE) PRIVATE KEY-----'''
tags = ["private-key", "pem"]

[[rules]]
id = "high-entropy-encoded-data"
description = "High-entropy base64-like string or binary secret"
regex = '''[A-Za-z0-9+/]{25,}={0,2}'''
tags = ["entropy", "binary"]
entropy = 4.5
entropyAlgorithm = "shannon"
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
NODE_OPTIONS=--max-old-space-size=8192 npx lint-staged --concurrent false
bash scripts/pre-commit-gitleaks.sh
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
minimum_pre_commit_version: '2.19.0'

repos:
- repo: local
hooks:
- id: gitleaks-staged
name: GitLeaks staged secret scan
entry: bash scripts/pre-commit-gitleaks.sh
language: system
pass_filenames: false
stages: [commit]
55 changes: 55 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Security and Secret Management

This guide describes how SubTrackr protects sensitive credentials and how contributors should manage secrets.

## Secret Scanning

SubTrackr now enforces GitLeaks-based secret scanning in both local pre-commit hooks and CI.

- `.gitleaks.toml`: GitLeaks configuration for Stellar secrets, API keys, JWTs, private keys, and high-entropy binary data.
- `.pre-commit-config.yaml`: Captures staged file scans before commit.
- `.github/workflows/secret-scan.yml`: Scans full git history on pushes to `main` and release branches.
- `.gitleaks.baseline.toml`: Known false positives are allowed via baseline exceptions.

## Contributor Workflow

1. Install repo hooks:
```bash
npm install
npx husky install
npx pre-commit install
```
2. Before committing, staged files are scanned automatically.
3. If GitLeaks detects a secret, fix the offending file before committing.

## What is scanned

- Stellar secret keys beginning with `S` and 56-character encoded payload.
- Stripe-style API keys such as `sk_live_...` and `sk_test_...`.
- JWT tokens like `eyJ...`.
- PEM private key blocks (`BEGIN RSA PRIVATE KEY`, `BEGIN EC PRIVATE KEY`, etc.).
- High-entropy base64-like strings or binary content with entropy above 4.5 bits/byte.

## CI Detection and Alerting

The CI workflow uses GitLeaks Docker image to run a full repository scan.

On detection:
- A Slack alert is sent to the configured security webhook.
- If a Stellar secret is found, the workflow attempts key rotation with `scripts/revoke-stellar-key.sh`.
- Pull requests receive an automated comment with remediation instructions.

## Remediation

If a secret is found in history:
1. Remove it from the file.
2. Rewrite git history to eliminate committed exposure.
3. Force-push the cleaned branch.
4. Rotate the exposed credentials in the affected system.

## Best Practices

- Never store secrets in source control.
- Use environment variables or secrets management tools.
- Rotate leaked keys immediately.
- Audit Git history regularly and keep `.gitleaks.baseline.toml` updated with only verified false positives.
21 changes: 21 additions & 0 deletions scripts/pre-commit-gitleaks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"

run_gitleaks() {
if command -v gitleaks >/dev/null 2>&1; then
gitleaks detect --source . --staged --config .gitleaks.toml --baseline .gitleaks.baseline.toml
elif command -v npx >/dev/null 2>&1; then
npx gitleaks detect --source . --staged --config .gitleaks.toml --baseline .gitleaks.baseline.toml
elif command -v docker >/dev/null 2>&1; then
docker run --rm -v "$REPO_ROOT":/repo -w /repo zricethezav/gitleaks:latest detect --source . --staged --config .gitleaks.toml --baseline .gitleaks.baseline.toml
else
echo "Error: gitleaks CLI is not installed, and Docker is unavailable."
echo "Install gitleaks or Docker to enable the pre-commit secret scan."
return 1
fi
}

run_gitleaks
90 changes: 90 additions & 0 deletions scripts/revoke-stellar-key.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat <<EOF
Usage: $0 --leaked-key <stellar-secret-key>

Automatically rotate a leaked Stellar secret key by creating a new signer key pair.
Requires:
STELLAR_HORIZON_URL
STELLAR_ROTATION_SIGNER_SECRET

EOF
exit 1
}

LEAKED_KEY=""
while [[ $# -gt 0 ]]; do
case "$1" in
--leaked-key)
LEAKED_KEY="$2"
shift 2
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done

if [[ -z "$LEAKED_KEY" ]]; then
echo "Error: --leaked-key is required"
usage
fi

if [[ -z "${STELLAR_HORIZON_URL:-}" ]]; then
echo "Error: STELLAR_HORIZON_URL must be set"
exit 1
fi

if [[ -z "${STELLAR_ROTATION_SIGNER_SECRET:-}" ]]; then
echo "Error: STELLAR_ROTATION_SIGNER_SECRET must be set"
exit 1
fi

command -v python3 >/dev/null 2>&1 || { echo "python3 is required"; exit 1; }

python3 <<'PY'
import os
import sys
from stellar_sdk import Keypair, Server, TransactionBuilder, Network

leaked_key = os.environ['LEAKED_KEY']
horizon_url = os.environ['STELLAR_HORIZON_URL']
signer_secret = os.environ['STELLAR_ROTATION_SIGNER_SECRET']

if not leaked_key.startswith('S'):
print('Invalid Stellar secret key format', file=sys.stderr)
sys.exit(1)

old_kp = Keypair.from_secret(leaked_key)
new_kp = Keypair.random()

server = Server(horizon_url=horizon_url)
account = server.load_account(old_kp.public_key)
base_fee = server.fetch_base_fee()
network_passphrase = Network.PUBLIC_NETWORK_PASSPHRASE

if 'testnet' in horizon_url:
network_passphrase = Network.TESTNET_NETWORK_PASSPHRASE

transaction = (
TransactionBuilder(
source_account=account,
network_passphrase=network_passphrase,
base_fee=base_fee,
)
.append_set_options_op(master_weight=0, low_threshold=1, med_threshold=1, high_threshold=1)
.append_set_options_op(signer={'ed25519_public_key': new_kp.public_key, 'weight': 1})
.set_timeout(180)
.build()

transaction.sign(old_kp)
transaction.sign(signer_secret)
response = server.submit_transaction(transaction)
print('Rotated Stellar secret key successfully:')
print('New public key:', new_kp.public_key)
print('New secret key:', new_kp.secret)
print('Transaction hash:', response['hash'])
PY