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.