Hockeypuck OpenPGP Keyserver on Ubuntu 24.04 on Azure User Guide
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
hockeypucksystemd service (binarieshockeypuck,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 on127.0.0.1:9626and its SKS reconciliation port on127.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'

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.

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

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

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'

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

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:


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.confand open port11370/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.