|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +# Ensure consistent locale for parsing. |
| 6 | +export LC_ALL=C |
| 7 | + |
| 8 | +# ANSI color codes |
| 9 | +readonly RED='\033[0;31m' |
| 10 | +readonly GREEN='\033[0;32m' |
| 11 | +readonly YELLOW='\033[1;33m' |
| 12 | +readonly PURPLE='\033[0;35m' |
| 13 | +readonly CYAN='\033[0;36m' |
| 14 | +readonly NC='\033[0m' # No Color |
| 15 | + |
| 16 | +# Colored logging functions |
| 17 | +log::info() { |
| 18 | + # https://stackoverflow.com/a/7287873/15011229 |
| 19 | + # |
| 20 | + # note: printf is used instead of echo to avoid backslash |
| 21 | + # processing and to properly handle values that begin with a '-'. |
| 22 | + printf "${CYAN}ℹ️ %s${NC}\n" "$*" >&2 |
| 23 | +} |
| 24 | + |
| 25 | +log::success() { |
| 26 | + printf "${GREEN}✅ %s${NC}\n" "$*" >&2 |
| 27 | +} |
| 28 | + |
| 29 | +log::warn() { |
| 30 | + printf "${YELLOW}⚠️ %s${NC}\n" "$*" >&2 |
| 31 | +} |
| 32 | + |
| 33 | +log::error() { |
| 34 | + printf "${RED}❌ ERROR: %s${NC}\n" "$*" >&2 |
| 35 | +} |
| 36 | + |
| 37 | +log::highlight() { |
| 38 | + printf "${PURPLE}📌 %s${NC}\n" "$*" >&2 |
| 39 | +} |
| 40 | + |
| 41 | +proc::fatal() { |
| 42 | + log::error "$@" |
| 43 | + exit 1 |
| 44 | +} |
| 45 | + |
| 46 | +usage() { |
| 47 | + cat <<'EOF' |
| 48 | +Usage: cryptpilot-enhance [OPTIONS] |
| 49 | +
|
| 50 | +Securely harden a VM disk image before encryption using virt-customize. |
| 51 | +All operations are performed in a single guest launch for optimal performance. |
| 52 | +
|
| 53 | +OPTIONS: |
| 54 | + --mode MODE Hardening level: 'full' or 'partial' |
| 55 | + - full: Maximum security (removes SSH, locks all passwords) |
| 56 | + - partial: Retains SSH with key-only access, milder user restrictions |
| 57 | + --image IMAGE_PATH Path to the disk image (qcow2 or raw format) |
| 58 | + --ssh-key KEY_FILE (Optional) Public SSH key file to inject for root login (partial mode only) |
| 59 | + --help Show this help message and exit |
| 60 | +
|
| 61 | +NOTES: |
| 62 | +- Requires libguestfs-tools (virt-customize). Install via: |
| 63 | + CentOS/RHEL: yum install -y libguestfs-tools |
| 64 | + Ubuntu: apt-get install -y libguestfs-tools |
| 65 | +- By default, virt-customize uses the 'libvirt' backend (via libvirtd). For better performance |
| 66 | + or to avoid daemon dependencies, set the backend explicitly using LIBGUESTFS_BACKEND: |
| 67 | + - To use direct QEMU without libvirt (recommended for CI/containers): |
| 68 | + export LIBGUESTFS_BACKEND=direct |
| 69 | + - Example: |
| 70 | + LIBGUESTFS_BACKEND=direct cryptpilot-enhance --mode partial --image ./disk.qcow2 |
| 71 | +- The 'direct' backend avoids libvirtd overhead and works in containerized or minimal environments. |
| 72 | +- Always test on a copy of the image before production use. |
| 73 | +EOF |
| 74 | +} |
| 75 | + |
| 76 | +# Parse arguments |
| 77 | +MODE="" IMAGE="" SSH_KEY="" |
| 78 | + |
| 79 | +while [[ "$#" -gt 0 ]]; do |
| 80 | + case $1 in |
| 81 | + --mode) |
| 82 | + MODE="$2" |
| 83 | + shift 2 |
| 84 | + ;; |
| 85 | + --image) |
| 86 | + IMAGE="$2" |
| 87 | + shift 2 |
| 88 | + ;; |
| 89 | + --ssh-key) |
| 90 | + SSH_KEY="$2" |
| 91 | + shift 2 |
| 92 | + ;; |
| 93 | + -h | --help) |
| 94 | + usage |
| 95 | + exit 0 |
| 96 | + ;; |
| 97 | + *) |
| 98 | + log::error "unknown argument $1" |
| 99 | + usage |
| 100 | + exit 1 |
| 101 | + ;; |
| 102 | + esac |
| 103 | +done |
| 104 | + |
| 105 | +# Validate inputs |
| 106 | +[[ -z "$MODE" ]] && { |
| 107 | + log::error "--mode is required." |
| 108 | + usage |
| 109 | + exit 1 |
| 110 | +} |
| 111 | +[[ ! "$MODE" =~ ^(full|partial)$ ]] && { |
| 112 | + log::error "--mode must be 'full' or 'partial'." |
| 113 | + exit 1 |
| 114 | +} |
| 115 | +[[ -z "$IMAGE" ]] && { |
| 116 | + log::error "--image is required." |
| 117 | + usage |
| 118 | + exit 1 |
| 119 | +} |
| 120 | +[[ ! -f "$IMAGE" ]] && { |
| 121 | + log::error "image file not found: $IMAGE" |
| 122 | + exit 1 |
| 123 | +} |
| 124 | +[[ "$MODE" == "partial" && -n "$SSH_KEY" && ! -f "$SSH_KEY" ]] && { |
| 125 | + log::error "SSH key file not found: $SSH_KEY" |
| 126 | + exit 1 |
| 127 | +} |
| 128 | + |
| 129 | +log::info "Starting image hardening..." |
| 130 | +log::highlight "Image: $IMAGE" |
| 131 | +log::highlight "Mode: $MODE" |
| 132 | + |
| 133 | +# Build virt-customize command (single execution) |
| 134 | +VIRT_CMD=( |
| 135 | + virt-customize |
| 136 | + --format=qcow2 |
| 137 | + -a "$IMAGE" |
| 138 | +) |
| 139 | + |
| 140 | +# Helper function to append --run-command |
| 141 | +add_run_cmd() { |
| 142 | + VIRT_CMD+=(--run-command "$1") |
| 143 | +} |
| 144 | + |
| 145 | +# ============================= |
| 146 | +# 1. Uninstall Cloud Assistant Agent (Aliyun Assist) |
| 147 | +# ============================= |
| 148 | +add_run_cmd ' |
| 149 | +# Stop Cloud Assistant daemon |
| 150 | +/usr/local/share/assist-daemon/assist_daemon --stop |
| 151 | +
|
| 152 | +# Stop Cloud Assistant service |
| 153 | +systemctl stop aliyun.service |
| 154 | +
|
| 155 | +# Remove Cloud Assistant daemon |
| 156 | +/usr/local/share/assist-daemon/assist_daemon --delete |
| 157 | +
|
| 158 | +# Uninstall package |
| 159 | +rpm -qa | grep aliyun_assist | xargs rpm -e |
| 160 | +
|
| 161 | +# Clean up leftover files and service configurations |
| 162 | +rm -rf /usr/local/share/aliyun-assist |
| 163 | +rm -rf /usr/local/share/assist-daemon |
| 164 | +rm -f /etc/systemd/system/aliyun.service |
| 165 | +rm -f /etc/init.d/aliyun-service |
| 166 | +' |
| 167 | + |
| 168 | +# ============================= |
| 169 | +# 2. Uninstall Cloud Security Center (Aegis/AntiKnight) |
| 170 | +# ============================= |
| 171 | +add_run_cmd ' |
| 172 | +# Download and execute uninstall script |
| 173 | +wget "http://update.aegis.aliyun.com/download/uninstall.sh" && chmod +x uninstall.sh && ./uninstall.sh |
| 174 | +' |
| 175 | + |
| 176 | +# ============================= |
| 177 | +# 3. Disable and Mask rpcbind Service |
| 178 | +# ============================= |
| 179 | +add_run_cmd ' |
| 180 | +systemctl stop rpcbind.service |
| 181 | +systemctl disable rpcbind.service |
| 182 | +systemctl mask rpcbind.service |
| 183 | +
|
| 184 | +systemctl stop rpcbind.socket |
| 185 | +systemctl disable rpcbind.socket |
| 186 | +systemctl mask rpcbind.socket |
| 187 | +' |
| 188 | + |
| 189 | +# ============================= |
| 190 | +# 4. Remove cloud-init |
| 191 | +# ============================= |
| 192 | +add_run_cmd ' |
| 193 | +yum remove -y cloud-init |
| 194 | +' |
| 195 | + |
| 196 | +# ============================= |
| 197 | +# 5. Optional: Restrict or Remove SSH Service |
| 198 | +# ============================= |
| 199 | +if [[ "$MODE" == "full" ]]; then |
| 200 | + add_run_cmd ' |
| 201 | + yum remove -y openssh-server |
| 202 | + ' |
| 203 | +elif [[ "$MODE" == "partial" ]]; then |
| 204 | + add_run_cmd ' |
| 205 | + # Disable password login, allow public key only |
| 206 | + sed -i "s/^#*PasswordAuthentication.*/PasswordAuthentication no/" /etc/ssh/sshd_config |
| 207 | + sed -i "s/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/" /etc/ssh/sshd_config |
| 208 | +
|
| 209 | + # Prevent root password login, but allow key-based login |
| 210 | + sed -i "s/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/" /etc/ssh/sshd_config |
| 211 | +
|
| 212 | + # Disable high-risk features |
| 213 | + sed -i "s/^#*X11Forwarding.*/X11Forwarding no/" /etc/ssh/sshd_config |
| 214 | + sed -i "s/^#*AllowTcpForwarding.*/AllowTcpForwarding no/" /etc/ssh/sshd_config |
| 215 | + ' |
| 216 | +fi |
| 217 | + |
| 218 | +# ============================= |
| 219 | +# 6. Disable Linux User Password Login (Prevent Console Access) |
| 220 | +# ============================= |
| 221 | +# shellcheck disable=SC2016 |
| 222 | +add_run_cmd ' |
| 223 | +#!/bin/bash |
| 224 | +
|
| 225 | +# 1. Lock root and admin account passwords: |
| 226 | +echo "1. Locking root and admin account passwords..." |
| 227 | +sed -i "s/\(^root:\)[^:]*/\1!!/" /etc/shadow |
| 228 | +sed -i "s/\(^admin:\)[^:]*/\1!!/" /etc/shadow |
| 229 | +echo "1. Root and admin account passwords locked!" |
| 230 | +
|
| 231 | +echo "2. Processing other user accounts (excluding root and admin)..." |
| 232 | +# Define list of excluded usernames |
| 233 | +exclude_users=("root" "admin") |
| 234 | +
|
| 235 | +while IFS=: read -r username _ _ _ _ homedir user_shell; do |
| 236 | + # Check if shell is one of the interactive shells |
| 237 | + case $user_shell in |
| 238 | + "/bin/bash"|"/bin/sh"|"/bin/zsh" \ |
| 239 | + |"/usr/bin/bash"|"/usr/bin/sh"|"/usr/bin/zsh" \ |
| 240 | + |"/usr/local/bin/bash"|"/usr/local/bin/sh"|"/usr/local/bin/zsh") |
| 241 | + |
| 242 | + # Skip if username is in exclude list |
| 243 | + if [[ " ${exclude_users[@]} " =~ " $username " ]]; then |
| 244 | + continue |
| 245 | + fi |
| 246 | + |
| 247 | + # Read password field from shadow |
| 248 | + pass=$(grep "^$username:" /etc/shadow | cut -d: -f2) |
| 249 | + |
| 250 | + # If already locked (!, !!, *, etc.), skip deletion |
| 251 | + if [[ "$pass" == "!" || "$pass" == "!!" || "$pass" == *\!* || "$pass" == "*" ]]; then |
| 252 | + echo "Account $username is already locked or disabled, skipping." |
| 253 | + continue |
| 254 | + fi |
| 255 | + |
| 256 | + # Delete eligible user account and home directory |
| 257 | + echo "Removing account: $username" |
| 258 | + userdel -r "$username" 2>/dev/null || true |
| 259 | + if [[ -d "$homedir" ]]; then |
| 260 | + rm -rf "$homedir" 2>/dev/null || true |
| 261 | + fi |
| 262 | + ;; |
| 263 | +
|
| 264 | + *) |
| 265 | + # Ignore users with non-interactive shells |
| 266 | + continue |
| 267 | + ;; |
| 268 | + esac |
| 269 | +done < /etc/passwd |
| 270 | +echo "2. Other accounts (excluding root and admin) processed!" |
| 271 | +
|
| 272 | +echo "3. Removing directories ending with .DEL in home folders..." |
| 273 | +# Scan /etc/passwd again and clean *.DEL directories in valid home paths |
| 274 | +while IFS=: read -r username _ _ _ _ homedir user_shell; do |
| 275 | + case $user_shell in |
| 276 | + "/bin/bash"|"/bin/sh"|"/bin/zsh" \ |
| 277 | + |"/usr/bin/bash"|"/usr/bin/sh"|"/usr/bin/zsh" \ |
| 278 | + |"/usr/local/bin/bash"|"/usr/local/bin/sh"|"/usr/local/bin/zsh") |
| 279 | + if [[ -d "$homedir" ]]; then |
| 280 | + find "$homedir" -maxdepth 1 -type d -name "*.DEL" -exec rm -rf {} \; |
| 281 | + fi |
| 282 | + ;; |
| 283 | + esac |
| 284 | +done < /etc/passwd |
| 285 | +echo "3. Cleanup of *.DEL directories completed!" |
| 286 | +' |
| 287 | + |
| 288 | +# ============================= |
| 289 | +# 7. Clear Bash History |
| 290 | +# ============================= |
| 291 | +add_run_cmd ' |
| 292 | +history -c && history -w |
| 293 | +' |
| 294 | + |
| 295 | +# ============================= |
| 296 | +# 8. (Partial Only) Inject SSH Public Key |
| 297 | +# ============================= |
| 298 | +if [[ "$MODE" == "partial" && -n "$SSH_KEY" ]]; then |
| 299 | + log::info "Injecting SSH public key into root account..." |
| 300 | + VIRT_CMD+=(--ssh-inject "root:file:$SSH_KEY") |
| 301 | +fi |
| 302 | + |
| 303 | +# ============================= |
| 304 | +# Execute All Commands (Single Guest Boot) |
| 305 | +# ============================= |
| 306 | +log::highlight "Using virt-customize in single-launch mode for efficiency" |
| 307 | +"${VIRT_CMD[@]}" |
| 308 | + |
| 309 | +log::success "Hardening completed successfully!" |
0 commit comments