Tool helping you to publish your GPG (PGP) keys to DNS according to RFC7929
  • Go 93.6%
  • Shell 6.4%
Find a file
Heiko Schlittermann (HS12-RIPE) b8c404e2fe
Squashed commit of the following:
commit 6bc7f5b0d8
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Sun May 10 19:11:43 2026 +0200

    Revert "docs: inline concept SVG into README to enable interactive links"

    This reverts commit d3f14dc537.

commit d3f14dc537
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Sun May 10 19:10:28 2026 +0200

    docs: inline concept SVG into README to enable interactive links

commit 368c94ce4a
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Sun May 10 19:08:32 2026 +0200

    docs: use standard href and add tooltips to SVG links

commit 8cb0987cad
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Sun May 10 19:05:32 2026 +0200

    docs: fix missing branding link and improve SVG compatibility

commit 2eb4316b34
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Sun May 10 19:03:42 2026 +0200

    docs: add hyperlinks to SVG diagram

commit 4238f8a530
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Sun May 10 19:02:45 2026 +0200

    docs: add schlittermann.de branding to SVG

commit 6fa58be2fd
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Sun May 10 18:59:29 2026 +0200

    docs: theme SVG diagram to match schlittermann.de
2026-05-10 19:14:17 +02:00
completions feat: add glob-style pattern matching for keyring lookups (closes #5) 2026-05-09 20:48:39 +02:00
testdata fix: per-entity RDATA + output and perf cleanups 2026-05-09 09:22:18 +02:00
.gitignore chore: ignore built binary 2026-05-09 09:05:53 +02:00
.gogogo.conf add gogogo config 2026-05-09 22:39:20 +02:00
.golangci.yaml go: lint 2025-10-29 17:41:22 +01:00
check.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
check_test.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
CLAUDE.md refactor: dim warnings, add stripping reasons, update docs 2026-05-09 13:21:20 +02:00
completion.go chore: idiomatic go for io.Copy 2026-05-09 22:26:54 +02:00
completion_test.go feat: consolidate output flags into --output-format and add shell completion (closes #2) 2026-05-09 20:48:37 +02:00
domain.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
domain_test.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
expiry.go refactor: extract keyExpiry helper, simplify printWarnings, fix flag help text 2026-05-09 23:29:29 +02:00
explain.go refactor: extract keyExpiry helper, simplify printWarnings, fix flag help text 2026-05-09 23:29:29 +02:00
explain_test.go refactor: extract keyExpiry helper, simplify printWarnings, fix flag help text 2026-05-09 23:29:29 +02:00
glob.go refactor: deduplicate plain/wrapped output, extract expiry constant, document --warnings 2026-05-09 23:29:29 +02:00
glob_test.go feat: add glob-style pattern matching for keyring lookups (closes #5) 2026-05-09 20:48:39 +02:00
go.mod refactor: dim warnings, add stripping reasons, update docs 2026-05-09 13:21:20 +02:00
go.sum refactor: dim warnings, add stripping reasons, update docs 2026-05-09 13:21:20 +02:00
gpg-publish-concept.svg Squashed commit of the following: 2026-05-10 19:14:17 +02:00
gpg.go refactor: move runGPG to gpg.go, revert to path.Match 2026-05-09 20:48:42 +02:00
label.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
label_test.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
LICENSE feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
main.go refactor: extract keyExpiry helper, simplify printWarnings, fix flag help text 2026-05-09 23:29:29 +02:00
main_test.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
nsupdate.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
nsupdate_test.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
openpgpkey.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
openpgpkey_test.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
rdata.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
rdata_test.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
README.md refactor: deduplicate plain/wrapped output, extract expiry constant, document --warnings 2026-05-09 23:29:29 +02:00
strip.go refactor: extract keyExpiry helper, simplify printWarnings, fix flag help text 2026-05-09 23:29:29 +02:00
strip_test.go refactor: extract keyExpiry helper, simplify printWarnings, fix flag help text 2026-05-09 23:29:29 +02:00
ttl.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
ttl_test.go feat: add Apache 2.0 license and file headers 2026-05-09 23:29:29 +02:00
warn.go refactor: extract keyExpiry helper, simplify printWarnings, fix flag help text 2026-05-09 23:29:29 +02:00

gpg-publish

gpg-publish concept

The Missing Link for OPENPGPKEY DNS Records

Publishing GPG keys to DNS shouldn't require manual hex-editing or breaking your zone files with oversized TXT blobs. gpg-publish is a dedicated CLI that cleanly distills your local keyring into RFC 7929 compliant DNS records, instantly making your encryption keys globally discoverable.

Why you need it:

  • Intelligent Key Minimization: DNS hates large records. gpg-publish automatically strips third-party signatures, photo IDs, and expired subkeys, guaranteeing a minimal, DNS-friendly footprint.
  • Zero-Friction Discovery: Match your workflow. Pull keys by exact email, use glob patterns (*@yourdomain.com), or pipe directly from standard input.
  • Zone & Automation Ready: Outputs exactly what you need. Choose compact terminal output, wrapped zone-file strings, or fully automated nsupdate transactions with built-in nxrrset safeguards.
  • Close the Loop with --check: Don't just fire and forget. Instantly compare your local keyring against live DNS records to spot missing, mismatched, or outdated keys in seconds.

A small tool to generate OPENPGPKEY DNS records from a GPG public key.

The tool retrieves a GPG key, extracts its User IDs (email addresses), and generates a corresponding DNS record for each valid identity. It can get the key from your local GPG keyring, a file, or standard input.

The output conforms to RFC 7929:

  • Each record contains a minimal key — only the matching User ID, its self-signature, and non-expired subkeys. Third-party signatures, unrelated User IDs, User Attributes (photos), and expired subkeys are stripped. Stripping details are reported as warnings to stderr when --warnings is enabled.
  • When fetching from the local keyring, gpg --export-options export-minimal is used for an initial size reduction before programmatic stripping.
  • Multiple User IDs with the same email on one entity are deduplicated into a single record (the DNS label depends only on the email address). A warning is emitted if different entities (different keys) claim the same email, as this creates conflicting records.

The tool also checks key validity:

  • Expired keys are silently skipped.
  • Keys expiring within 30 days will be used, but a warning will be printed to standard error.

Warnings about stripped key elements are printed to stderr only when --warnings is enabled. When stderr is a terminal, they appear dimmed (ANSI faint) to reduce visual noise.

Usage

gpg-publish [flags] <target>

Target

The target argument can be one of the following:

  • An email address: The tool will call gpg --export --export-options export-minimal <email> to retrieve the key from your local keyring.
  • A glob pattern: If the target contains * or ? characters, it is treated as a glob pattern matching email addresses in your keyring. The tool finds all matching keys (with at least 30 days of remaining validity) and exports them together. For example, '*@example.com' matches all identities under the example.com domain.
  • A file path: The tool will read the key from the specified file.
  • -: The tool will read the key from standard input.

Flags

  • --domain / -d string: Filter output to identities in a specific domain. Can be repeated.

    • example.com — exact domain match only
    • .example.com — matches example.com and all subdomains
    • *.example.com — matches subdomains only (not the bare domain)

    When omitted, records are emitted for all valid identities in the key.

  • --check / -c: Compare generated records against live DNS instead of printing them. Reports OK, DIFF (record exists but differs), or MISS (no record in DNS) per identity. The summary distinguishes counts: e.g. "DNS check failed: 1 not found, 1 mismatched". Exits non-zero if any record differs or is missing.

  • --resolver string: DNS resolver to use with --check (e.g. 8.8.8.8, 8.8.8.8:53, or ns1.example.com). Hostnames are resolved using the system resolver. Defaults to the first nameserver from /etc/resolv.conf.

  • --verbose / -v: With --check, show key metadata (fingerprint, creation date, expiry, identities) for both local and DNS keys when a DIFF is detected.

  • --output-format format: Output format (default short in a terminal, wrapped when piped). Choose one of:

    • short: Compact one-line format with TTL, record type, an abbreviated key (first 4 chars + + last 4 chars), and comment (human-readable, useful for terminal output)
    • wrapped: Zone-file records with ( ... ) wrapping for large keys
    • plain: Zone-file records on a single line (no wrapping)
    • nsupdate: nsupdate commands. Each record is its own transaction, guarded with prereq nxrrset <label> OPENPGPKEY so it fails cleanly if a record already exists.
  • --force: With --output-format=nsupdate, replace existing records instead of bailing out. Emits update delete <label> OPENPGPKEY before update add so any previous record is removed first.

  • --completion shell: Print shell completion script and exit. Supported shells: bash, zsh, fish. Pipe to your shell's completion directory to enable auto-completion for gpg-publish.

--check and --output-format are mutually exclusive.

  • --ttl value: TTL for DNS records (default 1h). Accepts seconds (3600) or duration strings (1h, 30m, 1h30m). Applies to all output formats.

  • --version: Print version information and exit.

  • --warnings / -w: Show warnings about stripped key content (User Attributes, third-party signatures, expired subkeys) on stderr. Off by default.

Examples

1. From GPG Keyring Generate records for all identities in the key for hs@schlittermann.de.

gpg-publish hs@schlittermann.de

2. Exact domain filter Emit records only for identities whose domain is exactly schlittermann.de.

gpg-publish --domain schlittermann.de mykey.asc

3. Domain plus all subdomains Emit records for schlittermann.de and any subdomain (e.g. ml.schlittermann.de).

gpg-publish --domain .schlittermann.de mykey.asc

4. Subdomains only Emit records for subdomain identities, not the bare domain.

gpg-publish --domain '*.schlittermann.de' mykey.asc

5. Multiple domain filters Combine filters to cover the domain and a specific subdomain set.

gpg-publish --domain schlittermann.de --domain '*.schlittermann.de' mykey.asc

6. From Standard Input Pipe a key directly from gpg and generate records for all identities.

gpg --export hs@schlittermann.de | gpg-publish -

7. Glob pattern — all keys under a domain Expand a glob pattern to match all keys in your keyring whose email addresses match. Only keys with at least 30 days of remaining validity are included.

gpg-publish '*@schlittermann.de'
gpg-publish 'user*@example.com'
gpg-publish --output-format=nsupdate '*@example.com' | nsupdate -v

8. Check records against DNS Verify that published DNS records match the current key.

gpg-publish --check hs@schlittermann.de
gpg-publish --check --domain schlittermann.de hs@schlittermann.de
gpg-publish --check --resolver 8.8.8.8 --domain schlittermann.de hs@schlittermann.de

9. Generate nsupdate commands Produce output for piping into nsupdate(1). By default each transaction is guarded with prereq nxrrset so it fails cleanly if a record already exists. Add --force to replace any existing records.

gpg-publish --output-format=nsupdate hs@schlittermann.de | nsupdate -v
gpg-publish --output-format=nsupdate --ttl 7200 --domain schlittermann.de mykey.asc
gpg-publish --output-format=nsupdate --force hs@schlittermann.de | nsupdate -v

10. Install shell completion Generate and install completion for your shell.

gpg-publish --completion bash | sudo tee /etc/bash_completion.d/gpg-publish
gpg-publish --completion zsh | sudo tee /usr/local/share/zsh/site-functions/_gpg-publish
gpg-publish --completion fish | sudo tee /usr/local/share/fish/vendor_completions.d/gpg-publish.fish