Skip to content

Commit 7a0c600

Browse files
author
Kevin Wang
committed
refactor(dstack-ingress): give each challenge type its own top-level script
1 parent 77fb6d2 commit 7a0c600

12 files changed

Lines changed: 860 additions & 879 deletions

File tree

custom-domain/dstack-ingress/scripts/build-combined-pems.sh

Lines changed: 0 additions & 26 deletions
This file was deleted.

custom-domain/dstack-ingress/scripts/cert-manager.sh

Lines changed: 0 additions & 118 deletions
This file was deleted.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#!/bin/bash
2+
# dns01.sh - certificates via certbot and a DNS provider API.
3+
#
4+
# entrypoint.sh validates the shared settings and then execs this file. There is
5+
# no shared orchestration above it and no interface it has to implement -- this
6+
# is simply the program that runs for dns-01, top to bottom.
7+
8+
set -e
9+
10+
source /scripts/functions.sh
11+
source /scripts/haproxy-lib.sh
12+
source /scripts/evidence-lib.sh
13+
14+
# certbot stores certificates under /etc/letsencrypt/live/<domain>/.
15+
cert_fullchain_path() { echo "/etc/letsencrypt/live/$(cert_dir_name "$1")/fullchain.pem"; }
16+
cert_privkey_path() { echo "/etc/letsencrypt/live/$(cert_dir_name "$1")/privkey.pem"; }
17+
18+
setup_py_env() {
19+
if [ ! -d /opt/app-venv ]; then
20+
echo "Creating application virtual environment"
21+
python3 -m venv --system-site-packages /opt/app-venv
22+
fi
23+
24+
# Activate venv for subsequent steps
25+
# shellcheck disable=SC1091
26+
source /opt/app-venv/bin/activate
27+
28+
if [ ! -f /.venv_bootstrapped ]; then
29+
echo "Bootstrapping certbot dependencies"
30+
pip install --upgrade pip
31+
pip install certbot requests boto3 botocore
32+
touch /.venv_bootstrapped
33+
fi
34+
35+
ln -sf /opt/app-venv/bin/certbot /usr/local/bin/certbot
36+
echo 'source /opt/app-venv/bin/activate' > /etc/profile.d/app-venv.sh
37+
}
38+
39+
setup_certbot_env() {
40+
# Ensure the virtual environment is active for certbot configuration
41+
# shellcheck disable=SC1091
42+
source /opt/app-venv/bin/activate
43+
44+
if [ "${DNS_PROVIDER}" = "route53" ]; then
45+
mkdir -p /root/.aws
46+
47+
cat <<EOF >/root/.aws/config
48+
[profile certbot]
49+
role_arn=${AWS_ROLE_ARN}
50+
source_profile=certbot-source
51+
region=${AWS_REGION:-us-east-1}
52+
EOF
53+
54+
cat <<EOF >/root/.aws/credentials
55+
[certbot-source]
56+
aws_access_key_id=${AWS_ACCESS_KEY_ID}
57+
aws_secret_access_key=${AWS_SECRET_ACCESS_KEY}
58+
EOF
59+
60+
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
61+
export AWS_PROFILE=certbot
62+
fi
63+
64+
# Use the unified certbot manager to install plugins and setup credentials
65+
echo "Installing DNS plugins and setting up credentials"
66+
certman.py setup
67+
if [ $? -ne 0 ]; then
68+
echo "Error: Failed to setup certbot environment"
69+
exit 1
70+
fi
71+
}
72+
73+
set_alias_record() {
74+
local domain="$1"
75+
echo "Setting alias record for $domain"
76+
dnsman.py set_alias \
77+
--domain "$domain" \
78+
--content "$GATEWAY_DOMAIN"
79+
80+
if [ $? -ne 0 ]; then
81+
echo "Error: Failed to set alias record for $domain"
82+
exit 1
83+
fi
84+
echo "Alias record set for $domain"
85+
}
86+
87+
txt_record_value() { echo "${APP_ID:-$DSTACK_APP_ID}:${PORT}"; }
88+
89+
set_txt_record() {
90+
local domain="$1"
91+
local txt_domain
92+
txt_domain=$(txt_record_name "$domain")
93+
94+
dnsman.py set_txt \
95+
--domain "$txt_domain" \
96+
--content "$(txt_record_value)"
97+
98+
if [ $? -ne 0 ]; then
99+
echo "Error: Failed to set TXT record for $domain"
100+
exit 1
101+
fi
102+
}
103+
104+
set_caa_record() {
105+
local domain="$1"
106+
if [ "$SET_CAA" != "true" ]; then
107+
echo "Skipping CAA record setup"
108+
return
109+
fi
110+
111+
local ACCOUNT_URI
112+
local account_file
113+
114+
if ! account_file=$(get_letsencrypt_account_file); then
115+
echo "Warning: Cannot set CAA record - account file not found"
116+
echo "This is not critical - certificates can still be issued without CAA records"
117+
return
118+
fi
119+
120+
local caa_domain caa_tag
121+
caa_domain="${domain#\*.}"
122+
caa_tag=$(caa_tag_for "$domain")
123+
124+
ACCOUNT_URI=$(jq -j '.uri' "$account_file")
125+
echo "Adding CAA record ($caa_tag) for $caa_domain, accounturi=$ACCOUNT_URI"
126+
dnsman.py set_caa \
127+
--domain "$caa_domain" \
128+
--caa-tag "$caa_tag" \
129+
--caa-value "letsencrypt.org;validationmethods=dns-01;accounturi=$ACCOUNT_URI"
130+
131+
if [ $? -ne 0 ]; then
132+
echo "Warning: Failed to set CAA record for $domain"
133+
echo "This is not critical - certificates can still be issued without CAA records"
134+
echo "Consider disabling CAA records by setting SET_CAA=false if this continues to fail"
135+
fi
136+
}
137+
138+
process_domain() {
139+
local domain="$1"
140+
141+
set_alias_record "$domain"
142+
set_txt_record "$domain"
143+
# 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"
146+
set_caa_record "$domain"
147+
issue_certificate "$domain"
148+
}
149+
150+
issue_certificate() {
151+
source /opt/app-venv/bin/activate
152+
certman.py auto --domain "$1" --email "$CERTBOT_EMAIL"
153+
}
154+
155+
build_combined_pems() {
156+
local domain fullchain privkey combined
157+
mkdir -p /etc/haproxy/certs
158+
while IFS= read -r domain; do
159+
[[ -n "$domain" ]] || continue
160+
fullchain=$(cert_fullchain_path "$domain")
161+
privkey=$(cert_privkey_path "$domain")
162+
combined="/etc/haproxy/certs/${domain}.pem"
163+
if [ -f "$fullchain" ] && [ -f "$privkey" ]; then
164+
cat "$fullchain" "$privkey" > "$combined"
165+
chmod 600 "$combined"
166+
echo "Combined PEM created: ${combined}"
167+
else
168+
echo "Warning: Cert files missing for ${domain}, skipping"
169+
fi
170+
done <<<"$(get-all-domains.sh)"
171+
}
172+
173+
collect_evidence() {
174+
local account domain
175+
if ! account=$(get_letsencrypt_account_file); then
176+
echo "Error: cannot generate evidence without the ACME account file" >&2
177+
return 1
178+
fi
179+
evidence_reset
180+
evidence_collect_account "$account"
181+
while IFS= read -r domain; do
182+
[[ -n "$domain" ]] || continue
183+
evidence_collect_cert "$domain" "$(cert_fullchain_path "$domain")"
184+
done <<<"$(get-all-domains.sh)"
185+
evidence_finalize
186+
}
187+
188+
# One pass over every domain: make the DNS records right, then issue or renew.
189+
# Idempotent, so the first pass and every later one are the same code.
190+
run_pass() {
191+
local domain status failed=0 changed=0
192+
while IFS= read -r domain; do
193+
[[ -n "$domain" ]] || continue
194+
# `if` context, not a bare command: process_domain returns 2 for
195+
# "nothing to do", and under `set -e` a bare non-zero would kill the
196+
# whole script on every quiet renewal pass.
197+
if ( process_domain "$domain" ); then status=0; else status=$?; fi
198+
case $status in
199+
0) changed=1 ;;
200+
2) ;;
201+
*) echo "Certificate management failed for $domain" >&2; failed=1 ;;
202+
esac
203+
done <<<"$(get-all-domains.sh)"
204+
205+
if [ "$changed" -eq 1 ]; then
206+
collect_evidence || echo "Evidence generation failed" >&2
207+
build_combined_pems || echo "Combined PEM build failed" >&2
208+
haproxy_reload || true
209+
fi
210+
[ "$failed" -eq 0 ]
211+
}
212+
213+
cert_loop() {
214+
local attempt=0 delay
215+
while true; do
216+
if run_pass; then
217+
attempt=0
218+
delay=${RENEW_INTERVAL:-43200}
219+
else
220+
attempt=$((attempt + 1))
221+
delay=$((60 * attempt))
222+
[ "$delay" -gt 1800 ] && delay=1800
223+
echo "[$(date)] Pass failed (attempt ${attempt}); retrying in ${delay}s"
224+
fi
225+
echo "[$(date)] Next certificate check in ${delay}s"
226+
sleep "$delay"
227+
done
228+
}
229+
230+
setup_py_env
231+
setup_certbot_env
232+
load_dstack_identity
233+
234+
# dns-01 needs no inbound traffic, so keep the original order: certificate
235+
# first, serving second. A failure here is fatal, as it always was.
236+
run_pass
237+
build_combined_pems
238+
239+
haproxy_emit_global
240+
haproxy_emit_tls_frontend ":${PORT}"
241+
haproxy_emit_backends
242+
243+
evidence_start_server
244+
cert_loop &
245+
246+
exec "$@"

0 commit comments

Comments
 (0)