mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-05-18 11:53:56 +08:00
103
README.md
103
README.md
@@ -146,6 +146,7 @@
|
||||
| 🌐 DNS mode | Use DNS TXT records |
|
||||
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
|
||||
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
|
||||
| 📌 DNS persist mode | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
|
||||
|
||||
---
|
||||
|
||||
@@ -396,7 +397,51 @@ acme.sh --renew -d example.com
|
||||
|
||||
---
|
||||
|
||||
### 🔟 Issue Certificates of Different Key Types (ECC or RSA)
|
||||
### 🔟 Use DNS Persist Mode
|
||||
|
||||
📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
|
||||
|
||||
DNS persist mode lets you place a **single, long‑lived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals.
|
||||
|
||||
#### 🪄 Step 1: Print the TXT record value
|
||||
|
||||
```bash
|
||||
acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] [--dns-persist-days 365]
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--server <ca>` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. |
|
||||
| `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. |
|
||||
| `--dns-persist-ca-name <name>` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. |
|
||||
| `--dns-persist-days <N>` | Adds `persistUntil=<unix-timestamp>` to the record, set to N days from now. The CA will refuse new validations against the record after that time. Omit for a record with no expiry. |
|
||||
|
||||
You should get an output like:
|
||||
|
||||
```sh
|
||||
TXT persist domain:_validation-persist.example.com
|
||||
TXT persist value :"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789"
|
||||
```
|
||||
|
||||
#### ✍️ Step 2: Add the TXT record to your DNS
|
||||
|
||||
Add the printed `TXT persist domain` / `TXT persist value` pair as a TXT record at your DNS provider, then wait for it to propagate.
|
||||
|
||||
#### 📜 Step 3: Issue the certificate
|
||||
|
||||
```bash
|
||||
acme.sh --issue -d example.com --dns-persist
|
||||
```
|
||||
|
||||
✅ **Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly.
|
||||
|
||||
> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣1️⃣ Issue Certificates of Different Key Types (ECC or RSA)
|
||||
|
||||
Just set the `keylength` to a valid, supported value.
|
||||
|
||||
@@ -427,7 +472,7 @@ acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com -
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣1️⃣ Issue Wildcard Certificates
|
||||
### 1️⃣2️⃣ Issue Wildcard Certificates
|
||||
|
||||
It's simple! Just give a wildcard domain as the `-d` parameter:
|
||||
|
||||
@@ -439,9 +484,9 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣2️⃣ How to Renew Certificates
|
||||
### 1️⃣3️⃣ How to Renew Certificates
|
||||
|
||||
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days.
|
||||
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days, **or earlier when the CA's ARI says so** (see below).
|
||||
|
||||
However, you can force a renewal:
|
||||
|
||||
@@ -455,9 +500,41 @@ acme.sh --renew -d example.com --force
|
||||
acme.sh --renew -d example.com --force --ecc
|
||||
```
|
||||
|
||||
#### 📡 ACME Renewal Information (ARI) — RFC 9773
|
||||
|
||||
If the CA exposes a `renewalInfo` endpoint in its ACME directory (Let's Encrypt, ZeroSSL, etc.), `acme.sh` follows [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html) automatically — **no flag needed, no opt-in**:
|
||||
|
||||
| What | When | Why |
|
||||
|------|------|-----|
|
||||
| 🔍 **Polls `suggestedWindow`** | Every cron run, before deciding to skip | Lets the CA shift the renewal time forward in case of an incident (key compromise, mass revocation, etc.) |
|
||||
| 🎯 **Picks a random renewal time** inside the window | Right after a successful issuance/renewal | Disperses renewals across the network so all clients don't hit the CA at the same instant |
|
||||
| 🔗 **Sends `replaces=<certID>`** in `newOrder` | On renewal | Lets the CA correlate the new order with the certificate it supersedes (RFC 9773 §5) |
|
||||
| ↩️ **Retries without `replaces`** | If the CA rejects with `alreadyReplaced` or an ARI validation error | Robust against edge cases (e.g. switching CAs, retired issuers) |
|
||||
|
||||
**Renewal trigger logic:** the cert is renewed if **any one** of the following becomes true:
|
||||
|
||||
1. `--force` is given
|
||||
2. The CA's **ARI `suggestedWindow` has started**
|
||||
3. The cached `Le_NextRenewTime` has passed (default fallback for CAs without ARI)
|
||||
|
||||
You can see the resulting next renewal time (already ARI-picked when applicable) in:
|
||||
|
||||
```sh
|
||||
acme.sh --info -d example.com
|
||||
# Look for: Le_NextRenewTimeStr=...
|
||||
```
|
||||
|
||||
For the live ARI window the CA is currently advertising, run with `--debug 2`:
|
||||
|
||||
```sh
|
||||
acme.sh --renew -d example.com --debug 2 2>&1 | grep -i 'ARI suggestedWindow'
|
||||
```
|
||||
|
||||
> 💡 If your CA does not advertise `renewalInfo`, `acme.sh` falls back to the classic 30-day rule — no behavior change.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣3️⃣ How to Stop Certificate Renewal
|
||||
### 1️⃣4️⃣ How to Stop Certificate Renewal
|
||||
|
||||
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
|
||||
|
||||
@@ -471,7 +548,7 @@ The cert/key file is not removed from the disk.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣4️⃣ How to Upgrade acme.sh
|
||||
### 1️⃣5️⃣ How to Upgrade acme.sh
|
||||
|
||||
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
|
||||
|
||||
@@ -495,25 +572,25 @@ acme.sh --upgrade --auto-upgrade 0
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣5️⃣ Issue a Certificate from an Existing CSR
|
||||
### 1️⃣6️⃣ Issue a Certificate from an Existing CSR
|
||||
|
||||
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣6️⃣ Send Notifications in Cronjob
|
||||
### 1️⃣7️⃣ Send Notifications in Cronjob
|
||||
|
||||
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣7️⃣ Under the Hood
|
||||
### 1️⃣8️⃣ Under the Hood
|
||||
|
||||
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣8️⃣ Acknowledgments
|
||||
### 1️⃣9️⃣ Acknowledgments
|
||||
|
||||
| Project | Link |
|
||||
|---------|------|
|
||||
@@ -555,7 +632,7 @@ Support this project with your organization. Your logo will show up here with a
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣9️⃣ License & Others
|
||||
### 2️⃣0️⃣ License & Others
|
||||
|
||||
📄 **License:** GPLv3
|
||||
|
||||
@@ -565,7 +642,7 @@ Support this project with your organization. Your logo will show up here with a
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣0️⃣ Donate
|
||||
### 2️⃣1️⃣ Donate
|
||||
|
||||
> 💝 Your donation makes **acme.sh** better!
|
||||
|
||||
@@ -577,7 +654,7 @@ Support this project with your organization. Your logo will show up here with a
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣1️⃣ About This Repository
|
||||
### 2️⃣2️⃣ About This Repository
|
||||
|
||||
> [!NOTE]
|
||||
> This repository is officially maintained by <strong>ZeroSSL</strong> as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community!
|
||||
|
||||
358
acme.sh
358
acme.sh
@@ -59,6 +59,7 @@ DEFAULT_OPENSSL_BIN="openssl"
|
||||
VTYPE_HTTP="http-01"
|
||||
VTYPE_DNS="dns-01"
|
||||
VTYPE_ALPN="tls-alpn-01"
|
||||
VTYPE_DNS_PERSIST="dns-persist-01"
|
||||
|
||||
ID_TYPE_DNS="dns"
|
||||
ID_TYPE_IP="ip"
|
||||
@@ -71,6 +72,7 @@ NO_VALUE="no"
|
||||
|
||||
W_DNS="dns"
|
||||
W_ALPN="alpn"
|
||||
W_DNS_PERSIST="dns_persist"
|
||||
DNS_ALIAS_PREFIX="="
|
||||
|
||||
MODE_STATELESS="stateless"
|
||||
@@ -1013,6 +1015,24 @@ _checkcert() {
|
||||
fi
|
||||
}
|
||||
|
||||
#file
|
||||
_enddate() {
|
||||
_cf="$1"
|
||||
_res="$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -enddate -in "$_cf")"
|
||||
if [ "$?" != "0" ] || [ -z "$_res" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$_res" in
|
||||
notAfter=*)
|
||||
echo "${_res#notAfter=}"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#Usage: hashalg [outputhex]
|
||||
#Output Base64-encoded digest
|
||||
_digest() {
|
||||
@@ -1037,6 +1057,25 @@ _digest() {
|
||||
|
||||
}
|
||||
|
||||
#Usage: certpath hashalg
|
||||
#Output certificate fingerprint without colons
|
||||
_fingerprint() {
|
||||
cert="$1"
|
||||
alg="$2"
|
||||
if [ -z "$alg" ]; then
|
||||
_usage "Usage: _fingerprint certpath hashalg"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
|
||||
# openssl prints "SHA1 Fingerprint=AA:BB:CC:..."; strip prefix and colons.
|
||||
${ACME_OPENSSL_BIN:-openssl} x509 -in "$cert" -noout -fingerprint -"$alg" | sed 's/.*=//; s/://g'
|
||||
else
|
||||
_err "$alg is not supported yet"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#Usage: hashalg secret_hex [outputhex]
|
||||
#Output binary hmac
|
||||
_hmac() {
|
||||
@@ -1844,6 +1883,25 @@ _date2time() {
|
||||
return 1
|
||||
}
|
||||
|
||||
#support the output format of openssl -enddate:
|
||||
# Apr 01 08:10:33 2022 GMT to 1641283833
|
||||
_ssldate2time() {
|
||||
#Linux
|
||||
if date -u -d "$1" +"%s" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
#Solaris
|
||||
if gdate -u -d "$1" +"%s" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
#Mac/BSD
|
||||
if date -j -f "%b %d %T %Y %Z" "$1" +"%s" 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
_err "Cannot parse _ssldate2time $1"
|
||||
return 1
|
||||
}
|
||||
|
||||
_utc_date() {
|
||||
date -u "+%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
@@ -4028,6 +4086,104 @@ deactivateaccount() {
|
||||
fi
|
||||
}
|
||||
|
||||
#domain wildcard ca_name days
|
||||
#Print the TXT record(s) the user must add to enable persistent DNS validation
|
||||
#per draft-ietf-acme-dns-persist-01.
|
||||
makednspersistvalue() {
|
||||
_mdpv_domain="$1"
|
||||
_mdpv_wildcard="$2"
|
||||
_mdpv_ca_name="$3"
|
||||
_mdpv_days="$4"
|
||||
|
||||
if [ -z "$_mdpv_domain" ]; then
|
||||
_err "Please specify a domain with -d."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$_mdpv_days" ]; then
|
||||
case "$_mdpv_days" in
|
||||
'' | *[!0-9]*)
|
||||
_err "--dns-persist-days must be a positive integer, got: $_mdpv_days"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ "$_mdpv_days" -lt 1 ]; then
|
||||
_err "--dns-persist-days must be at least 1."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_initpath
|
||||
|
||||
_accUri="$(_readcaconf ACCOUNT_URL)"
|
||||
if [ -z "$_accUri" ]; then
|
||||
_info "No account is registered for $ACME_DIRECTORY yet, registering one now..."
|
||||
if ! _regAccount "$DEFAULT_ACCOUNT_KEY_LENGTH"; then
|
||||
_err "Cannot register account."
|
||||
return 1
|
||||
fi
|
||||
_accUri="$(_readcaconf ACCOUNT_URL)"
|
||||
fi
|
||||
|
||||
if [ -z "$_accUri" ]; then
|
||||
_err "Cannot determine the ACME account URL."
|
||||
return 1
|
||||
fi
|
||||
_debug "Account URL" "$_accUri"
|
||||
|
||||
_txt_name="_validation-persist.$_mdpv_domain"
|
||||
|
||||
_txt_suffix="; accounturi=$_accUri"
|
||||
if [ "$_mdpv_wildcard" = "1" ]; then
|
||||
_txt_suffix="$_txt_suffix; policy=wildcard"
|
||||
fi
|
||||
if [ -n "$_mdpv_days" ]; then
|
||||
_persist_until=$(_math "$(_time)" + "$_mdpv_days" \* 86400)
|
||||
_txt_suffix="$_txt_suffix; persistUntil=$_persist_until"
|
||||
_info "persistUntil set to $(__green "$(_time2str "$_persist_until")") ($_mdpv_days days from now)"
|
||||
fi
|
||||
|
||||
if [ -n "$_mdpv_ca_name" ]; then
|
||||
_info ""
|
||||
_info "Add the following DNS TXT record to enable persistent DNS validation:"
|
||||
_info ""
|
||||
_info "$(printf 'TXT persist domain:%s' "$(__green "$_txt_name")")"
|
||||
_info "$(printf 'TXT persist value :%s' "$(__green "\"$_mdpv_ca_name$_txt_suffix\"")")"
|
||||
_info ""
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "Fetching ACME directory: $ACME_DIRECTORY"
|
||||
_dir_resp="$(_get "$ACME_DIRECTORY" "" 30)"
|
||||
if [ "$?" != "0" ] || [ -z "$_dir_resp" ]; then
|
||||
_err "Cannot fetch ACME directory: $ACME_DIRECTORY"
|
||||
return 1
|
||||
fi
|
||||
_dir_resp="$(echo "$_dir_resp" | _json_decode)"
|
||||
_debug2 _dir_resp "$_dir_resp"
|
||||
|
||||
_caa_array="$(echo "$_dir_resp" | tr -d ' \r\n\t' | _egrep_o '"caaIdentities":\[[^]]*\]')"
|
||||
_debug2 _caa_array "$_caa_array"
|
||||
_caaids="$(echo "$_caa_array" | sed 's/.*\[//' | sed 's/\].*//' | tr ',' '\n' | tr -d '"')"
|
||||
_debug2 _caaids "$_caaids"
|
||||
|
||||
if [ -z "$_caaids" ]; then
|
||||
_err "The directory does not include 'caaIdentities'. Please specify --dns-persist-ca-name explicitly."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info ""
|
||||
_info "Add ANY ONE of the following DNS TXT records to enable persistent DNS validation."
|
||||
_info "(You only need to add one; pick whichever issuer identity you prefer.)"
|
||||
for _id in $_caaids; do
|
||||
[ -z "$_id" ] && continue
|
||||
_info ""
|
||||
_info "$(printf 'TXT persist domain:%s' "$(__green "$_txt_name")")"
|
||||
_info "$(printf 'TXT persist value :%s' "$(__green "\"$_id$_txt_suffix\"")")"
|
||||
done
|
||||
_info ""
|
||||
}
|
||||
|
||||
# domain folder file
|
||||
_findHook() {
|
||||
_hookdomain="$1"
|
||||
@@ -4709,13 +4865,41 @@ issue() {
|
||||
if [ "$_certificate_profile" ]; then
|
||||
_newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\""
|
||||
fi
|
||||
|
||||
# RFC 9773 Section 5: include "replaces" only when this is an actual
|
||||
# renewal (--renew path), the CA advertises renewalInfo, and a prior
|
||||
# cert exists. --issue (even with --force) is not a renewal per RFC 9773
|
||||
# which speaks of "a clear predecessor certificate" issued by this CA.
|
||||
_replaces_certID=""
|
||||
if [ "$_ACME_IS_RENEW" = "1" ] && [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then
|
||||
_replaces_certID="$(_getARICertID "$CERT_PATH")"
|
||||
_debug "Adding ARI replaces" "$_replaces_certID"
|
||||
fi
|
||||
|
||||
_debug "STEP 1, Ordering a Certificate"
|
||||
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
|
||||
_newOrderReplacesObj="$_newOrderObj"
|
||||
if [ "$_replaces_certID" ]; then
|
||||
_newOrderReplacesObj="$_newOrderObj,\"replaces\": \"$_replaces_certID\""
|
||||
fi
|
||||
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderReplacesObj}"; then
|
||||
_err "Error creating new order."
|
||||
_clearup
|
||||
_on_issue_err "$_post_hook"
|
||||
return 1
|
||||
fi
|
||||
# RFC 9773 Section 5 only defines the "alreadyReplaced" error, but real CAs
|
||||
# (Let's Encrypt) may also reject with a malformed error if the prior cert
|
||||
# was issued by a different issuer / different CA. Retry without "replaces"
|
||||
# whenever the failure mentions ARI or the replaces field.
|
||||
if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then
|
||||
_info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'."
|
||||
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
|
||||
_err "Error creating new order."
|
||||
_clearup
|
||||
_on_issue_err "$_post_hook"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if _contains "$response" "invalid"; then
|
||||
if echo "$response" | _normalizeJson | grep '"status":"invalid"' >/dev/null 2>&1; then
|
||||
_err "Create new order with invalid status."
|
||||
@@ -4806,7 +4990,9 @@ $_authorizations_map"
|
||||
|
||||
vtype="$VTYPE_HTTP"
|
||||
#todo, v2 wildcard force to use dns
|
||||
if _startswith "$_currentRoot" "$W_DNS"; then
|
||||
if [ "$_currentRoot" = "$W_DNS_PERSIST" ]; then
|
||||
vtype="$VTYPE_DNS_PERSIST"
|
||||
elif _startswith "$_currentRoot" "$W_DNS"; then
|
||||
vtype="$VTYPE_DNS"
|
||||
fi
|
||||
|
||||
@@ -4864,18 +5050,7 @@ $_authorizations_map"
|
||||
fi
|
||||
|
||||
if [ -z "$keyauthorization" ]; then
|
||||
token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')"
|
||||
_debug token "$token"
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
_err "Cannot get domain token $entry"
|
||||
_clearup
|
||||
_on_issue_err "$_post_hook"
|
||||
return 1
|
||||
fi
|
||||
|
||||
uri="$(echo "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)"
|
||||
|
||||
_debug uri "$uri"
|
||||
|
||||
if [ -z "$uri" ]; then
|
||||
@@ -4884,8 +5059,26 @@ $_authorizations_map"
|
||||
_on_issue_err "$_post_hook"
|
||||
return 1
|
||||
fi
|
||||
keyauthorization="$token.$thumbprint"
|
||||
_debug keyauthorization "$keyauthorization"
|
||||
|
||||
if [ "$vtype" = "$VTYPE_DNS_PERSIST" ]; then
|
||||
# dns-persist-01 challenges have no token; the TXT record is
|
||||
# provisioned out-of-band. Use a non-empty placeholder so the
|
||||
# downstream code does not treat this entry as already verified.
|
||||
keyauthorization="$VTYPE_DNS_PERSIST"
|
||||
_debug keyauthorization "$keyauthorization"
|
||||
else
|
||||
token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')"
|
||||
_debug token "$token"
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
_err "Cannot get domain token $entry"
|
||||
_clearup
|
||||
_on_issue_err "$_post_hook"
|
||||
return 1
|
||||
fi
|
||||
keyauthorization="$token.$thumbprint"
|
||||
_debug keyauthorization "$keyauthorization"
|
||||
fi
|
||||
fi
|
||||
|
||||
dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url"
|
||||
@@ -5427,7 +5620,7 @@ $_authorizations_map"
|
||||
Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime")
|
||||
_savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr"
|
||||
|
||||
if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then
|
||||
if [ -z "$Le_RenewalDays" ]; then
|
||||
Le_RenewalDays="$DEFAULT_RENEW"
|
||||
else
|
||||
_savedomainconf "Le_RenewalDays" "$Le_RenewalDays"
|
||||
@@ -5486,11 +5679,49 @@ $_authorizations_map"
|
||||
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
||||
fi
|
||||
fi
|
||||
elif [ "$Le_RenewalDays" -lt "0" ]; then
|
||||
_enddate_value=$(_enddate "$CERT_PATH")
|
||||
if [ "$?" != "0" ] || [ -z "$_enddate_value" ]; then
|
||||
_err "Failed to get certificate end date for $CERT_PATH"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_endtime=$(_ssldate2time "$_enddate_value")
|
||||
if [ "$?" != "0" ] || [ -z "$_endtime" ]; then
|
||||
_err "Cannot parse _enddate_value: $_enddate_value"
|
||||
return 1
|
||||
fi
|
||||
Le_NextRenewTime=$(_math "$_endtime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
|
||||
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
||||
else
|
||||
Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60)
|
||||
Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400)
|
||||
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
||||
fi
|
||||
|
||||
# RFC 9773 ARI: if the CA exposes renewalInfo, override Le_NextRenewTime
|
||||
# with a time picked at random within the suggestedWindow. This both gives
|
||||
# the CA full control over renewal scheduling and disperses renewals across
|
||||
# the network so all clients don't hit the CA at the same instant.
|
||||
if [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ] && [ -z "$_notAfter" ]; then
|
||||
_ari_resp_new="$(_get_ARI "$CERT_PATH")"
|
||||
_debug2 "_ari_resp_new" "$_ari_resp_new"
|
||||
_ari_start_new="$(echo "$_ari_resp_new" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')"
|
||||
_ari_end_new="$(echo "$_ari_resp_new" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')"
|
||||
if [ "$_ari_start_new" ] && [ "$_ari_end_new" ]; then
|
||||
_ari_start_t_new="$(_date2time "$(echo "$_ari_start_new" | sed 's/\.[0-9]*//')")"
|
||||
_ari_end_t_new="$(_date2time "$(echo "$_ari_end_new" | sed 's/\.[0-9]*//')")"
|
||||
if [ "$_ari_start_t_new" ] && [ "$_ari_end_t_new" ] && [ "$_ari_end_t_new" -gt "$_ari_start_t_new" ]; then
|
||||
_ari_window=$(_math "$_ari_end_t_new" - "$_ari_start_t_new")
|
||||
_ari_offset=$(_math "$(_time)" % "$_ari_window")
|
||||
Le_NextRenewTime=$(_math "$_ari_start_t_new" + "$_ari_offset")
|
||||
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
||||
_info "ARI suggestedWindow: $(__green "$_ari_start_new") to $(__green "$_ari_end_new")"
|
||||
_info "Next renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
_savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr"
|
||||
_savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
|
||||
|
||||
@@ -5586,7 +5817,31 @@ renew() {
|
||||
_debug2 "initpath again."
|
||||
_initpath "$Le_Domain" "$_isEcc"
|
||||
|
||||
if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
|
||||
# ARI (RFC 9773): fetch the CA's suggestedWindow on every renewal check.
|
||||
# If the window has started, renew now even if Le_NextRenewTime is in the future.
|
||||
_ari_should_renew=""
|
||||
if [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then
|
||||
if _initAPI && [ "$ACME_RENEWAL_INFO" ]; then
|
||||
_ari_resp="$(_get_ARI "$CERT_PATH")"
|
||||
_debug2 "_ari_resp" "$_ari_resp"
|
||||
_ari_start="$(echo "$_ari_resp" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')"
|
||||
_ari_end="$(echo "$_ari_resp" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')"
|
||||
_debug "ARI suggestedWindow.start" "$_ari_start"
|
||||
_debug "ARI suggestedWindow.end" "$_ari_end"
|
||||
if [ "$_ari_start" ]; then
|
||||
_ari_start_t="$(_date2time "$(echo "$_ari_start" | sed 's/\.[0-9]*//')")"
|
||||
_debug "_ari_start_t" "$_ari_start_t"
|
||||
if [ "$_ari_start_t" ] && [ "$(_time)" -ge "$_ari_start_t" ]; then
|
||||
_info "ARI suggestedWindow has started ($(__green "$_ari_start")), proceeding with renewal."
|
||||
_ari_should_renew="1"
|
||||
else
|
||||
_info "ARI suggestedWindow starts at: $(__green "$_ari_start")"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$FORCE" ] && [ -z "$_ari_should_renew" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
|
||||
_info "Skipping. Next renewal time is: $(__green "$Le_NextRenewTimeStr")"
|
||||
_info "Add '$(__red '--force')' to force renewal."
|
||||
if [ -z "$_ACME_IN_RENEWALL" ]; then
|
||||
@@ -6274,13 +6529,13 @@ installcronjob() {
|
||||
return 1
|
||||
fi
|
||||
_info "Installing cron job"
|
||||
if ! $_CRONTAB -l | grep "$PROJECT_ENTRY --cron"; then
|
||||
if ! $_CRONTAB -l 2>/dev/null | grep "$PROJECT_ENTRY --cron"; then
|
||||
if _exists uname && uname -a | grep SunOS >/dev/null; then
|
||||
_CRONTAB_STDIN="$_CRONTAB --"
|
||||
else
|
||||
_CRONTAB_STDIN="$_CRONTAB -"
|
||||
fi
|
||||
$_CRONTAB -l | {
|
||||
$_CRONTAB -l 2>/dev/null | {
|
||||
cat
|
||||
echo "$random_minute $random_hour * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null"
|
||||
} | $_CRONTAB_STDIN
|
||||
@@ -6608,21 +6863,29 @@ _getSerial() {
|
||||
}
|
||||
|
||||
#cert
|
||||
_get_ARI() {
|
||||
#Compute the ARI/replaces certID for a cert: base64url(AKI).base64url(Serial)
|
||||
#per RFC 9773 Section 4.1.
|
||||
_getARICertID() {
|
||||
_cert="$1"
|
||||
_aki=$(_getAKI "$_cert")
|
||||
_ser=$(_getSerial "$_cert")
|
||||
_debug2 "_aki" "$_aki"
|
||||
_debug2 "_ser" "$_ser"
|
||||
|
||||
_akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)"
|
||||
_akiurl="$(echo "$_aki" | _h2b | _base64 | _url_replace)"
|
||||
_debug2 "_akiurl" "$_akiurl"
|
||||
_serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)"
|
||||
_serurl="$(echo "$_ser" | _h2b | _base64 | _url_replace)"
|
||||
_debug2 "_serurl" "$_serurl"
|
||||
|
||||
_ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl"
|
||||
_get "$_ARI_URL"
|
||||
printf "%s.%s" "$_akiurl" "$_serurl"
|
||||
}
|
||||
|
||||
#cert
|
||||
_get_ARI() {
|
||||
_cert="$1"
|
||||
_ari_certID="$(_getARICertID "$_cert")"
|
||||
_ARI_URL="$ACME_RENEWAL_INFO/$_ari_certID"
|
||||
_get "$_ARI_URL"
|
||||
}
|
||||
|
||||
# Detect profile file if not specified as environment variable
|
||||
@@ -7158,6 +7421,8 @@ Commands:
|
||||
--update-account Update account info.
|
||||
--register-account Register account key.
|
||||
--deactivate-account Deactivate the account.
|
||||
--make-dns-persist-value Print the DNS TXT record(s) to enable persistent DNS validation
|
||||
(draft-ietf-acme-dns-persist-01). Use with -d <domain>.
|
||||
--create-account-key Create an account private key, professional use.
|
||||
--install-cronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job.
|
||||
--uninstall-cronjob Uninstall the cron job. The 'uninstall' command can do this automatically.
|
||||
@@ -7205,6 +7470,10 @@ Parameters:
|
||||
--dns [dns_hook] Use dns manual mode or dns api. Defaults to manual mode when argument is omitted.
|
||||
See: $_DNS_API_WIKI
|
||||
|
||||
--dns-persist Use dns-persist-01 validation (draft-ietf-acme-dns-persist-01).
|
||||
Requires the persistent _validation-persist TXT record to already
|
||||
exist. Use '--make-dns-persist-value' to print the value to add.
|
||||
|
||||
--dnssleep <seconds> The time in seconds to wait for all the txt records to propagate in dns api mode.
|
||||
It's not necessary to use this by default, $PROJECT_NAME polls dns status by DOH automatically.
|
||||
-k, --keylength <bits> Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384, ec-521.
|
||||
@@ -7215,6 +7484,18 @@ Parameters:
|
||||
--eab-kid <eab_key_id> Key Identifier for External Account Binding.
|
||||
--eab-hmac-key <eab_hmac_key> HMAC key for External Account Binding.
|
||||
|
||||
--dns-persist-wildcard Used with '--make-dns-persist-value'. Adds 'policy=wildcard' to the
|
||||
generated TXT record so the issuer is also authorized for wildcards
|
||||
and subdomains (draft-ietf-acme-dns-persist-01).
|
||||
--dns-persist-ca-name <name> Used with '--make-dns-persist-value'. Use the given CA identity domain
|
||||
(e.g. 'ssl.com') as the issuer-domain-name in the TXT record. If
|
||||
omitted, the identities are read from the ACME directory's
|
||||
'caaIdentities' field and one record is printed per identity.
|
||||
--dns-persist-days <N> Used with '--make-dns-persist-value'. Add a 'persistUntil' field to
|
||||
the TXT record so the record self-expires N days from now (the CA
|
||||
will refuse new validations against the record after that time).
|
||||
If omitted, the record has no expiry.
|
||||
|
||||
|
||||
These parameters are to install the cert to nginx/Apache or any other server after issue/renew a cert:
|
||||
|
||||
@@ -7235,6 +7516,7 @@ Parameters:
|
||||
-m, --email <email> Specifies the account email, only valid for the '--install' and '--update-account' command.
|
||||
--accountkey <file> Specifies the account key path, only valid for the '--install' command.
|
||||
--days <ndays> Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_RENEW days.
|
||||
Negative values could be used to specify a number of days relative to the expiration date of the certificate.
|
||||
--httpport <port> Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer.
|
||||
--tlsport <port> Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer.
|
||||
--local-address <ip> Specifies the standalone/tls server listening address, in case you have multiple ip addresses.
|
||||
@@ -7585,6 +7867,9 @@ _process() {
|
||||
_valid_to=""
|
||||
_certificate_profile=""
|
||||
_extended_key_usage=""
|
||||
_dns_persist_wildcard=""
|
||||
_dns_persist_ca_name=""
|
||||
_dns_persist_days=""
|
||||
while [ ${#} -gt 0 ]; do
|
||||
case "${1}" in
|
||||
|
||||
@@ -7679,6 +7964,20 @@ _process() {
|
||||
--deactivate-account)
|
||||
_CMD="deactivateaccount"
|
||||
;;
|
||||
--make-dns-persist-value | --makednspersistvalue)
|
||||
_CMD="makednspersistvalue"
|
||||
;;
|
||||
--dns-persist-wildcard | --dnspersistwildcard)
|
||||
_dns_persist_wildcard="1"
|
||||
;;
|
||||
--dns-persist-ca-name | --dnspersistcaname)
|
||||
_dns_persist_ca_name="$2"
|
||||
shift
|
||||
;;
|
||||
--dns-persist-days | --dnspersistdays)
|
||||
_dns_persist_days="$2"
|
||||
shift
|
||||
;;
|
||||
--set-notify)
|
||||
_CMD="setnotify"
|
||||
;;
|
||||
@@ -7822,6 +8121,14 @@ _process() {
|
||||
_webroot="$_webroot,$wvalue"
|
||||
fi
|
||||
;;
|
||||
--dns-persist)
|
||||
wvalue="$W_DNS_PERSIST"
|
||||
if [ -z "$_webroot" ]; then
|
||||
_webroot="$wvalue"
|
||||
else
|
||||
_webroot="$_webroot,$wvalue"
|
||||
fi
|
||||
;;
|
||||
--dnssleep)
|
||||
_dnssleep="$2"
|
||||
Le_DNSSleep="$_dnssleep"
|
||||
@@ -8238,6 +8545,9 @@ _process() {
|
||||
deactivateaccount)
|
||||
deactivateaccount
|
||||
;;
|
||||
makednspersistvalue)
|
||||
makednspersistvalue "$_domain" "$_dns_persist_wildcard" "$_dns_persist_ca_name" "$_dns_persist_days"
|
||||
;;
|
||||
list)
|
||||
list "$_listraw" "$_domain"
|
||||
;;
|
||||
|
||||
158
deploy/windows_rdp.sh
Normal file
158
deploy/windows_rdp.sh
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# install a certificate on a Windows host over OpenSSH and bind it to the Remote
|
||||
# Desktop listener (RDP-Tcp).
|
||||
#
|
||||
# One ssh invocation does the whole job:
|
||||
# * the PFX is built locally, base64'd, and embedded as a string literal
|
||||
# inside a generated PowerShell script;
|
||||
# * the script is piped to `powershell.exe -Command -` over ssh. No scp,
|
||||
# no temp files on the Windows host.
|
||||
#
|
||||
# First run:
|
||||
# export DEPLOY_WIN_RDP_HOST=winserver.example.com
|
||||
# acme.sh --deploy -d winserver.example.com --deploy-hook windows_rdp
|
||||
#
|
||||
# Available variables:
|
||||
# DEPLOY_WIN_RDP_HOST required SSH host
|
||||
# DEPLOY_WIN_RDP_USER optional SSH user, must be a local administrator (can also by set via ssh_config)
|
||||
# DEPLOY_WIN_RDP_PORT optional SSH port, default 22
|
||||
# DEPLOY_WIN_RDP_SSH_OPTS optional extra ssh options, e.g.
|
||||
# "-i /root/.ssh/win_id_ed25519 -o StrictHostKeyChecking=yes"
|
||||
# DEPLOY_WIN_RDP_LISTENER optional RDP listener name, default RDP-Tcp
|
||||
# DEPLOY_WIN_RDP_RESTART optional "1" to restart TermService after install.
|
||||
# Active RDP sessions will drop!
|
||||
|
||||
windows_rdp_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
if ! _exists "ssh"; then
|
||||
_err "ssh is required but was not found in PATH."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- configuration ------------------------------------------------------
|
||||
_getdeployconf DEPLOY_WIN_RDP_HOST
|
||||
_getdeployconf DEPLOY_WIN_RDP_USER
|
||||
_getdeployconf DEPLOY_WIN_RDP_PORT
|
||||
_getdeployconf DEPLOY_WIN_RDP_SSH_OPTS
|
||||
_getdeployconf DEPLOY_WIN_RDP_LISTENER
|
||||
_getdeployconf DEPLOY_WIN_RDP_RESTART
|
||||
|
||||
if [ -z "$DEPLOY_WIN_RDP_HOST" ]; then
|
||||
_err "DEPLOY_WIN_RDP_HOST must be set."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_savedeployconf DEPLOY_WIN_RDP_HOST "$DEPLOY_WIN_RDP_HOST"
|
||||
[ -n "$DEPLOY_WIN_RDP_USER" ] && _savedeployconf DEPLOY_WIN_RDP_USER "$DEPLOY_WIN_RDP_USER"
|
||||
[ -n "$DEPLOY_WIN_RDP_PORT" ] && _savedeployconf DEPLOY_WIN_RDP_PORT "$DEPLOY_WIN_RDP_PORT"
|
||||
[ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ] && _savedeployconf DEPLOY_WIN_RDP_SSH_OPTS "$DEPLOY_WIN_RDP_SSH_OPTS"
|
||||
[ -n "$DEPLOY_WIN_RDP_LISTENER" ] && _savedeployconf DEPLOY_WIN_RDP_LISTENER "$DEPLOY_WIN_RDP_LISTENER"
|
||||
[ -n "$DEPLOY_WIN_RDP_RESTART" ] && _savedeployconf DEPLOY_WIN_RDP_RESTART "$DEPLOY_WIN_RDP_RESTART"
|
||||
|
||||
_port="${DEPLOY_WIN_RDP_PORT:-22}"
|
||||
_listener="${DEPLOY_WIN_RDP_LISTENER:-RDP-Tcp}"
|
||||
if [ -n "$DEPLOY_WIN_RDP_USER" ]; then
|
||||
_target="$DEPLOY_WIN_RDP_USER@$DEPLOY_WIN_RDP_HOST"
|
||||
else
|
||||
_target="$DEPLOY_WIN_RDP_HOST"
|
||||
fi
|
||||
_pfx_pass="acme"
|
||||
|
||||
# ---- build thumbprint + PFX locally ------------------------------------
|
||||
_thumb="$(_fingerprint "$_ccert" 'sha1')"
|
||||
if [ -z "$_thumb" ]; then
|
||||
_err "Failed to compute certificate thumbprint."
|
||||
return 1
|
||||
fi
|
||||
_debug "Thumbprint: $_thumb"
|
||||
|
||||
_debug "Building PFX at $_pfx_file"
|
||||
_pfx_file="$(_mktemp)"
|
||||
if ! _toPkcs "$_pfx_file" "$_ckey" "$_ccert" "$_cca" "$_pfx_pass"; then
|
||||
_err "Failed to build PFX archive."
|
||||
rm -f "$_pfx_file"
|
||||
return 1
|
||||
fi
|
||||
_pfx_b64=$(_base64 "multiline" <"$_pfx_file")
|
||||
rm -f "$_pfx_file"
|
||||
|
||||
# ---- build installer script --------------------------------------------
|
||||
if [ "$DEPLOY_WIN_RDP_RESTART" = "1" ]; then
|
||||
_restart_ps='Restart-Service -Name TermService -Force'
|
||||
else
|
||||
_restart_ps='# New RdP connections will pick up the new cert automatically.'
|
||||
fi
|
||||
|
||||
# Escape every literal `$` with `\$` so the shell does not expand it.
|
||||
# Values substituted from shell: $_pfx_b64, $_pfx_pass, $_thumb, $_listener.
|
||||
_ps1=$(
|
||||
cat <<PSEOF
|
||||
|
||||
\$ErrorActionPreference = 'Stop'
|
||||
|
||||
\$pfxBytes = [Convert]::FromBase64String('${_pfx_b64}')
|
||||
|
||||
# Note: It is quite important to use a X509Certificate2Collection here in any case, since we otherwise
|
||||
# could run into quite a lot of trouble when importing the certificate including its entire chain
|
||||
# and its private key. Windows might behave arbitrarily and not consistently import the certificate
|
||||
# at all - unless "Exportable" is included in the storage flags. However, then the certificate seems
|
||||
# unaccessible to TermService for some weird reasons despite all permissions being set (at least on my
|
||||
# Win 11 lab machine). This might be some security setting that prevents TermService from working with
|
||||
# exportable keys? I don't know - importing the entire collection including chain or not always fixes
|
||||
# the issues.
|
||||
#
|
||||
# Note2: If you should have kicked yourself out for some reason, then deleting the certificate will make
|
||||
# TermService restore the original, self-signed certificate after at least after the second login attempt.
|
||||
# Deleting the certificate can be easily accomplished via the Powershell, since SSH access will still be
|
||||
# present in any case - the following command should get you out of trouble:
|
||||
# \$cert = Get-ChildItem -Path 'Cert:\LocalMachine\My\\${_thumb}' | Select-Object -First 1 | Remove-Item
|
||||
|
||||
\$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]'MachineKeySet,PersistKeySet'
|
||||
\$certs = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
|
||||
\$certs.Import(\$pfxBytes, '${_pfx_pass}', \$flags)
|
||||
|
||||
\$store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'LocalMachine')
|
||||
\$store.Open('ReadWrite')
|
||||
\$store.AddRange(\$certs)
|
||||
\$store.Close()
|
||||
Write-Host "Installed certs into LocalMachine\\My"
|
||||
|
||||
\$ts = Get-CimInstance -Namespace root/cimv2/terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='${_listener}'"
|
||||
if (-not \$ts) { throw "Listener '${_listener}' not found." }
|
||||
Set-CimInstance -InputObject \$ts -Property @{SSLCertificateSHA1Hash="${_thumb}"}
|
||||
Write-Host "Listener ${_listener} now uses ${_thumb}"
|
||||
|
||||
${_restart_ps}
|
||||
PSEOF
|
||||
)
|
||||
_debug "Powershell script:${_ps1}"
|
||||
|
||||
# ---- run over a single ssh connection ----------------------------------
|
||||
_ssh_opts="-o BatchMode=yes -p $_port"
|
||||
if [ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ]; then
|
||||
_ssh_opts="$_ssh_opts $DEPLOY_WIN_RDP_SSH_OPTS"
|
||||
fi
|
||||
|
||||
_info "Deploying to $DEPLOY_WIN_RDP_HOST ..."
|
||||
# shellcheck disable=SC2086
|
||||
if ! printf '%s\n' "$_ps1" | ssh $_ssh_opts "$_target" \
|
||||
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'; then
|
||||
_err "Remote install failed. Re-run acme.sh with --debug to see the PowerShell output."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Certificate for $_cdomain deployed and bound to $_listener on $DEPLOY_WIN_RDP_HOST."
|
||||
return 0
|
||||
}
|
||||
267
dnsapi/dns_eurodns.sh
Normal file
267
dnsapi/dns_eurodns.sh
Normal file
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_eurodns_info='EuroDNS
|
||||
Site: eurodns.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_eurodns
|
||||
Options:
|
||||
EURODNS_APP_ID Application ID
|
||||
EURODNS_API_KEY API Key
|
||||
EURODNS_TTL TTL. Default: "600".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues
|
||||
Author: Nicolas Santorelli
|
||||
'
|
||||
|
||||
#
|
||||
# EuroDNS DNS API
|
||||
#
|
||||
# EuroDNS API documentation:
|
||||
# https://docapi.eurodns.com
|
||||
#
|
||||
# Usage:
|
||||
# export EURODNS_APP_ID="your-app-id"
|
||||
# export EURODNS_API_KEY="your-api-key"
|
||||
# acme.sh --issue --dns dns_eurodns -d example.com -d *.example.com
|
||||
#
|
||||
# The credentials will be saved in ~/.acme.sh/account.conf
|
||||
#
|
||||
# Optional:
|
||||
# export EURODNS_API_URL="https://rest-api.eurodns.com" # Default API URL
|
||||
# export EURODNS_TTL=600 # Default TTL (minimum 600 for EuroDNS)
|
||||
#
|
||||
|
||||
EURODNS_API_DEFAULT="https://rest-api.eurodns.com"
|
||||
EURODNS_TTL_DEFAULT=600
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#Usage: dns_eurodns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_eurodns_add() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using EuroDNS DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
|
||||
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
|
||||
EURODNS_TTL="${EURODNS_TTL:-$(_readaccountconf_mutable EURODNS_TTL)}"
|
||||
EURODNS_TTL="${EURODNS_TTL:-$EURODNS_TTL_DEFAULT}"
|
||||
|
||||
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
|
||||
EURODNS_APP_ID=""
|
||||
EURODNS_API_KEY=""
|
||||
_err "You didn't specify EuroDNS App ID and API Key."
|
||||
_err "Please export EURODNS_APP_ID and EURODNS_API_KEY and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable EURODNS_APP_ID "$EURODNS_APP_ID"
|
||||
_saveaccountconf_mutable EURODNS_API_KEY "$EURODNS_API_KEY"
|
||||
if [ "$EURODNS_API_URL" != "$EURODNS_API_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable EURODNS_API_URL "$EURODNS_API_URL"
|
||||
fi
|
||||
if [ "$EURODNS_TTL" != "$EURODNS_TTL_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable EURODNS_TTL "$EURODNS_TTL"
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_info "Adding TXT record"
|
||||
if _eurodns_add_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
|
||||
_info "Added TXT record successfully."
|
||||
return 0
|
||||
else
|
||||
_err "Failed to add TXT record."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#Usage: fulldomain txtvalue
|
||||
dns_eurodns_rm() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using EuroDNS DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
|
||||
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
|
||||
|
||||
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
|
||||
EURODNS_APP_ID=""
|
||||
EURODNS_API_KEY=""
|
||||
_err "You didn't specify EuroDNS App ID and API Key."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_info "Removing TXT record"
|
||||
if _eurodns_rm_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
|
||||
_info "Removed TXT record successfully."
|
||||
return 0
|
||||
else
|
||||
_err "Failed to remove TXT record."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_eurodns_rest GET "dns-zones/$h"
|
||||
if [ "$?" != "0" ]; then
|
||||
if [ "$_code" = "404" ]; then
|
||||
_debug "Zone $h not found, continuing..."
|
||||
else
|
||||
_err "API error looking up zone $h"
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
continue
|
||||
fi
|
||||
|
||||
if _contains "$response" '"name"'; then
|
||||
if [ "$i" = "1" ]; then
|
||||
_sub_domain="@"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
fi
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_eurodns_add_txt_record() {
|
||||
domain=$1
|
||||
subdomain=$2
|
||||
txtvalue=$3
|
||||
|
||||
data='[{"type":"TXT","host":"'"$subdomain"'","rdata":"'"$txtvalue"'","ttl":'"$EURODNS_TTL"'}]'
|
||||
|
||||
_debug "Adding TXT record via API"
|
||||
if _eurodns_rest POST "dns-zones/$domain/dns-records" "$data"; then
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
_err "Failed to add TXT record"
|
||||
return 1
|
||||
}
|
||||
|
||||
_eurodns_rm_txt_record() {
|
||||
domain=$1
|
||||
subdomain=$2
|
||||
txtvalue=$3
|
||||
|
||||
_debug "Getting current zone data for $domain"
|
||||
|
||||
if ! _eurodns_rest GET "dns-zones/$domain"; then
|
||||
_err "Failed to get zone data"
|
||||
return 1
|
||||
fi
|
||||
|
||||
zone_data=$(echo "$response" | _normalizeJson)
|
||||
_debug2 zone_data "$zone_data"
|
||||
|
||||
# Find the record ID matching our TXT record
|
||||
record_id=$(echo "$zone_data" | tr '{' '\n' | grep -F '"TXT"' | grep -F "\"$subdomain\"" | grep -F "\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]+' | cut -d : -f 2 | _head_n 1)
|
||||
_debug record_id "$record_id"
|
||||
|
||||
if [ -z "$record_id" ]; then
|
||||
_info "TXT record not found or already removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "Deleting TXT record $record_id"
|
||||
if ! _eurodns_rest DELETE "dns-zones/$domain/dns-records/$record_id"; then
|
||||
_err "Failed to delete TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: _eurodns_rest METHOD ENDPOINT [DATA]
|
||||
_eurodns_rest() {
|
||||
method=$1
|
||||
endpoint=$2
|
||||
data="$3"
|
||||
|
||||
export _H1="X-APP-ID: $EURODNS_APP_ID"
|
||||
export _H2="X-API-KEY: $EURODNS_API_KEY"
|
||||
export _H3="Content-Type: application/json"
|
||||
|
||||
url="$EURODNS_API_URL/$endpoint"
|
||||
|
||||
_debug2 url "$url"
|
||||
_debug2 method "$method"
|
||||
_debug2 data "$data"
|
||||
|
||||
: >"$HTTP_HEADER"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
response="$(_post "$data" "$url" "" "$method")"
|
||||
fi
|
||||
|
||||
_ret="$?"
|
||||
unset _H1 _H2 _H3
|
||||
_debug2 response "$response"
|
||||
|
||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
||||
_debug2 _code "$_code"
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Error calling API: $endpoint"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_code" != "200" ] && [ "$_code" != "201" ] && [ "$_code" != "204" ]; then
|
||||
if [ "$_code" != "404" ]; then
|
||||
_err "API error (HTTP $_code): $response"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
110
dnsapi/dns_firestorm.sh
Normal file
110
dnsapi/dns_firestorm.sh
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_firestorm_info='Firestorm.ch
|
||||
Site: firestorm.ch
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_firestorm
|
||||
Options:
|
||||
FST_Key Customer ID
|
||||
FST_Secret API Secret
|
||||
FST_Url API URL. Optional. Default "https://api.firestorm.ch/acme-dns".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6839
|
||||
Author: FireStorm GmbH
|
||||
'
|
||||
|
||||
FST_Url_DEFAULT="https://api.firestorm.ch/acme-dns"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_firestorm_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_firestorm_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
|
||||
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
|
||||
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
|
||||
|
||||
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
|
||||
_err "FST_Key and FST_Secret must be set"
|
||||
_err "Get your API credentials at https://admin.firestorm.ch"
|
||||
return 1
|
||||
fi
|
||||
|
||||
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
|
||||
|
||||
_saveaccountconf_mutable FST_Key "$FST_Key"
|
||||
_saveaccountconf_mutable FST_Secret "$FST_Secret"
|
||||
if [ "$FST_Url" != "$FST_Url_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable FST_Url "$FST_Url"
|
||||
else
|
||||
_clearaccountconf_mutable FST_Url
|
||||
fi
|
||||
|
||||
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
|
||||
|
||||
_info "Adding TXT record for $fulldomain"
|
||||
_debug "Subdomain" "$subdomain"
|
||||
_debug "TXT value" "$txtvalue"
|
||||
|
||||
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
|
||||
|
||||
response="$(_firestorm_api "update" "$body")"
|
||||
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
_info "TXT record added successfully"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to add TXT record: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: dns_firestorm_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_firestorm_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
|
||||
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
|
||||
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
|
||||
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
|
||||
|
||||
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
|
||||
_err "FST_Key and FST_Secret must be set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
|
||||
|
||||
_info "Removing TXT record for $fulldomain"
|
||||
|
||||
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
|
||||
|
||||
response="$(_firestorm_api "remove" "$body")"
|
||||
|
||||
if _contains "$response" "removed"; then
|
||||
_info "TXT record removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to remove TXT record: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# Escape special characters for safe JSON string interpolation
|
||||
_json_safe() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
_firestorm_api() {
|
||||
action=$1
|
||||
data=$2
|
||||
|
||||
export _H1="X-Api-User: $FST_Key"
|
||||
export _H2="X-Api-Key: $FST_Secret"
|
||||
export _H3="Content-Type: application/json"
|
||||
|
||||
_post "$data" "$FST_Url/$action" "" "POST"
|
||||
}
|
||||
Reference in New Issue
Block a user