Harden Ubuntu VM security update script
This commit is contained in:
parent
3a664b5a79
commit
013a27069b
@ -27,6 +27,7 @@ Read these first:
|
|||||||
- `remove_user_interactive.sh`
|
- `remove_user_interactive.sh`
|
||||||
- `remove_user_guided.sh`
|
- `remove_user_guided.sh`
|
||||||
- `remove_user_from_repos.sh`
|
- `remove_user_from_repos.sh`
|
||||||
|
- `scripts/`
|
||||||
- `git-work-safety-tools/`
|
- `git-work-safety-tools/`
|
||||||
- `github_access_scripts/`
|
- `github_access_scripts/`
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,8 @@ These are for scanning many repositories, checking dirty state, and performing s
|
|||||||
|
|
||||||
- Root `*.sh` files
|
- Root `*.sh` files
|
||||||
- Main Bash-based GitHub and maintenance utilities.
|
- Main Bash-based GitHub and maintenance utilities.
|
||||||
|
- `scripts/`
|
||||||
|
- Named operational scripts that are more self-contained than the older root-level helpers.
|
||||||
- `git-work-safety-tools/`
|
- `git-work-safety-tools/`
|
||||||
- Safer multi-repo git helpers.
|
- Safer multi-repo git helpers.
|
||||||
- `github_access_scripts/`
|
- `github_access_scripts/`
|
||||||
|
|||||||
@ -16,6 +16,7 @@ Start with:
|
|||||||
- `remove_user_interactive.sh`
|
- `remove_user_interactive.sh`
|
||||||
- `remove_user_guided.sh`
|
- `remove_user_guided.sh`
|
||||||
- `remove_user_from_repos.sh`
|
- `remove_user_from_repos.sh`
|
||||||
|
- `scripts/ubuntu-vm-security-update.sh` for Ubuntu VM hardening and update automation
|
||||||
|
|
||||||
### If You Need Multi-Repo Git Helpers
|
### If You Need Multi-Repo Git Helpers
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,14 @@ Key files:
|
|||||||
- `multi_repo_status.sh`
|
- `multi_repo_status.sh`
|
||||||
- `multi_repo_interactive_fix.sh`
|
- `multi_repo_interactive_fix.sh`
|
||||||
|
|
||||||
|
### `scripts/`
|
||||||
|
|
||||||
|
Self-contained operational scripts that do not fit the older root-level naming pattern.
|
||||||
|
|
||||||
|
Key files:
|
||||||
|
|
||||||
|
- `ubuntu-vm-security-update.sh`
|
||||||
|
|
||||||
### `github_access_scripts/`
|
### `github_access_scripts/`
|
||||||
|
|
||||||
Focused GitHub access checks.
|
Focused GitHub access checks.
|
||||||
|
|||||||
@ -1,21 +1,23 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
# vm-security-update.sh
|
# ubuntu-vm-security-update.sh
|
||||||
# Robust Ubuntu VM security update + unattended-upgrades setup.
|
# Robust Ubuntu VM security update + unattended-upgrades setup.
|
||||||
# No Ubuntu Pro / paid features.
|
# No Ubuntu Pro / paid features.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# sudo bash vm-security-update.sh
|
# sudo bash ubuntu-vm-security-update.sh
|
||||||
# sudo bash vm-security-update.sh --no-reboot
|
# sudo bash ubuntu-vm-security-update.sh --no-reboot
|
||||||
# sudo bash vm-security-update.sh --no-auto-reboot
|
# sudo bash ubuntu-vm-security-update.sh --no-auto-reboot
|
||||||
# sudo bash vm-security-update.sh --dry-run
|
# sudo bash ubuntu-vm-security-update.sh --dry-run
|
||||||
|
|
||||||
LOG_FILE="/var/log/vm-security-update.log"
|
LOG_FILE="/var/log/vm-security-update.log"
|
||||||
AUTO_REBOOT="true"
|
AUTO_REBOOT="true"
|
||||||
REBOOT_NOW="true"
|
REBOOT_NOW="true"
|
||||||
DRY_RUN="false"
|
DRY_RUN="false"
|
||||||
AUTO_REBOOT_TIME="04:00"
|
AUTO_REBOOT_TIME="04:00"
|
||||||
|
SSH_PORT_OVERRIDE=""
|
||||||
|
SCRIPT_NAME="$(basename "$0")"
|
||||||
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
export NEEDRESTART_MODE=a
|
export NEEDRESTART_MODE=a
|
||||||
@ -31,18 +33,22 @@ die() {
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: sudo bash $0 [options]
|
Usage: sudo bash $SCRIPT_NAME [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--no-reboot Do not reboot at the end even if reboot is required
|
--no-reboot Do not reboot at the end even if reboot is required
|
||||||
--no-auto-reboot Do not configure unattended-upgrades automatic reboot
|
--no-auto-reboot Do not configure unattended-upgrades automatic reboot
|
||||||
|
--auto-reboot-time HH:MM
|
||||||
|
Set unattended-upgrades auto reboot time (default: $AUTO_REBOOT_TIME)
|
||||||
|
--ssh-port PORT Override detected SSH port when configuring UFW
|
||||||
--dry-run Show what would be done without applying changes
|
--dry-run Show what would be done without applying changes
|
||||||
--help Show this help
|
--help Show this help
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
sudo bash $0
|
sudo bash $SCRIPT_NAME
|
||||||
sudo bash $0 --no-reboot
|
sudo bash $SCRIPT_NAME --no-reboot
|
||||||
sudo bash $0 --no-auto-reboot
|
sudo bash $SCRIPT_NAME --no-auto-reboot
|
||||||
|
sudo bash $SCRIPT_NAME --ssh-port 2222
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,23 +68,109 @@ run_bash() {
|
|||||||
bash -c "$*"
|
bash -c "$*"
|
||||||
}
|
}
|
||||||
|
|
||||||
for arg in "$@"; do
|
safe_write_file() {
|
||||||
case "$arg" in
|
local target="$1"
|
||||||
|
local mode="$2"
|
||||||
|
local tmp
|
||||||
|
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
cat > "$tmp"
|
||||||
|
|
||||||
|
if [[ -f "$target" ]] && cmp -s "$tmp" "$target"; then
|
||||||
|
log "No change needed for $target"
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Writing $target"
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -m "$mode" "$tmp" "$target"
|
||||||
|
rm -f "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
have_systemd() {
|
||||||
|
command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_service_enabled_and_restarted() {
|
||||||
|
local service="$1"
|
||||||
|
|
||||||
|
if ! have_systemd; then
|
||||||
|
log "systemd not detected. Skipping enable/restart for $service"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
run systemctl enable "$service"
|
||||||
|
run systemctl restart "$service"
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_ssh_port() {
|
||||||
|
local detected_port=""
|
||||||
|
|
||||||
|
if [[ -n "$SSH_PORT_OVERRIDE" ]]; then
|
||||||
|
echo "$SSH_PORT_OVERRIDE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sshd >/dev/null 2>&1; then
|
||||||
|
detected_port="$(sshd -T 2>/dev/null | awk '$1 == "port" {print $2; exit}')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$detected_port" ]] && [[ -f /etc/ssh/sshd_config ]]; then
|
||||||
|
detected_port="$(awk '
|
||||||
|
$1 ~ /^[Pp]ort$/ && $2 ~ /^[0-9]+$/ {print $2}
|
||||||
|
' /etc/ssh/sshd_config | tail -n 1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$detected_port" ]]; then
|
||||||
|
detected_port="22"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$detected_port"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_time_hhmm() {
|
||||||
|
[[ "$1" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
--no-reboot)
|
--no-reboot)
|
||||||
REBOOT_NOW="false"
|
REBOOT_NOW="false"
|
||||||
|
shift
|
||||||
;;
|
;;
|
||||||
--no-auto-reboot)
|
--no-auto-reboot)
|
||||||
AUTO_REBOOT="false"
|
AUTO_REBOOT="false"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--auto-reboot-time)
|
||||||
|
[[ $# -ge 2 ]] || die "--auto-reboot-time requires a value like 04:00"
|
||||||
|
validate_time_hhmm "$2" || die "Invalid --auto-reboot-time value: $2"
|
||||||
|
AUTO_REBOOT_TIME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--ssh-port)
|
||||||
|
[[ $# -ge 2 ]] || die "--ssh-port requires a numeric port"
|
||||||
|
[[ "$2" =~ ^[0-9]+$ ]] || die "Invalid --ssh-port value: $2"
|
||||||
|
(( "$2" >= 1 && "$2" <= 65535 )) || die "SSH port must be between 1 and 65535"
|
||||||
|
SSH_PORT_OVERRIDE="$2"
|
||||||
|
shift 2
|
||||||
;;
|
;;
|
||||||
--dry-run)
|
--dry-run)
|
||||||
DRY_RUN="true"
|
DRY_RUN="true"
|
||||||
|
shift
|
||||||
;;
|
;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
die "Unknown argument: $arg"
|
die "Unknown argument: $1"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@ -86,7 +178,7 @@ done
|
|||||||
trap 'die "Script failed near line $LINENO. Check $LOG_FILE for details."' ERR
|
trap 'die "Script failed near line $LINENO. Check $LOG_FILE for details."' ERR
|
||||||
|
|
||||||
if [[ "${EUID}" -ne 0 ]]; then
|
if [[ "${EUID}" -ne 0 ]]; then
|
||||||
die "Please run as root: sudo bash $0"
|
die "Please run as root: sudo bash $SCRIPT_NAME"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
touch "$LOG_FILE"
|
touch "$LOG_FILE"
|
||||||
@ -97,6 +189,7 @@ log "Starting Ubuntu VM security update setup"
|
|||||||
log "Dry run: $DRY_RUN"
|
log "Dry run: $DRY_RUN"
|
||||||
log "Auto reboot for future unattended updates: $AUTO_REBOOT"
|
log "Auto reboot for future unattended updates: $AUTO_REBOOT"
|
||||||
log "Reboot now if required: $REBOOT_NOW"
|
log "Reboot now if required: $REBOOT_NOW"
|
||||||
|
log "Automatic reboot time: $AUTO_REBOOT_TIME"
|
||||||
log "============================================================"
|
log "============================================================"
|
||||||
|
|
||||||
if [[ ! -f /etc/os-release ]]; then
|
if [[ ! -f /etc/os-release ]]; then
|
||||||
@ -153,48 +246,48 @@ run apt-get full-upgrade -y
|
|||||||
log "Removing unused packages and cleaning apt cache"
|
log "Removing unused packages and cleaning apt cache"
|
||||||
run apt-get autoremove --purge -y
|
run apt-get autoremove --purge -y
|
||||||
run apt-get autoclean
|
run apt-get autoclean
|
||||||
|
run apt-get clean
|
||||||
|
|
||||||
log "Configuring unattended-upgrades daily security updates"
|
log "Configuring unattended-upgrades daily security updates"
|
||||||
|
|
||||||
run_bash "cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
|
safe_write_file /etc/apt/apt.conf.d/20auto-upgrades 0644 <<'EOF'
|
||||||
APT::Periodic::Update-Package-Lists \"1\";
|
APT::Periodic::Update-Package-Lists "1";
|
||||||
APT::Periodic::Download-Upgradeable-Packages \"1\";
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||||
APT::Periodic::AutocleanInterval \"7\";
|
APT::Periodic::AutocleanInterval "7";
|
||||||
APT::Periodic::Unattended-Upgrade \"1\";
|
APT::Periodic::Unattended-Upgrade "1";
|
||||||
EOF"
|
EOF
|
||||||
|
|
||||||
# Keep origins simple and Ubuntu-safe. This uses distro variables so it works across Ubuntu versions.
|
# Keep origins simple and Ubuntu-safe. This uses distro variables so it works across Ubuntu versions.
|
||||||
run_bash "cat > /etc/apt/apt.conf.d/51unattended-upgrades-custom <<'EOF'
|
safe_write_file /etc/apt/apt.conf.d/52bytelyst-unattended-upgrades 0644 <<EOF
|
||||||
Unattended-Upgrade::Origins-Pattern {
|
Unattended-Upgrade::Origins-Pattern {
|
||||||
\"origin=Ubuntu,codename=\${distro_codename},label=Ubuntu\";
|
"origin=Ubuntu,codename=\${distro_codename},label=Ubuntu";
|
||||||
\"origin=Ubuntu,codename=\${distro_codename}-security,label=Ubuntu\";
|
"origin=Ubuntu,codename=\${distro_codename}-security,label=Ubuntu";
|
||||||
\"origin=Ubuntu,codename=\${distro_codename}-updates,label=Ubuntu\";
|
"origin=Ubuntu,codename=\${distro_codename}-updates,label=Ubuntu";
|
||||||
};
|
};
|
||||||
|
|
||||||
Unattended-Upgrade::Package-Blacklist {
|
Unattended-Upgrade::Package-Blacklist {
|
||||||
};
|
};
|
||||||
|
|
||||||
Unattended-Upgrade::Remove-Unused-Kernel-Packages \"true\";
|
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
||||||
Unattended-Upgrade::Remove-New-Unused-Dependencies \"true\";
|
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
|
||||||
Unattended-Upgrade::Remove-Unused-Dependencies \"true\";
|
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||||
|
|
||||||
Unattended-Upgrade::SyslogEnable \"true\";
|
Unattended-Upgrade::SyslogEnable "true";
|
||||||
Unattended-Upgrade::Verbose \"false\";
|
Unattended-Upgrade::Verbose "false";
|
||||||
|
|
||||||
Unattended-Upgrade::Mail \"\";
|
Unattended-Upgrade::Mail "";
|
||||||
Unattended-Upgrade::MailReport \"on-change\";
|
Unattended-Upgrade::MailReport "on-change";
|
||||||
|
|
||||||
Unattended-Upgrade::OnlyOnACPower \"false\";
|
Unattended-Upgrade::OnlyOnACPower "false";
|
||||||
Unattended-Upgrade::Skip-Updates-On-Metered-Connections \"false\";
|
Unattended-Upgrade::Skip-Updates-On-Metered-Connections "false";
|
||||||
|
|
||||||
Unattended-Upgrade::Automatic-Reboot \"${AUTO_REBOOT}\";
|
Unattended-Upgrade::Automatic-Reboot "${AUTO_REBOOT}";
|
||||||
Unattended-Upgrade::Automatic-Reboot-WithUsers \"false\";
|
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
|
||||||
Unattended-Upgrade::Automatic-Reboot-Time \"${AUTO_REBOOT_TIME}\";
|
Unattended-Upgrade::Automatic-Reboot-Time "${AUTO_REBOOT_TIME}";
|
||||||
EOF"
|
EOF
|
||||||
|
|
||||||
log "Enabling unattended-upgrades service if present"
|
log "Enabling unattended-upgrades service if present"
|
||||||
run systemctl enable unattended-upgrades || true
|
ensure_service_enabled_and_restarted unattended-upgrades
|
||||||
run systemctl restart unattended-upgrades || true
|
|
||||||
|
|
||||||
log "Running unattended-upgrades dry-run validation"
|
log "Running unattended-upgrades dry-run validation"
|
||||||
if [[ "$DRY_RUN" == "false" ]]; then
|
if [[ "$DRY_RUN" == "false" ]]; then
|
||||||
@ -208,40 +301,40 @@ log "Installing and configuring basic SSH protection"
|
|||||||
run apt-get install -y ufw fail2ban
|
run apt-get install -y ufw fail2ban
|
||||||
|
|
||||||
# UFW: allow current SSH port safely.
|
# UFW: allow current SSH port safely.
|
||||||
SSH_PORT="$(awk '
|
SSH_PORT="$(detect_ssh_port)"
|
||||||
$1 ~ /^[Pp]ort$/ && $2 ~ /^[0-9]+$/ {print $2}
|
|
||||||
' /etc/ssh/sshd_config | tail -n 1)"
|
|
||||||
|
|
||||||
if [[ -z "$SSH_PORT" ]]; then
|
|
||||||
SSH_PORT="22"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Detected SSH port: $SSH_PORT"
|
log "Detected SSH port: $SSH_PORT"
|
||||||
|
|
||||||
run ufw allow "${SSH_PORT}/tcp" comment "SSH"
|
run ufw allow "${SSH_PORT}/tcp" comment "SSH"
|
||||||
run ufw --force enable
|
run ufw --force enable
|
||||||
run ufw status verbose | tee -a "$LOG_FILE"
|
if [[ "$DRY_RUN" == "false" ]]; then
|
||||||
|
ufw status verbose | tee -a "$LOG_FILE"
|
||||||
|
else
|
||||||
|
log "DRY RUN: skipping ufw status command"
|
||||||
|
fi
|
||||||
|
|
||||||
# Fail2ban minimal SSH jail.
|
# Fail2ban minimal SSH jail.
|
||||||
run_bash "mkdir -p /etc/fail2ban/jail.d"
|
run mkdir -p /etc/fail2ban/jail.d
|
||||||
|
|
||||||
run_bash "cat > /etc/fail2ban/jail.d/sshd.local <<'EOF'
|
safe_write_file /etc/fail2ban/jail.d/sshd.local 0644 <<'EOF'
|
||||||
[sshd]
|
[sshd]
|
||||||
enabled = true
|
enabled = true
|
||||||
port = ssh
|
port = ssh
|
||||||
filter = sshd
|
filter = sshd
|
||||||
backend = systemd
|
backend = auto
|
||||||
maxretry = 5
|
maxretry = 5
|
||||||
findtime = 10m
|
findtime = 10m
|
||||||
bantime = 1h
|
bantime = 1h
|
||||||
EOF"
|
EOF
|
||||||
|
|
||||||
run systemctl enable fail2ban
|
ensure_service_enabled_and_restarted fail2ban
|
||||||
run systemctl restart fail2ban
|
|
||||||
|
if [[ "$DRY_RUN" == "false" ]] && have_systemd && command -v fail2ban-client >/dev/null 2>&1; then
|
||||||
|
fail2ban-client ping | tee -a "$LOG_FILE" || die "fail2ban-client ping failed"
|
||||||
|
fi
|
||||||
|
|
||||||
log "Checking package integrity database availability"
|
log "Checking package integrity database availability"
|
||||||
if [[ "$DRY_RUN" == "false" ]]; then
|
if [[ "$DRY_RUN" == "false" ]]; then
|
||||||
debsums_init_log="/tmp/debsums-init.log"
|
|
||||||
if command -v debsums >/dev/null 2>&1; then
|
if command -v debsums >/dev/null 2>&1; then
|
||||||
log "debsums installed. You can later run: sudo debsums -s"
|
log "debsums installed. You can later run: sudo debsums -s"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user