Skip to content
Notes from the field

Notes from the field

LetsEncrypt with DNS-01 validation for Unifi

LetsEncrypt with DNS-01 validation for Unifi

2017-07-15

Update 2021-01-08: this is now out of date. See my updated post with a much easier method.

I have a number of Ubiquiti UAPs, and I manage them with the UniFi app, installed on a linode server. Like any publicly hosted server, i want to use a trusted SSL certificate, and for that, I chose LetsEncrypt with DNS-01 validation, as i found a useful helper script by thatsamguy on the UniFi forums. I use AWS Route53 to host the DNS zone.

UniFi doesn’t have built in support for LetsEncrypt, so I put together a simple solution using the DNS-01 validation method. Here’s how i did it:

  1. Created a new, unprivileged user on the host:
    sudo adduser dehydrated
  2. Created the directory to store the certificates. I chose to use the default /etc/letsencrypt folder
    sudo mkdir /etc/letsencrypt
  3. I granted the dehydrated user full access to this folder:
    sudo chown dehydrated:dehydrated /etc/letsencrypt
  4. installed the dependencies which are in the repo:
    sudo apt-get install --no-install-recommends jq sed findutils s-nail
  5. installed the cli53 dependency which is not in the repo by following the instructions on the git readme.md file
  6. logged in as the dehydrated user:
    su - dehydrated
  7. fetched dehydrated and made it executable:
    wget https://raw.githubusercontent.com/lukas2511/dehydrated/master/dehydrated
    chmod +x dehydrated
  8. fetched the dehydrated route53 hook script and made it executable:
    wget https://raw.githubusercontent.com/whereisaaron/dehydrated-route53-hook-script/master/hook.sh
    chmod +x hook.sh
  9. Created a new IAM access policy in AWS. I found that the sample policy given with the root 53 hook readme.md didn’t work – here’s the policy that I added:
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "route53:ListHostedZones",
                    "route53:ListHostedZonesByName",
                    "route53:ListResourceRecordSets"
                ],
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "route53:ListResourceRecordSets",
                    "route53:ChangeResourceRecordSets"
                ],
                "Resource": "arn:aws:route53:::hostedzone/*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "route53:GetChange"
                ],
                "Resource": "arn:aws:route53:::change/*"
            }
        ]
    }
  10. Created a new IAM user in AWS and assigned the policy to them (and only this policy), taking a note of the AWS Access Key and AWS Secret Access Key.
  11. Created a new file to store the key:
    mkdir ~/.aws
    nano ~/.aws/credentials

    The file looks like this (obviously, put your own data in there):

    [default]
    aws_access_key_id = AKIAIJFAKGII4MDS3KHA
    aws_secret_access_key = Q+XoOGa5J3AS39as593Ds1f5F91zRy0btkfW
  12. Created a new config file:
    nano ~/config

    The file looks like this (edited from the sample):

    ########################################################
    # This is the main config file for dehydrated          #
    #                                                      #
    # This file is looked for in the following locations:  #
    # $SCRIPTDIR/config (next to this script)              #
    # /usr/local/etc/dehydrated/config                     #
    # /etc/dehydrated/config                               #
    # ${PWD}/config (in current working-directory)         #
    #                                                      #
    # Default values of this config are in comments        #
    ########################################################
    
    # Resolve names to addresses of IP version only. (curl)
    # supported values: 4, 6
    # default: 
    #IP_VERSION=
    
    # Path to certificate authority (default: https://acme-v01.api.letsencrypt.org/directory)
    #CA="https://acme-v01.api.letsencrypt.org/directory"
    # CA="https://acme-staging.api.letsencrypt.org/directory"
    
    # Path to certificate authority license terms redirect (default: https://acme-v01.api.letsencrypt.org/terms)
    #CA_TERMS="https://acme-v01.api.letsencrypt.org/terms"
    # CA_TERMS="https://acme-staging.api.letsencrypt.org/terms"
    
    # Path to license agreement (default: )
    #LICENSE=""
    
    # Which challenge should be used? Currently http-01 and dns-01 are supported
    CHALLENGETYPE="dns-01"
    
    # Path to a directory containing additional config files, allowing to override
    # the defaults found in the main configuration file. Additional config files
    # in this directory needs to be named with a '.sh' ending.
    # default: 
    #CONFIG_D=
    
    # Base directory for account key, generated certificates and list of domains (default: $SCRIPTDIR -- uses config directory if undefined)
    #BASEDIR=$SCRIPTDIR
    
    # File containing the list of domains to request certificates for (default: $BASEDIR/domains.txt)
    #DOMAINS_TXT="${BASEDIR}/domains.txt"
    
    # Output directory for generated certificates
    #CERTDIR="${BASEDIR}/certs"
    
    # Directory for account keys and registration information
    #ACCOUNTDIR="${BASEDIR}/accounts"
    
    # Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: /var/www/dehydrated)
    #WELLKNOWN="/var/www/dehydrated"
    
    # Default keysize for private keys (default: 4096)
    #KEYSIZE="4096"
    
    # Path to openssl config file (default:  - tries to figure out system default)
    #OPENSSL_CNF=
    
    # Extra options passed to the curl binary (default: )
    #CURL_OPTS=
    
    # Program or function called in certain situations
    #
    # After generating the challenge-response, or after failed challenge (in this case altname is empty)
    # Given arguments: clean_challenge|deploy_challenge altname token-filename token-content
    #
    # After successfully signing certificate
    # Given arguments: deploy_cert domain path/to/privkey.pem path/to/cert.pem path/to/fullchain.pem
    #
    # BASEDIR and WELLKNOWN variables are exported and can be used in an external program
    # default: 
    HOOK=${BASEDIR}/hook.sh
    
    # Chain clean_challenge|deploy_challenge arguments together into one hook call per certificate (default: no)
    HOOK_CHAIN="no"
    
    # Minimum days before expiration to automatically renew certificate (default: 30)
    #RENEW_DAYS="30"
    
    # Regenerate private keys instead of just signing new certificates on renewal (default: yes)
    #PRIVATE_KEY_RENEW="yes"
    
    # Create an extra private key for rollover (default: no)
    #PRIVATE_KEY_ROLLOVER="no"
    
    # Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
    #KEY_ALGO=rsa
    
    # E-mail to use during the registration (default: )
    [email protected]
    
    # Lockfile location, to prevent concurrent access (default: $BASEDIR/lock)
    #LOCKFILE="${BASEDIR}/lock"
    
    # Option to add CSR-flag indicating OCSP stapling to be mandatory (default: no)
    #OCSP_MUST_STAPLE="no"
    
    # Fetch OCSP responses (default: no)
    #OCSP_FETCH="no"
    
    # Issuer chain cache directory (default: $BASEDIR/chains)
    #CHAINCACHE="${BASEDIR}/chains"
    
    # Automatic cleanup (default: no)
    AUTO_CLEANUP="yes"
  13. Created a new file with the list of domains to register:
    nano ~/domains.txt

    The file looks like this (obviously, put your own data in there):

    myhost.mydomain.com
  14. Checked that the certificate registers correctly – note that if you are having trouble, you should enable the “staging” CA / terms file in config while you troubleshoot to avoid hitting letsencrypt limits:
    [email protected]:~$ ./dehydrated --cron --accept-terms --out /etc/letsencrypt
    # INFO: Using main config file /home/dehydrated/config
    + Generating account key...
    + Registering account key with ACME server...
    Processing myhost.mydomain.com
     + Signing domains...
     + Creating new directory /home/dehydrated/certs/myhost.mydomain.com ...
     + Generating private key...
     + Generating signing request...
     + Requesting challenge for myhost.mydomain.com...
    Creating challenge record for myhost.mydomain.com in zone cynexia.net
    Created record: '_acme-challenge.myhost.mydomain.com. 60 IN TXT "cpE1VF_xshMm1IVY1Y66Kk9Zb_7jA2VFkP65WuNgu3Q"'
    Waiting for sync...................................
    Completed
     + Responding to challenge for myhost.mydomain.com...
    Deleting challenge record for myhost.mydomain.com from zone mydomain.com
    1 record sets deleted
     + Challenge is valid!
     + Requesting certificate...
     + Checking certificate...
     + Done!
     + Creating fullchain.pem...
     + Using cached chain!
     + Done!
    + Running automatic cleanup
  15. Created the helper script, remembering to edit the domain (find/replace) and the certificate path:
    nano updateunificert
    My file looks like this:

     

    !/bin/bash
    PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games';
    openssl pkcs12 -export -in /etc/letsencrypt/myhost.mydomain.com/fullchain.pem -inkey /etc/letsencrypt/myhost.mydomain.com/privkey.pem -out /etc/letsencrypt/myhost.mydomain.com/cert_and_key.p12 -name tomcat -CAfile /etc/letsencrypt/myhost.mydomain.com/chain.pem -caname root -password pass:aaa;
    rm -f /etc/letsencrypt/myhost.mydomain.com/keystore;
    keytool -importkeystore -srcstorepass aaa -deststorepass aircontrolenterprise -destkeypass aircontrolenterprise -srckeystore /etc/letsencrypt/myhost.mydomain.com/cert_and_key.p12 -srcstoretype PKCS12 -alias tomcat -keystore /etc/letsencrypt/myhost.mydomain.com/keystore;
    keytool -import -trustcacerts -alias unifi -deststorepass aircontrolenterprise -file /etc/letsencrypt/myhost.mydomain.com/chain.pem -noprompt -keystore /etc/letsencrypt/myhost.mydomain.com/keystore;
    mv /var/lib/unifi/keystore /var/lib/unifi/keystore-backup;
    cp /etc/letsencrypt/myhost.mydomain.com/keystore /var/lib/unifi/keystore;
    service unifi restart;
  16. Created cron jobs to trigger the cert update daily:
    crontab -e
    I added this line:

     

    20 5 * * * /home/dehydrated/dehydrated --cron --accept-terms --out /etc/letsencrypt >/dev/null 2>&1
  17. returned to my previous shell:
    exit
  18. Created cron jobs to install the new cert daily:
    sudo crontab -e
    I added this line:

     

    29 5 * * * /home/dehydrated/updateunificert >/dev/null 2>&1

Share this:

  • Click to email a link to a friend (Opens in new window)
  • Click to share on Facebook (Opens in new window)
  • Click to share on LinkedIn (Opens in new window)
  • Click to share on Twitter (Opens in new window)
  • Click to share on WhatsApp (Opens in new window)

how-to

Post navigation

PREVIOUS
How to upgrade VMWare ESXi on HP Gen8 Microserver
NEXT
The NVIDIA GPU Accelerated Cloud (NGC) for Deep Learning and HPC
Comments are closed.

Archives

The standard disclaimer…

The views, thoughts, and opinions expressed in the text belong solely to the me, and not necessarily to the my employer, organization, committee or other group that I belong to or am associated with.

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.
© 2023 Rob Aleck, licensed under CC BY-NC 4.0
Go to mobile version