Goal
Learn bash scripting fundamentals tailored for security workflows and master automating GPG encryption operations in scripts.
Prerequisites: Weeks 1-9 (encryption, GPG, system administration)
This is Part 1 of 2 - Covers scripting fundamentals and GPG automation.
1. Why Automate Security Workflows?
The Problem: Manual Tasks Get Skipped
Security fatigue is real:
- Manual backups → forgotten until disaster strikes
- Key rotation → procrastinated indefinitely
- Log reviews → “I’ll do it tomorrow” (never happens)
- Security updates → Delayed due to inconvenience
Result: Security degrades over time
The Solution: Automation + Encryption
Automated workflows ensure:
- Backups happen daily without thinking
- Logs get reviewed and archived
- Keys rotate on schedule
- Security tasks run consistently
Goal: “Set it and forget it” security that actually works
2. Bash Scripting Fundamentals Refresher
Basic Script Structure
#!/bin/bash
# Description: What this script does
# Author: Your name
# Date: 2025-10-14
# Variables
SOURCE_DIR="/home/user/documents"
BACKUP_DIR="/mnt/backup"
# Functions
function backup_files() {
echo "Starting backup..."
# Backup logic here
}
# Main execution
backup_files
echo "Backup complete!"
Essential Bash Concepts for Security Scripts
Variables and quoting:
FILE="sensitive document.txt" # Space in filename
cp "$FILE" backup/ # MUST quote variable
Error handling:
#!/bin/bash
set -e # Exit on any error
set -u # Exit on undefined variable
set -o pipefail # Exit if any command in pipe fails
# Now script will stop if anything goes wrong
Testing and conditionals:
if [ -f "/path/to/file" ]; then
echo "File exists"
elif [ -d "/path/to/dir" ]; then
echo "Directory exists"
else
echo "Neither exists"
fi
3. GPG in Scripts: Encrypted Workflows
Unattended GPG in scripts (do this safely)
Problem: Scripts can’t enter passphrases interactively.
The wrong fix you’ll see everywhere: cranking default-cache-ttl/max-cache-ttl
to something huge (days or months) so gpg-agent never asks again.
⚠️ Don’t do this. A passphrase cached for weeks is a passphrase that no longer protects anything — anyone with access to your running session can use your key the whole time. You’d be spending effort to defeat the protection you set up in Week 3. The passphrase exists precisely so a stolen key file is useless; a long cache throws that away.
The right fix: don’t make a human passphrase do an automation job. Pick one:
Option A — dedicated automation subkey with no passphrase (most common). Generate a separate signing/encryption subkey used only by scripts, protect the host instead of the key (file permissions, disk encryption, limited account), and keep your real passphrase-protected key elsewhere:
# Add an automation subkey to a key you control, then export ONLY that subkey
gpg --quick-add-key <FINGERPRINT> ed25519 sign 1y # short expiry, rotate it
gpg --export-secret-subkeys <SUBKEY>! > automation-subkey.gpg
# Move it to the automation host; protect the host, rotate on schedule.
Option B — loopback pinentry, passphrase from a secrets manager (never hardcoded).
The passphrase still exists; it’s injected at runtime from pass, gopass, a
systemd credential, or a vault — not cached for days, not written in the script:
PASSPHRASE="$(pass show gpg/automation)" # fetched at runtime, never stored in the script
echo "$PASSPHRASE" | gpg --batch --yes --pinentry-mode loopback \
--passphrase-fd 0 --decrypt secret.gpg > plaintext
unset PASSPHRASE
Option C — airgapped/offline signing. For high-value keys, scripts prepare the data and a human signs on an offline machine (see Week 9). Slowest, safest.
If you must use a short agent cache for an interactive session, keep it short (minutes), and understand it only covers that session:
# Short, session-scoped convenience — NOT for unattended 24/7 scripts
echo "default-cache-ttl 600" >> ~/.gnupg/gpg-agent.conf # 10 minutes
echo "max-cache-ttl 7200" >> ~/.gnupg/gpg-agent.conf # 2 hours hard cap
gpgconf --kill gpg-agent
Rule of thumb: the passphrase guards the key; automation guards the host. Don’t swap those jobs.
Encrypting Files in Scripts
#!/bin/bash
# encrypt-file.sh - Encrypt a file with GPG
FILE="$1"
RECIPIENT="[email protected]"
if [ ! -f "$FILE" ]; then
echo "Error: File $FILE not found"
exit 1
fi
# Encrypt and sign
gpg --encrypt --sign --recipient "$RECIPIENT" --output "${FILE}.gpg" "$FILE"
# Securely delete original
shred -u "$FILE"
echo "Encrypted: ${FILE}.gpg"
Decrypting Files in Scripts
#!/bin/bash
# decrypt-file.sh - Decrypt GPG file
GPG_FILE="$1"
if [ ! -f "$GPG_FILE" ]; then
echo "Error: File $GPG_FILE not found"
exit 1
fi
# Decrypt (gpg-agent handles passphrase)
gpg --decrypt --output "${GPG_FILE%.gpg}" "$GPG_FILE"
echo "Decrypted: ${GPG_FILE%.gpg}"
4. Automated Encrypted Backups
Simple Encrypted Backup Script
#!/bin/bash
# encrypted-backup.sh - Daily encrypted backup
set -euo pipefail # Exit on errors
# Configuration
SOURCE="/home/$USER/documents"
BACKUP_BASE="/mnt/backup"
DATE=$(date +%Y-%m-%d)
BACKUP_DIR="${BACKUP_BASE}/${DATE}"
GPG_RECIPIENT="[email protected]"
LOG_FILE="/var/log/encrypted-backup.log"
# Logging function
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# Create backup directory
mkdir -p "$BACKUP_DIR"
log "Starting backup to $BACKUP_DIR"
# Tar + GPG encryption pipeline
tar -czf - "$SOURCE" | \
gpg --encrypt --recipient "$GPG_RECIPIENT" \
--output "${BACKUP_DIR}/backup-${DATE}.tar.gz.gpg"
# Verify backup was created
if [ -f "${BACKUP_DIR}/backup-${DATE}.tar.gz.gpg" ]; then
SIZE=$(du -h "${BACKUP_DIR}/backup-${DATE}.tar.gz.gpg" | cut -f1)
log "Backup successful: ${SIZE}"
else
log "ERROR: Backup failed!"
exit 1
fi
# Clean up old backups (keep last 7 days)
find "$BACKUP_BASE" -type d -mtime +7 -exec rm -rf {} \;
log "Old backups cleaned up"
Incremental Encrypted Backups with rsync
#!/bin/bash
# rsync-encrypted-backup.sh - Incremental backups
set -euo pipefail
SOURCE="/home/$USER/documents"
BACKUP_DIR="/mnt/backup/current"
SNAPSHOT_DIR="/mnt/backup/snapshots/$(date +%Y-%m-%d)"
# Rsync with hard links for space efficiency
rsync -av --delete \
--link-dest="$BACKUP_DIR" \
"$SOURCE/" "$SNAPSHOT_DIR/"
# Encrypt the snapshot
tar -czf - "$SNAPSHOT_DIR" | \
gpg --encrypt --recipient "[email protected]" \
--output "${SNAPSHOT_DIR}.tar.gz.gpg"
# Remove unencrypted snapshot
rm -rf "$SNAPSHOT_DIR"
echo "Incremental backup complete: ${SNAPSHOT_DIR}.tar.gz.gpg"
5. Encrypted Logging and Audit Trails
Creating Encrypted Log Files
#!/bin/bash
# log-encrypted.sh - Append to encrypted log
LOG_FILE="/var/log/sensitive.log.gpg"
TEMP_LOG="/tmp/log-temp-$$"
GPG_RECIPIENT="[email protected]"
# If encrypted log exists, decrypt it
if [ -f "$LOG_FILE" ]; then
gpg --decrypt "$LOG_FILE" > "$TEMP_LOG" 2>/dev/null
fi
# Append new log entry
echo "[$(date)] $1" >> "$TEMP_LOG"
# Re-encrypt
gpg --encrypt --recipient "$GPG_RECIPIENT" \
--output "$LOG_FILE" "$TEMP_LOG"
# Securely delete temp file
shred -u "$TEMP_LOG"
Usage:
./log-encrypted.sh "User logged in from 192.168.1.100"
./log-encrypted.sh "Backup completed successfully"
Rotating Encrypted Logs
#!/bin/bash
# rotate-logs.sh - Rotate and archive encrypted logs
LOG_FILE="/var/log/sensitive.log.gpg"
ARCHIVE_DIR="/var/log/archive"
DATE=$(date +%Y-%m)
# Create archive directory
mkdir -p "$ARCHIVE_DIR"
# Move current log to archive
if [ -f "$LOG_FILE" ]; then
mv "$LOG_FILE" "${ARCHIVE_DIR}/sensitive-${DATE}.log.gpg"
echo "Log rotated: ${ARCHIVE_DIR}/sensitive-${DATE}.log.gpg"
fi
# Clean up logs older than 90 days
find "$ARCHIVE_DIR" -name "*.log.gpg" -mtime +90 -delete
6. Automated Key Management
Key Expiration Reminder Script
#!/bin/bash
# check-key-expiration.sh - Warn about expiring GPG keys
DAYS_WARNING=30
EMAIL="[email protected]"
# Get key expiration dates
gpg --list-keys --with-colons "$EMAIL" | \
grep ^pub | \
while IFS=: read -r type trust length algo keyid date expires rest; do
if [ -n "$expires" ]; then
EXPIRE_DATE=$(date -d "@$expires" +%Y-%m-%d)
DAYS_UNTIL=$(( (expires - $(date +%s)) / 86400 ))
if [ $DAYS_UNTIL -lt $DAYS_WARNING ]; then
echo "WARNING: Key $keyid expires in $DAYS_UNTIL days ($EXPIRE_DATE)"
# Send email alert
echo "Your GPG key expires soon!" | mail -s "Key Expiration Warning" "$EMAIL"
fi
fi
done
Automated Subkey Rotation
#!/bin/bash
# rotate-subkey.sh - Generate new encryption subkey
set -euo pipefail
KEY_ID="[email protected]"
EXPIRE_DATE="+1y" # New key valid for 1 year
echo "Generating new encryption subkey..."
# Generate new subkey (non-interactive)
gpg --quick-add-key "$KEY_ID" rsa4096 encr "$EXPIRE_DATE"
# Export updated public key
gpg --armor --export "$KEY_ID" > pubkey-$(date +%Y-%m-%d).asc
# Backup secret keys (encrypted)
gpg --armor --export-secret-keys "$KEY_ID" | \
gpg --encrypt --recipient "$KEY_ID" \
--output "secret-backup-$(date +%Y-%m-%d).gpg"
echo "Subkey rotation complete"
echo "Upload new public key: pubkey-$(date +%Y-%m-%d).asc"
7. Error Handling Best Practices
#!/bin/bash
set -euo pipefail # Exit on error
# Trap errors and clean up
trap 'echo "Error on line $LINENO"; exit 1' ERR
# Check prerequisites
command -v gpg >/dev/null 2>&1 || { echo "GPG not installed"; exit 1; }
# Validate inputs
if [ -z "$SOURCE" ]; then
echo "Error: SOURCE variable not set"
exit 1
fi
# Your script logic here
Up Next
Week 10b covers cron scheduling, security maintenance automation, and building your complete security automation suite.
Key Takeaways
- Automation prevents security fatigue - Consistent execution beats manual effort
- gpg-agent enables scripted encryption - Cache passphrase for automation
- Always use
set -euo pipefail- Scripts should fail loudly on errors - Encrypt your logs too - Audit trails are sensitive data
- Automate key management - Expiration reminders and rotation
- Test scripts manually first - Debug before scheduling