Skip to content

Commit 5befb07

Browse files
h4x3rotabclaude
andcommitted
Harden haproxy config generation and input validation
- Factor common global/defaults/frontend config into emit_haproxy_preamble() and emit_evidence_backend() to eliminate duplication between single-domain and multi-domain setup functions - Add sanitize_positive_integer() and sanitize_haproxy_timeout() validators; validate MAXCONN, TIMEOUT_CONNECT/CLIENT/SERVER, EVIDENCE_PORT at startup - Fix renewal-daemon to log why HAProxy reload failed (missing PID file vs failed signal) instead of silently swallowing errors - Move loop-scoped local declarations before the while loop in setup_haproxy_cfg_multi() - Guard e2e test against wildcard DOMAIN input - Wrap phala deploy in error check so test exits on deployment failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ae6117f commit 5befb07

5 files changed

Lines changed: 91 additions & 59 deletions

File tree

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

Lines changed: 44 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@ fi
2626
if ! TXT_PREFIX=$(sanitize_dns_label "$TXT_PREFIX"); then
2727
exit 1
2828
fi
29+
if ! MAXCONN=$(sanitize_positive_integer "$MAXCONN" "MAXCONN"); then
30+
exit 1
31+
fi
32+
if ! TIMEOUT_CONNECT=$(sanitize_haproxy_timeout "$TIMEOUT_CONNECT" "TIMEOUT_CONNECT"); then
33+
exit 1
34+
fi
35+
if ! TIMEOUT_CLIENT=$(sanitize_haproxy_timeout "$TIMEOUT_CLIENT" "TIMEOUT_CLIENT"); then
36+
exit 1
37+
fi
38+
if ! TIMEOUT_SERVER=$(sanitize_haproxy_timeout "$TIMEOUT_SERVER" "TIMEOUT_SERVER"); then
39+
exit 1
40+
fi
41+
if ! EVIDENCE_PORT=$(sanitize_positive_integer "$EVIDENCE_PORT" "EVIDENCE_PORT"); then
42+
exit 1
43+
fi
2944

3045
# Warn about deprecated L7 env vars
3146
for var in CLIENT_MAX_BODY_SIZE PROXY_READ_TIMEOUT PROXY_SEND_TIMEOUT PROXY_CONNECT_TIMEOUT; do
@@ -87,11 +102,11 @@ setup_certbot_env() {
87102

88103
setup_py_env
89104

90-
# Generate haproxy.cfg for single-domain mode (DOMAIN + TARGET_ENDPOINT)
91-
setup_haproxy_cfg() {
92-
local target_hostport
93-
target_hostport=$(parse_target_endpoint "$TARGET_ENDPOINT")
94-
105+
# Emit common haproxy global/defaults/frontend preamble.
106+
# Both single-domain and multi-domain modes share this identical config.
107+
emit_haproxy_preamble() {
108+
# "crt <dir>" loads all PEM files from the directory.
109+
# ALPN is appended conditionally via ${ALPN:+ alpn ${ALPN}}.
95110
cat <<EOF >/etc/haproxy/haproxy.cfg
96111
global
97112
log stdout format raw local0
@@ -125,15 +140,10 @@ EOF
125140
use_backend be_evidence if is_evidence
126141
EVIDENCE_BLOCK
127142
fi
143+
}
128144

129-
cat <<EOF >>/etc/haproxy/haproxy.cfg
130-
131-
default_backend be_upstream
132-
133-
backend be_upstream
134-
server app1 ${target_hostport}
135-
EOF
136-
145+
# Append the evidence backend block to haproxy.cfg
146+
emit_evidence_backend() {
137147
if [ "$EVIDENCE_SERVER" = "true" ]; then
138148
cat <<EOF >>/etc/haproxy/haproxy.cfg
139149
@@ -145,41 +155,27 @@ EOF
145155
fi
146156
}
147157

148-
# Generate haproxy.cfg for multi-domain mode (ROUTING_MAP)
149-
setup_haproxy_cfg_multi() {
150-
cat <<EOF >/etc/haproxy/haproxy.cfg
151-
global
152-
log stdout format raw local0
153-
maxconn ${MAXCONN}
154-
pidfile /var/run/haproxy/haproxy.pid
155-
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
156-
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
157-
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
158-
ssl-default-bind-curves secp384r1
158+
# Generate haproxy.cfg for single-domain mode (DOMAIN + TARGET_ENDPOINT)
159+
setup_haproxy_cfg() {
160+
local target_hostport
161+
target_hostport=$(parse_target_endpoint "$TARGET_ENDPOINT")
159162

160-
defaults
161-
log global
162-
mode tcp
163-
option tcplog
164-
timeout connect ${TIMEOUT_CONNECT}
165-
timeout client ${TIMEOUT_CLIENT}
166-
timeout server ${TIMEOUT_SERVER}
163+
emit_haproxy_preamble
167164

168-
frontend tls_in
169-
bind :${PORT} ssl crt /etc/haproxy/certs/${ALPN:+ alpn ${ALPN}}
165+
cat <<EOF >>/etc/haproxy/haproxy.cfg
166+
167+
default_backend be_upstream
168+
169+
backend be_upstream
170+
server app1 ${target_hostport}
170171
EOF
171172

172-
if [ "$EVIDENCE_SERVER" = "true" ]; then
173-
cat <<'EVIDENCE_BLOCK' >>/etc/haproxy/haproxy.cfg
173+
emit_evidence_backend
174+
}
174175

175-
# Route /evidences requests to local evidence HTTP server
176-
tcp-request inspect-delay 5s
177-
tcp-request content accept if WAIT_END
178-
acl is_evidence payload(0,0) -m beg "GET /evidences"
179-
acl is_evidence payload(0,0) -m beg "HEAD /evidences"
180-
use_backend be_evidence if is_evidence
181-
EVIDENCE_BLOCK
182-
fi
176+
# Generate haproxy.cfg for multi-domain mode (ROUTING_MAP)
177+
setup_haproxy_cfg_multi() {
178+
emit_haproxy_preamble
183179

184180
# Parse ROUTING_MAP and generate use_backend rules + backend sections
185181
# Support both newline-separated and comma-separated formats
@@ -189,12 +185,13 @@ EVIDENCE_BLOCK
189185
local backend_rules=""
190186
local backend_sections=""
191187
local first_be_name=""
188+
local domain target be_name
192189

193190
while IFS= read -r line; do
194191
[[ -n "$line" ]] || continue
195192
[[ "$line" == \#* ]] && continue
196-
local domain="${line%%=*}"
197-
local target="${line#*=}"
193+
domain="${line%%=*}"
194+
target="${line#*=}"
198195
domain=$(echo "$domain" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
199196
target=$(echo "$target" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
200197
[[ -n "$domain" && -n "$target" ]] || continue
@@ -203,7 +200,7 @@ EVIDENCE_BLOCK
203200
target=$(parse_target_endpoint "$target")
204201

205202
# Generate safe backend name from domain
206-
local be_name="be_$(echo "$domain" | sed 's/[^A-Za-z0-9]/_/g')"
203+
be_name="be_$(echo "$domain" | sed 's/[^A-Za-z0-9]/_/g')"
207204

208205
if [ -z "$first_be_name" ]; then
209206
first_be_name="$be_name"
@@ -227,15 +224,7 @@ backend ${be_name}
227224

228225
echo "$backend_sections" >> /etc/haproxy/haproxy.cfg
229226

230-
if [ "$EVIDENCE_SERVER" = "true" ]; then
231-
cat <<EOF >>/etc/haproxy/haproxy.cfg
232-
233-
backend be_evidence
234-
mode http
235-
http-request replace-path /evidences(.*) \1
236-
server evidence 127.0.0.1:${EVIDENCE_PORT}
237-
EOF
238-
fi
227+
emit_evidence_backend
239228
}
240229

241230
set_alias_record() {

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ sanitize_dns_label() {
7171
fi
7272
}
7373

74+
sanitize_positive_integer() {
75+
local candidate="$1"
76+
local name="${2:-value}"
77+
if [[ "$candidate" =~ ^[0-9]+$ ]] && (( candidate >= 1 )); then
78+
echo "$candidate"
79+
else
80+
echo "Error: Invalid ${name}: $candidate (must be a positive integer)" >&2
81+
return 1
82+
fi
83+
}
84+
85+
sanitize_haproxy_timeout() {
86+
local candidate="$1"
87+
local name="${2:-timeout}"
88+
if [[ "$candidate" =~ ^[0-9]+(us|ms|s|m|h|d)?$ ]]; then
89+
echo "$candidate"
90+
else
91+
echo "Error: Invalid ${name}: $candidate (e.g. 10s, 5m, 86400s)" >&2
92+
return 1
93+
fi
94+
}
95+
7496
sanitize_proxy_timeout() {
7597
local candidate="$1"
7698
if [ -z "$candidate" ]; then

custom-domain/dstack-ingress/scripts/renewal-daemon.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ while true; do
2525
build-combined-pems.sh || echo "Combined PEM build failed"
2626

2727
# Graceful reload: send SIGUSR2 to haproxy master process
28-
if ! kill -USR2 "$(cat /var/run/haproxy/haproxy.pid 2>/dev/null)" 2>/dev/null; then
29-
echo "HAProxy reload failed" >&2
28+
local pidfile="/var/run/haproxy/haproxy.pid"
29+
if [ ! -f "$pidfile" ]; then
30+
echo "HAProxy reload failed: PID file $pidfile not found" >&2
31+
elif ! kill -USR2 "$(cat "$pidfile")"; then
32+
echo "HAProxy reload failed: SIGUSR2 to PID $(cat "$pidfile") failed" >&2
3033
else
3134
echo "Certificate renewed and HAProxy reloaded successfully"
3235
fi

custom-domain/dstack-ingress/scripts/tests/e2e-test.sh

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ BOOT_TIMEOUT="${BOOT_TIMEOUT:-300}"
3636
READY_TIMEOUT="${READY_TIMEOUT:-600}"
3737

3838
# Derived domains for multi-protocol testing
39+
if [[ "$DOMAIN" == \** ]]; then
40+
echo "Error: DOMAIN must not be a wildcard for e2e testing (got $DOMAIN)" >&2
41+
exit 1
42+
fi
3943
GRPC_DOMAIN="grpc-${DOMAIN}"
4044

4145
CVM_NAME="ingress-e2e-$(date +%s)"
@@ -147,7 +151,7 @@ DOMAINS_VAL="${DOMAIN},${GRPC_DOMAIN}"
147151
ROUTING_MAP_VAL="${DOMAIN}=whoami:80,${GRPC_DOMAIN}=grpcbin:9000"
148152

149153
log "Deploying CVM: $CVM_NAME"
150-
phala deploy \
154+
if ! phala deploy \
151155
-c "$COMPOSE_FILE" \
152156
-n "$CVM_NAME" \
153157
-t "$INSTANCE_TYPE" \
@@ -157,7 +161,10 @@ phala deploy \
157161
-e "GATEWAY_DOMAIN=${GATEWAY_DOMAIN}" \
158162
-e "DOMAINS=${DOMAINS_VAL}" \
159163
-e "ROUTING_MAP=${ROUTING_MAP_VAL}" \
160-
--wait
164+
--wait; then
165+
fail "CVM deployment failed"
166+
exit 1
167+
fi
161168

162169
log "CVM deployed, waiting for boot..."
163170

custom-domain/dstack-ingress/scripts/tests/test_sanitizers.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ assert_equal "$(sanitize_proxy_timeout 30s)" "30s" "sanitize_proxy_timeout accep
5555
assert_equal "$(sanitize_proxy_timeout 5m)" "5m" "sanitize_proxy_timeout accepts minutes suffix"
5656
assert_equal "$(sanitize_proxy_timeout 1h)" "1h" "sanitize_proxy_timeout accepts hours suffix"
5757
assert_equal "$(sanitize_proxy_timeout '')" "" "sanitize_proxy_timeout accepts empty value"
58+
assert_equal "$(sanitize_positive_integer 4096 MAXCONN)" "4096" "sanitize_positive_integer accepts 4096"
59+
assert_equal "$(sanitize_positive_integer 1 MAXCONN)" "1" "sanitize_positive_integer accepts 1"
60+
assert_equal "$(sanitize_haproxy_timeout 10s TIMEOUT_CONNECT)" "10s" "sanitize_haproxy_timeout accepts 10s"
61+
assert_equal "$(sanitize_haproxy_timeout 86400s TIMEOUT_CLIENT)" "86400s" "sanitize_haproxy_timeout accepts 86400s"
62+
assert_equal "$(sanitize_haproxy_timeout 5m TIMEOUT)" "5m" "sanitize_haproxy_timeout accepts 5m"
63+
assert_equal "$(sanitize_haproxy_timeout 500ms TIMEOUT)" "500ms" "sanitize_haproxy_timeout accepts 500ms"
64+
assert_equal "$(sanitize_haproxy_timeout 100us TIMEOUT)" "100us" "sanitize_haproxy_timeout accepts 100us"
65+
assert_equal "$(sanitize_haproxy_timeout 1d TIMEOUT)" "1d" "sanitize_haproxy_timeout accepts 1d"
5866

5967
# Failing cases
6068
assert_fails "sanitize_port rejects non-numeric" sanitize_port abc
@@ -89,6 +97,9 @@ else
8997
fi
9098

9199
assert_fails "sanitize_dns_label rejects invalid characters" sanitize_dns_label "bad*label"
100+
assert_fails "sanitize_positive_integer rejects zero" sanitize_positive_integer 0 MAXCONN
101+
assert_fails "sanitize_positive_integer rejects non-numeric" sanitize_positive_integer abc MAXCONN
102+
assert_fails "sanitize_haproxy_timeout rejects bare text" sanitize_haproxy_timeout abc TIMEOUT
92103

93104
if [[ $failures -eq 0 ]]; then
94105
echo "All sanitizer tests passed"

0 commit comments

Comments
 (0)