Skip to content
Draft
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
151 changes: 151 additions & 0 deletions .github/SECURITY_KEYS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Security Keys & GPG Signature Guide

This document explains how to set up GPG signing for this repository, configure the required GitHub Secrets, and verify signatures created by the automated workflow.

---

## Table of Contents

1. [Prerequisites](#prerequisites)
2. [Generate a GPG Key](#generate-a-gpg-key)
3. [Configure GitHub Secrets](#configure-github-secrets)
4. [Local Git Signing Setup](#local-git-signing-setup)
5. [Verifying File Signatures](#verifying-file-signatures)
6. [Workflow Behaviour](#workflow-behaviour)

---

## Prerequisites

- GPG (GnuPG) installed locally — `gpg --version`
- Repository admin access to configure GitHub Secrets

---

## Generate a GPG Key

If you do not yet have a GPG key (or need a new one):

```bash
gpg --full-generate-key
```

Select **RSA and RSA**, key size **4096**, and a suitable expiry. When prompted, enter your name and email.

List your keys to find the key ID:

```bash
gpg --list-secret-keys --keyid-format=long
```

Example output:

```
sec rsa4096/B732B308C0FE0BB3 2024-01-01 [SC]
C8040559438A554CAD747154B732B308C0FE0BB3
uid [ultimate] Your Name <you@example.com>
```

The key ID is `B732B308C0FE0BB3`.

---

## Configure GitHub Secrets

Two secrets are required by the workflow:

| Secret name | Value |
|-------------------|-----------------------------------------------|
| `GPG_PRIVATE_KEY` | ASCII-armored exported private key (see below)|
| `GPG_PASSPHRASE` | Passphrase protecting the private key |

### Export the private key

```bash
gpg --armor --export-secret-keys B732B308C0FE0BB3
```

Copy the full output (including the `-----BEGIN PGP PRIVATE KEY BLOCK-----` header and footer) and paste it as the value of the `GPG_PRIVATE_KEY` secret.

### Add secrets to GitHub

1. Open your repository on GitHub.
2. Go to **Settings → Secrets and variables → Actions**.
3. Click **New repository secret** for each secret above.

---

## Local Git Signing Setup

Run the provided helper script to configure your local environment:

```bash
bash .github/scripts/setup-gpg.sh
```

Or configure manually:

```bash
git config --global user.signingkey B732B308C0FE0BB3
git config --global commit.gpgsign true
git config --global tag.gpgsign true
```

---

## Verifying File Signatures

Each signed file will have a corresponding `.gpg.sig` file in the same directory.

### Import the signer's public key

```bash
gpg --import gpg-public-key.asc
```

Or fetch it directly from a key server:

```bash
gpg --keyserver keyserver.ubuntu.com --recv-keys B732B308C0FE0BB3
```

### Verify a single file

```bash
gpg --verify path/to/file.sh.gpg.sig path/to/file.sh
```

A successful verification looks like:

```
gpg: Signature made Mon 01 Jan 2024 00:00:00 UTC
gpg: using RSA key B732B308C0FE0BB3
gpg: Good signature from "Your Name <you@example.com>"
```

### Verify all signatures in the repository

```bash
find . -name '*.gpg.sig' | while read -r sig; do
original="${sig%.gpg.sig}"
echo "Verifying: $original"
gpg --verify "$sig" "$original" && echo " OK" || echo " FAILED"
done
```

---

## Workflow Behaviour

The **GPG Sign Modified Files** workflow (`.github/workflows/gpg-sign-files.yml`) runs automatically on every pull request event (`opened`, `synchronize`, `reopened`).

1. Identifies files modified in the PR that match these extensions:
- `.sh` — shell scripts
- `.py` — Python scripts
- `.yml` / `.yaml` — YAML configuration files
- `.tf` — Terraform files
2. Signs each matching file with a detached ASCII-armored GPG signature.
3. Commits the resulting `.gpg.sig` files back to the PR branch.
4. Posts a comment to the PR summarising which files were signed.

Files and directories listed in [`.gpg-ignore`](../.gpg-ignore) at the repository root are excluded from signing.
65 changes: 65 additions & 0 deletions .github/scripts/setup-gpg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# setup-gpg.sh
# Helper script to configure a local GPG environment for code signing.
# Run this once on your local machine before signing commits or files.
#
# Usage: bash .github/scripts/setup-gpg.sh

set -euo pipefail

echo "=== GPG Local Setup ==="

# ── 1. Check prerequisites ──────────────────────────────────────────────────
if ! command -v gpg &>/dev/null; then
echo "ERROR: gpg is not installed. Install it and re-run this script." >&2
exit 1
fi

# ── 2. List existing secret keys ────────────────────────────────────────────
echo ""
echo "Existing secret keys:"
gpg --list-secret-keys --keyid-format=long || true

# ── 3. Optional: import a key from file ─────────────────────────────────────
if [[ -n "${GPG_KEY_FILE:-}" ]]; then
echo ""
echo "Importing key from: $GPG_KEY_FILE"
gpg --import "$GPG_KEY_FILE"
fi

# ── 4. Select the signing key ────────────────────────────────────────────────
echo ""
if [[ -z "${GPG_KEY_ID:-}" ]]; then
echo "Enter the GPG key ID you want to use for signing"
echo "(e.g. the 16-character hex ID shown above):"
read -r GPG_KEY_ID
fi

if [[ -z "$GPG_KEY_ID" ]]; then
echo "ERROR: No GPG key ID provided." >&2
exit 1
fi

echo "Using key: $GPG_KEY_ID"

# ── 5. Configure git to sign commits with this key ──────────────────────────
git config --global user.signingkey "$GPG_KEY_ID"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
echo "git configured to sign commits and tags with key $GPG_KEY_ID"

# ── 6. Export the public key (for sharing / uploading to GitHub) ─────────────
PUB_KEY_FILE="gpg-public-key.asc"
gpg --armor --export "$GPG_KEY_ID" > "$PUB_KEY_FILE"
echo ""
echo "Public key exported to: $PUB_KEY_FILE"
echo "Upload this file's contents to GitHub → Settings → SSH and GPG keys."

# ── 7. Export the private key (for adding to GitHub Secrets) ─────────────────
echo ""
echo "To export your private key for use as the GPG_PRIVATE_KEY GitHub Secret, run:"
echo " gpg --armor --export-secret-keys $GPG_KEY_ID"
echo ""
echo "Then add the output as the GPG_PRIVATE_KEY secret in your repository settings."
echo ""
echo "=== Setup complete ==="
73 changes: 73 additions & 0 deletions .github/scripts/sign-files.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# sign-files.sh
# Signs a single file with GPG and stores the detached signature alongside it.
# Usage: sign-files.sh <file>
#
# Required environment variables:
# GPG_KEY_ID - Key ID to use for signing (set by the workflow)
# GPG_PASSPHRASE - Passphrase for the GPG private key

set -euo pipefail

FILE="${1:-}"

if [[ -z "$FILE" ]]; then
echo "Usage: $0 <file>" >&2
exit 1
fi

if [[ ! -f "$FILE" ]]; then
echo "File not found: $FILE" >&2
exit 1
fi

# Check whether this file matches a .gpg-ignore pattern
if [[ -f ".gpg-ignore" ]]; then
while IFS= read -r pattern || [[ -n "$pattern" ]]; do
# Skip blank lines and comments
[[ -z "$pattern" || "$pattern" == \#* ]] && continue
# shellcheck disable=SC2053
if [[ "$FILE" == $pattern ]]; then
echo "Skipping ignored file: $FILE"
exit 0
fi
done < ".gpg-ignore"
fi

SIG_FILE="${FILE}.gpg.sig"

echo "Signing: $FILE"

# Build the gpg signing command
GPG_SIGN_ARGS=(
--batch
--yes
--detach-sign
--armor
--output "$SIG_FILE"
)

if [[ -n "${GPG_KEY_ID:-}" ]]; then
GPG_SIGN_ARGS+=(--local-user "$GPG_KEY_ID")
fi

if [[ -n "${GPG_PASSPHRASE:-}" ]]; then
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
"${GPG_SIGN_ARGS[@]}" "$FILE"
else
gpg "${GPG_SIGN_ARGS[@]}" "$FILE"
fi

# Verify the signature was created successfully
if [[ -f "$SIG_FILE" ]]; then
echo "Signature created: $SIG_FILE"
if gpg --verify "$SIG_FILE" "$FILE"; then
echo "Signature verified successfully."
else
echo "Signature verification FAILED for $FILE" >&2
exit 1
fi
else
echo "ERROR: Signature file was not created for $FILE" >&2
exit 1
fi
107 changes: 107 additions & 0 deletions .github/workflows/gpg-sign-files.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: GPG Sign Modified Files

on:
pull_request:
types: [opened, synchronize, reopened]

permissions:
contents: write
pull-requests: write

jobs:
sign-files:
name: Sign Modified Files with GPG
runs-on: ubuntu-latest

steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Install GPG
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq gnupg

- name: Import GPG private key
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
# Trust the imported key ultimately
KEY_ID=$(gpg --list-secret-keys --keyid-format=long \
| grep '^sec' | head -n1 \
| awk '{print $2}' | cut -d'/' -f2)
echo "${KEY_ID}:6:" | gpg --import-ownertrust
echo "GPG_KEY_ID=$KEY_ID" >> "$GITHUB_ENV"

- name: Get list of modified files
id: changed-files
run: |
BASE_SHA=${{ github.event.pull_request.base.sha }}
HEAD_SHA=${{ github.event.pull_request.head.sha }}
CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" \
| grep -E '\.(sh|py|yml|yaml|tf)$' || true)
echo "Changed files matching target extensions:"
echo "$CHANGED"
# Store as newline-delimited list in an env file
{
echo "FILES<<EOF"
echo "$CHANGED"
echo "EOF"
} >> "$GITHUB_ENV"

- name: Sign modified files
if: env.FILES != ''
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
chmod +x .github/scripts/sign-files.sh
echo "$FILES" | tr '\n' '\0' | xargs -0 -I{} \
bash -c '[ -n "{}" ] && .github/scripts/sign-files.sh "{}"'

- name: Commit signatures
if: env.FILES != ''
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add '*.gpg.sig'
if git diff --cached --quiet; then
echo "No new or changed signatures to commit."
else
git commit -m "chore: add/update GPG signatures for modified files [skip ci]"
git push origin HEAD:${{ github.head_ref }}
fi

- name: Post signature report to PR
if: env.FILES != ''
uses: actions/github-script@v7
with:
script: |
const files = process.env.FILES.split('\n').filter(Boolean);
const sigLines = files.map(f => `- \`${f}\` → \`${f}.gpg.sig\``).join('\n');
const body = [
'## 🔏 GPG Signature Report',
'',
'The following files were signed with GPG on this PR:',
'',
sigLines,
'',
'To verify a signature locally, run:',
'```bash',
'gpg --verify <file>.gpg.sig <file>',
'```',
'',
'See [`.github/SECURITY_KEYS.md`](.github/SECURITY_KEYS.md) for full verification instructions.',
].join('\n');

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
Loading