HackBlue

Tools · Python and Bash for security automation · 11 / 20

Safer Bash scripts: set -euo pipefail, traps, and temporary files

Make Bash scripts fail loudly instead of continuing after errors, clean up with traps, create temporary files safely with mktemp, and log what the script does.

9 min read · practice only with authorization

Strict mode

By default, Bash keeps going after a failed command, treats unset variables as empty strings, and reports only the last command’s status in a pipeline. In security scripts, that can mean deleting the wrong directory or reporting success when a collection step failed. set -e exits on errors, set -u fails on unset variables, and set -o pipefail makes a pipeline fail if any part fails.

Snippet
#!/usr/bin/env bash
set -euo pipefail
IFS=
#x27;\n\t' OUT_DIR="${1:?usage: $0 OUTPUT_DIR}" log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >&2; }

Traps and temp files

mktemp creates files and directories with unpredictable names and safe permissions, avoiding races in shared /tmp. A trap on EXIT removes them whether the script succeeds or fails. A trap on ERR can print the failing line number.

Snippet
WORK=$(mktemp -d)
trap 'rm -rf -- "$WORK"' EXIT
trap 'log "error on line $LINENO"' ERR

log "collecting into $WORK"
ss -tanp > "$WORK/ss.txt"
journalctl -u ssh --since '-1 day' --no-pager > "$WORK/ssh.log"
tar -czf "$OUT_DIR/collect-$(hostname)-$(date -u +%Y%m%dT%H%M%SZ).tgz" -C "$WORK" .
log done

Lab: watch strict mode save you

Write a script that runs rm -rf "$TARGET/"* where TARGET is misspelled as TARGT. Without set -u, the expansion becomes /* in a test container; do this only inside a throwaway container. With set -u, the script stops with “unbound variable” before rm runs. Then make a pipeline with a failing first command, such as false | sort, and compare $? with and without pipefail.

Snippet
docker run --rm -it debian:stable-slim bash
# inside the container only:
cat > t.sh <<'EOF'
set -u
TARGET=/tmp/demo
rm -rf "$TARGT/"*
EOF
bash t.sh; echo exit=$?
bash -c 'false | sort; echo no-pipefail=$?'
bash -c 'set -o pipefail; false | sort; echo pipefail=$?'

Caveats

set -e has surprising exceptions: commands in if conditions, parts of && and || lists, and commands in some subshells do not trigger it. Check critical commands explicitly. For anything long or complex, consider Python, which has clearer error handling.

Diagram

Strict mode pieces

Each one closes a specific trap.

  • set -e

    Exit when a command fails.

  • set -u

    Fail on unset variables.

  • set -o pipefail

    Pipeline fails if any stage fails.

  • mktemp + trap EXIT

    Safe temp files, always cleaned up.

  • ${1:?message}

    Required arguments with a clear error.

Check yourself

1. Which option makes Bash stop when a variable is used before being set?

2. Without pipefail, what is the exit status of false | sort?

3. How should a script create a temporary directory?