commit 21b237f0dacac80ec7fafcb8c7e2fec9af3346ae Author: claude-macmini Date: Sun Sep 13 18:31:35 2026 +0200 WKD (advanced method): nginx-image met gpg-wks-client-gebouwde hu/-boom (infra#88) Co-Authored-By: Claude Opus 5 diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..580181b --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,30 @@ +name: build-and-push +on: + push: + branches: [main] + workflow_dispatch: +env: + IMAGE: git.doc-gen.eu/wsfcqyxbu1/wkd # lowercase namespace! +jobs: + build: + runs-on: docker-builder + steps: + - uses: actions/checkout@v4 + - name: Login bij Forgejo registry + run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.doc-gen.eu -u wSFcQyxBu1 --password-stdin + - name: Build image (bouwt de hu/-boom met gpg-wks-client; faalt bij een foute/privé-sleutel) + run: docker build --pull -t "${IMAGE}:latest" -t "${IMAGE}:${{ github.sha }}" . + - name: Controle in de gebouwde image (policy per domein aanwezig) + run: | + docker run --rm "${IMAGE}:latest" sh -c 'set -e; cd /usr/share/nginx/html/.well-known/openpgpkey; for d in */; do test -f "$d/policy"; done; find . -type f | sort' + - name: Push image + run: | + docker push "${IMAGE}:latest" + docker push "${IMAGE}:${{ github.sha }}" + - name: ntfy bij falen + if: failure() + run: | + curl -s -o /dev/null -H "Authorization: Bearer ${{ secrets.NTFY_TOKEN }}" \ + -H "Title: WKD CI-build FAALDE" -H "Priority: high" -H "Tags: rotating_light" \ + -d "build.yml van wSFcQyxBu1/wkd faalde (run ${{ github.run_number }}, sha ${{ github.sha }})." \ + https://ntfy.3838.eu/docgen-backup || true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a52e1c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# WKD (OpenPGP Web Key Directory) — publieke PGP-sleutels van de eigenaar, advanced method +# (https://openpgpkey./.well-known/openpgpkey//hu/?l=). +# Route-B image-app op de derden-box. Alles in git (alleen PUBLIEKE sleutels — nooit een privésleutel +# of passphrase in deze repo); sleutel toevoegen/vernieuwen = commit + push → CI → watcher deployt. +# +# Twee stappen: +# 1. build-stage: GnuPG's eigen gpg-wks-client bouwt uit keys//.asc de hu/-boom +# (z-base-32(SHA-1(lowercased local-part))) + het verplichte policy-bestand. Het GnuPG-tool +# garandeert de juiste hash én exporteert alléén de UID van dat adres (spec: andere UIDs MOETEN weg). +# 2. runtime: nginx serveert de boom met de juiste headers (application/octet-stream, CORS *). +# Bumpen = de twee FROM-tags (Docker Hub: library/alpine, library/nginx). + +FROM alpine:3.22.5 AS build +RUN apk add --no-cache gnupg gnupg-wks-client +COPY keys/ /keys/ +COPY build-wkd.sh /build-wkd.sh +RUN sh /build-wkd.sh /keys /out + +FROM nginx:1.30.4-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /out/ /usr/share/nginx/html/.well-known/openpgpkey/ +EXPOSE 80 diff --git a/README.md b/README.md new file mode 100644 index 0000000..518f829 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# wkd — OpenPGP Web Key Directory (publieke PGP-sleutels, advanced method) + +Mailclients (GnuPG, Thunderbird, Mailvelope, Proton-lookup, …) vinden hiermee automatisch de **publieke** +PGP-sleutel bij een mailadres. Route-B image-app op de **derden-box**; per domein een subdomein +`openpgpkey.` → `https://openpgpkey./.well-known/openpgpkey//hu/?l=`. + +``` +push naar main → Forgejo Actions (act_runner docgen): stage 1 bouwt met gpg-wks-client de hu/-boom + uit keys//.asc, stage 2 = nginx die 'm serveert + → push naar git.doc-gen.eu/wsfcqyxbu1/wkd:latest + → host-watcher (docgen, |remote-entry) ziet de nieuwe digest → Coolify deployt op de derden-box +``` + +- **Alles staat in git** (publieke sleutels zijn geen geheim); geen volumes, geen env, geen back-up nodig. +- **Alleen publieke sleutels.** De build faalt op een `PRIVATE KEY BLOCK`, op een bestandsnaam die niet + bij de domeinmap hoort, en op een sleutel zonder UID met exact dat adres. +- **Hash-regel** (spec, draft-koch-openpgp-webkey-service): `hu/`; + wij rekenen 'm nooit zelf uit — `gpg-wks-client --install-key` doet dat, en exporteert alléén de UID + van dat adres (andere UIDs moeten eruit). Controle: `gpg-wks-client --print-wkd-hash `. +- **Sleutel toevoegen/vernieuwen:** `keys//.asc` (= `gpg --export --armor `) → + commit + push. Nieuw domein = nieuwe map `keys//` + DNS `openpgpkey.` + domein op de + Coolify-app. +- **Headers:** `Content-Type: application/octet-stream`, `Access-Control-Allow-Origin: *`; `policy` is leeg + (verplicht bestand). Toegangslog staat uit (geen sporen van wie welk adres opvraagt). +- **Versie:** `FROM alpine:` (build) + `FROM nginx:-alpine` (runtime) in de `Dockerfile`. + +Beheer/runbook: infra-repo `wSFcQyxBu1/infra`, `runbooks/wkd-pgp-sleutels.md`; issue infra#88. diff --git a/build-wkd.sh b/build-wkd.sh new file mode 100644 index 0000000..7464b95 --- /dev/null +++ b/build-wkd.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# Bouwt de WKD-boom (advanced method) uit keys//.asc met gpg-wks-client. +# Gebruik: build-wkd.sh → //{policy,hu/} +# Faalt hard (build breekt) als een .asc geen publieke sleutel is, een privésleutel bevat, de +# bestandsnaam niet bij de map hoort, of de sleutel geen UID met precies dat adres heeft. +set -eu +KEYS=$1; OUT=$2 +export GNUPGHOME=$(mktemp -d); chmod 700 "$GNUPGHOME" +mkdir -p "$OUT" +n=0 +for domdir in "$KEYS"/*/; do + [ -d "$domdir" ] || continue + domain=$(basename "$domdir" | tr 'A-Z' 'a-z') + mkdir -p "$OUT/$domain/hu" + : > "$OUT/$domain/policy" # verplicht; leeg volstaat (spec §3.1) + for f in "$domdir"*.asc; do + [ -f "$f" ] || continue + addr=$(basename "$f" .asc) + case "$addr" in + *@"$domain") ;; + *) echo "FOUT: $f — bestandsnaam moet @$domain.asc zijn" >&2; exit 1 ;; + esac + if grep -q 'PRIVATE KEY BLOCK' "$f"; then + echo "FOUT: $f bevat een PRIVÉSLEUTEL — nooit in deze repo" >&2; exit 1 + fi + gpg --batch --quiet --import "$f" + # exact-match op het adres (), niet substring + fpr=$(gpg --batch --with-colons --list-keys "<$addr>" 2>/dev/null | awk -F: '/^fpr/{print $10; exit}') + if [ -z "$fpr" ]; then + echo "FOUT: $f — geen sleutel met een UID die exact <$addr> bevat" >&2; exit 1 + fi + # --install-key: exporteert binair, filtert op deze UID, schrijft /hu/ (+ policy) + gpg-wks-client --batch --quiet --directory "$OUT" --install-key "$fpr" "$addr" + hash=$(gpg-wks-client --print-wkd-hash "$addr" | awk '{print $1}') + [ -s "$OUT/$domain/hu/$hash" ] || { echo "FOUT: $OUT/$domain/hu/$hash niet aangemaakt" >&2; exit 1; } + echo "OK $addr → $domain/hu/$hash ($fpr)" + n=$((n+1)) + done +done +rm -rf "$GNUPGHOME" +echo "Klaar: $n sleutel(s)." +find "$OUT" -type f | sort diff --git a/keys/2487.eu/README.md b/keys/2487.eu/README.md new file mode 100644 index 0000000..f2c5754 --- /dev/null +++ b/keys/2487.eu/README.md @@ -0,0 +1,8 @@ +# keys/2487.eu — publieke PGP-sleutels voor `*@2487.eu` + +Eén bestand per adres: **`@2487.eu.asc`** = `gpg --export --armor ` (ASCII-armored +**publieke** sleutel; de UID moet exact dat adres bevatten — de build controleert dat en faalt anders). + +- **Nooit** een privésleutel (`PRIVATE KEY BLOCK`) of passphrase hier — de build weigert die. +- Sleutel vernieuwen/intrekken = het `.asc` vervangen (of verwijderen) + commit + push. +- Een domeinmap zónder `.asc` levert alleen het (lege) `policy`-bestand — dat is genoeg voor een geldige WKD. diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..a07e937 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,25 @@ +# WKD-server (advanced method). Alleen /.well-known/openpgpkey/ heeft inhoud. +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + server_tokens off; + access_log off; # sleutelopvragingen niet loggen (geen adres-sporen) + + location = / { + default_type text/plain; + return 200 "OpenPGP Web Key Directory (advanced method). Zie /.well-known/openpgpkey//policy\n"; + } + + location /.well-known/openpgpkey/ { + # spec: sleutel binair als application/octet-stream; CORS-* voor browser-clients (Mailvelope e.d.) + types { } + default_type application/octet-stream; + add_header Access-Control-Allow-Origin "*" always; + add_header Cache-Control "public, max-age=3600" always; + autoindex off; + try_files $uri =404; # ?l= wordt genegeerd (statisch bestand) + } + + location / { return 404; } +}