]>
Commit | Line | Data |
---|---|---|
e90f3b84 | 1 | #!/usr/bin/env sh |
2 | ||
d795fac3 | 3 | WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS" |
83b1a98d | 4 | |
e90f3b84 | 5 | ######## Public functions ##################### |
6 | ||
3fdbbafc | 7 | # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
e90f3b84 | 8 | # Used to add txt record |
9 | # | |
10 | # Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate | |
11 | # | |
7250a300 | 12 | |
91607bb2 | 13 | dns_azure_add() { |
14 | fulldomain=$1 | |
15 | txtvalue=$2 | |
16 | ||
17 | AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}" | |
91607bb2 | 18 | if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then |
19 | AZUREDNS_SUBSCRIPTIONID="" | |
20 | AZUREDNS_TENANTID="" | |
21 | AZUREDNS_APPID="" | |
22 | AZUREDNS_CLIENTSECRET="" | |
7250a300 | 23 | _err "You didn't specify the Azure Subscription ID" |
91607bb2 | 24 | return 1 |
25 | fi | |
7250a300 | 26 | #save subscription id to account conf file. |
27 | _saveaccountconf_mutable AZUREDNS_SUBSCRIPTIONID "$AZUREDNS_SUBSCRIPTIONID" | |
e90f3b84 | 28 | |
7250a300 | 29 | AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}" |
30 | if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then | |
31 | _info "Using Azure managed identity" | |
32 | #save managed identity as preferred authentication method, clear service principal credentials from conf file. | |
33 | _saveaccountconf_mutable AZUREDNS_MANAGEDIDENTITY "$AZUREDNS_MANAGEDIDENTITY" | |
34 | _saveaccountconf_mutable AZUREDNS_TENANTID "" | |
35 | _saveaccountconf_mutable AZUREDNS_APPID "" | |
36 | _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "" | |
37 | else | |
38 | _info "You didn't ask to use Azure managed identity, checking service principal credentials" | |
39 | AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" | |
40 | AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" | |
41 | AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" | |
91607bb2 | 42 | |
7250a300 | 43 | if [ -z "$AZUREDNS_TENANTID" ]; then |
44 | AZUREDNS_SUBSCRIPTIONID="" | |
45 | AZUREDNS_TENANTID="" | |
46 | AZUREDNS_APPID="" | |
47 | AZUREDNS_CLIENTSECRET="" | |
48 | _err "You didn't specify the Azure Tenant ID " | |
49 | return 1 | |
50 | fi | |
91607bb2 | 51 | |
7250a300 | 52 | if [ -z "$AZUREDNS_APPID" ]; then |
53 | AZUREDNS_SUBSCRIPTIONID="" | |
54 | AZUREDNS_TENANTID="" | |
55 | AZUREDNS_APPID="" | |
56 | AZUREDNS_CLIENTSECRET="" | |
57 | _err "You didn't specify the Azure App ID" | |
58 | return 1 | |
59 | fi | |
60 | ||
61 | if [ -z "$AZUREDNS_CLIENTSECRET" ]; then | |
62 | AZUREDNS_SUBSCRIPTIONID="" | |
63 | AZUREDNS_TENANTID="" | |
64 | AZUREDNS_APPID="" | |
65 | AZUREDNS_CLIENTSECRET="" | |
66 | _err "You didn't specify the Azure Client Secret" | |
67 | return 1 | |
68 | fi | |
69 | ||
70 | #save account details to account conf file, don't opt in for azure manages identity check. | |
71 | _saveaccountconf_mutable AZUREDNS_MANAGEDIDENTITY "false" | |
72 | _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID" | |
73 | _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID" | |
74 | _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET" | |
91607bb2 | 75 | fi |
e90f3b84 | 76 | |
7250a300 | 77 | accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") |
3fdbbafc | 78 | |
dd171ca4 | 79 | if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then |
80 | _err "invalid domain" | |
81 | return 1 | |
91607bb2 | 82 | fi |
83 | _debug _domain_id "$_domain_id" | |
84 | _debug _sub_domain "$_sub_domain" | |
85 | _debug _domain "$_domain" | |
86 | ||
dd171ca4 | 87 | acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01" |
91607bb2 | 88 | _debug "$acmeRecordURI" |
83b1a98d | 89 | # Get existing TXT record |
90 | _azure_rest GET "$acmeRecordURI" "" "$accesstoken" | |
91 | values="{\"value\":[\"$txtvalue\"]}" | |
92 | timestamp="$(_time)" | |
93 | if [ "$_code" = "200" ]; then | |
9e3c931b | 94 | vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"")" |
83b1a98d | 95 | _debug "existing TXT found" |
96 | _debug "$vlist" | |
9e3c931b | 97 | existingts="$(echo "$response" | _egrep_o "\"acmetscheck\"\\s*:\\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")" |
83b1a98d | 98 | if [ -z "$existingts" ]; then |
99 | # the record was not created by acme.sh. Copy the exisiting entires | |
100 | existingts=$timestamp | |
101 | fi | |
102 | _diff="$(_math "$timestamp - $existingts")" | |
103 | _debug "existing txt age: $_diff" | |
104 | # only use recently added records and discard if older than 2 hours because they are probably orphaned | |
105 | if [ "$_diff" -lt 7200 ]; then | |
106 | _debug "existing txt value: $vlist" | |
107 | for v in $vlist; do | |
108 | values="$values ,{\"value\":[\"$v\"]}" | |
109 | done | |
110 | fi | |
111 | fi | |
112 | # Add the txtvalue TXT Record | |
113 | body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"TTL\":10, \"TXTRecords\":[$values]}}" | |
91607bb2 | 114 | _azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken" |
115 | if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then | |
83b1a98d | 116 | _info "validation value added" |
224e0c29 | 117 | return 0 |
91607bb2 | 118 | else |
83b1a98d | 119 | _err "error adding validation value ($_code)" |
e90f3b84 | 120 | return 1 |
dd171ca4 | 121 | fi |
e90f3b84 | 122 | } |
123 | ||
124 | # Usage: fulldomain txtvalue | |
125 | # Used to remove the txt record after validation | |
126 | # | |
127 | # Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/delete | |
128 | # | |
91607bb2 | 129 | dns_azure_rm() { |
130 | fulldomain=$1 | |
131 | txtvalue=$2 | |
132 | ||
133 | AZUREDNS_SUBSCRIPTIONID="${AZUREDNS_SUBSCRIPTIONID:-$(_readaccountconf_mutable AZUREDNS_SUBSCRIPTIONID)}" | |
91607bb2 | 134 | if [ -z "$AZUREDNS_SUBSCRIPTIONID" ]; then |
135 | AZUREDNS_SUBSCRIPTIONID="" | |
136 | AZUREDNS_TENANTID="" | |
137 | AZUREDNS_APPID="" | |
138 | AZUREDNS_CLIENTSECRET="" | |
139 | _err "You didn't specify the Azure Subscription ID " | |
140 | return 1 | |
141 | fi | |
142 | ||
7250a300 | 143 | AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}" |
144 | if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then | |
145 | _info "Using Azure managed identity" | |
146 | else | |
147 | _info "You didn't ask to use Azure managed identity, checking service principal credentials" | |
148 | AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}" | |
149 | AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}" | |
150 | AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}" | |
91607bb2 | 151 | |
7250a300 | 152 | if [ -z "$AZUREDNS_TENANTID" ]; then |
153 | AZUREDNS_SUBSCRIPTIONID="" | |
154 | AZUREDNS_TENANTID="" | |
155 | AZUREDNS_APPID="" | |
156 | AZUREDNS_CLIENTSECRET="" | |
157 | _err "You didn't specify the Azure Tenant ID " | |
158 | return 1 | |
159 | fi | |
91607bb2 | 160 | |
7250a300 | 161 | if [ -z "$AZUREDNS_APPID" ]; then |
162 | AZUREDNS_SUBSCRIPTIONID="" | |
163 | AZUREDNS_TENANTID="" | |
164 | AZUREDNS_APPID="" | |
165 | AZUREDNS_CLIENTSECRET="" | |
166 | _err "You didn't specify the Azure App ID" | |
167 | return 1 | |
168 | fi | |
169 | ||
170 | if [ -z "$AZUREDNS_CLIENTSECRET" ]; then | |
171 | AZUREDNS_SUBSCRIPTIONID="" | |
172 | AZUREDNS_TENANTID="" | |
173 | AZUREDNS_APPID="" | |
174 | AZUREDNS_CLIENTSECRET="" | |
175 | _err "You didn't specify the Azure Client Secret" | |
176 | return 1 | |
177 | fi | |
91607bb2 | 178 | fi |
e90f3b84 | 179 | |
7250a300 | 180 | accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET") |
3fdbbafc | 181 | |
dd171ca4 | 182 | if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then |
183 | _err "invalid domain" | |
184 | return 1 | |
91607bb2 | 185 | fi |
186 | _debug _domain_id "$_domain_id" | |
187 | _debug _sub_domain "$_sub_domain" | |
188 | _debug _domain "$_domain" | |
189 | ||
dd171ca4 | 190 | acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01" |
91607bb2 | 191 | _debug "$acmeRecordURI" |
83b1a98d | 192 | # Get existing TXT record |
193 | _azure_rest GET "$acmeRecordURI" "" "$accesstoken" | |
194 | timestamp="$(_time)" | |
195 | if [ "$_code" = "200" ]; then | |
40cda922 | 196 | vlist="$(echo "$response" | _egrep_o "\"value\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"" | grep -v -- "$txtvalue")" |
83b1a98d | 197 | values="" |
198 | comma="" | |
199 | for v in $vlist; do | |
200 | values="$values$comma{\"value\":[\"$v\"]}" | |
201 | comma="," | |
202 | done | |
203 | if [ -z "$values" ]; then | |
204 | # No values left remove record | |
205 | _debug "removing validation record completely $acmeRecordURI" | |
206 | _azure_rest DELETE "$acmeRecordURI" "" "$accesstoken" | |
207 | if [ "$_code" = "200" ] || [ "$_code" = '204' ]; then | |
208 | _info "validation record removed" | |
209 | else | |
210 | _err "error removing validation record ($_code)" | |
211 | return 1 | |
212 | fi | |
213 | else | |
214 | # Remove only txtvalue from the TXT Record | |
215 | body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"TTL\":10, \"TXTRecords\":[$values]}}" | |
216 | _azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken" | |
217 | if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then | |
218 | _info "validation value removed" | |
224e0c29 | 219 | return 0 |
83b1a98d | 220 | else |
221 | _err "error removing validation value ($_code)" | |
222 | return 1 | |
223 | fi | |
224 | fi | |
91607bb2 | 225 | fi |
e90f3b84 | 226 | } |
227 | ||
228 | ################### Private functions below ################################## | |
229 | ||
230 | _azure_rest() { | |
91607bb2 | 231 | m=$1 |
232 | ep="$2" | |
233 | data="$3" | |
234 | accesstoken="$4" | |
235 | ||
83b1a98d | 236 | MAX_REQUEST_RETRY_TIMES=5 |
237 | _request_retry_times=0 | |
238 | while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do | |
239 | _debug3 _request_retry_times "$_request_retry_times" | |
240 | export _H1="authorization: Bearer $accesstoken" | |
241 | export _H2="accept: application/json" | |
242 | export _H3="Content-Type: application/json" | |
243 | # clear headers from previous request to avoid getting wrong http code on timeouts | |
19c43451 | 244 | : >"$HTTP_HEADER" |
83b1a98d | 245 | _debug "$ep" |
246 | if [ "$m" != "GET" ]; then | |
247 | _secure_debug2 "data $data" | |
248 | response="$(_post "$data" "$ep" "" "$m")" | |
249 | else | |
250 | response="$(_get "$ep")" | |
251 | fi | |
224e0c29 | 252 | _ret="$?" |
83b1a98d | 253 | _secure_debug2 "response $response" |
9e3c931b | 254 | _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" |
83b1a98d | 255 | _debug "http response code $_code" |
256 | if [ "$_code" = "401" ]; then | |
257 | # we have an invalid access token set to expired | |
258 | _saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "0" | |
259 | _err "access denied make sure your Azure settings are correct. See $WIKI" | |
260 | return 1 | |
261 | fi | |
262 | # See https://docs.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific#general-rest-and-retry-guidelines for retryable HTTP codes | |
224e0c29 | 263 | if [ "$_ret" != "0" ] || [ -z "$_code" ] || [ "$_code" = "408" ] || [ "$_code" = "500" ] || [ "$_code" = "503" ] || [ "$_code" = "504" ]; then |
83b1a98d | 264 | _request_retry_times="$(_math "$_request_retry_times" + 1)" |
265 | _info "REST call error $_code retrying $ep in $_request_retry_times s" | |
266 | _sleep "$_request_retry_times" | |
267 | continue | |
268 | fi | |
269 | break | |
270 | done | |
271 | if [ "$_request_retry_times" = "$MAX_REQUEST_RETRY_TIMES" ]; then | |
272 | _err "Error Azure REST called was retried $MAX_REQUEST_RETRY_TIMES times." | |
273 | _err "Calling $ep failed." | |
dd171ca4 | 274 | return 1 |
91607bb2 | 275 | fi |
83b1a98d | 276 | response="$(echo "$response" | _normalizeJson)" |
91607bb2 | 277 | return 0 |
e90f3b84 | 278 | } |
279 | ||
280 | ## Ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-service-to-service#request-an-access-token | |
281 | _azure_getaccess_token() { | |
7250a300 | 282 | managedIdentity=$1 |
283 | tenantID=$2 | |
284 | clientID=$3 | |
285 | clientSecret=$4 | |
91607bb2 | 286 | |
83b1a98d | 287 | accesstoken="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}" |
288 | expires_on="${AZUREDNS_TOKENVALIDTO:-$(_readaccountconf_mutable AZUREDNS_TOKENVALIDTO)}" | |
289 | ||
290 | # can we reuse the bearer token? | |
291 | if [ -n "$accesstoken" ] && [ -n "$expires_on" ]; then | |
292 | if [ "$(_time)" -lt "$expires_on" ]; then | |
293 | # brearer token is still valid - reuse it | |
294 | _debug "reusing bearer token" | |
295 | printf "%s" "$accesstoken" | |
296 | return 0 | |
297 | else | |
298 | _debug "bearer token expired" | |
299 | fi | |
300 | fi | |
301 | _debug "getting new bearer token" | |
302 | ||
7250a300 | 303 | if [ "$managedIdentity" = true ]; then |
304 | # https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http | |
305 | export _H1="Metadata: true" | |
306 | response="$(_get http://169.254.169.254/metadata/identity/oauth2/token\?api-version=2018-02-01\&resource=https://management.azure.com/)" | |
307 | response="$(echo "$response" | _normalizeJson)" | |
308 | accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") | |
309 | expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") | |
310 | else | |
311 | export _H1="accept: application/json" | |
312 | export _H2="Content-Type: application/x-www-form-urlencoded" | |
313 | body="resource=$(printf "%s" 'https://management.core.windows.net/' | _url_encode)&client_id=$(printf "%s" "$clientID" | _url_encode)&client_secret=$(printf "%s" "$clientSecret" | _url_encode)&grant_type=client_credentials" | |
314 | _secure_debug2 "data $body" | |
315 | response="$(_post "$body" "https://login.microsoftonline.com/$tenantID/oauth2/token" "" "POST")" | |
316 | _ret="$?" | |
317 | _secure_debug2 "response $response" | |
318 | response="$(echo "$response" | _normalizeJson)" | |
319 | accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") | |
320 | expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") | |
321 | fi | |
91607bb2 | 322 | |
8c887574 | 323 | if [ -z "$accesstoken" ]; then |
83b1a98d | 324 | _err "no acccess token received. Check your Azure settings see $WIKI" |
dd171ca4 | 325 | return 1 |
91607bb2 | 326 | fi |
224e0c29 | 327 | if [ "$_ret" != "0" ]; then |
91607bb2 | 328 | _err "error $response" |
329 | return 1 | |
330 | fi | |
83b1a98d | 331 | _saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$accesstoken" |
332 | _saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "$expires_on" | |
91607bb2 | 333 | printf "%s" "$accesstoken" |
334 | return 0 | |
e90f3b84 | 335 | } |
336 | ||
337 | _get_root() { | |
91607bb2 | 338 | domain=$1 |
339 | subscriptionId=$2 | |
340 | accesstoken=$3 | |
9e3c931b | 341 | i=1 |
91607bb2 | 342 | p=1 |
343 | ||
344 | ## Ref: https://docs.microsoft.com/en-us/rest/api/dns/zones/list | |
345 | ## returns up to 100 zones in one response therefore handling more results is not not implemented | |
346 | ## (ZoneListResult with continuation token for the next page of results) | |
347 | ## Per https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#dns-limits you are limited to 100 Zone/subscriptions anyways | |
348 | ## | |
12956679 | 349 | _azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?\$top=500&api-version=2017-09-01" "" "$accesstoken" |
971a85a6 | 350 | # Find matching domain name in Json response |
91607bb2 | 351 | while true; do |
352 | h=$(printf "%s" "$domain" | cut -d . -f $i-100) | |
353 | _debug2 "Checking domain: $h" | |
354 | if [ -z "$h" ]; then | |
355 | #not valid | |
356 | _err "Invalid domain" | |
357 | return 1 | |
358 | fi | |
e90f3b84 | 359 | |
360 | if _contains "$response" "\"name\":\"$h\"" >/dev/null; then | |
971a85a6 | 361 | _domain_id=$(echo "$response" | _egrep_o "\\{\"id\":\"[^\"]*\\/$h\"" | head -n 1 | cut -d : -f 2 | tr -d \") |
e90f3b84 | 362 | if [ "$_domain_id" ]; then |
9e3c931b | 363 | if [ "$i" = 1 ]; then |
364 | #create the record at the domain apex (@) if only the domain name was provided as --domain-alias | |
365 | _sub_domain="@" | |
366 | else | |
367 | _sub_domain=$(echo "$domain" | cut -d . -f 1-$p) | |
368 | fi | |
e90f3b84 | 369 | _domain=$h |
370 | return 0 | |
371 | fi | |
372 | return 1 | |
91607bb2 | 373 | fi |
374 | p=$i | |
375 | i=$(_math "$i" + 1) | |
376 | done | |
377 | return 1 | |
e90f3b84 | 378 | } |