├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── resources └── systemd │ ├── udm-le.service │ └── udm-le.timer ├── udm-le.env └── udm-le.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: kchristensen 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Version Information (please complete the following information):** 27 | - UniFi OS: [e.g. 1.10.0-11] 28 | - Hardware Type: [e.g. UDM, UDMB] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: kchristensen 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm using Route53 as a DNS Provider and [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .secrets/* 2 | lego 3 | .lego/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Thanks for wanting to contribute! If you'd like to propose a feature by all means open a feature request and if it's something myself, or someone else would like to hack together, you might get lucky. 4 | 5 | If you're a do it yourself kind of person, feel free to open a pull request and outline what you're trying to accomplish and I'll be happy to review it as soon as I can. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kyle Christensen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Let's Encrypt for Ubiquiti UniFi OS 2 | 3 | ## Overview 4 | 5 | This should work on UniFi devices running UniFi OS 2.x or later, including: 6 | 7 | * UniFi Dream Machine 8 | * UniFi Dream Machine Pro 9 | * UniFi Dream Machine SE 10 | * UniFi Dream Router 11 | * UniFi Dream Wall 12 | * UniFi Express 13 | * UniFi Network Video Recorder 14 | * UniFi Network Video Recorder Professional 15 | 16 | This script supports issuing Let's Encrypt SSL certificates via DNS using [Lego](https://go-acme.github.io/lego/). 17 | 18 | Out of the box, it has tested support for select [DNS providers](#dns-providers) but with little work you could get it working with any of the supported [Lego DNS Providers](https://go-acme.github.io/lego/dns/). 19 | 20 | ## Installation 21 | 22 | 1. Copy the contents of this repo to your device at `/data/udm-le`. 23 | 2. Edit `/data/udm-le/udm-le.env` and tweak variables to meet your needs. 24 | 3. If necessary, create and populate the `/data/udm-le/.secrets` directory with the files required by your DNS provider. 25 | 4. Run `/data/udm-le/udm-le.sh initial`. This will handle your initial certificate generation and setup a systemd service to start the service on boot, as well as a systemd timer to attempt certificate renewal each morning between 0300 and 0305. 26 | 27 | ## Uninstallation 28 | 29 | ```bash 30 | # Disable udm-le from running at boot 31 | systemctl disable udm-le 32 | 33 | # Delete any udm-le related data 34 | rm -rf /data/udm-le /mnt/data/udm-le 35 | rm -f /etc/systemd/system/udm-le.* 36 | 37 | # Delete any generated certificates, and restart services to generate new self-signed certificates 38 | rm -f /data/unifi-core/config/*.crt /data/unifi-core/config/*.key /data/unifi-core/config/*.pem 39 | systemctl restart unifi-core 40 | systemctl restart freeradius 41 | ``` 42 | 43 | ## DNS Providers 44 | 45 | ### AWS Route53 46 | 47 | If you use Amazon Route53 as your DNS provider, set the `DNS_PROVIDER` to `route53` and configure variables in `udm-le.env` that start with `AWS_`. 48 | 49 | ### Azure DNS 50 | 51 | If not done already, [delegate a domain to an Azure DNS zone](https://docs.microsoft.com/en-us/azure/dns/dns-delegate-domain-azure-dns). 52 | 53 | Assuming the DNS zone lives in subscription `00000000-0000-0000-0000-000000000000` and resource group `udm-le`, with help of the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/) provision an identity to manage the DNS zone by running: 54 | 55 | ```bash 56 | # Login 57 | az login 58 | 59 | # Create a service principal with contributor (default) permissions over the godns resource group 60 | az ad sp create-for-rbac --name godns --scope /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/udm-le --role contributor 61 | ``` 62 | 63 | ### Cloudflare 64 | 65 | In your Cloudflare account settings, create an API token with the following permissions: 66 | 67 | * Zone > Zone > Read 68 | * Zone > DNS > Edit 69 | 70 | Once you have your token generated, add the value to `udm-le.env`. 71 | 72 | ### Digital Ocean 73 | 74 | If you use DigitalOcean as your DNS provider, set your `DNS_PROVIDER` to `digitalocean` and configure your `DO_AUTH_TOKEN`. Note: Quoting your `DO_AUTH_TOKEN` seems to cause issues with Lego. 75 | 76 | ### DuckDNS 77 | 78 | If you use DuckDNS as your DNS provider, set your `DNS_PROVIDER` to `duckdns` and configure your `DUCKDNS_TOKEN`. 79 | 80 | ### Gandi Live DNS (v5) 81 | 82 | If you use Gandi Live DNS (v5) as your DNS provider, set your `DNS_PROVIDER` to `gandiv5` and configure your `GANDIV5_PERSONAL_ACCESS_TOKEN`. You can obtain your Personal Access Token (PAT) from your [account settings](https://account.gandi.net/). 83 | 84 | ### Google Cloud DNS 85 | 86 | GCP Cloud DNS can be configured by establishing a service account with the role [`roles/dns.admin`](https://cloud.google.com/iam/docs/understanding-roles#dns-roles) and exporting a [service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) for that service account. Ensure that `gcloud` is set for `DNS_PROVIDER` in `udm-le.env`, and `GCE_SERVICE_ACCOUNT_FILE` references the path to the service account key (e.g. `./root/.secrets/my_service_account.json`) . Create a new directory called `.secrets` in `/data/udm-le` and add the service account file. 87 | 88 | The CLI will output a JSON object. Use the printed properties to initialize your configuration in [udm-le.env](./udm-le.env). 89 | 90 | Note: 91 | 92 | * The `password` value is a secret and as such you may want to omit it from [udm-le.env](./udm-le.env) and instead set it in a `.secrets/client-secret.txt` file 93 | * The `appId` value is what [Lego](https://go-acme.github.io/lego/) calls a client id 94 | 95 | ### Google Domains 96 | 97 | If you use Google Domains as your DNS provider, set the `DNS_PROVIDER` to `googledomains` and configure `GOOGLE_DOMAINS_ACCESS_TOKEN` with your access token. You can create an access token in your Google Domains dashboard under YOUR_DOMAIN > Security > ACME DNS API. 98 | 99 | ### Linode DNS 100 | 101 | If you use Linode as your DNS provider, set your `DNS_PROVIDER` to `linode` and configure `LINODE_TOKEN` with the value of an API token. The API token must have a scope which allows Read/Write access to "Domains". API tokens can be created in the Linode Control panel. 102 | 103 | ### Loopia 104 | 105 | If you use Loopia as your DNS provider, set your `DNS_PROVIDER` to `loopia` and configure `LOOPIA_API_USER` and `LOOPIA_API_PASSWORD`. The API user must be created at the [loopia customer zone](https://customerzone.loopia.com/api) with the following privileges: 106 | 107 | * addZoneRecord 108 | * getZoneRecords 109 | * removeZoneRecord 110 | * removeSubdomain 111 | 112 | ### Name.com 113 | 114 | Follow [these instructions](https://www.name.com/support/articles/360007597874-signing-up-for-api-access) from name.com support to enable api access. 115 | 116 | At the time of writing, the first few steps our out of date and I had to click `API for resellers` under the more menu which should get you to step 3. 117 | 118 | If using Multifactor to login then you will need to read [this article](https://www.name.com/support/articles/360007989433-using-api-with-two-step-authentication) about how to disable multifactor for api only. 119 | 120 | There are two values needed for the `udm-le.env` file: your name.com username; your generated api token for production. 121 | 122 | ### Oracle Cloud Infrastructure (OCI) DNS 123 | 124 | To configure the Oracle Cloud Infrastructure (OCI) DNS provider, you will need a [private API signing key](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm) and your [tenancy and user account OCIDs](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#five). The quickest way to get all that is to install the [OCI CLI](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/cliconcepts.htm) locally and use its [interactive setup process](https://docs.oracle.com/en-us/iaas/Content/API/SDKDocs/cliinstall.htm#configfile). 125 | 126 | The setup process will create a `~/.oci/config` directory in which you can find your tenancy and user account OCIDs and key fingerprint and the API signing key will be stored in `~/.oci/oci_api_key.pem`. The following CLI command will return the compartment OCID for the specified OCI DNS zone: 127 | 128 | ```bash 129 | $ oci dns zone get --zone-name-or-id example.com | jq -r '.data."compartment-id"' 130 | ocid1.compartment.oc1..secret 131 | ``` 132 | 133 | #### To configure the provider 134 | 135 | > **Important: do not wrap the values of the `OCI_*` variables in `udm-le.env` with quotes. The lack of quotes around the example values provided in [`udm-le.env`](./udm-le.env) is intentional and must be maintained. 136 | 137 | 1. Set the `DNS_PROVIDER` value to `"oraclecloud"` 138 | 1. Uncomment and copy the values from each `~/.oci/config` variable to the similarly named `OCI_*` variable in `udm-le.env`. 139 | 1. Create a new directory at `/data/udm-le/.secrets` and copy the `oci_api_key.pem` file that directory. 140 | 141 | ### Zonomi 142 | 143 | If you use Zonomi as your DNS provider, set your `DNS_PROVIDER` to `zonomi` and configure your `ZONOMI_API_KEY`. 144 | 145 | The API key can be obtained [in your control panel](https://zonomi.com/app/cp/apikeys.jsp) under the DNS key type. 146 | -------------------------------------------------------------------------------- /resources/systemd/udm-le.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Lets Encrypt certificate renewal 3 | [Service] 4 | Environment=HOME=/root 5 | Type=oneshot 6 | RemainAfterExit=false 7 | TimeoutSec=15m 8 | WorkingDirectory=/data/udm-le 9 | ExecStart=/data/udm-le/udm-le.sh renew 10 | -------------------------------------------------------------------------------- /resources/systemd/udm-le.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Run Lets Encrypt renewal daily and at startup 3 | After=unifi.service unifi-core.service 4 | [Timer] 5 | OnStartupSec=300 6 | OnCalendar=*-*-* 03:00:00 7 | RandomizedDelaySec=300 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /udm-le.env: -------------------------------------------------------------------------------- 1 | # 2 | # Required configuration 3 | # 4 | 5 | # Email for LetsEncrypt certificate issuance 6 | CERT_EMAIL="your@email.com" 7 | 8 | # The FQDN of your UDMP (comma separated fqdns are supported) 9 | CERT_HOSTS="whatever.hostname.com,*.whatever.anotherhostname.com" 10 | 11 | # The number of days left on a certificate before renewal 12 | CERT_DAYS_BEFORE_RENEWAL="30" 13 | 14 | # Enable updating certificate keystore used by Captive Portal and WiFiman as well as device certificate 15 | ENABLE_CAPTIVE="no" 16 | 17 | # Import only the server certificate for use with Captive Portal and WiFiman. 18 | # WiFiman requires a single certificate in the .crt file and does not work if 19 | # the full chain is imported as this includes the CA intermediate certificates. 20 | # Setting NO_BUNDLE="yes" only has effect if ENABLE_CAPTIVE="yes". 21 | # WARNING: Experimental support. Not serving the full certificate chain may result in 22 | # some clients not being able to connect to Captive Portal if they do not already have 23 | # a cached copy of the CA intermediate certificate(s) and are unable to download them. 24 | NO_BUNDLE="no" 25 | 26 | # Defines the key type to be used. 27 | # Lego supported values are: RSA2048, RSA3072, RSA4096, RSA8192, EC256 and EC384, however 28 | # using values other than RSA2048 is known to cause issues with UniFiOS. 29 | # 30 | # For up to date support, refer to: 31 | # https://github.com/go-acme/lego/blob/0ab907c183d7b9371c7cf35336a54eb3cfd27634/cmd/setup.go#L96 32 | KEY_TYPE="RSA2048" 33 | 34 | # Enable updating Radius support 35 | ENABLE_RADIUS="no" 36 | 37 | # Disable support for CNAME resolution. When false, allows resolving _acme-challenge.* if you 38 | # have a CNAME pointing to a different domain. This is generally not something people need, so leave 39 | # this alone unless you've explicitly set up a CNAME and understand the implications. 40 | LEGO_DISABLE_CNAME_SUPPORT=true 41 | 42 | # The DNS resolver used to verify records. Change this to a public DNS resolver if you have 43 | # modified your UDM's upstream DNS servers to point to an internal resolver that is the 44 | # authoritative name server for any domain that you are trying to request certificates for. 45 | DNS_RESOLVER="127.0.0.1:53" 46 | 47 | # 48 | # DNS provider configuration 49 | # See README.md file for more details 50 | # 51 | 52 | # AWS Route53 53 | #DNS_PROVIDER="route53" 54 | #AWS_ACCESS_KEY_ID="" 55 | #AWS_SECRET_ACCESS_KEY="" 56 | #AWS_REGION="" 57 | #AWS_HOSTED_ZONE_ID="" 58 | 59 | # Azure 60 | #DNS_PROVIDER="azure" 61 | #AZURE_CLIENT_ID="" 62 | #AZURE_CLIENT_SECRET_FILE="/data/udm-le/.secrets/client-secret.txt" 63 | #AZURE_ENVIRONMENT="public" 64 | #AZURE_RESOURCE_GROUP="udm-le" 65 | #AZURE_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000" 66 | #AZURE_TENANT_ID="" 67 | 68 | # CloudFlare 69 | DNS_PROVIDER="cloudflare" 70 | CLOUDFLARE_DNS_API_TOKEN="YOUR_CLOUDFLARE_API_TOKEN" 71 | 72 | # Digital Ocean 73 | # Note: Quoting your DO_AUTH_TOKEN below seems to cause issues 74 | #DNS_PROVIDER="digitalocean" 75 | #DO_AUTH_TOKEN="AUTH_TOKEN" 76 | 77 | # DuckDNS 78 | #DNS_PROVIDER="duckdns" 79 | #DUCKDNS_TOKEN="AUTH_TOKEN" 80 | 81 | # Google Cloud DNS 82 | # Note: The default path for the service account file is /root/.secrets 83 | #DNS_PROVIDER="gcloud" 84 | #GCE_SERVICE_ACCOUNT_FILE="/data/udm-le/.secrets/sa.json" 85 | #GCE_PROPAGATION_TIMEOUT="3600" 86 | 87 | # Google Domains 88 | #DNS_PROVIDER="googledomains" 89 | #GOOGLE_DOMAINS_ACCESS_TOKEN="ACCESS_TOKEN" 90 | 91 | # Linode DNS 92 | #DNS_PROVIDER="linode" 93 | #LINODE_TOKEN="" 94 | #LINODE_PROPAGATION_TIMEOUT="120" 95 | 96 | # Loopia 97 | #DNS_PROVIDER="loopia" 98 | #LOOPIA_API_USER="YOUR_API_USER@loopiaapi" 99 | #LOOPIA_API_PASSWORD="YOUR_API_PASSWORD" 100 | 101 | # Gandi Live DNS (v5) 102 | # Gandi PAT https://docs.gandi.net/en/managing_an_organization/organizations/personal_access_token.html# 103 | # LEGO Reference https://go-acme.github.io/lego/dns/gandiv5/ 104 | #DNS_PROVIDER="gandiv5" 105 | #GANDIV5_API_KEY="AUTH_TOKEN" # DEPRECATED 106 | #GANDIV5_PERSONAL_ACCESS_TOKEN="PERSONAL_ACCESS_TOKEN" # Replace with your Gandi Personal Access Token 107 | #GANDIV5_HTTP_TIMEOUT="10" # API request timeout in seconds (Default: 10) 108 | #GANDIV5_POLLING_INTERVAL="20" # Time between DNS propagation check in seconds (Default: 20) 109 | #GANDIV5_PROPAGATION_TIMEOUT="1200" # Maximum waiting time for DNS propagation in seconds (Default: 1200) 110 | #GANDIV5_TTL="300" # The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) 111 | 112 | # Name.com 113 | # Note: You need to use the your name.com username and not the api key name. 114 | #DNS_PROVIDER="namedotcom" 115 | #NAMECOM_USERNAME="YOUR_NAMECOM_USERNAME" 116 | #NAMECOM_API_TOKEN="YOUR_NAMECOM_API_TOKEN" 117 | 118 | # Oracle Cloud Infrastructure (OCI) DNS 119 | # 120 | # DO NOT WRAP ANY OF THE OCI_ VARIABLES IN QUOTES! See README.md for details. 121 | # 122 | #DNS_PROVIDER="oraclecloud" 123 | # If OCI_PRIVKEY_FILE is password protected, uncomment the following line: 124 | #OCI_PRIVKEY_PASS=password 125 | #OCI_PRIVKEY_FILE=/data/udm-le/.secrets/oci_api_key.pem 126 | # The following values can be found in ~/.oci/config after 127 | #OCI_PUBKEY_FINGERPRINT=00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 128 | #OCI_TENANCY_OCID=ocid1.tenancy.oc1..secret 129 | #OCI_COMPARTMENT_OCID=ocid1.compartment.oc1..secret 130 | #OCI_USER_OCID=ocid1.user.oc1..secret 131 | #OCI_REGION=us-ashburn-1 132 | 133 | # Zonomi 134 | #DNS_PROVIDER="zonomi" 135 | #ZONOMI_API_KEY="AUTH_TOKEN" 136 | 137 | # 138 | # Change stuff below at your own risk 139 | # 140 | 141 | # Extra arguments to pass to LEGO 142 | # For example, to pass --dns.propagation-disable-ans to disable Authoritative Name Server (ANS) checking. 143 | EXTRA_ARGS="" 144 | 145 | # DNS_RESOLVERS supports a host:port if you need to override system DNS 146 | DNS_RESOLVERS="" 147 | 148 | # Changing below requires changing line 7 of udm-le.sh, as well as the paths within systemd service files 149 | UDM_LE_PATH="/data/udm-le" 150 | 151 | # LetsEncrypt Configuration 152 | LEGO_VERSION="4.22.2" 153 | LEGO_SHA1="f604e16bf8bd8bc8f12397ccfa3e2998aac08a49" 154 | LEGO_DOWNLOAD_URL="https://github.com/go-acme/lego/releases/download/v${LEGO_VERSION}/lego_v${LEGO_VERSION}_linux_arm64.tar.gz" 155 | LEGO_BINARY="${UDM_LE_PATH}/lego" 156 | LEGO_PATH="${UDM_LE_PATH}/.lego" 157 | 158 | # Java Configuration 159 | JAVA_BINARY="/usr/bin/java" 160 | 161 | # These should only change if Unifi-OS core changes require it 162 | CERT_IMPORT_CMD="java -jar /usr/lib/unifi/lib/ace.jar import_key_cert" 163 | UBIOS_CONTROLLER_CERT_PATH="/data/unifi-core/config" 164 | UBIOS_RADIUS_CERT_PATH="/etc/freeradius/3.0/certs" 165 | UNIFIOS_CERT_PATH="/data/unifi-core/config" 166 | UNIFIOS_KEYSTORE_PATH="/usr/lib/unifi/data" 167 | UNIFIOS_KEYSTORE_CERT_ALIAS="unifi" 168 | UNIFIOS_KEYSTORE_PASSWORD="aircontrolenterprise" 169 | -------------------------------------------------------------------------------- /udm-le.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set error mode 4 | set -e 5 | 6 | # Ensure permissions on udm-le.env are sane 7 | if [ $(stat --printf "%04a" /data/udm-le/udm-le.env) != "0600" ]; then 8 | chmod 0600 udm-le.env 9 | fi 10 | 11 | # Load environment variables 12 | set -a 13 | source /data/udm-le/udm-le.env 14 | set +a 15 | 16 | # Setup additional variables for later 17 | LEGO_ARGS="--dns ${DNS_PROVIDER} --dns.resolvers ${DNS_RESOLVER} --email ${CERT_EMAIL} --key-type ${KEY_TYPE:-RSA2048} ${EXTRA_ARGS}" 18 | LEGO_FORCE_INSTALL=false 19 | JAVA_FORCE_INSTALL=false 20 | RESTART_SERVICES=false 21 | 22 | # Show usage 23 | usage() { 24 | echo "Usage: udm-le.sh action [ --restart-services ]" 25 | echo "Actions:" 26 | echo " - udm-le.sh create_services: Force (re-)creates systemd service and timer for automated renewal." 27 | echo " - udm-le.sh initial: Generate new certificate and set up cron job to renew at 03:00 each morning." 28 | echo " - udm-le.sh install_lego: Force (re-)installs lego, using LEGO_VERSION from udm-le.env." 29 | echo " - udm-le.sh renew: Renew certificate if due for renewal." 30 | echo " - udm-le.sh update_keystore: Update keystore used by Captive Portal/WiFiman" 31 | echo " with either full certificate chain (if NO_BUNDLE='no') or server certificate only (if NO_BUNDLE='yes')." 32 | echo "" 33 | echo "Options:" 34 | echo " --restart-services: Force restart of services even if certificate was not renewed." 35 | echo "" 36 | echo "WARNING: NO_BUNDLE option is only supported experimentally. Setting it to 'yes' is required to make WiFiman work," 37 | echo "but may result in some clients not being able to connect to Captive Portal if they do not already have a cached" 38 | echo "copy of the CA intermediate certificate(s) and are unable to download them." 39 | } 40 | 41 | # Get command line options 42 | OPTIONS=$(getopt -o h --long help,restart-services -- "$@") 43 | if [ $? -ne 0 ]; then 44 | echo "Incorrect option provided" 45 | exit 1 46 | fi 47 | 48 | eval set -- "$OPTIONS" 49 | while [ : ]; do 50 | case "$1" in 51 | -h | --help) 52 | usage 53 | exit 0 54 | shift 55 | ;; 56 | --restart-services) 57 | RESTART_SERVICES=true 58 | shift 59 | ;; 60 | --) 61 | shift 62 | break 63 | ;; 64 | esac 65 | done 66 | 67 | create_services() { 68 | # Create systemd service and timers (for renewal) 69 | echo "create_services(): Creating udm-le systemd service and timer" 70 | cp -f "${UDM_LE_PATH}/resources/systemd/udm-le.service" /etc/systemd/system/udm-le.service 71 | cp -f "${UDM_LE_PATH}/resources/systemd/udm-le.timer" /etc/systemd/system/udm-le.timer 72 | systemctl daemon-reload 73 | systemctl enable udm-le.timer 74 | } 75 | 76 | deploy_certs() { 77 | # Deploy certificates for the controller and optionally for the captive portal and radius server 78 | 79 | # Re-write CERT_NAME if it is a wildcard cert. Replace * with _ 80 | LEGO_CERT_NAME=${CERT_NAME/\*/_} 81 | if [ "$(find -L "${UDM_LE_PATH}"/.lego -type f -name "${LEGO_CERT_NAME}".crt -mmin -5)" ]; then 82 | echo "deploy_certs(): New certificate was generated, time to deploy it" 83 | 84 | cp -f "${UDM_LE_PATH}"/.lego/certificates/"${LEGO_CERT_NAME}".crt "${UBIOS_CONTROLLER_CERT_PATH}"/unifi-core.crt 85 | cp -f "${UDM_LE_PATH}"/.lego/certificates/"${LEGO_CERT_NAME}".key "${UBIOS_CONTROLLER_CERT_PATH}"/unifi-core.key 86 | chmod 644 "${UBIOS_CONTROLLER_CERT_PATH}"/unifi-core.crt "${UBIOS_CONTROLLER_CERT_PATH}"/unifi-core.key 87 | 88 | if [ "$ENABLE_CAPTIVE" == "yes" ]; then 89 | update_keystore 90 | fi 91 | 92 | if [ "$ENABLE_RADIUS" == "yes" ]; then 93 | cp -f "${UDM_LE_PATH}"/.lego/certificates/"${LEGO_CERT_NAME}".crt "${UBIOS_RADIUS_CERT_PATH}"/server.pem 94 | cp -f "${UDM_LE_PATH}"/.lego/certificates/"${LEGO_CERT_NAME}".key "${UBIOS_RADIUS_CERT_PATH}"/server-key.pem 95 | chmod 644 "${UBIOS_RADIUS_CERT_PATH}"/server.pem "${UBIOS_RADIUS_CERT_PATH}"/server-key.pem 96 | fi 97 | 98 | RESTART_SERVICES=true 99 | fi 100 | } 101 | 102 | restart_services() { 103 | # Restart services if certificates have been deployed, or we're forcing it on the command line 104 | if [ "${RESTART_SERVICES}" == true ]; then 105 | echo "restart_services(): Restarting unifi-core" 106 | systemctl restart unifi-core &>/dev/null 107 | 108 | if [ "$ENABLE_CAPTIVE" == "yes" ]; then 109 | echo "restart_services(): Restarting unifi" 110 | systemctl restart unifi &>/dev/null 111 | fi 112 | 113 | if [ "$ENABLE_RADIUS" == "yes" ]; then 114 | echo "restart_services(): Restarting freeradius server" 115 | pkill -f freeradius &>/dev/null 116 | fi 117 | else 118 | echo "restart_services(): RESTART_SERVICES is set to false, skipping service restarts" 119 | fi 120 | } 121 | 122 | update_keystore() { 123 | # Update the java keystore with the new certificate 124 | if [ "$NO_BUNDLE" == "yes" ]; then 125 | # Only import server certifcate to keystore. WiFiman requires a single certificate in the .crt file 126 | # and does not work if the full chain is imported as this includes the CA intermediate certificates. 127 | echo "update_keystore(): Importing server certificate only" 128 | 129 | # Export only the server certificate from the full chain bundle 130 | openssl x509 -in "${UNIFIOS_CERT_PATH}"/unifi-core.crt >"${UNIFIOS_CERT_PATH}"/unifi-core-server-only.crt 131 | 132 | # Bundle the private key and server-only certificate into a PKCS12 format file 133 | openssl pkcs12 \ 134 | -export \ 135 | -in "${UNIFIOS_CERT_PATH}"/unifi-core-server-only.crt \ 136 | -inkey "${UNIFIOS_CERT_PATH}"/unifi-core.key \ 137 | -name "${UNIFIOS_KEYSTORE_CERT_ALIAS}" \ 138 | -out "${UNIFIOS_KEYSTORE_PATH}"/unifi-core-key-plus-server-only-cert.p12 \ 139 | -password pass:"${UNIFIOS_KEYSTORE_PASSWORD}" 140 | 141 | # Backup the keystore before editing it. 142 | cp "${UNIFIOS_KEYSTORE_PATH}/keystore" "${UNIFIOS_KEYSTORE_PATH}/keystore_$(date +"%Y-%m-%d_%Hh%Mm%Ss").backup" 143 | 144 | # Delete the existing full chain from the keystore 145 | keytool -delete -alias unifi -keystore "${UNIFIOS_KEYSTORE_PATH}/keystore" -deststorepass "${UNIFIOS_KEYSTORE_PASSWORD}" 146 | 147 | # Import the server-only certificate and private key from the PKCS12 file 148 | keytool -importkeystore \ 149 | -alias "${UNIFIOS_KEYSTORE_CERT_ALIAS}" \ 150 | -destkeypass "${UNIFIOS_KEYSTORE_PASSWORD}" \ 151 | -destkeystore "${UNIFIOS_KEYSTORE_PATH}/keystore" \ 152 | -deststorepass "${UNIFIOS_KEYSTORE_PASSWORD}" \ 153 | -noprompt \ 154 | -srckeystore "${UNIFIOS_KEYSTORE_PATH}/unifi-core-key-plus-server-only-cert.p12" \ 155 | -srcstorepass "${UNIFIOS_KEYSTORE_PASSWORD}" \ 156 | -srcstoretype PKCS12 157 | else 158 | # Import full certificate chain bundle to keystore 159 | echo "update_keystore(): Importing full certificate chain bundle" 160 | 161 | # Bundle the private key and server-only certificate into a PKCS12 format file 162 | openssl pkcs12 \ 163 | -export \ 164 | -in "${UNIFIOS_CERT_PATH}"/unifi-core.crt \ 165 | -inkey "${UNIFIOS_CERT_PATH}"/unifi-core.key \ 166 | -name "${UNIFIOS_KEYSTORE_CERT_ALIAS}" \ 167 | -out "${UNIFIOS_KEYSTORE_PATH}"/unifi-core-key-plus-server-full-cert.p12 \ 168 | -password pass:"${UNIFIOS_KEYSTORE_PASSWORD}" 169 | 170 | # Backup the keystore before editing it. 171 | cp "${UNIFIOS_KEYSTORE_PATH}/keystore" "${UNIFIOS_KEYSTORE_PATH}/keystore_$(date +"%Y-%m-%d_%Hh%Mm%Ss").backup" 172 | 173 | # Delete the existing full chain from the keystore 174 | keytool -delete -alias unifi -keystore "${UNIFIOS_KEYSTORE_PATH}/keystore" -deststorepass "${UNIFIOS_KEYSTORE_PASSWORD}" 175 | 176 | # Import the server-only certificate and private key from the PKCS12 file 177 | keytool -importkeystore \ 178 | -alias "${UNIFIOS_KEYSTORE_CERT_ALIAS}" \ 179 | -destkeypass "${UNIFIOS_KEYSTORE_PASSWORD}" \ 180 | -destkeystore "${UNIFIOS_KEYSTORE_PATH}/keystore" \ 181 | -deststorepass "${UNIFIOS_KEYSTORE_PASSWORD}" \ 182 | -noprompt \ 183 | -srckeystore "${UNIFIOS_KEYSTORE_PATH}/unifi-core-key-plus-server-full-cert.p12" \ 184 | -srcstorepass "${UNIFIOS_KEYSTORE_PASSWORD}" \ 185 | -srcstoretype PKCS12 186 | 187 | fi 188 | } 189 | 190 | install_lego() { 191 | # Check if lego exists already, do nothing 192 | if [ ! -f "${LEGO_BINARY}" ] || [ "${LEGO_FORCE_INSTALL}" = true ]; then 193 | echo "install_lego(): Attempting lego installation" 194 | 195 | # Download and extract lego release 196 | echo "install_lego(): Downloading lego v${LEGO_VERSION} from ${LEGO_DOWNLOAD_URL}" 197 | wget -qO "/tmp/lego_release-${LEGO_VERSION}.tar.gz" "${LEGO_DOWNLOAD_URL}" 198 | 199 | echo "install_lego(): Extracting lego binary from release and placing at ${LEGO_BINARY}" 200 | tar -xozvf "/tmp/lego_release-${LEGO_VERSION}.tar.gz" --directory="${UDM_LE_PATH}" lego 201 | 202 | # Verify lego binary integrity 203 | echo "install_lego(): Verifying integrity of lego binary" 204 | LEGO_HASH=$(sha1sum "${LEGO_BINARY}" | awk '{print $1}') 205 | if [ "${LEGO_HASH}" = "${LEGO_SHA1}" ]; then 206 | echo "install_lego(): Verified lego v${LEGO_VERSION}:${LEGO_SHA1}" 207 | chmod +x "${LEGO_BINARY}" 208 | else 209 | echo "install_lego(): Verification failure, lego binary sha1 was ${LEGO_HASH}, expected ${LEGO_SHA1}. Cleaning up and aborting" 210 | rm -f "${UDM_LE_PATH}/lego" "/tmp/lego_release-${LEGO_VERSION}.tar.gz" 211 | exit 1 212 | fi 213 | else 214 | echo "install_lego(): Lego binary is already installed at ${LEGO_BINARY}, no operation necessary" 215 | fi 216 | } 217 | 218 | install_java() { 219 | # We only need java if captive portal is enabled 220 | if [ "${ENABLE_CAPTIVE}" != "yes" ]; then 221 | echo "install_java(): ENABLE_CAPTIVE is set to '${ENABLE_CAPTIVE}', no Java required" 222 | return 223 | fi 224 | 225 | # Check if java exists already, do nothing 226 | if [ ! -f "${JAVA_BINARY}" ] || [ "${JAVA_FORCE_INSTALL}" = true ]; then 227 | echo "install_java(): Attempting Java installation" 228 | 229 | # install jre via apt 230 | apt install -y --no-install-recommends default-jre-headless 231 | else 232 | echo "install_java(): Java binary is already installed at ${JAVA_BINARY}, no operation necessary" 233 | fi 234 | } 235 | 236 | # Support alternative DNS resolvers 237 | if [ "${DNS_RESOLVERS}" != "" ]; then 238 | LEGO_ARGS="${LEGO_ARGS} --dns.resolvers ${DNS_RESOLVERS}" 239 | fi 240 | 241 | # Support multiple certificate SANs 242 | for DOMAIN in $(echo "$CERT_HOSTS" | tr "," "\n"); do 243 | if [ -z "$CERT_NAME" ]; then 244 | CERT_NAME=$DOMAIN 245 | fi 246 | LEGO_ARGS="${LEGO_ARGS} -d ${DOMAIN}" 247 | done 248 | 249 | case $1 in 250 | create_services) 251 | echo "create_services(): Creating services" 252 | create_services 253 | ;; 254 | initial) 255 | install_lego 256 | install_java 257 | create_services 258 | echo "initial(): Attempting certificate generation" 259 | echo "initial(): ${LEGO_BINARY} --path \"${LEGO_PATH}\" ${LEGO_ARGS} --accept-tos run" 260 | ${LEGO_BINARY} --path "${LEGO_PATH}" ${LEGO_ARGS} --accept-tos run && deploy_certs && restart_services 261 | echo "initial(): Starting udm-le systemd timer" 262 | systemctl start udm-le.timer 263 | ;; 264 | install_lego) 265 | echo "install_lego(): Forcing installation of lego" 266 | LEGO_FORCE_INSTALL=true 267 | install_lego 268 | ;; 269 | install_java) 270 | echo "install_java(): Forcing installation of java" 271 | JAVA_FORCE_INSTALL=true 272 | install_java 273 | ;; 274 | renew) 275 | echo "renew(): Attempting certificate renewal" 276 | echo "renew(): ${LEGO_BINARY} --path \"${LEGO_PATH}\" ${LEGO_ARGS} renew --days ${CERT_DAYS_BEFORE_RENEWAL:-30}" 277 | ${LEGO_BINARY} --path "${LEGO_PATH}" ${LEGO_ARGS} renew --days "${CERT_DAYS_BEFORE_RENEWAL:-30}" && deploy_certs && restart_services 278 | ;; 279 | test_deploy) 280 | echo "test_deploy(): Attempting to deploy certificate" 281 | deploy_certs 282 | ;; 283 | update_keystore) 284 | echo "update_keystore(): Attempting to update keystore used by hotspot Captive Portal and WiFiman" 285 | RESTART_SERVICES=true 286 | update_keystore && restart_services 287 | ;; 288 | *) 289 | echo "ERROR: No valid action provided." 290 | usage 291 | exit 1 292 | ;; 293 | esac 294 | --------------------------------------------------------------------------------