Continuous Deployment to Maven Central with Apache Maven

Stephen Connolly's picture

Introduction

A few months back I wrote about how we use Apache Maven for Continuous Delivery/Deployment. In that example, we are publishing to CloudBees’ internal repository. One of the major powers behind Apache Maven has been its associated central repository as a means of exchanging artifacts between organizations and open source projects.

One of the current requirements for artifact hosting on Maven Central is that the artifacts have an associated GPG signature from a published key.

Now while we could argue about the utility of this key:

  • How would you know which keys were the correct keys to check each artifact against?
  • Does anyone even check that the downloaded artifacts match the key signature?
  • Etc.

The reality is that right now you need a GPG signature to get your artifact into Maven Central.

When you are manually releasing artifacts for just your own projects to Maven Central, meeting the GPG signature requirements should not be overly difficult:

  1. Generate the GPG key on your machine
  2. Configure the pom.xml to use that key

    <plugin>
      <artifactId>maven-gpg-plugin</artifactId>
      <configuration>
        ...
        <keyname>...</keyname>
        ...
      </configuration>
    </plugin>
    
  3. Publish the key to an appropriate key registry
  4. Success!

If you have any issues, my experience has been that Sonatype has been only too happy to help.

But what happens if you want to have your favorite CI/CD server run the deployments?

Well, when that happens you will run headlong straight into all the protections that the GPG project have added to prevent automated tools from signing things with GPG keys!

Honestly I had this really funny GIF of an idiot running straight into a brick wall... you know the kind of funny where you laugh so much it actually hurts... but we couldn't get the usage rights so instead all we have is this silly stick-person

You might feel like giving up at that stage… well I’ve got your back. Here are two techniques you can use to automate a deployment to Maven Central.

Option 1: Don’t use the GPG tooling

GPG signatures follow a specification (just don’t ask me for the link to that specification… I’m reliably informed that there is a specification… I’m hoping that the specification is not “the code”)

If the GPG tooling itself is being hostile to automation, perhaps we could use different tooling.

That is exactly the approach my colleague Kohsuke came up with when he wrote pgp-maven-plugin

To use this plugin you just:

  1. Disable the Maven GPG plugin in your pom.xml (by removing it or by configuring it to skip)

    <plugin>
      <artifactId>maven-gpg-plugin</artifactId>
      <configuration>
        <skip>true</skip>
      </configuration>
    </plugin>
  2. Add the PGP Maven plugin to your pom.xml

    <plugin>
      <groupId>org.kohsuke</groupId>
      <artifactId>pgp-maven-plugin</artifactId>
      <executions>
        <execution>
          <goals>
            <goal>sign</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  3. Have your build server set the PGP_SECRETKEY and PGP_PASSPHRASE environment variables to keyfile:path/to/key.asc and literal:your-key-secret-passphrase respectively 
  4. Run Maven

The main issue I have with this approach is that either your automated builds are now different from manual builds or you need to set the appropriate environment variables locally. If - like me - you have multiple projects that each use different keys, this becomes really annoying 

Option 2: Configure GPG to allow headless signing

The GPG project does not make it impossible to have headless signing… it just makes it non-obvious because they want to ensure it is something you really want to do.

What you need to do is create an archive file with the content of the GPG HOME directory that has just the configuration files and only the keys you require

To start, you’re going to need the following gpg.conf file which tells GPG to use an agent and enables loopback pin entry

use-agent 
pinentry-mode loopback

Next we configure GPG Agent to allow loopback pin entry with the gpg-agent.conf file

allow-loopback-pinentry

Here’s a worked example of setting up the key 

This assumes that the identifier of the key you want to use is stored in $KEY_ID

  1. Start by changing to a temporary directory (as we do not want to affect your normal ~/.gnupg home directory:

    cd /tmp
  2. Make a directory called .gnupg to make extracting the archive easier:

    mkdir .gnupg
  3. Change the permissions so that GPG will allow you to use it as a home:

    chmod 700 .gnupg
  4. Create the gpg.conf file:

    cat > gpg.conf << EOF
    use-agent
    pinentry-mode loopback
    EOF
  5. Create the gpg-agent.conf file:

    cat > gpg-agent.conf << EOF
    allow-loopback-pinentry
    EOF
  6. Export the secret key from your ~/.gnupg home and import it into the home that the CI/CD server will use:

    gpg --export-secret-key -a $KEY_ID | GNUPGHOME=/tmp/.gnupg/ gpg --allow-secret-key-import --import
  7. Modify the newly imported key to be trusted and use a different passphrase:

    GNUPGHOME=/tmp/.gnupg/ gpg --edit-key $KEY_ID
    trust
    5
    y
    passwd
    secret-passphrase
    secret-passphrase
    quit
  8. Create the archive of only the required files:

    tar -czvf gnupg.home.tar.gz -C .. .gnupg/gpg.conf .gnupg/gpg-agent.conf .gnupg/trustdb.gpg .gnupg/private-keys-v1.d

Once you have this archive you can upload both it and the secret passphrase you used as secrets for your CI/CD server. For example, in Jenkins, you would upload use the passphrase as a secret text and the file as a secret file.

Then you can use this file like so:

export GNUPGHOME=...
gpgconf --kill gpg-agent || true
rm -rf "${GNUPGHOME}"
mkdir -p "$(dirname "${GNUPGHOME}")"
tar -xzvof "${GNUPGHOME_ARCHIVE}" -d "$(dirname "${GNUPGHOME}")"
gpgconf --launch gpg-agent

...

mvn release:prepare release:perform

...

gpgconf --kill gpg-agent || true
rm -rf "${GNUPGHOME}"

You will also need to configure the Maven GPG plugin to use the agent

<plugin>
  <artifactId>maven-gpg-plugin</artifactId>
  <configuration>
    <useAgent>true</useAgent>
    <passphrase>${env.GPG_PASSPHRASE}</passphrase>
    <gpgArguments>
      <arg>--batch</arg>
      <arg>--pinentry-mode</arg>
      <arg>loopback</arg>
    </gpgArguments>
  </configuration>
</plugin>

If you want a real-life example, you can see this Jenkinsfile and pom.xml I use for one of my side-projects - which combines the GPG archive with the continuous deployment from my older blog post.

Security concerns

Irrespective of which option you pick, you are going to be storing the GPG key on your CI/CD server, so you should take appropriate steps to ensure that the secrets are suitably secure. For example, the Jenkins instance I use for deployment - while hosted on a third party cloud - is only accessible behind a VPN as opposed to being accessible from the public internet.

Stephen ConnollyStephen Connolly has over 25 years experience in software development. He is involved in a number of open source projects, including Jenkins. Stephen was one of the first non-Sun committers to the Jenkins project and developed the weather icons. Stephen lives in Dublin, Ireland - where the weather icons are particularly useful. Follow Stephen on Twitter, GitHub and on his blog.