Web Key Directory (WKD): publish and find keys by email
A Web Key Directory (WKD) lets the owner of a mail domain publish its users' public keys on its own HTTPS web server. Anyone who knows an address like alice@example.org can then fetch the current key with gpg --locate-keys alice@example.org, without a central keyserver. Setting one up takes a folder of static files and a few lines of web server configuration.
How a lookup works
WKD is described in the Internet-Draft draft-koch-openpgp-webkey-service. As of November 2026 the current version is -22, from July 2026. A client turns the email address into a URL in four steps:
- Take the part before the
@(the local part) and change ASCII capital letters to lowercase.Joe.Doebecomesjoe.doe. - Hash it with SHA-1.
- Encode the 160-bit hash with z-base-32. The result is always 32 characters, here
iy9q119eutrkn8s1mk4r39qejnbu3n5q. - Build the URL with the domain and that string. The original local part goes in a parameter named
l.
There are two URL forms. Clients must try the advanced method first. It uses a dedicated host called openpgpkey in the mail domain:
https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe
Only if the name openpgpkey.example.org does not exist in DNS do clients fall back to the direct method on the domain itself:
https://example.org/.well-known/openpgpkey/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe
The server returns the key in binary form, not ASCII armor, ideally with the type application/octet-stream. The key must carry a user ID with that address. The draft also requires a policy file at …/.well-known/openpgpkey/example.org/policy (advanced) or …/.well-known/openpgpkey/policy (direct). An empty file is enough.
The SHA-1 step does not hide anything. It only turns any local part into a safe, fixed-length file name. Clients must keep only the user ID they asked for and drop the others. They must follow redirects only to other https URLs, and must never answer a password prompt (HTTP 401), because WKD data is public.
You can print the hash and the URL for any address:
$ gpg-wks-client --print-wkd-hash Joe.Doe@Example.ORG
iy9q119eutrkn8s1mk4r39qejnbu3n5q joe.doe@example.org
$ gpg-wks-client --print-wkd-url Joe.Doe@Example.ORG
https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=joe.doe
On Debian and Ubuntu, gpg-wks-client comes in its own package: sudo apt install gpg-wks-client.
Find a key with WKD
GnuPG has supported WKD since 2.1.12 (2016) and has used it by default since 2.1.23 (2017). Its default --auto-key-locate setting is local,wkd. So when you encrypt to an address and have no key for it, gpg asks the address's Web Key Directory. To look a key up on purpose:
gpg --locate-keys torvalds@kernel.org
--locate-keys uses a matching key from your keyring if you have one. --locate-external-keys always asks the network, which is useful for refreshing a key. To force a WKD-only lookup:
gpg --auto-key-locate clear,nodefault,wkd --locate-external-keys torvalds@kernel.org
kernel.org uses the direct method. A lookup in November 2026 printed:
gpg: key 79BE3E4300411886: public key "Linus Torvalds <torvalds@kernel.org>" imported
gpg: Total number processed: 1
gpg: imported: 1
gpg: automatically retrieved 'torvalds@kernel.org' via WKD
pub rsa2048 2011-09-20 [SC]
ABAF11C65A2970B130ABE3C479BE3E4300411886
uid [ unknown] Linus Torvalds <torvalds@kernel.org>
sub rsa2048 2011-09-20 [E]
WKD proves that the key came from the domain's web server over HTTPS. That is good protection for everyday use, but it is not a personal check. For high-stakes use, still compare the fingerprint through another channel. Outside GnuPG, the GnuPG wiki lists WKD support in GpgOL for Outlook (since 2.2.0), KMail (since 5.6), Claws Mail, Mailvelope (since 3.0.0) and OpenKeychain-based Android mail apps.
Publish keys as static files
You need a web server with a trusted TLS certificate for the domain, or for openpgpkey. plus the domain. GnuPG's gpg-wks-client --install-key builds the folder for you from your keyring. The example below uses a key for alice@example.org. Create the output folder first, because the tool will not create it:
mkdir openpgpkey
gpg --list-options show-only-fpr-mbox -k @example.org | gpg-wks-client -v --install-key
The first command lists every key in your keyring with an @example.org address, as "fingerprint address" pairs. The second writes one file per address:
gpg-wks-client: using key with user id 'Alice Example <alice@example.org>'
gpg-wks-client: directory 'openpgpkey/example.org' created
gpg-wks-client: directory 'openpgpkey/example.org/hu' created
gpg-wks-client: policy file 'openpgpkey/example.org/policy' created
gpg-wks-client: key F1C81BEF8B0CA5139E91229CD6B1FCF01404A8FB published for 'alice@example.org'
The result follows the advanced layout:
openpgpkey/
└── example.org/
├── policy
└── hu/
└── kei1q4tipxxu1yj79k9kfukdhfy631xe
Each file holds the key with only the user ID for that address, as the draft requires. Revocations are added by default (--add-revocs). If a key also has addresses at other domains, the tool creates a folder for each of those domains too, for example openpgpkey/other.example/. Publish only the folder for the domain you serve. Then copy the folder to the web server:
- Advanced method (recommended): create a DNS record for
openpgpkey.example.orgpointing to your web server, and get a certificate that covers that name. Then place the folder so that the file is served athttps://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/kei1q4tipxxu1yj79k9kfukdhfy631xe. - Direct method: copy the contents of
openpgpkey/example.org/(thehufolder andpolicy) tohttps://example.org/.well-known/openpgpkey/. Make sureopenpgpkey.example.orgdoes not exist in DNS. If you use wildcard DNS, the draft says to add an empty TXT record foropenpgpkeyso that the wildcard does not match it.
sudo mkdir -p /var/www/openpgpkey/.well-known
sudo cp -r openpgpkey /var/www/openpgpkey/.well-known/
Run the build again, and copy the files again, whenever a key changes: a new subkey, a longer expiry date or a revocation. A stale file keeps serving the old key.
nginx configuration
A separate HTTPS server block for the advanced method. The empty types block makes nginx send every file in the folder as application/octet-stream:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name openpgpkey.example.org;
ssl_certificate /etc/ssl/openpgpkey.example.org/fullchain.pem;
ssl_certificate_key /etc/ssl/openpgpkey.example.org/privkey.pem;
root /var/www/openpgpkey;
location /.well-known/openpgpkey/ {
types { }
default_type application/octet-stream;
add_header Access-Control-Allow-Origin "*" always;
autoindex off;
try_files $uri =404;
}
location / {
return 404;
}
}
For the direct method, put the same location /.well-known/openpgpkey/ block inside the existing server block for example.org, whose root holds the .well-known/openpgpkey/hu/ folder. The Access-Control-Allow-Origin header is optional. It lets browser-based tools, such as web mail extensions and online WKD checkers, read the key; kernel.org's directory sends it, for example. Check and reload:
sudo nginx -t
sudo systemctl reload nginx
Apache configuration
The same for Apache httpd 2.4. ForceType is a core directive; the Header line needs mod_headers:
<VirtualHost *:443>
ServerName openpgpkey.example.org
DocumentRoot /var/www/openpgpkey
SSLEngine on
SSLCertificateFile /etc/ssl/openpgpkey.example.org/fullchain.pem
SSLCertificateKeyFile /etc/ssl/openpgpkey.example.org/privkey.pem
<Directory "/var/www/openpgpkey/.well-known/openpgpkey">
Options -Indexes
ForceType application/octet-stream
Header always set Access-Control-Allow-Origin "*"
Require all granted
</Directory>
</VirtualHost>
On Debian or Ubuntu, save it as /etc/apache2/sites-available/openpgpkey.conf, then:
sudo a2enmod ssl headers
sudo a2ensite openpgpkey
sudo apachectl configtest
sudo systemctl reload apache2
No web server? Use keys.openpgp.org
keys.openpgp.org offers WKD as a service. Point the openpgpkey name of your domain at it with a CNAME record:
openpgpkey.example.org. 300 IN CNAME wkd.keys.openpgp.org.
Addresses at your domain that have been verified on keys.openpgp.org then become available over WKD. You can check the DNS part with:
curl 'https://wkd.keys.openpgp.org/status/?domain=openpgpkey.example.org'
The GnuPG wiki calls such central services a stopgap. The service, not your domain, then decides which key is served. See keys.openpgp.org for uploading and verifying.
Web Key Service for mail providers
Static files suit a person or a small team. A mail provider with many users can run a Web Key Service (WKS) instead. That is the update protocol in the same draft, implemented by GnuPG's gpg-wks-server. A user's mail client sends the key to the provider's submission address. The provider mails back an encrypted confirmation request, and the key is published once the user answers it. gpg-wks-client --supported user@example.org tells you whether a domain offers this. According to the GnuPG wiki, mail providers such as Posteo and Proton already publish their users' keys in a Web Key Directory.
Test your setup
Use a throwaway home directory so the test does not touch your keyring and cannot use a key you already have:
gpg --homedir "$(mktemp -d)" --verbose --locate-keys alice@example.org
Look for automatically retrieved 'alice@example.org' via WKD. A second check, with more detail:
gpg-wks-client --check -v alice@example.org
Check the raw HTTP response, the content type and the policy file:
curl -sI 'https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/kei1q4tipxxu1yj79k9kfukdhfy631xe?l=alice'
curl -sI 'https://openpgpkey.example.org/.well-known/openpgpkey/example.org/policy'
Both should return status 200, and the key should come back as application/octet-stream. A request for https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/ should not list the files.
Common mistakes
- The openpgpkey name exists but serves nothing. If
openpgpkey.example.orgresolves, clients use the advanced method and do not fall back to the direct one. A missing or broken server there does not trigger the fallback. Either serve WKD on that name or remove the record. - Certificate does not cover openpgpkey. Clients reject the connection. Add the name to your certificate.
- No policy file. The draft requires one, even an empty one.
- ASCII-armored files. Serve the binary key.
gpg-wks-client --install-keydoes this for you. - Directory listings switched on. The draft says the server should not list the folder. A listing would let anyone collect every published address.
- Forgetting to update. Rebuild after every key change, especially after a revocation.
- Keys for other domains. WKD only works for addresses at the domain that serves it. Clients look for alice@other.example only at other.example, so do not copy other domains' folders to your server.
Sources
- OpenPGP Web Key Directory, draft-koch-openpgp-webkey-service-22, IETF (July 2026)
- draft-koch-openpgp-webkey-service, IETF Datatracker
- WKD, GnuPG wiki
- WKDHosting, GnuPG wiki
- gpg-wks-client, Using the GNU Privacy Guard
- GPG configuration options (--auto-key-locate), Using the GNU Privacy Guard
- GnuPG NEWS (2.1.12, 2.1.23)
- gpg-wks-client in Debian trixie, Debian packages
- Usage: WKD as a service, keys.openpgp.org
- ngx_http_core_module (types, default_type, try_files), nginx documentation
- ngx_http_headers_module (add_header), nginx documentation
- Apache core features (ForceType, Options), Apache HTTP Server 2.4
- mod_headers, Apache HTTP Server 2.4