Skip to content

Commit 3fe378a

Browse files
owineclaudehappy-otter
committed
refactor(deployment): remove unnecessary env var escaping and document best practices
**Changes:** 1. **health-check.sh**: Removed unnecessary manual bracket escaping for CRITICAL_SERVICES - Environment variables don't undergo shell parsing or glob expansion - Manual escaping with ${VAR//\[/\\[} was unnecessary and fragile - Now uses raw $CRITICAL_SERVICES passed via env, preserving valid JSON 2. **IMPLEMENTATION_GUIDE.md**: Added comprehensive "Shell Escaping Best Practices" section - When to use `printf '%q'` (positional arguments only) - When escaping is NOT needed (environment variables) - SSH command quoting strategies (single vs double quotes) - Deprecated manual bracket escaping patterns - Complete examples and quick reference table - Common pitfalls to avoid **Rationale:** Environment variables passed via `env VAR="value"` to SSH don't undergo shell parsing or glob expansion, so manual escaping is unnecessary. Only positional arguments passed to remote scripts need escaping via `printf '%q'`. **Related:** Fixes similar to detect-removed-stacks.sh glob expansion bug Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <[email protected]> Co-Authored-By: Happy <[email protected]>
1 parent 31c8b53 commit 3fe378a

File tree

2 files changed

+151
-5
lines changed

2 files changed

+151
-5
lines changed

scripts/deployment/IMPLEMENTATION_GUIDE.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,152 @@ SSH_USER=test SSH_HOST=test ./scripts/deployment/health-check.sh \
297297
- Calling repositories require no changes
298298
- Behavior identical to current implementation
299299

300+
### Shell Escaping Best Practices
301+
302+
**Critical**: Understand when and how to escape arguments to prevent shell glob expansion and injection attacks.
303+
304+
#### When to Use `printf '%q'` (Positional Arguments)
305+
306+
Use `printf '%q'` for **positional arguments** passed to remote scripts:
307+
308+
```bash
309+
# Correct: Escape positional arguments
310+
STACKS_ESCAPED=$(printf '%q ' "${STACKS[@]}")
311+
HAS_DOCKGE_ESCAPED=$(printf '%q' "$HAS_DOCKGE")
312+
313+
# Pass escaped values as positional arguments
314+
ssh_retry 3 5 "ssh $SSH_USER@$SSH_HOST /bin/bash -s $STACKS_ESCAPED $HAS_DOCKGE_ESCAPED" << 'EOF'
315+
# Remote script receives escaped arguments
316+
STACKS="$1"
317+
HAS_DOCKGE="$2"
318+
EOF
319+
```
320+
321+
**Why**: Positional arguments undergo shell parsing and glob expansion. Without escaping, characters like `[]`, `*`, `?` are interpreted as glob patterns.
322+
323+
#### When Escaping is NOT Needed (Environment Variables)
324+
325+
**DO NOT** escape variables passed via `env` or environment variable assignment:
326+
327+
```bash
328+
# Correct: No escaping needed for environment variables
329+
ssh_retry 3 5 "ssh $SSH_USER@$SSH_HOST env CRITICAL_SERVICES=\"$CRITICAL_SERVICES\" /bin/bash -s" << 'EOF'
330+
# Remote script receives raw JSON value
331+
echo "$CRITICAL_SERVICES" | jq '.[]'
332+
EOF
333+
```
334+
335+
**Why**: Environment variables don't undergo shell parsing or glob expansion. The value is passed directly to the remote shell environment. Values must remain valid (e.g., valid JSON) for the remote script to parse.
336+
337+
#### SSH Command Quoting Strategies
338+
339+
**Single vs Double Quotes**: Choose based on where variable expansion should occur.
340+
341+
```bash
342+
# Pattern 1: Double quotes (expansion on local shell)
343+
ssh_retry 3 5 "ssh $SSH_USER@$SSH_HOST /bin/bash -s \"$arg\""
344+
# Variables expand locally before SSH transmission
345+
346+
# Pattern 2: Single quotes (no expansion on local shell)
347+
ssh_retry 3 5 'ssh '"$SSH_USER"'@'"$SSH_HOST"' /bin/bash -s -- "'"$arg"'"'
348+
# Prevents local glob expansion, variable safely expanded within single-quoted segments
349+
```
350+
351+
**Use Pattern 2** when passing JSON arrays or values with glob characters (`[]`, `*`):
352+
353+
```bash
354+
# Correct: Prevents glob expansion of JSON array
355+
DELETED_FILES='["stack1/compose.yaml", "stack2/compose.yaml"]'
356+
ssh_retry 3 5 'ssh -o "StrictHostKeyChecking no" '"$SSH_USER"'@'"$SSH_HOST"' /bin/bash -s -- "'"$DELETED_FILES"'"'
357+
```
358+
359+
**Incorrect**: Double quotes allow glob expansion on local shell:
360+
361+
```bash
362+
# WRONG: Brackets will be interpreted as glob patterns
363+
DELETED_FILES='["stack1/compose.yaml"]'
364+
ssh_retry 3 5 "ssh $SSH_USER@$SSH_HOST /bin/bash -s \"$DELETED_FILES\""
365+
# Error: zsh:1: no matches found: [stack1/compose.yaml]
366+
```
367+
368+
#### Manual Bracket Escaping (Deprecated)
369+
370+
**AVOID** manual bracket escaping like `${VAR//\[/\\[}`:
371+
372+
```bash
373+
# WRONG: Manual escaping is error-prone and usually unnecessary
374+
ESCAPED_VAR="${VAR//\[/\\[}"
375+
ESCAPED_VAR="${ESCAPED_VAR//\]/\\]}"
376+
```
377+
378+
**Why**:
379+
- Environment variables don't need escaping
380+
- Positional arguments should use `printf '%q'`
381+
- Manual escaping is fragile and hard to maintain
382+
383+
#### Complete Examples
384+
385+
**Example 1: Passing JSON to Remote Script (Environment Variable)**
386+
387+
```bash
388+
# JSON value to pass (contains brackets)
389+
CRITICAL_SERVICES='["dockge", "traefik"]'
390+
391+
# Correct: Use environment variable (no escaping needed)
392+
ssh_retry 3 5 "ssh $SSH_USER@$SSH_HOST env CRITICAL_SERVICES=\"$CRITICAL_SERVICES\" /bin/bash -s" << 'EOF'
393+
# Remote script parses JSON directly
394+
echo "$CRITICAL_SERVICES" | jq -r '.[]'
395+
EOF
396+
```
397+
398+
**Example 2: Passing Stack List (Positional Arguments)**
399+
400+
```bash
401+
# Array of stack names
402+
STACKS=("stack1" "stack2" "stack-with-special-chars")
403+
404+
# Correct: Escape for positional arguments
405+
STACKS_ESCAPED=$(printf '%q ' "${STACKS[@]}")
406+
407+
# Pass as positional arguments
408+
ssh_retry 3 5 "ssh $SSH_USER@$SSH_HOST /bin/bash -s $STACKS_ESCAPED" << 'EOF'
409+
# Remote script receives escaped values as positional arguments
410+
for stack in "$@"; do
411+
echo "Processing: $stack"
412+
done
413+
EOF
414+
```
415+
416+
**Example 3: Passing JSON Array (Positional Argument)**
417+
418+
```bash
419+
# JSON array with glob characters
420+
DELETED_FILES='["stack1/compose.yaml", "stack2/compose.yaml"]'
421+
422+
# Correct: Single quotes prevent local glob expansion
423+
ssh_retry 3 5 'ssh '"$SSH_USER"'@'"$SSH_HOST"' /bin/bash -s -- "'"$DELETED_FILES"'"' << 'EOF'
424+
DELETED_FILES_JSON="$1"
425+
# Remote script parses JSON
426+
echo "$DELETED_FILES_JSON" | jq -r '.[]'
427+
EOF
428+
```
429+
430+
#### Quick Reference
431+
432+
| Scenario | Method | Example |
433+
|----------|--------|---------|
434+
| Positional arguments | `printf '%q'` | `$(printf '%q ' "${STACKS[@]}")` |
435+
| Environment variables | No escaping | `env VAR="$VALUE"` |
436+
| JSON in positional arg | Single quote pattern | `'ssh ... -s -- "'"$JSON"'"'` |
437+
| Simple strings | Double quotes | `"ssh $USER@$HOST /bin/bash -s \"$arg\""` |
438+
439+
#### Common Pitfalls
440+
441+
1. **Escaping environment variables**: Unnecessary and can break JSON parsing
442+
2. **Using double quotes for JSON**: Allows glob expansion on local shell
443+
3. **Manual bracket escaping**: Fragile, use `printf '%q'` or environment variables instead
444+
4. **Mixing quoting styles**: Be consistent with single/double quote patterns
445+
300446
---
301447

302448
## Final Impact

scripts/deployment/health-check.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,15 @@ log_info "Starting health check for stacks: $STACKS"
7474
log_info "Health check timeout: ${HEALTH_TIMEOUT}s, Command timeout: ${COMMAND_TIMEOUT}s"
7575

7676
# Execute health check via SSH with retry
77-
# Escape CRITICAL_SERVICES to prevent glob expansion on remote shell
78-
ESCAPED_CRITICAL_SERVICES="${CRITICAL_SERVICES//\[/\\[}"
79-
ESCAPED_CRITICAL_SERVICES="${ESCAPED_CRITICAL_SERVICES//\]/\\]}"
80-
8177
# Use printf %q to properly escape arguments for eval in ssh_retry
8278
# shellcheck disable=SC2086 # Word splitting is intentional for printf to process each stack
8379
STACKS_ESCAPED=$(printf '%q ' $STACKS)
8480
HAS_DOCKGE_ESCAPED=$(printf '%q' "$HAS_DOCKGE")
8581

82+
# Note: CRITICAL_SERVICES is passed via environment variable (not positional argument)
83+
# Environment variables don't undergo shell parsing or glob expansion, so no escaping needed
84+
# The value must remain valid JSON for jq parsing in the remote script
85+
8686
# Pass OP_TOKEN as positional argument (more secure than env vars in process list)
8787
# Token passed as $1, appears in SSH command locally but not in remote ps output
8888
set +e
@@ -559,7 +559,7 @@ HEALTH_RESULT=$({
559559
exit 0
560560
fi
561561
EOF
562-
} | ssh_retry 3 5 "ssh $SSH_USER@$SSH_HOST env HEALTH_TIMEOUT=\"$HEALTH_TIMEOUT\" COMMAND_TIMEOUT=\"$COMMAND_TIMEOUT\" CRITICAL_SERVICES=\"$ESCAPED_CRITICAL_SERVICES\" /bin/bash -s \"$OP_TOKEN\" $STACKS_ESCAPED $HAS_DOCKGE_ESCAPED")
562+
} | ssh_retry 3 5 "ssh $SSH_USER@$SSH_HOST env HEALTH_TIMEOUT=\"$HEALTH_TIMEOUT\" COMMAND_TIMEOUT=\"$COMMAND_TIMEOUT\" CRITICAL_SERVICES=\"$CRITICAL_SERVICES\" /bin/bash -s \"$OP_TOKEN\" $STACKS_ESCAPED $HAS_DOCKGE_ESCAPED")
563563
HEALTH_EXIT_CODE=$?
564564
set -e
565565

0 commit comments

Comments
 (0)