├── general_notes.md ├── LICENSE ├── manual_hook.rb └── README.md /general_notes.md: -------------------------------------------------------------------------------- 1 | [Common OpenSSL commands](https://www.sslshopper.com/article-most-common-openssl-commands.html) 2 | [Let's Encrypt Directory Structure](https://www.linode.com/docs/security/ssl/install-lets-encrypt-to-create-ssl-certificates) 3 | File layout 4 | > * **cert.pem**: server certificate only. 5 | * **chain.pem**: root and intermediate certificates only. 6 | * **fullchain.pem**: combination of server, root and intermediate certificates (replaces `cert.pem` and `chain.pem`). 7 | privkey.pem: private key (do **not** share this with anyone!). 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jamie Jones 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 | -------------------------------------------------------------------------------- /manual_hook.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'resolv' 4 | 5 | # Struct to store a challenge 6 | Challenge = Struct.new(:domain, :acme_domain, :txt_challenge) 7 | 8 | 9 | # Check if a challenge is resolved, returns true in that case 10 | def resolved?(dns, challenge) 11 | valid = false 12 | dns.each_resource(challenge[:acme_domain], Resolv::DNS::Resource::IN::TXT) { |resp| 13 | resp.strings.each do |curr_resp| 14 | if curr_resp == challenge[:txt_challenge] 15 | puts "✔ #{challenge[:acme_domain]}: Found #{curr_resp}, a match." 16 | return true 17 | end 18 | end 19 | valid = true 20 | puts "✘ #{challenge[:acme_domain]}: Found TXT record, but didn't match expected value of #{challenge[:txt_challenge]}" 21 | } 22 | if !valid 23 | puts "✘ #{challenge[:acme_domain]}: Found no TXT record" 24 | end 25 | return false 26 | end 27 | 28 | def setup_dns(challenges) 29 | # DNS Resolver 30 | dns = Resolv::DNS.new 31 | first_iteration = true 32 | until challenges.empty? # Until all challenges are resolved 33 | challenges.delete_if do |challenge| # Delete resolved challenges 34 | resolved?(dns, challenge) 35 | end 36 | 37 | if first_iteration 38 | challenges.each do |challenge| 39 | puts "Create TXT record for the domain: \'#{challenge[:acme_domain]}\'. TXT record:" 40 | puts "\'#{challenge[:txt_challenge]}\'" 41 | puts "Press enter when DNS has been updated..." 42 | $stdin.readline 43 | end 44 | first_iteration = false 45 | else 46 | unless challenges.empty? 47 | puts "Waiting to retry..." 48 | sleep 30 49 | end 50 | end 51 | end 52 | end 53 | 54 | def delete_dns(challenges) 55 | puts "Challenge complete. Leave TXT record in place to allow easier future refreshes." 56 | end 57 | 58 | if __FILE__ == $0 59 | # puts "ARGV: #{ARGV.inspect}" 60 | hook_stage = ARGV.shift 61 | challenges = [] 62 | while ARGV.length >= 3 63 | domain = ARGV.shift 64 | acme_domain = "_acme-challenge.#{domain}".sub(/\.\*\./, ".") 65 | ARGV.shift 66 | txt_challenge = ARGV.shift 67 | challenges.push Challenge.new(domain, acme_domain, txt_challenge) 68 | end 69 | 70 | # puts "hook_stage: #{hook_stage}" 71 | # puts "challenges: #{challenges.inspect}" 72 | 73 | if hook_stage == "deploy_challenge" 74 | setup_dns(challenges) 75 | elsif hook_stage == "clean_challenge" 76 | delete_dns(challenges) 77 | end 78 | 79 | end 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manual DNS hook for dehydrated 2 | 3 | This repository contains a ruby-based hook for the [`dehydrated`](dehydrated: https://github.com/lukas2511/dehydrated) project (a [Let's Encrypt](https://letsencrypt.org/), shell script ACME client) that allows a user to obtain a certificate from the _Let's Encrypt_ API via a DNS challenge. The hook will provide you with the domain and challenge details required for you to add to your DNS records, and poll until this change has propogated before allowing Let's Encrypt to confirm that changes. This is helpful for DNS providers and solutions that do not provide an API. This is an interactive hook to support those DNS providers that require manual interaction. 4 | 5 | Looking for a DNS provider with an API? Try AWS Route 53, Rackspace, or CloudFlare. 6 | 7 | Relevant Links: 8 | * dehydrated: https://github.com/lukas2511/dehydrated 9 | * Let's Encrypt: https://letsencrypt.org/ 10 | 11 | ## Required 12 | * git client for tool download 13 | * ruby installed and available on the PATH 14 | 15 | ## Installation 16 | Download the files for installation 17 | 18 | ``` bash 19 | $ git clone https://github.com/lukas2511/dehydrated.git 20 | $ git clone https://github.com/jbjonesjr/letsencrypt-manual-hook.git dehydrated/hooks/manual 21 | ``` 22 | 23 | ## Usage 24 | ### Certificate for single domain 25 | ``` bash 26 | # **Note:** The `dehyrdrated` client uses the following flags in this example 27 | # --cron (-c): Sign/renew non-existant/changed/expiring certificates. 28 | # --challenge (-t) [http-01|dns-01]: Which challenge should be used? Currently http-01 and dns-01 are supported 29 | # --domain (-d) [domain.tld]: Use specified domain name(s) instead of domains.txt entry (one certificate!) 30 | # --hook (-k) [path/to/hook.sh]: Use specified script for hooks 31 | 32 | git-projects$ ./dehydrated/dehydrated -c -t dns-01 -d jbjonesjr.com -k ./dehydrated/hooks/manual/manual_hook.rb 33 | # INFO: Using main config file /Users/jbjonesjr/lets-encrypt/letsencrypt-jbjonesjr.sh/config.sh 34 | Processing jbjonesjr.com with alternative names: blog.jbjonesjr.com 35 | + Signing domains... 36 | + Generating private key... 37 | + Generating signing request... 38 | + Requesting challenge for jbjonesjr.com... 39 | Create TXT record for the domain: '_acme-challenge.jbjonesjr.com'. TXT record: 40 | 'NT5EcszzzD2imO2IAWh81KqPHcx7nCSR8jHOEwKDjHQ' 41 | Press any key when DNS has been updated... 42 | 43 | Found NT5EcszzzD2imO2IAWh81KqPHcx7nCSR8jHOEwKDjHQ. match. 44 | + Responding to challenge for jbjonesjr.com... 45 | Challenge complete. Please delete this TXT record(or in bulk later). Press any key when DNS has been updated... 46 | 47 | + Challenge is valid! 48 | + Requesting certificate... 49 | + Checking certificate... 50 | + Done! 51 | + Creating fullchain.pem... 52 | deploy_cert 53 | jbjonesjr.com 54 | /Users/jbjonesjr/lets-encrypt/letsencrypt-jbjonesjr.sh/certs/jbjonesjr.com/cert.pem 55 | + Done! 56 | ``` 57 | 58 | ### Certificate with additional alias(es) 59 | ``` bash 60 | # **Note:** The `dehyrdrated` client uses the following flags in this example 61 | # --cron (-c): Sign/renew non-existant/changed/expiring certificates. 62 | # --challenge (-t) [http-01|dns-01]: Which challenge should be used? Currently http-01 and dns-01 are supported 63 | # --domain (-d) [domain.tld]: Use specified domain name(s) instead of domains.txt entry (one certificate!) 64 | # --hook (-k) [path/to/hook.sh]: Use specified script for hooks 65 | 66 | git-projects$ ./dehydrated/dehydrated -c -t dns-01 -d jbjonesjr.com -d blog.jbjonesjr.com -k ./dehydrated/hooks/manual/manual_hook.rb 67 | # INFO: Using main config file /Users/jbjonesjr/lets-encrypt/letsencrypt-jbjonesjr.sh/config.sh 68 | Processing jbjonesjr.com with alternative names: blog.jbjonesjr.com 69 | + Signing domains... 70 | + Generating private key... 71 | + Generating signing request... 72 | + Requesting challenge for jbjonesjr.com... 73 | + Requesting challenge for blog.jbjonesjr.com... 74 | Create TXT record for the domain: '_acme-challenge.jbjonesjr.com'. TXT record: 75 | 'NT5EcszzzD2imO2IAWh81KqPHcx7nCSR8jHOEwKDjHQ' 76 | Press any key when DNS has been updated... 77 | 78 | Found NT5EcszzzD2imO2IAWh81KqPHcx7nCSR8jHOEwKDjHQ. match. 79 | + Responding to challenge for jbjonesjr.com... 80 | Challenge complete. Please delete this TXT record(or in bulk later). Press any key when DNS has been updated... 81 | 82 | + Challenge is valid! 83 | Create TXT record for the domain: '_acme-challenge.blog.jbjonesjr.com'. TXT record: 84 | 'EHv_9kV6cfEdAsNBnlttr5ribvCpNqQRf6-R0kJLrh8' 85 | Press any key when DNS has been updated... 86 | 87 | Found EHv_9kV6cfEdAsNBnlttr5ribvCpNqQRf6-R0kJLrh8. match. 88 | + Responding to challenge for blog.jbjonesjr.com... 89 | Challenge complete. Please delete this TXT record(or in bulk later). Press any key when DNS has been updated... 90 | 91 | + Challenge is valid! 92 | + Requesting certificate... 93 | + Checking certificate... 94 | + Done! 95 | + Creating fullchain.pem... 96 | deploy_cert 97 | jbjonesjr.com 98 | /Users/jbjonesjr/lets-encrypt/letsencrypt-jbjonesjr.sh/certs/jbjonesjr.com/cert.pem 99 | + Done! 100 | ``` 101 | 102 | ### Inspecting resulting certificates 103 | After dehydrated has verified your domain ownership via TXT Record challenges, it provides you with a copy of the certificate signing request (csr), the private key used to identify your site, the resulting certificate and CA-chains. An example of the resulting certificate is below: 104 | ``` bash 105 | git-projects$ openssl x509 -in ./dehydrated/certs/jbjonesjr.com/cert.pem -noout -text 106 | Certificate: 107 | Data: 108 | Version: 3 (0x2) 109 | Serial Number: 110 | 03:60:e6:37:6c:f6:db:00:b8:c5:e8:2e:50:80:aa:8c:f7:d0 111 | Signature Algorithm: sha256WithRSAEncryption 112 | Issuer: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X3 113 | Validity 114 | Not Before: Oct 25 01:39:00 2016 GMT 115 | Not After : Jan 25 01:39:00 2017 GMT 116 | Subject: CN=jbjonesjr.com 117 | Subject Public Key Info: 118 | Public Key Algorithm: rsaEncryption 119 | RSA Public Key: (4096 bit) 120 | Modulus (4096 bit): 121 | 00:c3:bb:7e:5a:e7:db:a0:02:40:c0:ba:54:37:aa: 122 | 6d:2a:dc:21:8f:86:99:1e:bd:c4:41:49:bb:e7:37: 123 | 0c:d4:44:c0:e5:c0:fc:5c:3c:64:14:be:89:80:9b: 124 | d1:17:aa:45:da:88:d4:40:3c:9e:69:47:3f:17:c3: 125 | 1b:5b:94:89:48:3a:bf:ca:61:8f:c0:5c:7c:3a:0b: 126 | 90:f2:c4:68:2a:19:b5:f6:73:f4:cc:37:c8:dd:46: 127 | e0:da:ab:39:87:39:26:20:be:33:77:2d:ee:ee:4d: 128 | 17:e4:4d:8b:ac:30:8b:d1:e1:9c:7a:36:58:55:35: 129 | e8:7f:5e:c7:6a:29:45:fa:67:c0:61:2f:44:da:51: 130 | 0d:d1:d4:68:42:73:0d:c4:83:65:e4:cf:83:aa:1d: 131 | 0b:a0:96:4b:d3:39:03:3f:ef:8b:51:94:4c:e7:83: 132 | 92:25:d6:b9:6f:a5:1d:97:0f:75:9e:0f:f5:a1:c5: 133 | ce:26:8d:2c:57:65:97:4e:38:1e:40:91:2b:8e:a5: 134 | b5:88:12:fe:37:59:c1:1f:8e:a5:f9:c7:cd:f2:59: 135 | a1:1d:33:4a:0c:54:bb:c0:c0:8c:62:f0:2d:6b:00: 136 | 02:44:ce:72:20:79:6e:fa:a3:18:69:e0:07:a2:17: 137 | 56:35:6a:e4:64:9b:27:2d:c2:54:2e:8b:1e:ee:60: 138 | 08:36:34:d9:cc:b8:ee:2a:8f:dd:79:66:c4:fd:6c: 139 | f2:6c:c3:74:ab:d7:55:d5:15:60:ad:f5:c5:85:b0: 140 | 59:d8:00:bb:eb:cb:97:b0:74:fe:8b:3b:e4:50:0f: 141 | 99:78:61:fb:ff:c2:02:e3:9a:35:49:f6:0e:2b:48: 142 | a6:7a:48:e6:78:9e:1e:77:e1:16:1d:d1:6c:f3:91: 143 | c8:c9:25:b6:88:5f:74:d3:dc:f0:99:65:2f:10:f2: 144 | 6c:20:85:e0:c5:a6:3c:a7:96:a2:b6:af:de:b2:17: 145 | ec:68:07:f0:06:36:43:ae:98:a0:cb:e1:ae:5f:fe: 146 | 93:18:bc:44:b1:3b:e2:1b:ec:99:3d:1c:04:06:df: 147 | 59:f6:f5:bf:3d:79:e5:f6:9c:63:bb:ad:79:b2:b2: 148 | 1b:9c:35:40:fb:d9:ad:98:92:85:68:89:1e:a3:1e: 149 | d9:3f:5b:d3:bb:e4:9b:e5:ae:4a:0b:55:5c:62:d5: 150 | 16:ef:2f:54:65:46:9e:ba:3b:d3:f7:a6:de:7b:e1: 151 | 3b:3b:db:a0:5e:15:f9:d0:ed:62:52:75:83:6b:34: 152 | 9c:69:3d:06:13:42:20:f7:f5:cb:bc:e5:da:c9:7e: 153 | c2:d1:2a:ad:47:98:3a:ef:cc:58:67:bd:b1:50:2d: 154 | 27:21:f8:70:74:7a:1c:3d:bc:d1:f8:bc:5b:e4:54: 155 | a6:cc:7b 156 | Exponent: 65537 (0x10001) 157 | X509v3 extensions: 158 | X509v3 Key Usage: critical 159 | Digital Signature, Key Encipherment 160 | X509v3 Extended Key Usage: 161 | TLS Web Server Authentication, TLS Web Client Authentication 162 | X509v3 Basic Constraints: critical 163 | CA:FALSE 164 | X509v3 Subject Key Identifier: 165 | A4:3F:6D:69:0D:DA:D7:01:CF:7D:FA:D0:9F:4E:CB:83:3A:CF:59:3A 166 | X509v3 Authority Key Identifier: 167 | keyid:A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1 168 | 169 | Authority Information Access: 170 | OCSP - URI:http://ocsp.int-x3.letsencrypt.org/ 171 | CA Issuers - URI:http://cert.int-x3.letsencrypt.org/ 172 | 173 | X509v3 Subject Alternative Name: 174 | DNS:blog.jbjonesjr.com, DNS:jbjonesjr.com 175 | X509v3 Certificate Policies: 176 | Policy: 2.23.140.1.2.1 177 | Policy: 1.3.6.1.4.1.44947.1.1.1 178 | CPS: http://cps.letsencrypt.org 179 | User Notice: 180 | Explicit Text: This Certificate may only be relied upon by Relying Parties and only in accordance with the Certificate Policy found at https://letsencrypt.org/repository/ 181 | 182 | Signature Algorithm: sha256WithRSAEncryption 183 | 82:c6:41:7c:f9:4d:0f:25:a0:2d:24:b7:e6:56:a3:76:22:00: 184 | b9:ad:1c:1d:a9:3f:13:ba:7b:f3:53:73:7b:55:b3:ce:26:50: 185 | b5:df:c2:a9:d4:52:a3:fe:eb:b6:84:37:9d:f6:c3:b7:03:6f: 186 | 8d:9b:f6:67:b2:23:b0:27:87:36:e9:0a:cd:74:33:01:0c:61: 187 | dd:11:24:c0:64:b1:d7:d1:bd:8b:fe:99:7b:42:de:86:d9:d3: 188 | 17:32:0e:be:3f:a4:fc:f7:8a:34:de:a6:13:a9:20:5e:c0:81: 189 | 96:25:87:66:28:31:ef:e5:8d:6b:c7:39:4e:c5:c7:5f:31:49: 190 | ee:30:b7:21:a3:b2:83:2a:0c:5e:db:12:67:94:7e:cd:0c:3e: 191 | 78:34:53:d2:ca:03:4f:bc:3b:1c:be:f6:c9:8c:11:dc:48:01: 192 | 4e:c1:07:30:75:f9:60:90:ef:c1:d2:db:df:cc:57:ca:36:b5: 193 | cc:2a:73:a2:a3:70:f5:17:29:34:02:cd:4f:6a:f4:63:fe:6b: 194 | 5d:18:e1:46:75:61:42:ce:cf:9b:01:ab:88:1a:d2:74:91:19: 195 | 19:7f:dd:51:69:32:57:8e:07:34:4b:9a:84:97:81:df:4e:4e: 196 | 46:2a:8b:44:02:b7:5e:94:c0:66:28:3f:f2:f3:7a:a3:e4:ad: 197 | 1f:56:da:b5 198 | ``` 199 | 200 | ## This is too hard 201 | Hate the idea of having to update DNS records manually? Want to have a script that takes of this for you without cutting and pasting, and pressing the enter key? Try these other providers and their related hooks: 202 | * [Route 53](https://gist.github.com/asimihsan/d8d8f0f10bdc85fc6f8a) 203 | * [Rackspace](https://github.com/major/letsencrypt-rackspace-hook/) 204 | * [Cloudflare](https://github.com/kappataumu/letsencrypt-cloudflare-hook) 205 | * [DNS Simple](https://github.com/danp/letsencrypt-dnsimple) 206 | --------------------------------------------------------------------------------