Import Maven signing keys into an isolated temporary GPG home · Issue #1211 · actions/setup-java · GitHub
Skip to content

Import Maven signing keys into an isolated temporary GPG home #1211

Closed

Description

@jdubois

Description:

When gpg-private-key is provided, setup-java imports the key into the runner's default GPG keyring and later tries to remove one parsed fingerprint in the post action.

The current implementation:

  • writes the armored private key to a fixed $RUNNER_TEMP/private-key.asc path without an explicit restrictive file mode;
  • removes that file only after a successful gpg --import, so an import failure can leave the plaintext private key behind;
  • imports into the default GPG home rather than an action-owned keyring;
  • extracts only the first value matching a fixed 40-character fingerprint regex;
  • stores that one fingerprint for post-action deletion.

setup-java/src/gpg.ts

Lines 9 to 67 in ab597f9

The fingerprint is saved here:

setup-java/src/auth.ts

Lines 53 to 57 in ab597f9

if (gpgPrivateKey) {
core.info('Importing private gpg key');
const keyFingerprint = (await gpg.importKey(gpgPrivateKey)) || '';
core.saveState(constants.STATE_GPG_PRIVATE_KEY_FINGERPRINT, keyFingerprint);
}

The post action deletes that fingerprint from the default keyring here:

async function removePrivateKeyFromKeychain() {
if (core.getInput(constants.INPUT_GPG_PRIVATE_KEY, {required: false})) {
core.info('Removing private key from keychain');
try {
const keyFingerprint = core.getState(
constants.STATE_GPG_PRIVATE_KEY_FINGERPRINT
);
await gpg.deleteKey(keyFingerprint);
} catch (error) {
core.setFailed(
`Failed to remove private key due to: ${(error as Error).message}`
);
}
}

This creates several lifecycle and ownership problems, especially on persistent self-hosted runners or jobs that invoke setup-java more than once:

  1. If the same key already existed, setup-java's post action can delete user-managed key material that it did not create.
  2. If the input contains multiple primary keys, only one fingerprint is retained and the remaining imported keys can be left in the default keyring.
  3. If import fails, the fixed private-key file is not removed because cleanup is not in a finally block.
  4. A missing or unrecognized fingerprint is saved as an empty string, after which the post action still attempts key deletion.
  5. Multiple action/composite-action invocations can interfere with one another by importing and deleting keys from the same keyring.

The action already uses an isolated temporary GPG home with finally cleanup for package-signature verification, providing a useful implementation pattern:

setup-java/src/gpg.ts

Lines 69 to 123 in ab597f9

Task version:

actions/setup-java@ab597f914a6894678251803485c09f413263b889 (main, unreleased v6 development)

Platform:

  • Ubuntu
  • macOS
  • Windows

Runner type:

  • Hosted
  • Self-hosted

The impact is highest on persistent self-hosted runners, but repeated action invocations can expose the cleanup problem on hosted runners too.

Repro steps:

Pre-existing key deletion:

  1. On a self-hosted runner, import a private key into the runner user's default GPG keyring.
  2. Run setup-java with the same key supplied through gpg-private-key.
  3. After the setup-java post action, run gpg --list-secret-keys <fingerprint>.
  4. The key can be absent even though it existed before setup-java ran.

Incomplete multi-key cleanup:

  1. Export two primary private keys into one armored input, for example with gpg --armor --export-secret-keys <key1> <key2>.
  2. Supply that value through gpg-private-key.
  3. After the post action, inspect the default secret keyring.
  4. Only the first regex-matched fingerprint was recorded for deletion, so other imported key material can remain.

Import-failure cleanup:

  1. Supply malformed or otherwise non-importable private-key data.
  2. Let gpg --import fail.
  3. Inspect $RUNNER_TEMP/private-key.asc on the persistent runner.
  4. The file can remain because deletion occurs only after a successful import.

Expected behavior:

setup-java should not modify or delete pre-existing keys in the runner's default keyring, and private-key material should be removed reliably on both success and failure.

Actual behavior:

setup-java imports into the shared default keyring, tracks only one parsed fingerprint, and relies on fingerprint deletion to undo changes. This cannot distinguish action-owned keys from pre-existing keys and does not cover every failure or multi-key case.

Suggested fix:

  • Create an action-owned temporary GNUPGHOME with restrictive permissions.
  • Import gpg-private-key only into that isolated keyring.
  • Export GNUPGHOME so Maven/GPG commands in subsequent steps use the isolated keyring.
  • Store the owned GPG-home path as post-action state and remove the complete directory during cleanup instead of deleting fingerprints from the default keyring.
  • Use a unique private-key file with mode 0600, deleted in finally, or import without persisting the key to disk.
  • Make cleanup idempotent and safe across repeated setup-java invocations.
  • Add tests covering pre-existing matching keys, multiple imported keys, import failure, repeated invocations, and missing cleanup state.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions