From 39692ea89b7cff7940836e5f566467b53ba9ebd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 25 Aug 2026 20:05:12 +0000 Subject: [PATCH] how to actually issue a certificate, read off NPM's own source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verified against 2.15.1 rather than remembered, because the remembered version is wrong: every tutorial sends letsencrypt_email and letsencrypt_agree, and this NPM rejects both — `meta` is additionalProperties:false. the email comes from the NPM ACCOUNT, looked up in internal/certificate.js, which refuses with "a valid email address must be set on your user account" if it has none. that account here has one; checked. so for a name that already resolves publicly the whole body is provider plus domain_names. HTTP-01, nothing else to arrange. the DNS-01 shape is written down beside it for the private case, along with the fact that the agent cannot fill dns_provider_credentials in — officer never returns a stored DNS key, and reaching for one is the refusal-shaped mistake this skill already warns about. folded the old trailing "not built" section into it rather than leaving two places saying the same thing differently. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- skills/proxy-manager/SKILL.md | 55 ++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/skills/proxy-manager/SKILL.md b/skills/proxy-manager/SKILL.md index d8193c7..454c017 100644 --- a/skills/proxy-manager/SKILL.md +++ b/skills/proxy-manager/SKILL.md @@ -71,6 +71,51 @@ Creating something new needs no ceremony: report what you made. **Never infer a change from a question.** "Is `foo.example.com` up?" asks about `meta.nginx_online`. It is not an instruction to restart, disable or recreate anything. +## Issuing a certificate + +**Verified against NPM 2.15.1 on 2026-08-25, by reading its own source.** Most documentation and most +memory of this API are wrong in the same way — see the warning below. + +**If the name already resolves to this machine's public address**, Let's Encrypt can reach it on port 80 +and there is nothing else to arrange: + +```bash +curl -s -X POST -H "Authorization: Bearer $OFFICER_AGENT_KEY" -H "Host: $OFFICER_API_HOST" \ + -H 'content-type: application/json' \ + -d '{"provider":"letsencrypt","domain_names":["sub.example.com"]}' \ + "$OFFICER_API_URL/api/proxy-manager/npm/nginx/certificates" +``` + +That is the whole body. Then attach it to a host by `PUT`ting that host with `certificate_id` set to the +new certificate's `id`, and `ssl_forced: true` if it should redirect. + +**Do NOT send `letsencrypt_email` or `letsencrypt_agree`.** They are what every tutorial shows and NPM +2.15 does not accept them: `meta` is `additionalProperties: false`, so the request is rejected outright. +The email comes from the NPM ACCOUNT — `internal/certificate.js` looks up the owning user and refuses +with *"A valid email address must be set on your user account"* if it has none. That is a setting in +NPM's own UI, not something this API can supply. + +**If the name does NOT resolve publicly** — a private or VPN-only hostname — Let's Encrypt cannot visit +it, and the only proof it accepts is a DNS record: + +```jsonc +{ + "provider": "letsencrypt", + "domain_names": ["private.example.com"], + "meta": { + "dns_challenge": true, + "dns_provider": "cloudflare", // an id from /npm/nginx/certificates/dns-providers + "dns_provider_credentials": "dns_cloudflare_api_token = …", // that provider's ini template, filled in + "propagation_seconds": 60 + } +} +``` + +**You cannot fill in `dns_provider_credentials` yourself, and should not try.** Officer stores DNS +provider keys encrypted and never returns them — not to you, not to the owner. Reaching for one is the +refusal-shaped mistake this skill warns about below. The route that injects it server-side is not built +yet; until it is, a private hostname's certificate is something the owner does in NPM's own UI. + ## Certificates, read the way Let's Encrypt behaves Renewal runs at **30 days**, so under 30 is normal and not worth mentioning. **Under 14 means renewal has @@ -98,13 +143,3 @@ unauthenticated. Only the owner can, at `/proxy-manager?config=1`. - **403** — either the account lacks `proxy-manager`, or it has it at **read** level, which permits GET and refuses every write. Do not look for another way round; report it. - -## A private hostname needs a DNS credential, and that is not built - -TLS on a name that only resolves inside a VPN cannot use the usual challenge — Let's Encrypt cannot reach -it — so it needs a DNS-01 challenge, which needs that domain's provider API key. - -**Officer does not store DNS provider credentials yet.** `GET /npm/nginx/certificates/dns-providers` lists -what this NPM can drive and the field names each one wants, and a certificate can be created through the -pass-through if the owner supplies the credential in the request. Do not promise the smoother version: -`plugins/proxy-manager/README.md` has the design and none of it exists.