Skip to content

Commit 0dcd796

Browse files
author
Kevin Wang
committed
fix(dstack-ingress): count either issuance attempt as a change
1 parent 3c6c971 commit 0dcd796

3 files changed

Lines changed: 59 additions & 25 deletions

File tree

custom-domain/dstack-ingress/README.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ environment:
159159
| `DOMAIN` | Your domain (single-domain mode). Supports wildcards (`*.example.com`) |
160160
| `TARGET_ENDPOINT` | Backend address, e.g. `app:80` or `http://app:80` |
161161
| `GATEWAY_DOMAIN` | dstack gateway domain (e.g. `_.dstack-prod5.phala.network`) |
162-
| `CERTBOT_EMAIL` | Email for Let's Encrypt registration. Not required in tls-alpn-01 mode, where the preferred name is `ACME_EMAIL` |
162+
| `CERTBOT_EMAIL` | *(optional)* ACME contact address. `ACME_EMAIL` is accepted too, and is the preferred name in tls-alpn-01 mode |
163163
| `DNS_PROVIDER` | DNS provider (`cloudflare`, `linode`, `namecheap`) |
164164

165165
### Optional
@@ -172,7 +172,7 @@ environment:
172172
| `SET_CAA` | `false` | Enable CAA DNS record (dns-01 only; tls-alpn-01 cannot write DNS) |
173173
| `TXT_PREFIX` | `_dstack-app-address` | DNS TXT record prefix |
174174
| `CERTBOT_STAGING` | `false` | Use Let's Encrypt staging server. `ACME_STAGING` is the preferred name in tls-alpn-01 mode |
175-
| `ACME_EMAIL` | | Optional ACME contact address in tls-alpn-01 mode. Falls back to `CERTBOT_EMAIL`; there is no certbot on that path |
175+
| `ACME_EMAIL` | | ACME contact address, in either mode. Falls back to `CERTBOT_EMAIL`. Optional — see below |
176176
| `CHALLENGE_TYPE` | `dns-01` | `dns-01` (certbot + DNS credentials) or `tls-alpn-01` (lego, no DNS credentials) |
177177
| `DNS_SETUP_MODE` | `wait` | tls-alpn-01 only: `wait`, `print` or `webhook` — see below |
178178
| `DNS_SETUP_TIMEOUT` | `1800` | tls-alpn-01 only: seconds to wait for the records to appear |
@@ -307,16 +307,20 @@ Two consequences:
307307
means updating DNS. `DNS_SETUP_MODE=webhook` exists so this can be automated;
308308
doing it by hand means downtime on every redeploy.
309309

310-
### The ACME contact address is optional, and public
310+
## The ACME contact address is optional, and public
311311

312-
`ACME_EMAIL` may be left unset. RFC 8555 makes the ACME `contact` field
313-
optional, and Let's Encrypt stopped sending expiry notification mail in 2025, so
314-
setting one buys little.
312+
`ACME_EMAIL` (or `CERTBOT_EMAIL`) may be left unset in **either** mode. RFC 8555
313+
makes the ACME `contact` field optional, and Let's Encrypt stopped sending expiry
314+
notification mail in 2025, so setting one buys little.
315315

316316
It also does not stay private. The ACME account document is published as
317-
attestation evidence at `/evidences/acme-account.json`, so an address set here
318-
is readable by anyone who fetches the evidence endpoint. Leave it unset unless
319-
you specifically want a contact on the account.
317+
attestation evidence at `/evidences/acme-account.json`, so an address set here is
318+
readable by anyone who fetches the evidence endpoint. Leave it unset unless you
319+
specifically want a contact on the account.
320+
321+
Under the hood the two clients differ: lego simply omits the flag, while certbot
322+
needs `--register-unsafely-without-email` — its "unsafely" naming predates Let's
323+
Encrypt dropping expiry mail, and the container passes it for you.
320324

321325
### Limitations
322326

custom-domain/dstack-ingress/scripts/certman.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,18 @@ def _build_certbot_command(self, action: str, domain: str, email: str) -> List[s
286286
f"Credentials file does not exist: {credentials_file}")
287287

288288
if action == "certonly":
289-
base_cmd.extend(["--agree-tos", "--no-eff-email",
290-
"--email", email, "-d", domain])
289+
base_cmd.extend(["--agree-tos", "--no-eff-email"])
290+
# The ACME contact address is optional (RFC 8555 section 7.3), and
291+
# it is published: the account document is served as attestation
292+
# evidence, so an address set here is readable by anyone who
293+
# fetches it. certbot will not simply omit the flag, so ask for a
294+
# contactless account explicitly. Its "unsafely" naming predates
295+
# Let's Encrypt dropping expiry notification mail in 2025.
296+
if email:
297+
base_cmd.extend(["--email", email])
298+
else:
299+
base_cmd.append("--register-unsafely-without-email")
300+
base_cmd.extend(["-d", domain])
291301
if os.environ.get("CERTBOT_STAGING", "false") == "true":
292302
base_cmd.extend(["--staging"])
293303

@@ -501,15 +511,11 @@ def main():
501511
)
502512
sys.exit(1)
503513

504-
# Email is required for obtain and auto actions
505-
if args.action in ["obtain", "auto"] and not args.email:
506-
if not os.environ.get("CERTBOT_EMAIL"):
507-
print(
508-
"Error: --email is required or set CERTBOT_EMAIL environment variable",
509-
file=sys.stderr,
510-
)
511-
sys.exit(1)
512-
args.email = os.environ["CERTBOT_EMAIL"]
514+
# The contact address is optional; see _build_certbot_command.
515+
if not args.email:
516+
args.email = os.environ.get("ACME_EMAIL") or os.environ.get(
517+
"CERTBOT_EMAIL", ""
518+
)
513519

514520
success, needs_evidence = manager.run_action(
515521
args.domain, args.email, args.action

custom-domain/dstack-ingress/scripts/dns01.sh

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ source /scripts/functions.sh
1111
source /scripts/haproxy-lib.sh
1212
source /scripts/evidence-lib.sh
1313

14+
# ACME contact address. CERTBOT_EMAIL is the documented name on this path --
15+
# certbot really is the client here -- and ACME_EMAIL is accepted too so one
16+
# variable works in either mode. Optional either way.
17+
ACME_EMAIL=${ACME_EMAIL:-${CERTBOT_EMAIL:-}}
18+
export ACME_EMAIL
19+
1420
# certbot stores certificates under /etc/letsencrypt/live/<domain>/.
1521
cert_fullchain_path() { echo "/etc/letsencrypt/live/$(cert_dir_name "$1")/fullchain.pem"; }
1622
cert_privkey_path() { echo "/etc/letsencrypt/live/$(cert_dir_name "$1")/privkey.pem"; }
@@ -136,20 +142,38 @@ set_caa_record() {
136142
}
137143

138144
process_domain() {
139-
local domain="$1"
145+
local domain="$1" first status
140146

141147
set_alias_record "$domain"
142148
set_txt_record "$domain"
149+
143150
# The CAA record names our ACME account, so the account has to exist first:
144-
# try once (expected to fail on a fresh deployment), write CAA, try again.
145-
issue_certificate "$domain" || echo "First certificate attempt failed for $domain, retrying after the CAA record is set"
151+
# try once, write CAA, try again. The first attempt is expected to fail when
152+
# a CAA record from an earlier account is still in place.
153+
if issue_certificate "$domain"; then first=0; else first=$?; fi
154+
if [ "$first" -eq 1 ]; then
155+
echo "First certificate attempt failed for $domain, retrying after the CAA record is set"
156+
fi
157+
146158
set_caa_record "$domain"
147-
issue_certificate "$domain"
159+
160+
if issue_certificate "$domain"; then status=0; else status=$?; fi
161+
162+
# Either attempt producing a certificate counts as a change. Reporting only
163+
# the second one hid the common case: on a clean first deployment attempt
164+
# one obtains the certificate and attempt two reports "nothing to renew",
165+
# so the pass looked quiet and skipped evidence generation entirely.
166+
if [ "$first" -eq 0 ] || [ "$status" -eq 0 ]; then
167+
return 0
168+
fi
169+
return "$status"
148170
}
149171

150172
issue_certificate() {
151173
source /opt/app-venv/bin/activate
152-
certman.py auto --domain "$1" --email "$CERTBOT_EMAIL"
174+
# The contact address is optional; certman.py asks for a contactless
175+
# account when none is given.
176+
certman.py auto --domain "$1" ${ACME_EMAIL:+--email "$ACME_EMAIL"}
153177
}
154178

155179
build_combined_pems() {

0 commit comments

Comments
 (0)