88551536e6
Amp-Thread-ID: https://ampcode.com/threads/T-019cc404-c628-759e-a50b-f5eeea35b91f Co-authored-by: Amp <amp@ampcode.com>
536 lines
15 KiB
Bash
536 lines
15 KiB
Bash
#!/bin/bash
|
|
|
|
################################################
|
|
#### Salt Key Manager ####
|
|
#### Automate salt-key operations ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### Version: 1.00-030526 ####
|
|
################################################
|
|
|
|
set -o pipefail
|
|
|
|
SCRIPT_NAME=$(basename "$0")
|
|
readonly SCRIPT_NAME
|
|
|
|
# Default configuration
|
|
readonly DEFAULT_STALE_DAYS=30
|
|
readonly DEFAULT_CACHE_DIR="/var/cache/salt/master/minions"
|
|
|
|
# Configuration variables
|
|
DEBUG=${DEBUG:-}
|
|
|
|
# Runtime flags
|
|
ACTION=""
|
|
TARGET_MINION=""
|
|
STALE_DAYS=$DEFAULT_STALE_DAYS
|
|
EXPORT_PATH=""
|
|
BULK_FILE=""
|
|
AUTO_YES=false
|
|
USE_COLOR=true
|
|
|
|
# Colors
|
|
C_GREEN=""
|
|
C_YELLOW=""
|
|
C_RED=""
|
|
C_CYAN=""
|
|
C_RESET=""
|
|
|
|
handle_error() {
|
|
local exit_code=$1
|
|
local line_number=$2
|
|
echo "Error: $SCRIPT_NAME failed at line $line_number with exit code $exit_code" >&2
|
|
exit "$exit_code"
|
|
}
|
|
|
|
trap 'handle_error $? $LINENO' ERR
|
|
|
|
debug_echo() {
|
|
if [[ -n "$DEBUG" ]]; then
|
|
echo "[DEBUG] $*" >&2
|
|
fi
|
|
}
|
|
|
|
log_info() {
|
|
echo "[INFO] $*"
|
|
}
|
|
|
|
log_warn() {
|
|
echo "[WARN] $*" >&2
|
|
}
|
|
|
|
log_error() {
|
|
echo "[ERROR] $*" >&2
|
|
}
|
|
|
|
setup_colors() {
|
|
if [[ "$USE_COLOR" == true ]] && [[ -t 1 ]]; then
|
|
C_GREEN='\033[0;32m'
|
|
C_YELLOW='\033[0;33m'
|
|
C_RED='\033[0;31m'
|
|
C_CYAN='\033[0;36m'
|
|
C_RESET='\033[0m'
|
|
fi
|
|
}
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
Usage: $SCRIPT_NAME [ACTION] [OPTIONS]
|
|
|
|
Manage Salt minion keys — accept, reject, delete, verify, rotate, and
|
|
clean up stale keys.
|
|
|
|
ACTIONS:
|
|
--list List all keys by status with counts
|
|
--verify Show pending keys with fingerprints for verification
|
|
--accept-all Accept all pending keys
|
|
--accept MINION Accept a specific minion key
|
|
--reject MINION Reject a specific minion key
|
|
--delete MINION Delete a specific minion key
|
|
--rotate MINION Rotate a minion key (delete, re-accept on reconnect)
|
|
--cleanup-stale [DAYS] Delete keys for minions not seen in DAYS days (default: $DEFAULT_STALE_DAYS)
|
|
--export PATH Export all accepted key fingerprints to a file
|
|
--bulk-accept FILE Accept minions listed in a file (one per line)
|
|
|
|
OPTIONS:
|
|
--yes Skip confirmation prompts
|
|
--no-color Disable colored output
|
|
--help, -h Show this help message
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
DEBUG Enable debug output
|
|
|
|
EXAMPLES:
|
|
# List all keys with status
|
|
sudo $SCRIPT_NAME --list
|
|
|
|
# Show pending keys for verification
|
|
sudo $SCRIPT_NAME --verify
|
|
|
|
# Accept all pending keys
|
|
sudo $SCRIPT_NAME --accept-all --yes
|
|
|
|
# Accept a specific minion
|
|
sudo $SCRIPT_NAME --accept web01
|
|
|
|
# Clean up minions not seen in 60 days
|
|
sudo $SCRIPT_NAME --cleanup-stale 60
|
|
|
|
# Export fingerprints for auditing
|
|
sudo $SCRIPT_NAME --export /tmp/salt-keys.txt
|
|
|
|
# Bulk accept from a file
|
|
sudo $SCRIPT_NAME --bulk-accept /tmp/new-minions.txt --yes
|
|
EOF
|
|
}
|
|
|
|
count_keys() {
|
|
local status="$1"
|
|
salt-key --list "$status" 2>/dev/null | grep -cv "^$status\|^$" || echo 0
|
|
}
|
|
|
|
do_list() {
|
|
echo "Salt Key Status"
|
|
echo "==============="
|
|
echo ""
|
|
|
|
local accepted unaccepted denied rejected
|
|
accepted=$(count_keys "accepted")
|
|
unaccepted=$(count_keys "unaccepted")
|
|
denied=$(count_keys "denied")
|
|
rejected=$(count_keys "rejected")
|
|
|
|
printf ' %bAccepted:%b %d\n' "$C_GREEN" "$C_RESET" "$accepted"
|
|
printf ' %bPending:%b %d\n' "$C_YELLOW" "$C_RESET" "$unaccepted"
|
|
printf ' %bDenied:%b %d\n' "$C_RED" "$C_RESET" "$denied"
|
|
printf ' %bRejected:%b %d\n' "$C_RED" "$C_RESET" "$rejected"
|
|
echo ""
|
|
|
|
if ((accepted > 0)); then
|
|
printf '%bAccepted Keys:%b\n' "$C_GREEN" "$C_RESET"
|
|
salt-key --list accepted 2>/dev/null | grep -v "^Accepted Keys:" | sed 's/^/ /'
|
|
echo ""
|
|
fi
|
|
|
|
if ((unaccepted > 0)); then
|
|
printf '%bPending Keys:%b\n' "$C_YELLOW" "$C_RESET"
|
|
salt-key --list unaccepted 2>/dev/null | grep -v "^Unaccepted Keys:" | sed 's/^/ /'
|
|
echo ""
|
|
fi
|
|
|
|
if ((denied > 0)); then
|
|
printf '%bDenied Keys:%b\n' "$C_RED" "$C_RESET"
|
|
salt-key --list denied 2>/dev/null | grep -v "^Denied Keys:" | sed 's/^/ /'
|
|
echo ""
|
|
fi
|
|
|
|
if ((rejected > 0)); then
|
|
printf '%bRejected Keys:%b\n' "$C_RED" "$C_RESET"
|
|
salt-key --list rejected 2>/dev/null | grep -v "^Rejected Keys:" | sed 's/^/ /'
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
do_verify() {
|
|
local pending
|
|
pending=$(salt-key --list unaccepted 2>/dev/null | grep -v "^Unaccepted Keys:$" | grep -v "^$")
|
|
|
|
if [[ -z "$pending" ]]; then
|
|
log_info "No pending keys to verify"
|
|
return 0
|
|
fi
|
|
|
|
echo "Master Fingerprint:"
|
|
printf ' %b' "$C_CYAN"
|
|
salt-key -F master 2>/dev/null | grep -A1 "master.pub" | tail -1 | tr -d ' '
|
|
printf '%b\n\n' "$C_RESET"
|
|
|
|
echo "Pending Keys with Fingerprints:"
|
|
echo ""
|
|
|
|
while IFS= read -r minion; do
|
|
[[ -z "$minion" ]] && continue
|
|
minion=$(echo "$minion" | tr -d '[:space:]')
|
|
local fingerprint
|
|
fingerprint=$(salt-key -f "$minion" 2>/dev/null | grep -v "^Unaccepted Keys:" | awk '{print $2}' | head -1)
|
|
printf ' %b%-30s%b %s\n' "$C_YELLOW" "$minion" "$C_RESET" "${fingerprint:-unknown}"
|
|
done <<< "$pending"
|
|
|
|
echo ""
|
|
log_info "Verify each fingerprint matches the minion's local fingerprint:"
|
|
log_info " (on minion) salt-call --local key.finger"
|
|
}
|
|
|
|
do_accept_all() {
|
|
local pending
|
|
pending=$(count_keys "unaccepted")
|
|
|
|
if ((pending == 0)); then
|
|
log_info "No pending keys to accept"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Accepting $pending pending key(s)..."
|
|
|
|
if [[ "$AUTO_YES" != true ]]; then
|
|
echo "Accept all $pending pending keys? [y/N] "
|
|
read -r confirm
|
|
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
log_info "Aborted"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
salt-key -A -y 2>/dev/null
|
|
log_info "All pending keys accepted"
|
|
}
|
|
|
|
do_accept() {
|
|
local minion="$1"
|
|
log_info "Accepting key for: $minion"
|
|
|
|
if [[ "$AUTO_YES" != true ]]; then
|
|
local fingerprint
|
|
fingerprint=$(salt-key -f "$minion" 2>/dev/null | grep -v "^Unaccepted Keys:" | awk '{print $2}' | head -1)
|
|
echo "Fingerprint: ${fingerprint:-unknown}"
|
|
echo "Accept key for $minion? [y/N] "
|
|
read -r confirm
|
|
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
log_info "Aborted"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
salt-key -a "$minion" -y 2>/dev/null
|
|
log_info "Key accepted for $minion"
|
|
}
|
|
|
|
do_reject() {
|
|
local minion="$1"
|
|
log_info "Rejecting key for: $minion"
|
|
|
|
if [[ "$AUTO_YES" != true ]]; then
|
|
echo "Reject key for $minion? [y/N] "
|
|
read -r confirm
|
|
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
log_info "Aborted"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
salt-key -r "$minion" -y 2>/dev/null
|
|
log_info "Key rejected for $minion"
|
|
}
|
|
|
|
do_delete() {
|
|
local minion="$1"
|
|
log_info "Deleting key for: $minion"
|
|
|
|
if [[ "$AUTO_YES" != true ]]; then
|
|
echo "Delete key for $minion? This cannot be undone. [y/N] "
|
|
read -r confirm
|
|
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
log_info "Aborted"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
salt-key -d "$minion" -y 2>/dev/null
|
|
log_info "Key deleted for $minion"
|
|
}
|
|
|
|
do_rotate() {
|
|
local minion="$1"
|
|
log_info "Rotating key for: $minion"
|
|
log_info "This will delete the current key — the minion must reconnect to get a new key accepted"
|
|
|
|
if [[ "$AUTO_YES" != true ]]; then
|
|
echo "Rotate key for $minion? [y/N] "
|
|
read -r confirm
|
|
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
log_info "Aborted"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
salt-key -d "$minion" -y 2>/dev/null
|
|
log_info "Key deleted for $minion — accept the new key when the minion reconnects"
|
|
log_info "On the minion, restart salt-minion: systemctl restart salt-minion"
|
|
}
|
|
|
|
do_cleanup_stale() {
|
|
local days="$1"
|
|
log_info "Finding minions not seen in $days days..."
|
|
|
|
if [[ ! -d "$DEFAULT_CACHE_DIR" ]]; then
|
|
log_error "Minion cache directory not found: $DEFAULT_CACHE_DIR"
|
|
return 1
|
|
fi
|
|
|
|
local stale_minions=()
|
|
local cutoff
|
|
cutoff=$(date -d "-${days} days" +%s 2>/dev/null) || cutoff=$(date -v-"${days}"d +%s 2>/dev/null)
|
|
|
|
while IFS= read -r minion_dir; do
|
|
local minion_name
|
|
minion_name=$(basename "$minion_dir")
|
|
local last_modified
|
|
last_modified=$(stat -c %Y "$minion_dir" 2>/dev/null) || last_modified=$(stat -f %m "$minion_dir" 2>/dev/null) || continue
|
|
|
|
if ((last_modified < cutoff)); then
|
|
local days_ago=$(( ($(date +%s) - last_modified) / 86400 ))
|
|
stale_minions+=("$minion_name")
|
|
printf ' %b%-30s%b (last seen %d days ago)\n' "$C_RED" "$minion_name" "$C_RESET" "$days_ago"
|
|
fi
|
|
done < <(find "$DEFAULT_CACHE_DIR" -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
|
|
|
|
if [[ ${#stale_minions[@]} -eq 0 ]]; then
|
|
log_info "No stale minions found"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
log_info "Found ${#stale_minions[@]} stale minion(s)"
|
|
|
|
if [[ "$AUTO_YES" != true ]]; then
|
|
echo "Delete keys for all ${#stale_minions[@]} stale minions? [y/N] "
|
|
read -r confirm
|
|
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
|
|
log_info "Aborted"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
for minion in "${stale_minions[@]}"; do
|
|
salt-key -d "$minion" -y 2>/dev/null && log_info "Deleted key: $minion"
|
|
done
|
|
|
|
log_info "Stale key cleanup complete"
|
|
}
|
|
|
|
do_export() {
|
|
local output_path="$1"
|
|
log_info "Exporting accepted key fingerprints to $output_path..."
|
|
|
|
{
|
|
echo "# Salt Key Fingerprint Export"
|
|
echo "# Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
|
echo "# Master: $(hostname -f 2>/dev/null || hostname)"
|
|
echo "#"
|
|
echo "# Format: minion_id fingerprint"
|
|
echo ""
|
|
salt-key -F accepted 2>/dev/null | grep -v "^Accepted Keys:" | while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
echo "$line"
|
|
done
|
|
} > "$output_path"
|
|
|
|
local count
|
|
count=$(grep -cv "^#\|^$" "$output_path" 2>/dev/null) || count=0
|
|
log_info "Exported $count key fingerprint(s) to $output_path"
|
|
}
|
|
|
|
do_bulk_accept() {
|
|
local input_file="$1"
|
|
|
|
if [[ ! -f "$input_file" ]]; then
|
|
log_error "File not found: $input_file"
|
|
return 1
|
|
fi
|
|
|
|
local count=0
|
|
local failed=0
|
|
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" || "$line" == \#* ]] && continue
|
|
local minion_id="${line%%:*}"
|
|
minion_id=$(echo "$minion_id" | tr -d '[:space:]')
|
|
|
|
if salt-key -a "$minion_id" -y 2>/dev/null; then
|
|
log_info "Accepted: $minion_id"
|
|
count=$((count + 1))
|
|
else
|
|
log_error "Failed to accept: $minion_id"
|
|
failed=$((failed + 1))
|
|
fi
|
|
done < "$input_file"
|
|
|
|
log_info "Bulk accept complete: $count accepted, $failed failed"
|
|
}
|
|
|
|
parse_arguments() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--list)
|
|
ACTION="list"
|
|
shift
|
|
;;
|
|
--verify)
|
|
ACTION="verify"
|
|
shift
|
|
;;
|
|
--accept-all)
|
|
ACTION="accept-all"
|
|
shift
|
|
;;
|
|
--accept)
|
|
ACTION="accept"
|
|
TARGET_MINION="$2"
|
|
shift 2
|
|
;;
|
|
--reject)
|
|
ACTION="reject"
|
|
TARGET_MINION="$2"
|
|
shift 2
|
|
;;
|
|
--delete)
|
|
ACTION="delete"
|
|
TARGET_MINION="$2"
|
|
shift 2
|
|
;;
|
|
--rotate)
|
|
ACTION="rotate"
|
|
TARGET_MINION="$2"
|
|
shift 2
|
|
;;
|
|
--cleanup-stale)
|
|
ACTION="cleanup-stale"
|
|
if [[ -n "${2:-}" && "$2" =~ ^[0-9]+$ ]]; then
|
|
STALE_DAYS="$2"
|
|
shift 2
|
|
else
|
|
shift
|
|
fi
|
|
;;
|
|
--export)
|
|
ACTION="export"
|
|
EXPORT_PATH="$2"
|
|
shift 2
|
|
;;
|
|
--bulk-accept)
|
|
ACTION="bulk-accept"
|
|
BULK_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--yes)
|
|
AUTO_YES=true
|
|
shift
|
|
;;
|
|
--no-color)
|
|
USE_COLOR=false
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $1"
|
|
show_help >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
validate_requirements() {
|
|
if [[ $EUID -ne 0 ]]; then
|
|
log_error "This script must be run as root (use sudo)"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$ACTION" ]]; then
|
|
log_error "An action is required"
|
|
show_help >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v salt-key >/dev/null 2>&1; then
|
|
log_error "salt-key not found — is salt-master installed?"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$ACTION" == "accept" || "$ACTION" == "reject" || "$ACTION" == "delete" || "$ACTION" == "rotate" ]]; then
|
|
if [[ -z "$TARGET_MINION" ]]; then
|
|
log_error "Minion name is required for --$ACTION"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [[ "$ACTION" == "export" && -z "$EXPORT_PATH" ]]; then
|
|
log_error "Output path is required for --export"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$ACTION" == "bulk-accept" && -z "$BULK_FILE" ]]; then
|
|
log_error "Input file is required for --bulk-accept"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
parse_arguments "$@"
|
|
validate_requirements
|
|
setup_colors
|
|
|
|
case "$ACTION" in
|
|
list) do_list ;;
|
|
verify) do_verify ;;
|
|
accept-all) do_accept_all ;;
|
|
accept) do_accept "$TARGET_MINION" ;;
|
|
reject) do_reject "$TARGET_MINION" ;;
|
|
delete) do_delete "$TARGET_MINION" ;;
|
|
rotate) do_rotate "$TARGET_MINION" ;;
|
|
cleanup-stale) do_cleanup_stale "$STALE_DAYS" ;;
|
|
export) do_export "$EXPORT_PATH" ;;
|
|
bulk-accept) do_bulk_accept "$BULK_FILE" ;;
|
|
esac
|
|
|
|
debug_echo "Script completed successfully"
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
main "$@"
|
|
fi
|