This post describes a method for securely passing workflow variables/files between jobs (or steps) using GPG encryption.
In summary, the following scenarios are covered:
- Securing a variable between various steps of the same job
- File encryption to securely pass data between different jobs
Prerequisites and Assumptions
Examples presented use the following values during the GPG key creation:
Passphrase: mypassphrase
Name: GPG GHA
Email: myemail@mydomain.comGenerate GPG Key
Generate a gpg key:
$ gpg --full-generate-keyRespond to the interactive questions, and enter a passphrase for the key when prompted.
gpg (GnuPG) 2.2.19; Copyright (C) 2019 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Please select what kind of key you want:
(1) RSA and RSA (default)
(2) DSA and Elgamal
(3) DSA (sign only)
(4) RSA (sign only)
(14) Existing key from card
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072) 4096
Requested keysize is 4096 bits
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 365
GnuPG needs to construct a user ID to identify your key.
Real name: GPG GHA
Email address: myemail@mydomain.com
Comment: GPG key for Actions
You selected this USER-ID:
"GPG GHA (GPG key for Actions) <myemail@mydomain.com>"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? oOnce the key has been generated, the following output should appear:
gpg: key ************** marked as ultimately trusted
gpg: revocation certificate stored as '/home/******/.gnupg/openpgp-revocs.d/******************************.rev'
public and secret key created and signed.
pub rsa4096 YYYY-MM-DD [SC] [expires: YYYY-MM-DD]
******************************
uid GPG GHA (GPG key for Actions) <myemail@mydomain.com>
sub rsa4096 YYYY-MM-DD [E] [expires: YYYY-MM-DD]Note: a key revocation certificate has been generated at the following target, i.e,:
gpg: revocation certificate stored as '/home/******/.gnupg/openpgp-revocs.d/******************************.rev'Check our keyring to ensure the public key has been created.
$ gpg --list-keysOutput:
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 3 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 3u
gpg: next trustdb check due at YYYY-MM-DD
/home/******/.gnupg/pubring.kbx
----------------------------
pub rsa4096 YYYY-MM-DD [SC] [expires: YYYY-MM-DD]
******************************
uid [ultimate] GPG GHA (GPG key for Actions) <myemail@mydomain.com>
sub rsa4096 YYYY-MM-DD [E] [expires: YYYY-MM-DD]List our secret key:
$ gpg --list-secret-keysOutput:
/home/******/.gnupg/pubring.kbx
----------------------------
sec rsa4096 YYYY-MM-DD [SC] [expires: YYYY-MM-DD]
******************************
uid [ultimate] GPG GHA (GPG key for Actions) <myemail@mydomain.com>
ssb rsa4096 YYYY-MM-DD [E] [expires: YYYY-MM-DD]Export Secret Key
Export the secret key to output file enc-private.key.
$ gpg --armor --export-secret-key myemail@mydomain.com > enc-private.keyDefining GPG Key Details in GitHub Actions Secrets
We can choose to setup the GPG credentials at either the repository, organization or enterprise level. For this exercise, we’ll perform the setup for a repository.
Using the sample values mentioned earlier, the following repository actions secrets are created:
| GPG_PASSPHRASE | mypassphrase |
| GPG_SECRET_KEY | GPG private key, (contents of file enc-private.key) |
| GPG_USER_EMAIL | myemail@mydomain.com |



We can now reference these values in our workflows using the secrets context:
${{ secrets.GPG_PASSPHRASE }}
${{ secrets.GPG_SECRET_KEY }}
${{ secrets.GPG_USER_EMAIL }}Sample 1: Encrypt/Decrypt within the Same Workflow Job
The first sample workflow gpg-example.yml contains a single job with the following steps:
id: encrypt
- generates a random 20-character string and stores value in variable named
random_secret
random_secret="$(head -n1 /dev/urandom | od -A n -x -N10 --endian=big | tr -d '\n' | tr -d ' ')"- encrypts the variable using our GPG key
id: decrypt
- decrypts the variable and uses it
gpg-example.yml
name: GPG Encrypt/Decrypt Sample Job
on:
workflow_dispatch:
jobs:
gpg-encrypt-decrypt:
runs-on: [self-hosted]
steps:
- name: Generate random secret and encrypt
id: encrypt
run: |
gpg --allow-secret-key-import \
--batch --import <<<"${{ secrets.GPG_SECRET_KEY }}"
echo -e "5\ny\n" | \
gpg --batch \
--command-fd 0 \
--edit-key ${{ secrets.GPG_USER_EMAIL }} \
trust quit
random_secret="$(head -n1 /dev/urandom | od -A n -x -N10 --endian=big | tr -d '\n' | tr -d ' ')"
random_secret_encrypt="$(echo "$random_secret" | \
gpg --encrypt \
--batch \
--pinentry-mode loopback \
-r "${{ secrets.GPG_USER_EMAIL }}" \
--passphrase "${{ secrets.GPG_PASSPHRASE }}" \
--armor | \
base64 --wrap 0 \
)"
echo "random_secret_encrypt=${random_secret_encrypt}" >> $GITHUB_OUTPUT
- name: Decrypt the secret and use it
id: decrypt
run: |
gpg --allow-secret-key-import \
--batch \
--import <<<"${{ secrets.GPG_SECRET_KEY }}"
random_secret_decrypt="$(echo "${{ steps.encrypt.outputs.random_secret_encrypt }}" | \
base64 --decode | \
gpg --no-tty \
--decrypt \
--batch \
--pinentry-mode loopback \
-r "${{ secrets.GPG_USER_EMAIL }}" \
--quiet \
--passphrase "${{ secrets.GPG_PASSPHRASE }}" \
)"
## Decrypted value can be used from here onwards
echo now use the decrypted variable '$random_secret_decrypt'Output log from a sample run is shown below:



The decrypted variable value is never echoed in output logs, even if the option “Enable debug logging” is selected when triggering the workflow.
Sample 2: File Encryption to Pass Data between Different Jobs
The workflow (share-files-between-jobs-gpg.yml) presented in this section consists of 2 jobs.
encrypt_file:
- uses the same command as the previous workflow to create a random 20 byte string
- saves output to file my-secrets.txt
head -n1 /dev/urandom | od -A n -x -N10 --endian=big | tr -d '\n' | tr -d ' ' > my-secrets.txt- our gpg key is then used to encrypt the contents into output file my-secrets.gpg
- encrypted file is then “passed” on to the next job using the actions/upload-artifact action
decrypt_file:
- uses the actions/download-artifact action to “receive” the encrypted file from the previous job
- decrypts the file using our gpg key
share-files-between-jobs-gpg.yml
name: GPG Share data between jobs
on:
workflow_dispatch:
jobs:
encrypt_file:
runs-on: self-hosted
steps:
- name: Generate GPG encrypted file
id: encrypt
run: |
gpg --allow-secret-key-import \
--batch --import <<<"${{ secrets.GPG_SECRET_KEY }}"
echo -e "5\ny\n" | \
gpg --batch \
--command-fd 0 \
--edit-key ${{ secrets.GPG_USER_EMAIL }} \
trust quit
if test -f my-secrets.txt; then
rm my-secrets.txt
fi
if test -f my-secrets.gpg; then
rm my-secrets.gpg
fi
head -n1 /dev/urandom | od -A n -x -N10 --endian=big | tr -d '\n' | tr -d ' ' > my-secrets.txt
gpg --encrypt \
--batch \
--recipient ${{ secrets.GPG_USER_EMAIL }} \
--passphrase ${{ secrets.GPG_PASSPHRASE }} \
-o my-secrets.gpg my-secrets.txt
- name: Send/upload encrypted file to next job
uses: actions/upload-artifact@v4
with:
name: my-secrets
path: my-secrets.gpg
decrypt-file:
needs: encrypt_file
runs-on: self-hosted
steps:
- name: Download encrypted file
uses: actions/download-artifact@v4
with:
name: my-secrets
- name: Decrypt the file and use it
id: decrypt
run: |
gpg --allow-secret-key-import \
--batch \
--import <<<"${{ secrets.GPG_SECRET_KEY }}"
random_secret_decrypt="$(gpg \
--no-tty \
--decrypt \
--batch \
--quiet \
--pinentry-mode loopback \
--recipient ${{ secrets.GPG_USER_EMAIL }} \
--quiet \
--passphrase ${{ secrets.GPG_PASSPHRASE }} \
my-secrets.gpg 2>/dev/null \
)"
## Decrypted value can be used from here onwards
echo now use the decrypted variable $random_secret_decrypt