← Back to Wiki
Networking / Automation

Automate UniFi Controller SSL Certificate Renewal via its Local API

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.

Share on X

Step one: get real SSH access, without any key-upload UI

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.

Step two: find the real API by intercepting the UI's own requests

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:

ActionRequest
List certsGET /api/userCertificates
Upload newPOST /api/userCertificates, JSON body {"name", "cert", "key"}, not multipart. The cert field is the leaf plus intermediate chain concatenated into one PEM string
ActivatePUT /api/userCertificates/{id}/status, {"active": true}
DeleteDELETE /api/userCertificates/{id}

The real find: the same routes exist, unauthenticated, on localhost

UniFi's backend process listens on an internal localhost port and answers these same routes with no session or CSRF token at all. Confirmed from the console's own shell. A plain 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.

A real gotcha building the renewal automation

BE WARNED: the upload endpoint rejects a second certificate with the same "name" field as an existing entry. Even if that existing entry is inactive. You get "Certificate with such name or fingerprint already exists". That breaks a naive upload, activate, delete-old sequence when the new upload reuses the domain name as its label. Fix it by suffixing the upload's 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.

The result

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.