diff --git a/web/ProxiesBlock.tsx b/web/ProxiesBlock.tsx index a8e77fc..ffc5dea 100644 --- a/web/ProxiesBlock.tsx +++ b/web/ProxiesBlock.tsx @@ -127,29 +127,57 @@ export const ProxiesBlock = ({ servers }: { servers: Server[] }) => { }; /** - * Ask Let's Encrypt for a certificate, then attach it. + * Ask for a certificate over the DNS challenge, then attach it. * - * Two calls, because NPM issues a certificate as its own object and a host merely references one. The - * body is exactly `{provider, domain_names}`: `letsencrypt_email` and `letsencrypt_agree` are what - * every tutorial shows and NPM 2.15 rejects the request outright for including them. + * ── Why not the ordinary NPM certificates route ── + * + * That one proves the domain over HTTP, which needs the name to already resolve to this machine and + * port 80 to be reachable from the internet. For a name that does not yet point here — which is every + * name at the moment somebody is setting it up — certbot fails with "Some challenges have failed" and + * a 500 that says nothing about why. + * + * The DNS challenge has no such requirement: it proves ownership by writing a record. That needs the + * registrar's API key, and this must NOT go looking for one. Officer stores DNS provider keys + * encrypted and never returns them, to anybody. `/domains/certificate` is the route that exists for + * exactly this — it works out which provider owns the name from the Domains list, decrypts the key + * itself, and hands back NPM's answer. The key is used and never seen. + * + * ── It takes minutes ── + * + * A DNS record has to propagate before Let's Encrypt will look for it, and the default wait is two + * minutes. Said out loud in the button, because a spinner that sits for three minutes with no + * explanation is indistinguishable from one that has hung. */ const secure = async (host: Host) => { setBusy(`cert:${host.id}`); setFailed(false); - setOutput(null); + setOutput(`Asking for a certificate for ${host.domain_names.join(', ')}.\nThis waits for DNS to propagate and usually takes a few minutes.`); try { - const certificate = await clientRef.current.post<{ id: number }>('/proxy-manager/npm/nginx/certificates', { - provider: 'letsencrypt', + const certificate = await clientRef.current.post<{ id: number }>('/proxy-manager/domains/certificate', { domain_names: host.domain_names, }); await clientRef.current.put(`/proxy-manager/npm/nginx/proxy-hosts/${host.id}`, { certificate_id: certificate.id, ssl_forced: true, }); - setOutput(`Certificate issued for ${host.domain_names.join(', ')} and http now redirects to https.`); + setOutput(`Certificate issued for ${host.domain_names.join(', ')}. http now redirects to https.`); await load(); } catch (failure) { - setOutput(readableError(failure)); + /* + Its refusals are specific and each one is somebody's to fix, so they are translated rather than + shown raw — "Not in the domain list" is a true sentence that tells nobody what to do next. + */ + const message = readableError(failure); + const apex = host.domain_names[0]?.split('.').slice(-2).join('.') ?? 'the domain'; + setOutput( + message.includes('Not in the domain list') + ? `${apex} is not in Officer's Domains list, so it does not know whose DNS it is. Add it there first.` + : message.includes('No API key') + ? `${apex} is known but has no DNS API key stored. Add it in the Domains section.` + : message.includes('two DNS providers') + ? 'One certificate can only cover names from a single DNS provider. Ask for them separately.' + : message, + ); setFailed(true); } finally { setBusy(null); @@ -200,7 +228,7 @@ export const ProxiesBlock = ({ servers }: { servers: Server[] }) => { ) : ( )} - Add certificate + {busy === `cert:${host.id}` ? 'Issuing…' : 'Add certificate'} )} @@ -253,7 +281,9 @@ export const ProxiesBlock = ({ servers }: { servers: Server[] }) => {

- Removing or repointing a domain lives in Proxy Manager, where the whole picture is. + Certificates are proved over DNS with the key Officer holds for the domain, so a name works + before it points here — and takes a few minutes. Removing or repointing a domain lives in + Proxy Manager, where the whole picture is.

)}