Guide to securing Bash scripts in DevOps: secrets management, input validation, shellcheck, permission control, injection prevention, and security best practices.
In the previous post, we learned how to manage cloud with CLI tools. Now, we’ll discuss security — a critical topic often overlooked when writing Bash scripts.
Bash scripts run automatically everywhere — from CI/CD pipelines to cron jobs on servers. But without proper security, they can become major vulnerabilities: leaking API keys, enabling injection attacks, or being exploited to gain system access.
This post will guide you through managing secrets, validating input, setting permissions correctly, and scanning scripts with shellcheck to build secure Bash scripts in DevOps.
Why secure scripts?
Common risks
Risk
Description
Impact
Hardcoded secrets
API keys, passwords in code
Leaked when sharing/repo
Command injection
Unvalidated input executed directly
Server compromised
Insecure permissions
World-executable scripts
Anyone can run them
Unquoted variables
$var instead of "$var"
Unexpected behavior
Best practices overview
Never hardcode secrets — use env vars or vault
Always quote variables — "$var" instead of $var
Validate input — check format before using
Set restrictive permissions — chmod 700 for scripts
Scan with shellcheck — catch potential bugs
Secrets management
Never hardcode secrets
1
2
3
4
5
6
7
8
# BAD — never do thisAPI_KEY="sk-1234567890abcdef"DB_PASSWORD="mysecretpassword"./deploy.sh "$API_KEY"# GOOD — use environment variablesexportAPI_KEY="${API_KEY:?API_KEY is required}"./deploy.sh "$API_KEY"
# Get secret from Vaultget_vault_secret(){localpath="$1"localkey="$2"local secret
secret=$(vault kv get -field="$key""$path" 2>/dev/null)if[[ -z "$secret"]];thenecho"ERROR: Failed to get secret from $path/$key" >&2return1fiecho"$secret"}# UsageDB_PASS=$(get_vault_secret "secret/data/database""password")
# When to quote?# Always quote variable expansion to prevent word splitting and globbing# BAD — unquotedfile=$1# If $1 has spaces → breakrm $file# Dangerous!echo$var# Can be globbed *# GOOD — quotedfile="$1"rm "$file"echo"$var"# Real-world exampleprocess_file(){localfilepath="$1"# Always quote parameter expansionif[[ ! -f "$filepath"]];thenecho"File not found: $filepath" >&2return1fi# Command substitution should also be quotedlocal line_count
line_count=$(wc -l < "$filepath")echo"File has $line_count lines"}
When NOT to quote?
1
2
3
4
5
6
7
8
9
10
# When intentionally wanting word splitting# Example: iterate through listfor file in *.log;doecho"Processing: $file"done# When using [[ ]] (bash-specific, safer than [ ])if[[$var=="pattern"]];thenecho"Matched"fi
# SC2086: Double quote to prevent globbing and word splitting# BADecho$1# GOODecho"$1"# SC2046: Quote this to prevent word splitting# BADcd$(dirname $0)# GOODcd"$(dirname "$0")"# SC2034: Variable appears unused# WARNING — may be typo or forgotten to use# SC2155: Declare and assign separately# BADlocaloutput=$(command)# GOODlocal output
output=$(command)
#!/usr/bin/env bash
# ==============================================================================# Script : secure-deploy.sh# Purpose: Deploy application with security best practices# ==============================================================================set -euo pipefail
# ===== SECURITY CHECKS =====# Check script permissionsSCRIPT_PERMS=$(stat -f "%Lp""$0" 2>/dev/null || stat -c "%a""$0" 2>/dev/null)if[["$SCRIPT_PERMS" !="700"&&"$SCRIPT_PERMS" !="755"]];thenecho"WARNING: Script has loose permissions ($SCRIPT_PERMS). Recommended: 700" >&2fi# ===== ENVIRONMENT SETUP =====# Load secrets from .envif[[ -f ".env"]];thenset -a
source .env
set +a
elseecho"ERROR: .env file not found" >&2exit1fi# Validate required variablesfor var in APP_NAME DEPLOY_ENV SSH_HOST SSH_KEY;do : "${!var:?ERROR: $var is not set in environment or .env}"done# Validate SSH key fileif[[ ! -f "$SSH_KEY"]];thenecho"ERROR: SSH key not found: $SSH_KEY" >&2exit1fiSSH_KEY_PERMS=$(stat -f "%Lp""$SSH_KEY" 2>/dev/null || stat -c "%a""$SSH_KEY" 2>/dev/null)if[["$SSH_KEY_PERMS" !="600"]];thenecho"ERROR: SSH key has insecure permissions ($SSH_KEY_PERMS). Expected: 600" >&2echo"Fix with: chmod 600 $SSH_KEY" >&2exit1fi# ===== INPUT VALIDATION =====DEPLOY_VERSION="${1:-}"if[[ -z "$DEPLOY_VERSION"]];thenecho"Usage: $0 <version>" >&2echo"Example: $0 1.2.3" >&2exit1fi# Validate version format (semver-ish)if[[ ! "$DEPLOY_VERSION"=~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]];thenecho"ERROR: Invalid version format: $DEPLOY_VERSION (expected X.Y.Z)" >&2exit1fi# ===== DEPLOYMENT =====log(){echo"[$(date '+%Y-%m-%d %H:%M:%S')] $1"}log "Starting deployment of $APP_NAME v$DEPLOY_VERSION to $DEPLOY_ENV"# Deploy via SSHssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "deploy@${SSH_HOST}"\
"cd /opt/$APP_NAME && git pull && git checkout v$DEPLOY_VERSION && systemctl restart $APP_NAME"log "Deployment completed successfully"
Implementation notes
Secrets management: In production, don’t use .env files — use dedicated secrets managers (Vault, AWS Secrets Manager, GCP Secret Manager). .env is only suitable for development.
Audit logging: Log all configuration changes and secrets access. Meet compliance requirements (SOC 2, PCI DSS).
Secret rotation: Rotate secrets periodically. Scripts should support reloading secrets without restart.
Minimal privileges: Create dedicated service accounts with minimal permissions for each script. Don’t use root unless necessary.
Code review: Always do security-focused reviews when reviewing Bash scripts. Check for hardcoded secrets, missing validation, and insecure patterns.
Conclusion
Bash and Security is a critical topic in DevOps. From securely managing secrets, validating input, quoting variables, to scanning with shellcheck — each step helps reduce security risks.
A secure script not only protects sensitive data but also prevents injection attacks and ensures system stability.
In the next post, we’ll wrap up the series with Bash Best Practices: shebang, set -euo pipefail, modular functions, logging, and testing with bats.