Every device on a LAN behind an internal certificate authority can get real, browser-trusted HTTPS. Except the UniFi console itself. Its certificate manager is a browser-only click-through with no documented API. I did not want a real UniFi account credential sitting in a cron job, or a stolen session cookie. Reverse-engineering the console's own UI found a much better path.
UniFi OS's console settings expose an SSH toggle and a password-only "Change Password" control. There is no
key-upload option at the local console level. The per-admin "SSH Keys" feature some UniFi documentation
mentions belongs to the cloud identity system, not this local console UI. Here is the path to durable
key-based access. Set a one-time password through that control. Log in exactly once with it. Use that single
session to append a real public key to ~/.ssh/authorized_keys. Password auth is never needed
again. Every future automated session uses the key.
The certificate manager's request shapes are not published anywhere. I recovered them by monkey-patching
window.fetch in the console's own page context, right before triggering each UI action for real,
then reading back exactly what got sent. That surfaced the real, undocumented routes:
| Action | Request |
|---|---|
| List certs | GET /api/userCertificates |
| Upload new | POST /api/userCertificates, JSON body {"name", "cert", "key"},
not multipart. The cert field is the leaf plus intermediate chain concatenated into one PEM string |
| Activate | PUT /api/userCertificates/{id}/status, {"active": true} |
| Delete | DELETE /api/userCertificates/{id} |
curl
against that local port returns the real certificate list with zero authentication. Session auth is enforced
on the public HTTPS port, by the reverse proxy in front. The backend process trusts anything arriving from
localhost unconditionally. This is presumably the mechanism UniFi's own built-in Let's Encrypt auto-renewal
uses.
That is what makes real automation possible without a UniFi account password or a stolen session cookie in a
script. SSH in as the key-authenticated user from step one, then curl the local port directly.
No credentials to rotate. No session to keep alive. No browser automation in the renewal step at all.
name field with a timestamp. That
field is a UI label with no bearing on the certificate's content or hostname matching. The suffix is
cosmetic. It just has to be unique.
Keep the safer operation order despite the extra step. Create the new cert first. Verify it is active. Then delete the old one. Delete first and a failure partway through leaves the console with zero valid certificates installed. This way the worst case is a leftover old cert sitting alongside the new one.
A daily cron job checks the active certificate's real expiry date via the local API. Once it is within the threshold, the job mints a fresh certificate from the internal CA, uploads it, activates it and removes the previous one. Same renewal shape as every other internally certificated service. It just targets a completely different, undocumented install mechanism underneath. I verified it end to end with a real renewal run, not the skip-path when nothing needs renewing, before trusting it unattended.