Session Recording and Audit
Session Recording and Audit
Comprehensive session recording provides forensic capabilities and helps meet compliance requirements. Modern bastion hosts must capture not just commands but full session interactions for security analysis.
Implement advanced session recording:
#!/bin/bash
# bastion-session-recorder.sh
# Advanced session recording system
# Session recording configuration
RECORDING_BASE="/var/log/bastion-recordings"
RETENTION_DAYS=90
COMPRESSION_DAYS=7
# Initialize recording system
init_recording_system() {
# Create directory structure
mkdir -p "$RECORDING_BASE"/{active,completed,compressed,indexed}
# Install required tools
apt-get install -y asciinema ttyrec screen jq
# Create recording wrapper script
cat > /usr/local/bin/record-session << 'EOF'
#!/bin/bash
# Session recording wrapper
SESSION_ID="${USER}-$(date +%s)-$$"
RECORDING_DIR="$RECORDING_BASE/active/$SESSION_ID"
mkdir -p "$RECORDING_DIR"
# Record session metadata
cat > "$RECORDING_DIR/metadata.json" << METADATA
{
"session_id": "$SESSION_ID",
"user": "$USER",
"real_user": "${SUDO_USER:-$USER}",
"client_ip": "${SSH_CLIENT%% *}",
"start_time": "$(date -Iseconds)",
"command": "${SSH_ORIGINAL_COMMAND:-interactive}",
"tty": "$(tty)",
"environment": $(env | jq -R . | jq -s .)
}
METADATA
# Start recording based on session type
if [ -n "$SSH_ORIGINAL_COMMAND" ]; then
# Non-interactive command
script -q -f -c "$SSH_ORIGINAL_COMMAND" \
"$RECORDING_DIR/typescript" \
-t "$RECORDING_DIR/timing" 2>/dev/null
EXIT_CODE=$?
else
# Interactive session - use asciinema for better playback
asciinema rec \
--quiet \
--append \
--title "Bastion Session $SESSION_ID" \
--command "${SHELL:-/bin/bash}" \
"$RECORDING_DIR/session.cast"
EXIT_CODE=$?
fi
# Finalize recording
echo "$EXIT_CODE" > "$RECORDING_DIR/exit_code"
echo "$(date -Iseconds)" > "$RECORDING_DIR/end_time"
# Move to completed
mv "$RECORDING_DIR" "$RECORDING_BASE/completed/"
# Trigger post-processing
/usr/local/bin/process-recording "$SESSION_ID" &
exit $EXIT_CODE
EOF
chmod +x /usr/local/bin/record-session
}
# Process completed recordings
create_processing_script() {
cat > /usr/local/bin/process-recording << 'EOF'
#!/bin/bash
# Post-process session recordings
SESSION_ID=$1
RECORDING_DIR="$RECORDING_BASE/completed/$SESSION_ID"
# Extract text content for indexing
if [ -f "$RECORDING_DIR/typescript" ]; then
# Clean typescript file
cat "$RECORDING_DIR/typescript" | \
sed 's/\x1b\[[0-9;]*m//g' | \
sed 's/\r$//' > "$RECORDING_DIR/session_text.txt"
elif [ -f "$RECORDING_DIR/session.cast" ]; then
# Extract from asciinema
asciinema cat "$RECORDING_DIR/session.cast" | \
sed 's/\x1b\[[0-9;]*m//g' > "$RECORDING_DIR/session_text.txt"
fi
# Extract commands for analysis
grep -E '^\$ |^# |^> ' "$RECORDING_DIR/session_text.txt" > \
"$RECORDING_DIR/commands.txt" || true
# Scan for sensitive data
/usr/local/bin/scan-sensitive-data "$RECORDING_DIR/session_text.txt" > \
"$RECORDING_DIR/sensitive_data_scan.json"
# Generate searchable index
jq -n \
--arg id "$SESSION_ID" \
--arg user "$(jq -r .user "$RECORDING_DIR/metadata.json")" \
--arg start "$(jq -r .start_time "$RECORDING_DIR/metadata.json")" \
--arg commands "$(wc -l < "$RECORDING_DIR/commands.txt")" \
--arg duration "$(($(date -d "$(cat "$RECORDING_DIR/end_time")" +%s) - \
$(date -d "$(jq -r .start_time "$RECORDING_DIR/metadata.json")" +%s)))" \
'{
session_id: $id,
user: $user,
start_time: $start,
duration: $duration,
command_count: $commands,
indexed_at: now | todate
}' > "$RECORDING_BASE/indexed/${SESSION_ID}.json"
# Compress if older than threshold
if [ $COMPRESSION_DAYS -gt 0 ]; then
find "$RECORDING_BASE/completed" -name "$SESSION_ID" -type d -mtime +$COMPRESSION_DAYS -exec \
tar -czf "$RECORDING_BASE/compressed/${SESSION_ID}.tar.gz" {} \; -exec \
rm -rf {} \;
fi
EOF
chmod +x /usr/local/bin/process-recording
}
# Sensitive data scanner
create_sensitive_scanner() {
cat > /usr/local/bin/scan-sensitive-data << 'EOF'
#!/usr/bin/env python3
import re
import sys
import json
patterns = {
'aws_access_key': r'AKIA[0-9A-Z]{16}',
'aws_secret_key': r'[0-9a-zA-Z/+=]{40}',
'private_key': r'-----BEGIN (RSA |EC |)PRIVATE KEY-----',
'api_token': r'[a-zA-Z0-9]{32,}',
'password': r'(password|passwd|pwd)\s*[:=]\s*\S+',
'credit_card': r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b',
'ssn': r'\b\d{3}-\d{2}-\d{4}\b',
'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
}
def scan_file(filepath):
findings = []
with open(filepath, 'r', errors='ignore') as f:
content = f.read()
for pattern_name, pattern in patterns.items():
matches = re.finditer(pattern, content, re.IGNORECASE)
for match in matches:
# Get line number
line_num = content[:match.start()].count('\n') + 1
findings.append({
'type': pattern_name,
'line': line_num,
'position': match.start(),
'matched': match.group()[:20] + '...' if len(match.group()) > 20 else match.group()
})
return findings
if __name__ == '__main__':
if len(sys.argv) != 2:
print("Usage: scan-sensitive-data <file>")
sys.exit(1)
findings = scan_file(sys.argv[1])
print(json.dumps({
'scan_complete': True,
'findings_count': len(findings),
'findings': findings
}, indent=2))
EOF
chmod +x /usr/local/bin/scan-sensitive-data
}
# Session playback tool
create_playback_tool() {
cat > /usr/local/bin/bastion-playback << 'EOF'
#!/bin/bash
# Play back recorded sessions
usage() {
echo "Usage: bastion-playback [options] SESSION_ID"
echo "Options:"
echo " -s, --speed SPEED Playback speed multiplier (default: 1.0)"
echo " -f, --from TIME Start playback from TIME seconds"
echo " -t, --to TIME Stop playback at TIME seconds"
echo " -i, --info Show session information only"
echo " -h, --help Show this help"
}
# Default values
SPEED=1.0
FROM=0
TO=""
INFO_ONLY=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-s|--speed)
SPEED="$2"
shift 2
;;
-f|--from)
FROM="$2"
shift 2
;;
-t|--to)
TO="$2"
shift 2
;;
-i|--info)
INFO_ONLY=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
SESSION_ID="$1"
shift
;;
esac
done
if [ -z "$SESSION_ID" ]; then
usage
exit 1
fi
# Find recording
RECORDING_DIR=""
for dir in "$RECORDING_BASE"/{completed,compressed}; do
if [ -d "$dir/$SESSION_ID" ]; then
RECORDING_DIR="$dir/$SESSION_ID"
break
elif [ -f "$dir/${SESSION_ID}.tar.gz" ]; then
# Extract compressed recording
TEMP_DIR=$(mktemp -d)
tar -xzf "$dir/${SESSION_ID}.tar.gz" -C "$TEMP_DIR"
RECORDING_DIR="$TEMP_DIR/$SESSION_ID"
CLEANUP_TEMP=true
break
fi
done
if [ -z "$RECORDING_DIR" ] || [ ! -d "$RECORDING_DIR" ]; then
echo "Recording not found: $SESSION_ID"
exit 1
fi
# Show session info
if [ "$INFO_ONLY" = true ] || [ ! -t 1 ]; then
jq . "$RECORDING_DIR/metadata.json"
exit 0
fi
# Playback session
echo "Playing back session: $SESSION_ID"
echo "Press Ctrl+C to stop"
echo ""
if [ -f "$RECORDING_DIR/session.cast" ]; then
# Asciinema recording
asciinema play "$RECORDING_DIR/session.cast" --speed "$SPEED"
elif [ -f "$RECORDING_DIR/typescript" ] && [ -f "$RECORDING_DIR/timing" ]; then
# Script recording
scriptreplay --timing "$RECORDING_DIR/timing" \
--typescript "$RECORDING_DIR/typescript" \
--divisor "$SPEED"
else
echo "No playable recording found"
exit 1
fi
# Cleanup
if [ "$CLEANUP_TEMP" = true ]; then
rm -rf "$TEMP_DIR"
fi
EOF
chmod +x /usr/local/bin/bastion-playback
}
# Initialize all components
init_recording_system
create_processing_script
create_sensitive_scanner
create_playback_tool
# Create cleanup cron job
cat > /etc/cron.daily/bastion-recording-cleanup << EOF
#!/bin/bash
# Clean up old recordings
# Remove recordings older than retention period
find $RECORDING_BASE/compressed -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
# Remove empty directories
find $RECORDING_BASE -type d -empty -delete
# Generate usage report
du -sh $RECORDING_BASE/* > $RECORDING_BASE/storage_usage.txt
EOF
chmod +x /etc/cron.daily/bastion-recording-cleanup
echo "Session recording system initialized"