how to actually issue a certificate, read off NPM's own source
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 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user