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:
- Created a new, unprivileged user on the host:
sudo adduser dehydrated - Created the directory to store the certificates. I chose to use the default /etc/letsencrypt folder
sudo mkdir /etc/letsencrypt - I granted the dehydrated user full access to this folder:
sudo chown dehydrated:dehydrated /etc/letsencrypt - installed the dependencies which are in the repo:
sudo apt-get install --no-install-recommends jq sed findutils s-nail - installed the cli53 dependency which is not in the repo by following the instructions on the git readme.md file
- logged in as the dehydrated user:
su - dehydrated - fetched dehydrated and made it executable:
wget https://raw.githubusercontent.com/lukas2511/dehydrated/master/dehydrated
chmod +x dehydrated - 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 - 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/*"
}
]
} - 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.
- Created a new file to store the key:
mkdir ~/.aws
nano ~/.aws/credentialsThe file looks like this (obviously, put your own data in there):[default]
aws_access_key_id = AKIAIJFAKGII4MDS3KHA
aws_secret_access_key = Q+XoOGa5J3AS39as593Ds1f5F91zRy0btkfW - Created a new config file:
nano ~/configThe 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: )
CONTACT_EMAIL=myemail@mydomain.com
# 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" - Created a new file with the list of domains to register:
nano ~/domains.txtThe file looks like this (obviously, put your own data in there):myhost.mydomain.com - 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:
dehydrated@localhost:~$ ./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 - Created the helper script, remembering to edit the domain (find/replace) and the certificate path:
nano updateunificertMy 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; - Created cron jobs to trigger the cert update daily:
crontab -eI added this line:
20 5 * * * /home/dehydrated/dehydrated --cron --accept-terms --out /etc/letsencrypt >/dev/null 2>&1 - returned to my previous shell:
exit - Created cron jobs to install the new cert daily:
sudo crontab -eI added this line:
29 5 * * * /home/dehydrated/updateunificert >/dev/null 2>&1
No comments yet