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:
2026-08-25 20:05:12 +00:00
co-authored by Claude Opus 5
parent 07cf3317f8
commit 39692ea89b
+45 -10
View File
@@ -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.