Automatically Signing Apt Repos with gpg-agent

This is an enormous pain in the ass to get working correctly. To hopefully save you some butt pain, here’s how you actually do this, from end-to-end. So, the story is, you want to build an apt repo of your own stuff. In order to stop apt-get complaining every time you install something from this repo, you need to set it up as a signed repo. To keep things safe, you’ll need to set a passphrase on the key that you use to sign the repo. The issue is then, how do you automatically sign the packages from, say, a cron job, without actually sticking the passphrase in a script somewhere? In steps gpg-agent to save the day.

First of all, we set up the apt repo itself. There are lots of docs on how to do this out there. But here’s a quick fly through. Let’s say that your repo will live (on disk) in /myrepo. Create a directory /myrepo/conf. Create a file in conf called distributions with the following contents.

1
2
3
4
5
6
7
8
  Origin: my.repo.com
  Label: apt repository
  Codename: precise
  Architectures: amd64 i386
  Components: main
  Description: My Apt Repo
  SignWith: bofh@rage.com
  Pull: precise

Everything above is pretty self-explanatory. The main thing to note is :code:SignWith:bofh@rage.com. This is the identity we’ll be using to sign later on.

Next, create another file, conf/options. This is what you’ll put in it.

  verbose
  basedir /myrepo
  keepunusednewfiles

Install your favourite webserver (mine’s nginx) and point it at /myrepo. Here’s a super-basic nginx conf if you’re really stuck.

  server {
      root /myrepo;
      index index.html index.htm;
      server_name my.repo.com;
      location / {
          autoindex  on;
          try_files $uri $uri/ /index.html;
      }
  }

Now you’re going to generate a key to sign this puppy with.

  gpg --gen-key

You’ll want to choose an RSA key and give it the same email address as above : bofh@rage.com. Now you have an empty repo and a key. Not much use so far, but stick with it. We’ll get there.

You’ll want to add this block in the ~/.profile of the user who’s going to be building the repo. Probably best to put it at/towards the bottom.

  if test -f $HOME/.gpg-agent-info && kill -0 `cut -d: -f 2 $HOME/.gpg-agent-info` 2> /dev/null; then
    GPG_AGENT_INFO=`cat $HOME/.gpg-agent-info`
    export GPG_AGENT_INFO
  else
    eval `gpg-agent --default-cache-ttl 4294967295 --daemon --enable-ssh-support --write-env-file ~/.gpg-agent-info`
  fi

  if [ -f "${HOME}/.gpg-agent-info" ]; then
    . "${HOME}/.gpg-agent-info"
    export GPG_AGENT_INFO
    export SSH_AUTH_SOCK
    export SSH_AGENT_PID
  fi

  GPG_TTY=$(tty)

  export GPG_TTY

What the above does is spawn a gpg-agent daemon on login, unless there’s already one running. It also saves some environment variables for the running gpg-agent in ~/.gpg-agent-info which will be handy later on. I’ve also set –default-cache-ttl to it’s max (2^32), a.k.a, 135 or so years. You’ll probably reboot sooner than that I imagine. When gpg-agent first starts, it doesn’t know any passphrases, so you’ve got to manually enter it the first time. From then on, it’ll remember that passphrase for –default-cache-ttl seconds. So … indefinitely … basically.

Beware of an issue on Ubuntu 12.04. Near the top of the default ~/.bashrc is this:

  [ -z "$PS1" ] && return

This had me stumped for ages - wondering why my changes weren’t taking effect. If you’re not running an interactive shell, it bails out and doesn’t do anything below this point. No matter how much you curse - it turns out. In fact, I just moved my ~/.bashrc on this system out of the way altogether. I’m just using ~/.profile.

If you now log out and log back in, it should spawn a gpg-agent process. If it doesn’t there’s something wrong. Go back and check your various profile scripts.

Next, we need something to actually import the packages into the repo. In order to keep things simple, my build scripts just stick the deb packages that they build into the ‘/myrepo/pkgs_incoming’ directory. They are then picked up by a cron’d script and imported into the repo. Here’s a very basic little script, using reprepro for importing packages.

  #!/bin/bash -x
  BASEDIR="/myrepo"
  MAILTO="bofh@rage.com"
  source /home/ubuntu/.gpg-agent-info
  export GPG_AGENT_INFO
  cd $BASEDIR
  
  for new_pkg in `ls pkgs_incoming`; do
      echo $new_pkg
      reprepro --noguessgpgtty -Vb . includedeb precise pkgs_incoming/$new_pkg
      if [ $? != 0 ]; then
          echo "Import of $new_pkg failed, reporting ..."
          mv pkgs_incoming/$new_pkg pkgs_failed/
          echo "Import of pkg $new_pkg into apt.dotmobi.com repo failed." | mail -s "apt import failed : $new_pkg" $MAILTO
      else
          mv pkgs_incoming/$new_pkg pkgs_complete/
      fi
  done

If the import succeeds, the packages are moved to pkgs_complete. If they fail, they’re moved to pkgs_failed. Couldn’t be any simpler. Also note, that the script sources the ~/.gpg-agent-info file AND exports GPG_AGENT_INFO. This is because a user running under cron doesn’t have the same environment as a user logging in, say, via ssh.

Also remember, as I say, when gpg-agent has just been fired up, it doesn’t ‘remember’ any passphrases, so you’ll need to manually trigger an import. The easiest way to do this is to stick a deb in /myrepo/pkgs_incoming and run the above script. gpg-agent should prompt you to enter the passphrase for the key. Once you enter it, gpg-agent will remember it from then on - unless you kill it of course.

You should now have a repo with at least one package in it. How do you install a package in the repo on your client system? First of all, you need to export the public part of the key you used to sign the packages.

  gpg --export --armor bofh@rage.com > myrepo.pub

This command will spit out the public key. Copy this to your client system and import it there.

  sudo apt-key add myrepo.pub

Add the following line to the /etc/apt/sources.list file on your client system.

  deb http://my.repo.com/ precise main

Refresh your repos and install your awesome packages.

  sudo apt-get update
  sudo apt-get install myawesomepackage

Update : 2017-04-20

JoachimĀ Langenbach suggested changing the first line of your ~/.profile from:

  if test -f $HOME/.gpg-agent-info && kill -0 `cut -d: -f 2 $HOME/.gpg-agent-info` 2> /dev/null; then

to:

  if test -f $HOME/.gpg-agent-info && kill -0 `head -n 1 "$HOME/.gpg-agent-info" | cut -d: -f 2` 2> /dev/null; then

This ensures you only use the first line of the .gpg-agent-info file. Thanks Joachim.