Ho
Security Azure

Hockeypuck OpenPGP Keyserver on Ubuntu 24.04 on Azure User Guide

| Product: Hockeypuck OpenPGP Keyserver on Ubuntu 24.04 LTS on Azure

Overview

Hockeypuck is a keyserver for the OpenPGP web of trust. It accepts PGP public key submissions and answers lookups over the HKP protocol that gpg and other OpenPGP tools speak natively, so people and services can publish their public keys and find each other's, by user id, key id or full fingerprint, from a browser or straight from the command line.

Run as your own keyserver, Hockeypuck gives an organisation or community a place to distribute and retrieve public keys without depending on the wider public keyserver network. It is compatible with the SKS reconciliation protocol, so it can either stand alone as a private directory or, once you choose to, join a pool of peers and gossip keys with them.

The cloudimg image ships Hockeypuck as a complete, working keyserver rather than a stack to assemble. It bundles the PostgreSQL key store on the same VM, wired and migrated, so the HKP query and submit interface answers real requests the moment first boot finishes. A keyserver's query and submit interface is meant to be internet facing, so that surface is public by design; everything that is not meant to be public is locked to the loopback, and the database credential is generated uniquely on the first boot of your VM. Backed by 24/7 cloudimg support.

What is included:

  • Hockeypuck 2.3.3 built from the pinned upstream release tag, running as the hockeypuck systemd service (binaries hockeypuck, hockeypuck-load, hockeypuck-pbuild, hockeypuck-dump, hockeypuck-reload)
  • PostgreSQL 16 from the Ubuntu archive as the JSONB key store, bound to the loopback only
  • nginx on port 80 (browser search UI and HKP over HTTP) and on port 11371 (the standard hkp:// keyserver port), the only public surfaces
  • Hockeypuck's own HKP listener on 127.0.0.1:11373, its Prometheus metrics on 127.0.0.1:9626 and its SKS reconciliation port on 127.0.0.1:11370, none exposed to the network directly
  • A per VM PostgreSQL application password generated on first boot; no keyserver credential ships in the image
  • Anti abuse defaults: an oversized key limit that rejects the keys used in poisoning attacks, and nginx rate limiting on key submission
  • Shipped isolated from the public gossip pool by default, so it cannot ingest the poisoned global keyspace until you deliberately add peers
  • The unmodified upstream source at /usr/share/hockeypuck/source-2.3.3 (Hockeypuck is AGPL-3.0)
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point for an empty or lightly populated keyserver; measured idle usage is well under 1 GiB. If you plan to load a large keyset such as a full SKS dump, size up and attach a data disk for /var/lib/postgresql and /var/lib/hockeypuck (see Step 11). NSG inbound: allow 22/tcp from your management network, and 80/tcp plus 11371/tcp from wherever your users and OpenPGP clients reach the keyserver. Add 443/tcp once you put your own domain and TLS certificate in front for hkps:// (see Step 10).

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Hockeypuck by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create and Create. Add an inbound rule for 11371/tcp afterwards so standard hkp:// clients can reach the keyserver.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name hockeypuck \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm open-port --resource-group <your-rg> --name hockeypuck --port 80 --priority 1010
az vm open-port --resource-group <your-rg> --name hockeypuck --port 11371 --priority 1020

Step 3 - Connect to your VM

ssh azureuser@<public-ip>

Step 4 - Confirm the keyserver is running

On the first boot of your VM the image rotates the database password, brings PostgreSQL, Hockeypuck and nginx up, and writes a completion marker. Confirm all three services are active and that the HKP interface answers:

systemctl is-active postgresql@16-main.service hockeypuck.service nginx.service
test -f /var/lib/cloudimg/hockeypuck-firstboot.done && echo "first boot: complete"
curl -s -o /dev/null -w 'HKP stats: HTTP %{http_code}\n' 'http://127.0.0.1/pks/lookup?op=stats'

The PostgreSQL, Hockeypuck and nginx services all active after first boot, the first boot marker present, the built Hockeypuck version, and the HKP stats endpoint returning HTTP 200

Step 5 - Open the web search interface

Browse to http://<public-ip>/ (or http://<public-ip>:11371/). Hockeypuck serves an OpenPGP keyserver search page where anyone can look up a key by user id, key id or fingerprint. This interface is public by design - it is what makes the appliance a keyserver.

The Hockeypuck OpenPGP keyserver web search page served by the appliance

Step 6 - Submit and retrieve a key with gpg

From any machine with gpg, submit a public key to your keyserver and retrieve it again on a fresh client. This is the full round trip the appliance exists to perform - a key submitted over HKP is stored in PostgreSQL and served back on lookup:

# Publish your public key to the keyserver
gpg --keyserver hkp://<public-ip>:11371 --send-keys <your-key-fingerprint>

# Retrieve it (or someone else's) by fingerprint on another machine
gpg --keyserver hkp://<public-ip>:11371 --recv-keys <a-key-fingerprint>

# Or search by user id
gpg --keyserver hkp://<public-ip>:11371 --search-keys alice@example.org

A PGP key generated on one client, submitted to the keyserver with gpg send-keys, and retrieved on a fresh client with gpg recv-keys - the key arrives from the keyserver

The same lookup rendered in the browser shows the stored key with its user id and fingerprint:

The keyserver lookup result page showing a retrieved key - the eddsa Curve25519 primary key, the Ada Lovelace user id, the self signature and the encryption subkey

Step 7 - The per-VM database credential

The keyserver itself has no login - it is meant to be public. The one secret on the VM is the PostgreSQL application password, generated uniquely on first boot and written to /root/hockeypuck-credentials.txt (mode 0600, root only). The keyserver configuration stores no password; it is templated in from the environment at start. You can use the credential for database maintenance:

sudo stat -c '%a %U:%G' /root/hockeypuck-credentials.txt
PGPASSWORD='<HKP_DB_PASSWORD>' psql -h 127.0.0.1 -U hockeypuck -d hkp -c '\dt'

The credentials file at mode 600 root:root with the database password masked, the customer facing URLs carrying the VM's own public address, and the keyserver config templating the password from the environment rather than storing it

Step 8 - Security posture: everything private is on the loopback

A keyserver is public by design, but the database, the Prometheus metrics endpoint and the SKS reconciliation port are not - and they are all bound to the loopback with no public port. The /metrics path is additionally refused through the public proxy:

ss -tlnH | awk '{print $4}' | grep -E ':(5432|9626|11370|11373|80|11371)$' | sort -u
curl -s -o /dev/null -w '/metrics via public proxy: HTTP %{http_code} (expect 403)\n' 'http://127.0.0.1/metrics'
grep 'allowCIDRs' /etc/hockeypuck/hockeypuck.conf

The listener posture showing PostgreSQL, metrics, reconciliation and the Hockeypuck HKP port all on 127.0.0.1, only ports 80 and 11371 public, the public proxy returning 403 for /metrics, and reconciliation restricted to the loopback

Step 9 - Searching and server statistics from the browser

The web interface supports the same lookups as the command line. Search by user id to list matching keys, and open the statistics page to see what the server holds and how it is configured:

The keyserver index search returning keys that match a user id search

The Hockeypuck server statistics page showing the server identity and key totals

Step 10 - Submission and gossip policy (important)

By default this appliance is a private, isolated keyserver: it accepts key submissions (that is how you populate it) but it is not joined to the public SKS gossip pool, so it will never ingest the poisoned global keyspace, and it applies an oversized key limit plus nginx rate limiting on the submission path to blunt abuse. This is the safe default for an organisational keyserver.

  • Keep it private (recommended for most deployments). Leave the gossip configuration alone. Restrict who can submit at the network layer with your NSG if you want submission limited to trusted ranges.
  • Join a gossip pool. If you want to synchronise with other keyservers, add reconciliation partner peers under [hockeypuck.conflux.recon] in /etc/hockeypuck/hockeypuck.conf and open port 11370/tcp. Understand the trade off first: joining a pool means accepting the pool's keyspace, including keys that may have been deliberately flooded with signatures. Review the current upstream guidance at the Hockeypuck project before peering.

Edit the configuration and reload the service after any change:

sudo nano /etc/hockeypuck/hockeypuck.conf
sudo systemctl reload hockeypuck.service

Step 11 - HKPS / TLS and a large keyset

For hkps:// put your own domain and a TLS certificate in front of nginx (for example with certbot) and open 443/tcp. To import a large keyset such as an SKS dump, stop the service, run hockeypuck-load against the dump files, rebuild the prefix tree with hockeypuck-pbuild, then start the service again - and give PostgreSQL and /var/lib/hockeypuck a dedicated data disk sized for the keyset first.

Step 12 - Back up the key database

The keys live in PostgreSQL. Take a logical backup with pg_dump using the per VM credential from Step 7:

PGPASSWORD='<your-db-password>' pg_dump -h 127.0.0.1 -U hockeypuck -d hkp -Fc -f hkp-backup.dump

Support

This image packages Hockeypuck, an open source project of Casey Marshall and the Hockeypuck Contributors, licensed under the GNU Affero General Public License v3.0; the unmodified upstream source ships in the image at /usr/share/hockeypuck/source-2.3.3. cloudimg is not affiliated with, endorsed by, or sponsored by the Hockeypuck project. OpenPGP is a standard of the IETF OpenPGP working group. For help with this image, contact cloudimg support 24/7.