-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathentrypoint.sh
More file actions
421 lines (346 loc) · 12.2 KB
/
entrypoint.sh
File metadata and controls
421 lines (346 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
#!/bin/bash
set -e
source "/scripts/functions.sh"
PORT=${PORT:-443}
TXT_PREFIX=${TXT_PREFIX:-"_dstack-app-address"}
MAXCONN=${MAXCONN:-4096}
TIMEOUT_CONNECT=${TIMEOUT_CONNECT:-10s}
TIMEOUT_CLIENT=${TIMEOUT_CLIENT:-86400s}
TIMEOUT_SERVER=${TIMEOUT_SERVER:-86400s}
EVIDENCE_SERVER=${EVIDENCE_SERVER:-true}
EVIDENCE_PORT=${EVIDENCE_PORT:-80}
ALPN=${ALPN:-}
if ! PORT=$(sanitize_port "$PORT"); then
exit 1
fi
if ! DOMAIN=$(sanitize_domain "$DOMAIN"); then
exit 1
fi
if ! TARGET_ENDPOINT=$(sanitize_target_endpoint "$TARGET_ENDPOINT"); then
exit 1
fi
if ! TXT_PREFIX=$(sanitize_dns_label "$TXT_PREFIX"); then
exit 1
fi
if ! MAXCONN=$(sanitize_positive_integer "$MAXCONN" "MAXCONN"); then
exit 1
fi
if ! TIMEOUT_CONNECT=$(sanitize_haproxy_timeout "$TIMEOUT_CONNECT" "TIMEOUT_CONNECT"); then
exit 1
fi
if ! TIMEOUT_CLIENT=$(sanitize_haproxy_timeout "$TIMEOUT_CLIENT" "TIMEOUT_CLIENT"); then
exit 1
fi
if ! TIMEOUT_SERVER=$(sanitize_haproxy_timeout "$TIMEOUT_SERVER" "TIMEOUT_SERVER"); then
exit 1
fi
if ! EVIDENCE_PORT=$(sanitize_positive_integer "$EVIDENCE_PORT" "EVIDENCE_PORT"); then
exit 1
fi
if ! ALPN=$(sanitize_alpn "$ALPN"); then
exit 1
fi
# Warn about deprecated L7 env vars
for var in CLIENT_MAX_BODY_SIZE PROXY_READ_TIMEOUT PROXY_SEND_TIMEOUT PROXY_CONNECT_TIMEOUT PROXY_BUFFER_SIZE PROXY_BUFFERS PROXY_BUSY_BUFFERS_SIZE; do
if [ -n "${!var}" ]; then
echo "Warning: $var is ignored in TCP proxy mode"
fi
done
# Parse TARGET_ENDPOINT into host:port for haproxy backend
parse_target_endpoint() {
local endpoint="$1"
# Strip protocol prefix if present (http://, https://, grpc://)
local hostport="${endpoint#*://}"
# If no protocol was stripped, use as-is
if [ "$hostport" = "$endpoint" ]; then
hostport="$endpoint"
fi
# Strip any trailing path
hostport="${hostport%%/*}"
echo "$hostport"
}
echo "Setting up certbot environment"
setup_py_env() {
if [ ! -d /opt/app-venv ]; then
echo "Creating application virtual environment"
python3 -m venv --system-site-packages /opt/app-venv
fi
# Activate venv for subsequent steps
# shellcheck disable=SC1091
source /opt/app-venv/bin/activate
if [ ! -f /.venv_bootstrapped ]; then
echo "Bootstrapping certbot dependencies"
pip install --upgrade pip
pip install certbot requests boto3 botocore
touch /.venv_bootstrapped
fi
ln -sf /opt/app-venv/bin/certbot /usr/local/bin/certbot
echo 'source /opt/app-venv/bin/activate' > /etc/profile.d/app-venv.sh
}
setup_certbot_env() {
# Ensure the virtual environment is active for certbot configuration
# shellcheck disable=SC1091
source /opt/app-venv/bin/activate
if [ "${DNS_PROVIDER}" = "route53" ]; then
mkdir -p /root/.aws
cat <<EOF >/root/.aws/config
[profile certbot]
role_arn=${AWS_ROLE_ARN}
source_profile=certbot-source
region=${AWS_REGION:-us-east-1}
EOF
cat <<EOF >/root/.aws/credentials
[certbot-source]
aws_access_key_id=${AWS_ACCESS_KEY_ID}
aws_secret_access_key=${AWS_SECRET_ACCESS_KEY}
EOF
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
export AWS_PROFILE=certbot
fi
# Use the unified certbot manager to install plugins and setup credentials
echo "Installing DNS plugins and setting up credentials"
certman.py setup
if [ $? -ne 0 ]; then
echo "Error: Failed to setup certbot environment"
exit 1
fi
}
setup_py_env
# Emit common haproxy global/defaults/frontend preamble.
# Both single-domain and multi-domain modes share this identical config.
emit_haproxy_preamble() {
# "crt <dir>" loads all PEM files from the directory.
# ALPN is appended conditionally via ${ALPN:+ alpn ${ALPN}}.
cat <<EOF >/etc/haproxy/haproxy.cfg
global
log stdout format raw local0
maxconn ${MAXCONN}
pidfile /var/run/haproxy/haproxy.pid
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-bind-curves secp384r1
defaults
log global
mode tcp
option tcplog
timeout connect ${TIMEOUT_CONNECT}
timeout client ${TIMEOUT_CLIENT}
timeout server ${TIMEOUT_SERVER}
frontend tls_in
bind :${PORT} ssl crt /etc/haproxy/certs/${ALPN:+ alpn ${ALPN}}
EOF
if [ "$EVIDENCE_SERVER" = "true" ]; then
cat <<'EVIDENCE_BLOCK' >>/etc/haproxy/haproxy.cfg
# Route /evidences requests to the local evidence HTTP server.
# accept fires once 16 bytes have arrived — enough for the
# longest prefix we match ("HEAD /evidences" = 16 chars).
# Using req.len with a concrete threshold is critical: the
# previous payload(0,0) (length 0 = "whole buffer") deferred
# evaluation until the full inspect-delay because HAProxy
# cannot know when a TCP stream ends.
tcp-request inspect-delay 5s
tcp-request content accept if { req.len ge 16 }
acl is_evidence payload(0,16) -m beg "GET /evidences"
acl is_evidence payload(0,16) -m beg "HEAD /evidences"
use_backend be_evidence if is_evidence
EVIDENCE_BLOCK
fi
}
# Append the evidence backend block to haproxy.cfg
emit_evidence_backend() {
if [ "$EVIDENCE_SERVER" = "true" ]; then
cat <<EOF >>/etc/haproxy/haproxy.cfg
backend be_evidence
mode http
http-request replace-path /evidences(.*) \1
server evidence 127.0.0.1:${EVIDENCE_PORT}
EOF
fi
}
# Generate haproxy.cfg for single-domain mode (DOMAIN + TARGET_ENDPOINT)
setup_haproxy_cfg() {
local target_hostport
target_hostport=$(parse_target_endpoint "$TARGET_ENDPOINT")
emit_haproxy_preamble
cat <<EOF >>/etc/haproxy/haproxy.cfg
default_backend be_upstream
backend be_upstream
server app1 ${target_hostport}
EOF
emit_evidence_backend
}
# Generate haproxy.cfg for multi-domain mode (ROUTING_MAP)
setup_haproxy_cfg_multi() {
emit_haproxy_preamble
# Parse ROUTING_MAP and generate use_backend rules + backend sections
# Support both newline-separated and comma-separated formats
local routing_map_normalized
routing_map_normalized=$(echo "$ROUTING_MAP" | tr ',' '\n')
local backend_rules=""
local backend_sections=""
local first_be_name=""
local domain target be_name
while IFS= read -r line; do
[[ -n "$line" ]] || continue
[[ "$line" == \#* ]] && continue
domain="${line%%=*}"
target="${line#*=}"
domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
target=$(echo "$target" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -n "$domain" && -n "$target" ]] || continue
# Validate domain and target to prevent config injection
if ! domain=$(sanitize_domain "$domain"); then
echo "Error: Invalid domain in ROUTING_MAP: ${line}" >&2
exit 1
fi
if ! target=$(sanitize_target_endpoint "$target"); then
echo "Error: Invalid target in ROUTING_MAP: ${line}" >&2
exit 1
fi
# Strip protocol prefix from target if present
target=$(parse_target_endpoint "$target")
# Generate safe backend name from domain
be_name="be_$(echo "$domain" | sed 's/[^A-Za-z0-9]/_/g')"
if [ -z "$first_be_name" ]; then
first_be_name="$be_name"
fi
backend_rules="${backend_rules}
use_backend ${be_name} if { ssl_fc_sni -i ${domain} }"
backend_sections="${backend_sections}
backend ${be_name}
server s1 ${target}"
done <<< "$routing_map_normalized"
echo "$backend_rules" >> /etc/haproxy/haproxy.cfg
# Default to first backend in ROUTING_MAP
if [ -n "$first_be_name" ]; then
echo "" >> /etc/haproxy/haproxy.cfg
echo " default_backend ${first_be_name}" >> /etc/haproxy/haproxy.cfg
fi
echo "$backend_sections" >> /etc/haproxy/haproxy.cfg
emit_evidence_backend
}
set_alias_record() {
local domain="$1"
echo "Setting alias record for $domain"
dnsman.py set_alias \
--domain "$domain" \
--content "$GATEWAY_DOMAIN"
if [ $? -ne 0 ]; then
echo "Error: Failed to set alias record for $domain"
exit 1
fi
echo "Alias record set for $domain"
}
set_txt_record() {
local domain="$1"
local APP_ID
if [[ -S /var/run/dstack.sock ]]; then
DSTACK_APP_ID=$(curl -s --unix-socket /var/run/dstack.sock http://localhost/Info | jq -j .app_id)
export DSTACK_APP_ID
else
DSTACK_APP_ID=$(curl -s --unix-socket /var/run/tappd.sock http://localhost/prpc/Tappd.Info | jq -j .app_id)
export DSTACK_APP_ID
fi
APP_ID=${APP_ID:-"$DSTACK_APP_ID"}
local txt_domain
if [[ "$domain" == \*.* ]]; then
# Wildcard domain: *.myapp.com → _dstack-app-address-wildcard.myapp.com
txt_domain="${TXT_PREFIX}-wildcard.${domain#\*.}"
else
txt_domain="${TXT_PREFIX}.${domain}"
fi
dnsman.py set_txt \
--domain "$txt_domain" \
--content "$APP_ID:$PORT"
if [ $? -ne 0 ]; then
echo "Error: Failed to set TXT record for $domain"
exit 1
fi
}
set_caa_record() {
local domain="$1"
if [ "$SET_CAA" != "true" ]; then
echo "Skipping CAA record setup"
return
fi
local ACCOUNT_URI
local account_file
if ! account_file=$(get_letsencrypt_account_file); then
echo "Warning: Cannot set CAA record - account file not found"
echo "This is not critical - certificates can still be issued without CAA records"
return
fi
local caa_domain caa_tag
if [[ "$domain" == \*.* ]]; then
caa_domain="${domain#\*.}"
caa_tag="issuewild"
else
caa_domain="$domain"
caa_tag="issue"
fi
ACCOUNT_URI=$(jq -j '.uri' "$account_file")
echo "Adding CAA record ($caa_tag) for $caa_domain, accounturi=$ACCOUNT_URI"
dnsman.py set_caa \
--domain "$caa_domain" \
--caa-tag "$caa_tag" \
--caa-value "letsencrypt.org;validationmethods=dns-01;accounturi=$ACCOUNT_URI"
if [ $? -ne 0 ]; then
echo "Warning: Failed to set CAA record for $domain"
echo "This is not critical - certificates can still be issued without CAA records"
echo "Consider disabling CAA records by setting SET_CAA=false if this continues to fail"
fi
}
process_domain() {
local domain="$1"
echo "Processing domain: $domain"
set_alias_record "$domain"
set_txt_record "$domain"
renew-certificate.sh "$domain" || echo "First certificate renewal failed for $domain, will retry after set CAA record"
set_caa_record "$domain"
renew-certificate.sh "$domain"
}
bootstrap() {
echo "Bootstrap: Setting up domains"
local all_domains
all_domains=$(get-all-domains.sh)
if [ -z "$all_domains" ]; then
echo "Error: No domains found. Set either DOMAIN or DOMAINS environment variable"
exit 1
fi
echo "Found domains:"
echo "$all_domains"
while IFS= read -r domain; do
[[ -n "$domain" ]] || continue
process_domain "$domain"
done <<<"$all_domains"
# Generate evidences after all certificates are obtained
echo "Generating evidence files for all domains..."
generate-evidences.sh
touch /.bootstrapped
}
# Credentials are now handled by certman.py setup
# Setup certbot environment (venv is already created in Dockerfile)
setup_certbot_env
# Check if it's the first time the container is started
if [ ! -f "/.bootstrapped" ]; then
bootstrap
else
echo "Certificate for $DOMAIN already exists"
generate-evidences.sh
fi
# Build combined PEM files for haproxy
build-combined-pems.sh
# Generate haproxy config
if [ -n "$ROUTING_MAP" ]; then
setup_haproxy_cfg_multi
elif [ -n "$DOMAIN" ] && [ -n "$TARGET_ENDPOINT" ]; then
setup_haproxy_cfg
fi
# Start evidence HTTP server if enabled
if [ "$EVIDENCE_SERVER" = "true" ]; then
mini_httpd -d /evidences -p "${EVIDENCE_PORT}" -D -l /dev/stderr &
echo "Evidence server started on port ${EVIDENCE_PORT} (mini_httpd)"
fi
renewal-daemon.sh &
exec "$@"