acmesh-official / acme.sh

A pure Unix shell script implementing ACME client protocol
https://acme.sh
GNU General Public License v3.0
38.82k stars 4.93k forks source link

unifi deploy hook doesn't fully support Unifi Cloud Key #3326

Closed medmunds closed 3 years ago

medmunds commented 3 years ago

When --deploy-hook unifi is used on a Unifi Cloud Key, it only deploys the certificate to one of the two locations needed. After a successful deploy, opening the Cloud Key management page shows a browser certificate error.

Problem

A Unifi Cloud Key runs two https servers: the Unifi Controller app on port 8443 and the Cloud Key management app on port 443. The v2.8.8 unifi deploy hook only updates the keystore used for the Unifi Controller. This means browsing to the Cloud Key management app will show a certificate error, because the default self-signed cert is still being used.

(The unifi deploy hook works just fine for running Unifi Controller on your own machine; this is only a problem on Unifi's Cloud Key device.)

Steps to reproduce

[Substitute your own domain for "unifi.example.com"]

  1. On a Unifi Cloud Key device, install acme.sh and run it to issue a certificate for unifi.example.com (using whatever options you want).
  2. Run ./acme.sh --deploy -d "unifi.example.com" --deploy-hook unifi. Wait for it to complete successfully.
  3. Open a browser to the Unifi Controller app at https://unifi.example.com:8443. Observe that it is using your newly-issued, valid Let's Encrypt certificate, and there are no browser certificate errors. (The unifi deploy-hook works correctly for the Unifi Controller app.)
  4. Open a browser to the Unifi Cloud Key management app at https://unifi.example.com.

Results: browser error about untrusted certificate

Expected: should be using your new certificate just like the Controller app does on port 8443

Debug log

[probably not helpful; click to show] ``` root@UniFi-CloudKey:~/.acme.sh# ./acme.sh --version https://github.com/acmesh-official/acme.sh v2.8.8 root@UniFi-CloudKey:~/.acme.sh# ./acme.sh --deploy -d "unifi.example.com" --deploy-hook unifi --debug 2 [Mon Dec 28 14:46:21 PST 2020] Lets find script dir. [Mon Dec 28 14:46:21 PST 2020] _SCRIPT_='./acme.sh' [Mon Dec 28 14:46:21 PST 2020] _script='/root/.acme.sh/acme.sh' [Mon Dec 28 14:46:21 PST 2020] _script_home='/root/.acme.sh' [Mon Dec 28 14:46:21 PST 2020] Using default home:/root/.acme.sh [Mon Dec 28 14:46:21 PST 2020] Using config home:/root/.acme.sh [Mon Dec 28 14:46:21 PST 2020] LE_WORKING_DIR='/root/.acme.sh' https://github.com/acmesh-official/acme.sh v2.8.8 [Mon Dec 28 14:46:21 PST 2020] Running cmd: deploy [Mon Dec 28 14:46:21 PST 2020] Using config home:/root/.acme.sh [Mon Dec 28 14:46:21 PST 2020] default_acme_server [Mon Dec 28 14:46:21 PST 2020] ACME_DIRECTORY='https://acme-v02.api.letsencrypt.org/directory' [Mon Dec 28 14:46:21 PST 2020] _ACME_SERVER_HOST='acme-v02.api.letsencrypt.org' [Mon Dec 28 14:46:21 PST 2020] DOMAIN_PATH='/root/.acme.sh/unifi.example.com' [Mon Dec 28 14:46:21 PST 2020] _deployApi='/root/.acme.sh/deploy/unifi.sh' [Mon Dec 28 14:46:22 PST 2020] _cdomain='unifi.example.com' [Mon Dec 28 14:46:22 PST 2020] _ckey='/root/.acme.sh/unifi.example.com/unifi.example.com.key' [Mon Dec 28 14:46:22 PST 2020] _ccert='/root/.acme.sh/unifi.example.com/unifi.example.com.cer' [Mon Dec 28 14:46:22 PST 2020] _cca='/root/.acme.sh/unifi.example.com/ca.cer' [Mon Dec 28 14:46:22 PST 2020] _cfullchain='/root/.acme.sh/unifi.example.com/fullchain.cer' [Mon Dec 28 14:46:22 PST 2020] _unifi_keystore='/usr/lib/unifi/data/keystore' [Mon Dec 28 14:46:22 PST 2020] Generate import pkcs12 [Mon Dec 28 14:46:22 PST 2020] Modify unifi keystore: /usr/lib/unifi/data/keystore Importing keystore /tmp/tmp.e3ZAglrpHS to /usr/lib/unifi/data/keystore... Warning: Overwriting existing alias unifi in destination keystore [Mon Dec 28 14:46:29 PST 2020] Import keystore success! [Mon Dec 28 14:46:29 PST 2020] Run reload: service unifi restart [Mon Dec 28 14:48:17 PST 2020] Reload success! [Mon Dec 28 14:48:17 PST 2020] Success ```

Suggested fix

When running on a Cloud Key, the unifi deploy hook also needs to:

PR follows.

petrus9 commented 3 years ago

I have used this renew script to deploy to the unifi cloudkey on firmware v1.1.13 and Unifi 6.0.41 got it from this site: https://www.naschenweng.info/2017/01/06/securing-ubiquiti-unifi-cloud-key-encrypt-automatic-dns-01-challenge/

perhaps it can help?

`#!/bin/bash

Renew-hook for ACME / Let's encrypt

echo "** Configuring new Let's Encrypt certs" cd /etc/ssl/private rm -f /etc/ssl/private/cert.tar /etc/ssl/private/unifi.keystore.jks /etc/ssl/private/ssl-cert-snakeoil.key /etc/ssl/private/fullchain.pem

openssl pkcs12 -export -in /etc/ssl/private/cloudkey.crt -inkey /etc/ssl/private/cloudkey.key -out /etc/ssl/private/cloudkey.p12 -name unifi -password pass:aircontrolenterprise

keytool -importkeystore -deststorepass aircontrolenterprise -destkeypass aircontrolenterprise -destkeystore /usr/lib/unifi/data/keystore -srckeystore /etc/ssl/private/cloudkey.p12 -srcstoretype PKCS12 -srcstorepass aircontrolenterprise -alias unifi

rm -f /etc/ssl/private/cloudkey.p12 tar -cvf cert.tar chown root:ssl-cert /etc/ssl/private/ chmod 640 /etc/ssl/private/*

echo "** Testing Nginx and restarting" /usr/sbin/nginx -t /etc/init.d/nginx restart ; /etc/init.d/unifi restart`

medmunds commented 3 years ago

@petrus9 thanks, yes, I'd been working from Gerd Naschenweng's really helpful post, as well as James Ridgway's update from earlier this year.

The existing unifi.sh deploy hook already includes most of that renew script, but is missing the bit at the end about /etc/ssl/private and restarting nginx. I just submitted PR #3327 to add those parts. (BTW, it's not necessary to remove the snakeoil self-signed key, and the purpose and correct construction of cert.tar is sort-of documented in a Ubiqiti forum post, so I followed their guidance. But maybe not a bad idea to add the nginx config check.)

mjbnz commented 3 years ago

Note that with the new UnifiOS (Cloud key firmware version 2.0.0 or greater), there's no need for the java keystore to be maintained - all HTTPS traffic is via port 443 to the new SSO interface on the cloud key. The unifi deployment hook can potentially deal with this automagically, by looking for the existence of key and cert files to replace at /data/unifi-core/config/unifi-core.{key,crt}. Reloading after issue/renew would be with systemctl restart unifi-core.

I don't use this deploy hook, I have a custom deployment script using ssh keys and restricted commands, so I'm only commenting to provide information around a change that will soon be required.

petrus9 commented 3 years ago

Thanks for the update @medmunds! @mjbnz I am still using the cloudkey Gen1 that is still on the 1.0 firmware. Not sure if Ubiquiti will update the Gen1 Cloudkey to 2. So for a while at least it would be good for the hook to support both firmware versions.

mjbnz commented 3 years ago

@mjbnz I am still using the cloudkey Gen1 that is still on the 1.0 firmware. Not sure if Ubiquiti will update the Gen1 Cloudkey to 2. So for a while at least it would be good for the hook to support both firmware versions.

Yes, I personally don't believe the Gen1 will get v2.0, and yes, I agree - I wasn't terribly clear, supporting both was what I meant by "deal with this automagically".

medmunds commented 3 years ago

@mjbnz thanks for the tip. So do you think it would be reasonable for the unifi deploy hook to just check all three possible cert locations, and deploy to all that exist? Pseudocode:

# Deploy to java keystore on self-hosted Unifi Controller or CloudKey Gen1:
if exists /usr/lib/unifi/data/keystore:
  generate pkcs12 and import into java keystore
  service unifi restart

# Deploy to nginx on CloudKey Gen1
if exists /etc/ssl/private/cloudkey.key:
  deploy cert to /etc/ssl/private/cloudkey.{key,crt}
  service nginx restart

# Deploy to unifi-core on UnifiOS (CloudKey Gen2, Dream Machine, ...)
if exists /data/unifi-core/config/unifi-core.key:
  deploy cert to /data/unifi-core/config/unifi-core.{key,crt}
  systemctl restart unifi-core

if nothing deployed by this point:
   issue configuration error

(With appropriate config vars to support non-standard installation locations, as in the current unifi deploy hook.)

mjbnz commented 3 years ago

@mjbnz thanks for the tip. So do you think it would be reasonable for the unifi deploy hook to just check all three possible cert locations, and deploy to all that exist? Pseudocode:

Yes, I think that looks perfect. Nice one.

medmunds commented 3 years ago

Updated PR #3327 to support both generations of Cloud Key (as well as the self-hosted Unifi Controller it previously supported). I only have a Gen1 Cloud Key to test on; if someone running UnifiOS wanted to take a quick glance at the script that would be much appreciated.

petrus9 commented 3 years ago

@medmunds has this been added to the latest acme.sh, I can run a test on a CK gen2 with firmware 2.

petrus9 commented 3 years ago

@medmunds I tried with the current version of acme.sh v2.8.9 on Cloudkey Gen2 Plus firmware 2.0.27 Controller 6.0.45 but it did not work. This worked:

acme.sh --force --issue --dns dns_dynu -d mysubdomain.domain.org --pre-hook "tar -zcvf /root/.acme.sh/CloudKeySSL_date +%Y-%m-%d_%H.%M.%S.tgz /data/unifi-core/config/unifi-core.*" --fullchainpath /data/unifi-core/config/unifi-core.crt --keypath /data/unifi-core/config/unifi-core.key --reloadcmd "systemctl restart unifi-core.service"

medmunds commented 3 years ago

@petrus9 PR #3327 has not been merged yet, so this is not in acme.sh v2.8.9.

If you would like to test before that PR is merged, please install the latest acme.sh as usual, then (before running it) copy deploy/unifi.sh from the PR into your local acme.sh installation (overwriting the existing deploy/unifi.sh). Then try deploying with the unifi deploy-hook per the docs.

petrus9 commented 3 years ago

@medmunds Thanks! Will give a it try. Do you know if this also puts a cert in place for Unifi Protect. On the Cloudkey Gen2 Plus it includes port 7443 as the interface to Unifi protect and it can also host Unifi Access and Unifi Talk, not sure if these require a different cert store?

mjbnz commented 3 years ago

@medmunds Thanks! Will give a it try. Do you know if this also puts a cert in place for Unifi Protect. On the Cloudkey Gen2 Plus it includes port 7443 as the interface to Unifi protect and it can also host Unifi Access and Unifi Talk, not sure if these require a different cert store?

Port 7443 is no longer used on v2.x for general user access, it's all via 443 and the unifi-core frontend/reverse proxy.

Cert locations this deploy hook maintains are:

/usr/lib/unifi/data/keystore

/etc/ssl/private/cloudkey.{key,crt}

/data/unifi-core/config/unifi-core.{key,crt}

medmunds commented 3 years ago

Do you know if this also puts a cert in place for Unifi Protect

Per @mjbnz's answer above, I believe the answer is yes. However, it does not currently restart the Unifi Protect service, so that may not pick up the new cert immediately. @mjbnz, should I just add a systemctl restart unifi-protect (if systemctl -q is-active unifi-protect)?

petrus9 commented 3 years ago

I can confirm that the hook does not update the cert for Unifii protect on port 7443, after doing a service unifi-protect restart, the correct cert was deployed. Nor sure if this is needed though since there is nothing there anymore.. I think UBNT only wants us to access protect from the cloud.

mjbnz commented 3 years ago

I can confirm that the hook does not update the cert for Unifii protect on port 7443, after doing a service unifi-protect restart, the correct cert was deployed. Nor sure if this is needed though since there is nothing there anymore.. I think UBNT only wants us to access protect from the cloud.

As I said above, no, port 7443 is no longer used on CK Firmware v2+ for end user access. you now access the Protect web UI using the CK frontend on port 443.

mjbnz commented 3 years ago

Do you know if this also puts a cert in place for Unifi Protect

Per @mjbnz's answer above, I believe the answer is yes. However, it does not currently restart the Unifi Protect service, so that may not pick up the new cert immediately. @mjbnz, should I just add a systemctl restart unifi-protect (if systemctl -q is-active unifi-protect)?

Yes, you could restart unifi-protect like that, but as mentioned above, it's not user-facing, so you do not need to.

Edit: now that I say that though, if it's a CK on v1.x firmware (i.e., the unifi-core location doesn't exist), then if there's a unifi-protect service, it might be wise to restart it - in that instance, it uses the cloudkey.{crt,key} files in /etc/ssl/private for port 7443 which you do use to access the Protect web UI on CK firmware v1.x.