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