Running your own OpenPGP keyserver with Hockeypuck
Most people and organizations no longer need their own keyserver. A Web Key Directory or keys.openpgp.org covers the usual needs. If you do need one, for an internal network or to join the public sync network, the practical choice in November 2026 is Hockeypuck. This page shows how to set it up and what you take on by running it.
Do you need a keyserver?
Ask what problem you are solving first:
- "People should find keys for addresses at our domain." Publish a Web Key Directory. It is a folder of static files on your web server, it serves only your own users' keys, and GnuPG checks it by default.
- "I want my key to be findable." Upload it to keys.openpgp.org and confirm your address.
- "Our closed network or build systems need an HKP server we control." A private Hockeypuck server makes sense. Typical examples are an internal company directory, a lab without internet access, or CI jobs that must fetch signing keys reliably.
- "We want to help the public network." You can run a public Hockeypuck server and peer with others. This is a long-term commitment, with real legal and abuse work (see the security notes below).
Software choices
| Software | Language and storage | Status |
|---|---|---|
| Hockeypuck | Go, PostgreSQL 13 or later (tested up to 18) | Maintained. Latest release 2.4.1 (August 31, 2026). Syncs with other Hockeypuck servers over the SKS reconciliation protocol. AGPLv3. |
| Hagrid | Rust, SQLite (since v2.0) | The software of keys.openpgp.org. It verifies email addresses before publishing them. Its README says it is built only for that service and "not maintained or officially supported as deployable software." |
| SKS | OCaml, Berkeley DB | Obsolete. The last commit to its repository dates from March 2021, the public pool closed in June 2021, and its design cannot delete data. |
Hagrid is open source, and you can build it to study it: cargo run with a copied Rocket.toml starts it on port 8080, and it needs a working local sendmail for verification mails. For production, its authors point you to the running service, not to self-hosting. The rest of this page covers Hockeypuck.
Install Hockeypuck with Docker (recommended)
The Hockeypuck project recommends its docker-compose/standalone setup for most users. It bundles Hockeypuck, PostgreSQL, HAProxy (TLS termination and client allow/deny lists), Let's Encrypt certificates and Prometheus monitoring. It is tested on Ubuntu 24.04 and Debian 13. Before you start:
- A server with a public DNS name, for example
keys.example.com. - Firewall open for TCP ports 80, 443, 11370 (recon, only needed for peering) and 11371 (HKP).
- Docker with Compose installed. The bundled
./setup.bashinstallsdocker.io,docker-composeandfail2banwith apt. Read it first: it also adds a user namedubuntuto the docker group. - Your contact email and your key's full fingerprint.
Always deploy a release tag, not master, which the project calls alpha quality:
git clone https://github.com/hockeypuck/hockeypuck
cd hockeypuck
git checkout 2.4.1
cd contrib/docker-compose/standalone
./mksite.bash
./mksite.bash creates a .env file with a random database password. Edit it and set at least these values (no quotes, except around ALIAS_FQDNS):
FQDN=keys.example.com
ALIAS_FQDNS=""
EMAIL=admin@example.com
FINGERPRINT=0x0123456789ABCDEF0123456789ABCDEF01234567
Then generate the configuration, build, get a certificate and start:
./mkconfig.bash
docker compose build
./init-letsencrypt.bash
docker compose up -d
docker compose logs -f hockeypuck
Use docker-compose instead of docker compose if you have the older Compose v1. Hockeypuck's own settings are in hockeypuck/etc/hockeypuck.conf. After you change them, run docker compose restart.
Loading existing keys
A new server starts empty. That is fine for a private server: upload your organization's keys and you are done. To peer with public servers, you first need a recent dump of their keys. The project's ./sync-sks-dump.bash now stops with the message that "public keyserver dumps are no longer maintained" to prevent abuse. The project lists dump sources that give out rsync passwords on request. Once you have access:
export KEYDUMP=rsync://username:password@rsync.example.com/hockeypuck/dump
./sync-sks-dump.bash
docker compose up -d
Loading takes several hours or even days. When the key count stops climbing, ./clean-sks-dump.bash deletes the dump files and frees the disk space.
Install from source with systemd
If you prefer packages you manage yourself, build Hockeypuck directly. Version 2.4.1 needs Go 1.26 or later. Debian 13's golang-go is 1.24, which is too old, so install Go from go.dev first. Then, on Debian or Ubuntu:
sudo apt install git make postgresql
git clone https://github.com/hockeypuck/hockeypuck
cd hockeypuck
git checkout 2.4.1
make build
sudo make install
make install puts hockeypuck, hockeypuck-load, hockeypuck-dump and the other tools in /usr/bin. It also puts templates and web files in /var/lib/hockeypuck and a sample config in /etc/hockeypuck/. Create a system user, its prefix-tree folder and the database:
sudo adduser --system --group --home /var/lib/hockeypuck hockeypuck
sudo chown -R hockeypuck:hockeypuck /var/lib/hockeypuck
sudo -u hockeypuck mkdir -p /var/lib/hockeypuck/recon.db
sudo -u postgres createuser hockeypuck
sudo -u postgres createdb -O hockeypuck hkp
Write /etc/hockeypuck/hockeypuck.conf. This example is a private server behind a TLS reverse proxy. It serves only self-signatures and does not peer:
[hockeypuck]
loglevel="INFO"
indexTemplate="/var/lib/hockeypuck/templates/index.html.tmpl"
vindexTemplate="/var/lib/hockeypuck/templates/index.html.tmpl"
statsTemplate="/var/lib/hockeypuck/templates/stats.html.tmpl"
responseTemplate="/var/lib/hockeypuck/templates/response.html.tmpl"
webroot="/var/lib/hockeypuck/www"
hostname="keys.example.com"
contact="0123456789ABCDEF0123456789ABCDEF01234567"
[hockeypuck.hkp]
bind="127.0.0.1:11371"
logRequestDetails=false
[hockeypuck.hkp.queries]
selfSignedOnly=true
[hockeypuck.openpgp.db]
driver="postgres-jsonb"
dsn="database=hkp host=/var/run/postgresql port=5432 user=hockeypuck sslmode=disable"
[hockeypuck.conflux.recon]
reconAddr="127.0.0.1:11370"
allowCIDRs=["127.0.0.1/8"]
[hockeypuck.conflux.recon.leveldb]
path="/var/lib/hockeypuck/recon.db"
The project's Debian packaging uses this unit file. Save it as /etc/systemd/system/hockeypuck.service:
[Unit]
Description=hockeypuck
After=network.target postgresql.service
Wants=postgresql.service
[Service]
User=hockeypuck
Group=hockeypuck
LimitNOFILE=49152
Environment=HOME=/var/lib/hockeypuck
WorkingDirectory=/var/lib/hockeypuck
ExecStart=/usr/bin/hockeypuck -config /etc/hockeypuck/hockeypuck.conf
ExecReload=/bin/kill -USR1 $MAINPID
Restart=on-failure
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now hockeypuck
sudo journalctl -u hockeypuck -f
Put a TLS reverse proxy in front so clients can use hkps://keys.example.com. Hockeypuck can also serve HKPS itself, with a [hockeypuck.hkps] section holding bind, cert and key. A minimal nginx server block:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name keys.example.com;
ssl_certificate /etc/ssl/keys.example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/keys.example.com/privkey.pem;
client_max_body_size 2m;
location / {
proxy_pass http://127.0.0.1:11371;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Hockeypuck accepts keys up to 1 MiB by default (maxKeyLength), and an upload is a little larger once it is form-encoded. nginx's default request limit is exactly 1 MB, so the example raises it. To import a dump on this kind of install, stop the service and run, as the hockeypuck user:
sudo systemctl stop hockeypuck
sudo -u hockeypuck hockeypuck-load -config /etc/hockeypuck/hockeypuck.conf /path/to/dump/*.pgp
sudo systemctl start hockeypuck
Check that it works
The stats page shows the software, version, key count and peers:
curl -s 'https://keys.example.com/pks/lookup?op=stats&options=mr'
The reply is JSON with fields such as "version", "hostname", "software":"Hockeypuck" and "Total" (the number of keys). Then send a key and fetch it back with GnuPG:
gpg --keyserver hkps://keys.example.com --send-keys 0123456789ABCDEF0123456789ABCDEF01234567
gpg --keyserver hkps://keys.example.com --recv-keys 0123456789ABCDEF0123456789ABCDEF01234567
curl -s 'https://keys.example.com/pks/lookup?op=index&options=mr&search=alice@example.org'
To make the server the default for your users, put keyserver hkps://keys.example.com in their ~/.gnupg/dirmngr.conf. The HKP page explains the lookup URLs.
Peering with other servers
Peers reconcile their key sets over port 11370 and then fetch the differences over HKP. Add one section per partner, using only letters, digits and underscores in the name. Allow inbound recon from the partner, and remove the 127.0.0.1 restriction on reconAddr:
[hockeypuck.conflux.recon]
reconAddr=":11370"
[hockeypuck.conflux.recon.partner.keys_partner_example]
httpAddr="keys.partner.example:11371"
reconAddr="keys.partner.example:11370"
Load a dump before you join, so you do not have to sync millions of keys one by one. Peer only with servers on the same Hockeypuck minor version, such as 2.4.x with 2.4.x. The project does not guarantee sync between different minor versions. If a peer's key count drifts far from yours for days, the standalone setup includes ./ptree-rebuild.bash to rebuild the prefix tree. Hockeypuck can also push updates one way to older servers by email or HKP, with a [hockeypuck.pks] section. That is the same idea the 1990s email keyservers used, described on the email keyserver page.
Security and legal notes
- Keys hold personal data. User IDs contain names and email addresses. The SKS pool ended in June 2021 because of "even more GDPR takedown requests". Before you run a public server, decide how you will handle deletion requests, and get advice on the privacy law that applies to you.
- Deleting keys. To stop a key coming back from peers, add its full fingerprint to
blacklistin[hockeypuck.openpgp]. Then delete it with the standalone./delete-keys.bash FINGERPRINT, or with a signed request to/pks/deletefrom one of youradminKeys. Deleting a key without blacklisting it first does not stop peers from sending it again. - Separate admin and contact keys. The docs strongly recommend that the contact key shown on the stats page is not one of the admin keys.
- Certificate flooding. Keep the default size limits (
maxKeyLength,maxPacketLength,maxSigPacketLength). Peers with different limits sync less well, and the limits block oversized keys.selfSignedOnly=truestops your server from handing out third-party signatures, the vector of the 2019 attack (see keyserver history). - Address harvesting. Spammers have collected addresses from keyservers.
keywordSearchDisabled=trueallows only fingerprint and key-ID lookups. Since 2.4, searches must match an address or full user ID exactly unless you setenableInexact=true. - Logging. Leave
logRequestDetails=falseon public servers. The docs note that a public keyserver may not want to log lookups, for privacy reasons. - No identity checks. Hockeypuck, like SKS before it, publishes whatever is uploaded. Tell your users to check fingerprints, or use WKD or keys.openpgp.org where verified addresses matter.
- Upgrades. Follow the release branch, back up PostgreSQL before minor upgrades, and expect a reindex after upgrading. Run
hockeypuck-reloadwhen jumping more than one minor version.
Sources
- Hockeypuck OpenPGP keyserver (features, latest release)
- Configuration, Hockeypuck
- Build and install from source, Hockeypuck
- Install with Docker, Hockeypuck
- Populating, Hockeypuck
- Remote administration, Hockeypuck
- Hockeypuck README, GitHub
- Standalone docker-compose deployment (2.4.1), Hockeypuck
- Data sources, Hockeypuck
- debian/hockeypuck.service, Hockeypuck
- Hockeypuck 2.4.1 release, GitHub
- Hagrid README, GitLab
- SKS Keyserver repository, GitHub
- sks-keyservers.net home page, July 3, 2021 (Internet Archive)
- FAQ, MIT PGP Public Key Server
- golang-go in Debian trixie, Debian packages
- Download and install, The Go Programming Language
- ngx_http_core_module (client_max_body_size), nginx documentation
- ngx_http_proxy_module, nginx documentation