Deployment guide

Hockeypuck

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.

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 stand alone as a private directory or, when you choose, join a pool of peers and gossip keys with them.

This 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.

What you get:

  • 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, so 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
  • Isolation from the public gossip pool by default, so the keyserver 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 SecureAnyCloud support

Prerequisites

You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.

Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting size 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, choose a larger size and attach a data disk for /var/lib/postgresql and /var/lib/hockeypuck (see Step 11).

In the network security group (NSG), allow inbound 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 SecureAnyCloud, and select Create.

On the Basics tab, 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).

Select Review + create, then Create. Afterwards, add an inbound rule for 11371/tcp so that standard hkp:// clients can reach the keyserver.

Step 2 - Deploy from the Azure CLI

Accept the Marketplace terms, create the VM and open ports 80 and 11371:

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:hockeypuck:default:latest
az vm create \
  --resource-group <your-rg> \
  --name hockeypuck \
  --image sac1647283583153:hockeypuck:default:latest \
  --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

Connect over SSH as azureuser:

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 that 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/sac/data/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, because it is meant to be public. The one secret on the VM is the PostgreSQL application password. It is generated uniquely on first boot and written to /root/hockeypuck-credentials.txt (mode 0600, root only).

The keyserver configuration stores no password. The password 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. All three are 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, which is how you populate it, but it is not joined to the public SKS gossip pool, so it never ingests the poisoned global keyspace. It also 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. To limit submission to trusted ranges, restrict who can submit at the network layer with your NSG.
  • Join a gossip pool. 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, first give PostgreSQL and /var/lib/hockeypuck a dedicated data disk sized for the keyset. Then stop the service, run hockeypuck-load against the dump files, rebuild the prefix tree with hockeypuck-pbuild, and start the service again.

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

For help with this image, contact SecureAnyCloud support, available 24/7.

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.

SecureAnyCloud is not affiliated with, endorsed by, or sponsored by the Hockeypuck project. OpenPGP is a standard of the IETF OpenPGP working group.

Deploy on Azure

Launch Hockeypuck OpenPGP Keyserver on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure