How to Encrypt Artifacts Before Upload in GitHub Actions
Artifacts are downloadable by anyone with repo access, so anything sensitive must be encrypted before it leaves the step.
Encrypt the file with symmetric GPG using a passphrase from secrets, upload the ciphertext, and decrypt only where needed.
Steps
- Produce the file you need to protect.
- Encrypt it with gpg symmetric using a passphrase stored in secrets.
- Upload only the encrypted .gpg file as the artifact.
- Decrypt it later with the same passphrase where it is consumed.
Workflow
name: Encrypt Artifact
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: make secret-bundle
- run: |
gpg --batch --yes --symmetric --cipher-algo AES256 \
--passphrase "${{ secrets.ARTIFACT_KEY }}" bundle.tar
- uses: actions/upload-artifact@v4
with:
name: bundle
path: bundle.tar.gpgNotes
- Never upload the plaintext alongside the ciphertext, or the encryption buys you nothing.
- Latchkey managed runners run these encrypt-and-upload jobs cheaper and self-heal mid-run.
Verify it actually works
A workflow that runs is not a workflow that works. Confirm the behaviour on a real event rather than on a manual dispatch, because trigger conditions, permissions, and context values all differ between the two.
# 1. validate the file before pushing
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color
# 2. trigger the real event, not workflow_dispatch
git commit --allow-empty -m "ci: verify trigger" && git push
# 3. watch it and read the conclusion, not just the colour
gh run watch
gh run view --log-failedWhat usually goes wrong first
- The workflow file must exist on the default branch before scheduled or dispatch triggers appear at all.
GITHUB_TOKENpermissions default to read-only in many organisations. Declare apermissions:block listing every scope the job needs.- Fork pull requests get a read-only token and no access to secrets, regardless of workflow configuration.
actions/checkoutgives you depth 1 on a detached HEAD, so anything needing history or a branch name needsfetch-depth: 0.