Last active
February 21, 2026 14:13
-
-
Save maciekish/6fb4665c5c5c9f2c6a8641bacaf6282e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Remote run (from target Linux host): | |
| # curl -fsSL https://gist.githubusercontent.com/maciekish/6fb4665c5c5c9f2c6a8641bacaf6282e/raw | bash | |
| # Optional args: | |
| # curl -fsSL https://gist.githubusercontent.com/maciekish/6fb4665c5c5c9f2c6a8641bacaf6282e/raw | bash -s -- --dry-run --yes | |
| # Source gist page: | |
| # https://gist.github.com/maciekish/6fb4665c5c5c9f2c6a8641bacaf6282e | |
| set -Eeuo pipefail | |
| IFS=$'\n\t' | |
| SCRIPT_VERSION="1.0.0" | |
| LOG_PREFIX="[algo-cleanup]" | |
| DRY_RUN=0 | |
| ASSUME_YES=0 | |
| FORCE=0 | |
| KEEP_ALGO_USER=0 | |
| KEEP_PACKAGES=0 | |
| PURGE_EXTRA_TOOLS=0 | |
| SKIP_SSHD_RESET=0 | |
| NO_RESTART=0 | |
| NO_FIREWALL_FLUSH=0 | |
| BACKUP_DIR="" | |
| TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)" | |
| CURRENT_LOGIN_USER="${SUDO_USER:-${USER:-}}" | |
| readonly SERVICE_UNITS=( | |
| "wg-quick@wg0.service" | |
| "strongswan-starter.service" | |
| "strongswan.service" | |
| "dnscrypt-proxy.socket" | |
| "dnscrypt-proxy.service" | |
| "netfilter-persistent.service" | |
| "privacy-shutdown-cleanup.service" | |
| ) | |
| readonly CRON_ENTRY_NAMES=( | |
| "Adblock hosts update" | |
| "Enhanced privacy log rotation" | |
| "Privacy auto cleanup" | |
| "Privacy auto cleanup weekly" | |
| "Privacy auto cleanup monthly" | |
| ) | |
| readonly ALGO_FILE_PATHS=( | |
| "/etc/systemd/network/10-algo-lo100.network" | |
| "/etc/netplan/99-algo-ipv6-egress.yaml" | |
| "/etc/systemd/system/dnscrypt-proxy.service.d/99-algo.conf" | |
| "/etc/systemd/system/dnscrypt-proxy.service.d/90-security-hardening.conf" | |
| "/etc/systemd/system/dnscrypt-proxy.socket.d/10-algo-override.conf" | |
| "/etc/systemd/system/wg-quick@wg0.service.d/90-security-hardening.conf" | |
| "/etc/systemd/system/strongswan-starter.service.d/100-CustomLimitations.conf" | |
| "/etc/systemd/system/strongswan.service.d/100-CustomLimitations.conf" | |
| "/etc/systemd/system/privacy-shutdown-cleanup.service" | |
| "/etc/apparmor.d/usr.bin.dnscrypt-proxy" | |
| "/etc/apparmor.d/local/usr.lib.ipsec.charon" | |
| "/etc/rsyslog.d/45-privacy-minimal.conf" | |
| "/etc/rsyslog.d/46-privacy-ssh-filter.conf" | |
| "/etc/rsyslog.d/47-privacy-auth-filter.conf" | |
| "/etc/rsyslog.d/48-privacy-kernel-filter.conf" | |
| "/etc/rsyslog.d/49-privacy-vpn-filter.conf" | |
| "/etc/logrotate.d/99-privacy-enhanced" | |
| "/etc/logrotate.d/99-auth-privacy" | |
| "/etc/logrotate.d/99-kern-privacy" | |
| "/usr/local/sbin/adblock.sh" | |
| "/usr/local/bin/privacy-auto-cleanup.sh" | |
| "/usr/local/bin/privacy-log-cleanup.sh" | |
| "/usr/local/bin/privacy-monitor.sh" | |
| "/var/log/privacy-cleanup.log" | |
| "/etc/sysctl.d/99-privacy.conf" | |
| "/etc/apt/apt.conf.d/10periodic" | |
| "/etc/apt/apt.conf.d/50unattended-upgrades" | |
| "/etc/apt/apt.conf.d/50-dnscrypt-proxy-unattended-upgrades" | |
| "/etc/iptables/rules.v4" | |
| "/etc/iptables/rules.v6" | |
| "/etc/ipsec.conf" | |
| "/etc/ipsec.secrets" | |
| "/etc/strongswan.conf" | |
| "/etc/strongswan.d/charon.conf" | |
| "/etc/ipsec.d/crls/algo.root.pem" | |
| "/etc/sudoers.d/10-algo-user" | |
| ) | |
| readonly ALGO_DIR_PATHS=( | |
| "/etc/wireguard" | |
| "/etc/dnscrypt-proxy" | |
| "/var/cache/dnscrypt-proxy" | |
| "/etc/ipsec.d" | |
| "/etc/strongswan.d" | |
| "/var/lib/strongswan" | |
| "/var/jail" | |
| ) | |
| readonly CORE_PACKAGES=( | |
| "dnscrypt-proxy" | |
| "wireguard" | |
| "wireguard-tools" | |
| "strongswan" | |
| "strongswan-starter" | |
| "strongswan-charon" | |
| "strongswan-libcharon" | |
| "strongswan-libcharon-extra-plugins" | |
| "strongswan-pki" | |
| "strongswan-swanctl" | |
| "netfilter-persistent" | |
| "iptables-persistent" | |
| "unattended-upgrades" | |
| ) | |
| readonly EXTRA_TOOL_PACKAGES=( | |
| "apparmor-utils" | |
| "cgroup-tools" | |
| "uuid-runtime" | |
| "screen" | |
| "git" | |
| ) | |
| usage() { | |
| cat <<'EOF' | |
| Usage: algo-uninstall-cleanup.sh [options] | |
| Comprehensively remove host-side changes made by trailofbits/algo. | |
| Run as root on the target Linux host. | |
| Options: | |
| -n, --dry-run Show what would run without changing the system | |
| -y, --yes Skip interactive confirmation | |
| --force Run even if no Algo markers are detected | |
| --keep-algo-user Keep the cloud-init "algo" login user | |
| --keep-packages Keep installed packages (remove configs/users only) | |
| --purge-extra-tools Also purge extra tools installed by Algo (git/screen/etc) | |
| --skip-sshd-reset Do not reset cloud-init sshd_config to distro defaults | |
| --no-restart Do not restart services at end | |
| --no-firewall-flush Do not flush iptables/ip6tables | |
| --backup-dir <dir> Override backup output dir (default: /var/backups/algo-cleanup-<timestamp>) | |
| -h, --help Show this help | |
| Examples: | |
| sudo ./algo-uninstall-cleanup.sh --dry-run | |
| sudo ./algo-uninstall-cleanup.sh --yes | |
| sudo ./algo-uninstall-cleanup.sh --yes --keep-algo-user --skip-sshd-reset | |
| EOF | |
| } | |
| log() { | |
| printf '%s %s\n' "$LOG_PREFIX" "$*" | |
| } | |
| warn() { | |
| printf '%s WARNING: %s\n' "$LOG_PREFIX" "$*" >&2 | |
| } | |
| die() { | |
| printf '%s ERROR: %s\n' "$LOG_PREFIX" "$*" >&2 | |
| exit 1 | |
| } | |
| print_cmd() { | |
| local rendered="" | |
| printf -v rendered '%q ' "$@" | |
| printf '%s' "${rendered% }" | |
| } | |
| run_cmd() { | |
| if (( DRY_RUN )); then | |
| log "[dry-run] $(print_cmd "$@")" | |
| return 0 | |
| fi | |
| "$@" | |
| } | |
| run_ignore_failure() { | |
| if (( DRY_RUN )); then | |
| log "[dry-run] $(print_cmd "$@")" | |
| return 0 | |
| fi | |
| "$@" >/dev/null 2>&1 || true | |
| } | |
| parse_args() { | |
| while (( $# > 0 )); do | |
| case "$1" in | |
| -n|--dry-run) | |
| DRY_RUN=1 | |
| ;; | |
| -y|--yes) | |
| ASSUME_YES=1 | |
| ;; | |
| --force) | |
| FORCE=1 | |
| ;; | |
| --keep-algo-user) | |
| KEEP_ALGO_USER=1 | |
| ;; | |
| --keep-packages) | |
| KEEP_PACKAGES=1 | |
| ;; | |
| --purge-extra-tools) | |
| PURGE_EXTRA_TOOLS=1 | |
| ;; | |
| --skip-sshd-reset) | |
| SKIP_SSHD_RESET=1 | |
| ;; | |
| --no-restart) | |
| NO_RESTART=1 | |
| ;; | |
| --no-firewall-flush) | |
| NO_FIREWALL_FLUSH=1 | |
| ;; | |
| --backup-dir) | |
| shift | |
| (( $# > 0 )) || die "--backup-dir requires a value" | |
| BACKUP_DIR="$1" | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| die "Unknown option: $1 (use --help)" | |
| ;; | |
| esac | |
| shift | |
| done | |
| } | |
| require_root_linux() { | |
| [[ "$(uname -s)" == "Linux" ]] || die "This script supports Linux target hosts only." | |
| (( EUID == 0 )) || die "Run as root (sudo)." | |
| } | |
| detect_algo_markers() { | |
| local markers=0 | |
| [[ -f /etc/systemd/network/10-algo-lo100.network ]] && (( markers++ )) | |
| [[ -f /etc/wireguard/wg0.conf ]] && (( markers++ )) | |
| [[ -f /etc/rsyslog.d/49-privacy-vpn-filter.conf ]] && (( markers++ )) | |
| [[ -f /etc/systemd/system/privacy-shutdown-cleanup.service ]] && (( markers++ )) | |
| [[ -f /usr/local/sbin/adblock.sh ]] && (( markers++ )) | |
| getent passwd strongswan >/dev/null && (( markers++ )) | |
| getent passwd algo >/dev/null && (( markers++ )) | |
| if [[ -f /etc/ipsec.conf ]] && grep -q 'ikev2-pubkey' /etc/ipsec.conf; then | |
| (( markers++ )) | |
| fi | |
| if [[ -f /etc/ssh/sshd_config ]] && grep -Eq '^[[:space:]]*AllowGroups[[:space:]]+algo([[:space:]]|$)' /etc/ssh/sshd_config; then | |
| (( markers++ )) | |
| fi | |
| if [[ -f /etc/ssh/sshd_config ]] && grep -q 'ANSIBLE MANAGED BLOCK ssh_tunneling_role' /etc/ssh/sshd_config; then | |
| (( markers++ )) | |
| fi | |
| echo "$markers" | |
| } | |
| confirm_action() { | |
| local marker_count="$1" | |
| log "Detected $marker_count Algo marker(s)." | |
| if (( ASSUME_YES )); then | |
| return | |
| fi | |
| cat <<EOF | |
| This script will remove Algo VPN host-side changes, including: | |
| - VPN services/configs (WireGuard, StrongSwan, DNSCrypt) | |
| - Algo-created users/groups (including cloud-init user "algo" unless --keep-algo-user) | |
| - Algo firewall/sysctl/cron/rsyslog/logrotate/privacy artifacts | |
| It can affect SSH access if your current access depends on Algo's sshd_config. | |
| Backup location: ${BACKUP_DIR} | |
| EOF | |
| printf 'Continue? [y/N]: ' | |
| local answer | |
| read -r answer | |
| [[ "$answer" =~ ^[Yy]$ ]] || die "Aborted." | |
| } | |
| create_backup() { | |
| if [[ -z "$BACKUP_DIR" ]]; then | |
| BACKUP_DIR="/var/backups/algo-cleanup-${TIMESTAMP}" | |
| fi | |
| if (( DRY_RUN )); then | |
| log "[dry-run] would create backup dir: $BACKUP_DIR" | |
| return | |
| fi | |
| mkdir -p "$BACKUP_DIR" | |
| date -u +"%Y-%m-%dT%H:%M:%SZ" > "${BACKUP_DIR}/created_at_utc.txt" | |
| # Operational snapshots | |
| (crontab -u root -l || true) > "${BACKUP_DIR}/root.crontab.before" | |
| (getent passwd || true) > "${BACKUP_DIR}/passwd.before" | |
| (getent group || true) > "${BACKUP_DIR}/group.before" | |
| (dpkg -l || true) > "${BACKUP_DIR}/dpkg.before" | |
| (sysctl -a 2>/dev/null || true) > "${BACKUP_DIR}/sysctl.before" | |
| (iptables-save 2>/dev/null || true) > "${BACKUP_DIR}/iptables.before.v4" | |
| (ip6tables-save 2>/dev/null || true) > "${BACKUP_DIR}/iptables.before.v6" | |
| local existing_paths=() | |
| local path | |
| for path in \ | |
| /etc/ssh/sshd_config \ | |
| /etc/pam.d/login \ | |
| /etc/pam.d/sshd \ | |
| /etc/systemd/resolved.conf \ | |
| /etc/systemd/journald.conf \ | |
| /etc/sysctl.conf \ | |
| /etc/modules \ | |
| /etc/modules-load.d \ | |
| /etc/logrotate.d/rsyslog \ | |
| /etc/logrotate.d/rsyslog.disabled \ | |
| /root/.bashrc \ | |
| /home/ubuntu/.bashrc \ | |
| "${ALGO_FILE_PATHS[@]}" \ | |
| "${ALGO_DIR_PATHS[@]}"; do | |
| if [[ -e "$path" || -L "$path" ]]; then | |
| existing_paths+=("$path") | |
| fi | |
| done | |
| if (( ${#existing_paths[@]} > 0 )); then | |
| tar -czf "${BACKUP_DIR}/files.before.tar.gz" --absolute-names "${existing_paths[@]}" >/dev/null 2>&1 || true | |
| fi | |
| log "Backup written to ${BACKUP_DIR}" | |
| } | |
| remove_exact_line() { | |
| local file="$1" | |
| local line="$2" | |
| [[ -f "$file" ]] || return 0 | |
| grep -Fqx "$line" "$file" || return 0 | |
| if (( DRY_RUN )); then | |
| log "[dry-run] remove exact line from ${file}: ${line}" | |
| return 0 | |
| fi | |
| local tmp | |
| tmp="$(mktemp)" | |
| awk -v target="$line" '$0 != target {print}' "$file" > "$tmp" | |
| cat "$tmp" > "$file" | |
| rm -f "$tmp" | |
| } | |
| remove_lines_regex() { | |
| local file="$1" | |
| local regex="$2" | |
| [[ -f "$file" ]] || return 0 | |
| if ! grep -Eq "$regex" "$file"; then | |
| return 0 | |
| fi | |
| run_cmd sed -i -E "\\#${regex}#d" "$file" | |
| } | |
| remove_block_between_markers() { | |
| local file="$1" | |
| local start_marker="$2" | |
| local end_marker="$3" | |
| [[ -f "$file" ]] || return 0 | |
| grep -Fq "$start_marker" "$file" || return 0 | |
| if (( DRY_RUN )); then | |
| log "[dry-run] remove block in ${file}: ${start_marker} ... ${end_marker}" | |
| return 0 | |
| fi | |
| local tmp | |
| tmp="$(mktemp)" | |
| awk -v start="$start_marker" -v end="$end_marker" ' | |
| $0 == start {skip=1; next} | |
| $0 == end {skip=0; next} | |
| !skip {print} | |
| ' "$file" > "$tmp" | |
| cat "$tmp" > "$file" | |
| rm -f "$tmp" | |
| } | |
| remove_ansible_cron_entry() { | |
| local entry_name="$1" | |
| command -v crontab >/dev/null 2>&1 || return 0 | |
| local tmp_old tmp_new | |
| tmp_old="$(mktemp)" | |
| tmp_new="$(mktemp)" | |
| if ! crontab -u root -l > "$tmp_old" 2>/dev/null; then | |
| rm -f "$tmp_old" "$tmp_new" | |
| return 0 | |
| fi | |
| awk -v marker="#Ansible: ${entry_name}" ' | |
| $0 == marker {skip=1; next} | |
| skip==1 {skip=0; next} | |
| {print} | |
| ' "$tmp_old" > "$tmp_new" | |
| if ! cmp -s "$tmp_old" "$tmp_new"; then | |
| if (( DRY_RUN )); then | |
| log "[dry-run] remove root cron entry: ${entry_name}" | |
| else | |
| crontab -u root "$tmp_new" | |
| fi | |
| fi | |
| rm -f "$tmp_old" "$tmp_new" | |
| } | |
| remove_cron_entries() { | |
| local name | |
| for name in "${CRON_ENTRY_NAMES[@]}"; do | |
| remove_ansible_cron_entry "$name" | |
| done | |
| } | |
| stop_disable_units() { | |
| command -v systemctl >/dev/null 2>&1 || return 0 | |
| local unit | |
| for unit in "${SERVICE_UNITS[@]}"; do | |
| run_ignore_failure systemctl disable --now "$unit" | |
| done | |
| } | |
| reset_sshd_if_algo_managed() { | |
| local sshd_cfg="/etc/ssh/sshd_config" | |
| [[ -f "$sshd_cfg" ]] || return 0 | |
| remove_block_between_markers \ | |
| "$sshd_cfg" \ | |
| "# BEGIN ANSIBLE MANAGED BLOCK ssh_tunneling_role" \ | |
| "# END ANSIBLE MANAGED BLOCK ssh_tunneling_role" | |
| if (( SKIP_SSHD_RESET )); then | |
| return 0 | |
| fi | |
| if ! grep -Eq '^[[:space:]]*AllowGroups[[:space:]]+algo([[:space:]]|$)' "$sshd_cfg"; then | |
| return 0 | |
| fi | |
| log "Detected cloud-init Algo sshd_config; resetting to distro default." | |
| if [[ -f /usr/share/openssh/sshd_config ]]; then | |
| run_cmd install -o root -g root -m 0644 /usr/share/openssh/sshd_config "$sshd_cfg" | |
| return 0 | |
| fi | |
| if command -v apt-get >/dev/null 2>&1; then | |
| run_cmd env DEBIAN_FRONTEND=noninteractive \ | |
| apt-get -y install --reinstall -o Dpkg::Options::=--force-confnew openssh-server | |
| return 0 | |
| fi | |
| warn "Could not find /usr/share/openssh/sshd_config or apt-get; removing AllowGroups algo line only." | |
| remove_lines_regex "$sshd_cfg" '^[[:space:]]*AllowGroups[[:space:]]+algo([[:space:]]|$)' | |
| } | |
| restore_pam_motd_entries() { | |
| local file | |
| for file in /etc/pam.d/login /etc/pam.d/sshd; do | |
| [[ -f "$file" ]] || continue | |
| remove_exact_line "$file" "# MOTD DISABLED" | |
| if ! grep -Eq 'pam_motd\.so' "$file"; then | |
| if (( DRY_RUN )); then | |
| log "[dry-run] append pam_motd lines to ${file}" | |
| else | |
| { | |
| echo "session optional pam_motd.so motd=/run/motd.dynamic" | |
| echo "session optional pam_motd.so noupdate" | |
| } >> "$file" | |
| fi | |
| fi | |
| done | |
| } | |
| restore_journald_conf() { | |
| local conf="/etc/systemd/journald.conf" | |
| [[ -f "$conf" ]] || return 0 | |
| local backups=() | |
| local backup | |
| while IFS= read -r backup; do | |
| backups+=("$backup") | |
| done < <(ls -1tr /etc/systemd/journald.conf.* 2>/dev/null || true) | |
| if (( ${#backups[@]} > 0 )); then | |
| log "Restoring ${conf} from oldest backup: ${backups[0]}" | |
| run_cmd cp -f -- "${backups[0]}" "$conf" | |
| return 0 | |
| fi | |
| remove_lines_regex "$conf" '^[[:space:]]*#?[[:space:]]*(MaxRetentionSec|MaxFileSec|SystemMaxUse|SystemMaxFileSize|ForwardToSyslog|Storage)[[:space:]]*=' | |
| } | |
| remove_privacy_bashrc_lines() { | |
| local file | |
| for file in /root/.bashrc /home/ubuntu/.bashrc; do | |
| [[ -f "$file" ]] || continue | |
| remove_exact_line "$file" "# Privacy enhancement: disable bash history" | |
| remove_exact_line "$file" "export HISTFILE=/dev/null" | |
| remove_exact_line "$file" "export HISTSIZE=0" | |
| remove_exact_line "$file" "export HISTFILESIZE=0" | |
| remove_exact_line "$file" "unset HISTFILE" | |
| done | |
| } | |
| remove_generated_bash_logout() { | |
| local file="/etc/bash.bash_logout" | |
| [[ -f "$file" ]] || return 0 | |
| if grep -Fq "Generated by Algo VPN privacy role" "$file"; then | |
| run_cmd rm -f -- "$file" | |
| fi | |
| } | |
| remove_af_key_persistence() { | |
| remove_exact_line "/etc/modules" "af_key" | |
| local modules_file | |
| shopt -s nullglob | |
| for modules_file in /etc/modules-load.d/*.conf; do | |
| remove_exact_line "$modules_file" "af_key" | |
| # Remove empty config files left behind | |
| if [[ -f "$modules_file" ]] && [[ ! -s "$modules_file" ]]; then | |
| run_cmd rm -f -- "$modules_file" | |
| fi | |
| done | |
| shopt -u nullglob | |
| } | |
| remove_sysctl_keys() { | |
| local sysctl_file="/etc/sysctl.conf" | |
| if [[ -f "$sysctl_file" ]]; then | |
| local key escaped | |
| for key in \ | |
| "net.ipv4.ip_forward" \ | |
| "net.ipv4.conf.all.forwarding" \ | |
| "net.ipv4.conf.all.route_localnet" \ | |
| "net.ipv6.conf.all.forwarding" \ | |
| "kernel.printk" \ | |
| "kernel.dmesg_restrict"; do | |
| escaped="${key//./\\.}" | |
| remove_lines_regex "$sysctl_file" "^[[:space:]]*${escaped}[[:space:]]*=" | |
| done | |
| fi | |
| # Runtime rollback where safe | |
| run_ignore_failure sysctl -w net.ipv4.ip_forward=0 | |
| run_ignore_failure sysctl -w net.ipv4.conf.all.forwarding=0 | |
| run_ignore_failure sysctl -w net.ipv4.conf.all.route_localnet=0 | |
| run_ignore_failure sysctl -w net.ipv6.conf.all.forwarding=0 | |
| run_ignore_failure sysctl -w kernel.dmesg_restrict=0 | |
| } | |
| remove_algo_paths() { | |
| local path | |
| for path in "${ALGO_FILE_PATHS[@]}"; do | |
| if [[ -e "$path" || -L "$path" ]]; then | |
| run_cmd rm -rf -- "$path" | |
| fi | |
| done | |
| for path in "${ALGO_DIR_PATHS[@]}"; do | |
| if [[ -e "$path" || -L "$path" ]]; then | |
| run_cmd rm -rf -- "$path" | |
| fi | |
| done | |
| } | |
| restore_logrotate_default_if_needed() { | |
| if [[ -f /etc/logrotate.d/rsyslog.disabled && ! -e /etc/logrotate.d/rsyslog ]]; then | |
| run_cmd mv /etc/logrotate.d/rsyslog.disabled /etc/logrotate.d/rsyslog | |
| fi | |
| } | |
| delete_user_if_present() { | |
| local username="$1" | |
| getent passwd "$username" >/dev/null || return 0 | |
| if [[ "$CURRENT_LOGIN_USER" == "$username" ]]; then | |
| warn "Skipping removal of active login user '${username}'." | |
| return 0 | |
| fi | |
| run_ignore_failure pkill -KILL -u "$username" | |
| run_ignore_failure userdel -r "$username" | |
| } | |
| remove_algo_users_and_groups() { | |
| local jail_users=() | |
| local user | |
| while IFS=: read -r user _ _ _ _ home _; do | |
| if [[ "$home" == /var/jail/* ]]; then | |
| jail_users+=("$user") | |
| fi | |
| done < /etc/passwd | |
| for user in "${jail_users[@]}"; do | |
| delete_user_if_present "$user" | |
| done | |
| delete_user_if_present "strongswan" | |
| if (( ! KEEP_ALGO_USER )); then | |
| delete_user_if_present "algo" | |
| else | |
| log "Keeping 'algo' user as requested." | |
| fi | |
| if getent group algo >/dev/null; then | |
| local gid secondary_members | |
| gid="$(getent group algo | cut -d: -f3)" | |
| secondary_members="$(getent group algo | cut -d: -f4)" | |
| if awk -F: -v g="$gid" '$4 == g {found=1} END{exit found ? 0 : 1}' /etc/passwd; then | |
| warn "Keeping group 'algo' because it is still a primary group for existing user(s)." | |
| elif [[ -n "$secondary_members" ]]; then | |
| warn "Keeping group 'algo' because it still has member(s): ${secondary_members}" | |
| else | |
| run_ignore_failure groupdel algo | |
| fi | |
| fi | |
| } | |
| purge_algo_packages() { | |
| if (( KEEP_PACKAGES )); then | |
| log "Keeping packages as requested (--keep-packages)." | |
| return 0 | |
| fi | |
| command -v apt-get >/dev/null 2>&1 || { | |
| warn "apt-get not found; skipping package purge." | |
| return 0 | |
| } | |
| local candidates=("${CORE_PACKAGES[@]}") | |
| if (( PURGE_EXTRA_TOOLS )); then | |
| candidates+=("${EXTRA_TOOL_PACKAGES[@]}") | |
| fi | |
| local installed=() | |
| local pkg | |
| for pkg in "${candidates[@]}"; do | |
| if dpkg-query -W -f='${Status}' "$pkg" 2>/dev/null | grep -q '^install ok installed$'; then | |
| installed+=("$pkg") | |
| fi | |
| done | |
| if (( ${#installed[@]} == 0 )); then | |
| log "No candidate Algo packages currently installed." | |
| return 0 | |
| fi | |
| log "Purging packages: ${installed[*]}" | |
| run_cmd env DEBIAN_FRONTEND=noninteractive apt-get -y purge "${installed[@]}" | |
| run_cmd env DEBIAN_FRONTEND=noninteractive apt-get -y autoremove --purge | |
| } | |
| flush_firewall_rules() { | |
| if (( NO_FIREWALL_FLUSH )); then | |
| log "Skipping firewall flush (--no-firewall-flush)." | |
| return 0 | |
| fi | |
| local tool table | |
| for tool in iptables ip6tables; do | |
| command -v "$tool" >/dev/null 2>&1 || continue | |
| run_ignore_failure "$tool" -P INPUT ACCEPT | |
| run_ignore_failure "$tool" -P FORWARD ACCEPT | |
| run_ignore_failure "$tool" -P OUTPUT ACCEPT | |
| for table in filter nat mangle raw security; do | |
| run_ignore_failure "$tool" -t "$table" -F | |
| run_ignore_failure "$tool" -t "$table" -X | |
| done | |
| done | |
| run_ignore_failure ip xfrm state flush | |
| run_ignore_failure ip xfrm policy flush | |
| } | |
| reset_iptables_alternatives() { | |
| command -v update-alternatives >/dev/null 2>&1 || return 0 | |
| run_ignore_failure update-alternatives --auto iptables | |
| run_ignore_failure update-alternatives --auto ip6tables | |
| } | |
| reload_runtime() { | |
| remove_lines_regex /etc/systemd/resolved.conf '^[[:space:]]*FallbackDNS[[:space:]]*=' | |
| run_ignore_failure modprobe -r af_key | |
| run_ignore_failure sysctl --system | |
| if command -v netplan >/dev/null 2>&1; then | |
| run_ignore_failure netplan apply | |
| fi | |
| if command -v systemctl >/dev/null 2>&1; then | |
| run_ignore_failure systemctl daemon-reload | |
| if (( ! NO_RESTART )); then | |
| run_ignore_failure systemctl restart systemd-networkd | |
| run_ignore_failure systemctl restart systemd-resolved | |
| run_ignore_failure systemctl restart rsyslog | |
| run_ignore_failure systemctl restart systemd-journald | |
| run_ignore_failure systemctl restart ssh | |
| run_ignore_failure systemctl restart sshd | |
| fi | |
| fi | |
| } | |
| cleanup_empty_dropin_dirs() { | |
| local dir | |
| for dir in \ | |
| /etc/systemd/system/dnscrypt-proxy.service.d \ | |
| /etc/systemd/system/dnscrypt-proxy.socket.d \ | |
| /etc/systemd/system/wg-quick@wg0.service.d \ | |
| /etc/systemd/system/strongswan-starter.service.d \ | |
| /etc/systemd/system/strongswan.service.d; do | |
| if [[ -d "$dir" ]]; then | |
| run_ignore_failure rmdir "$dir" | |
| fi | |
| done | |
| } | |
| main() { | |
| parse_args "$@" | |
| require_root_linux | |
| if [[ -z "$BACKUP_DIR" ]]; then | |
| BACKUP_DIR="/var/backups/algo-cleanup-${TIMESTAMP}" | |
| fi | |
| local marker_count | |
| marker_count="$(detect_algo_markers)" | |
| if (( marker_count == 0 && ! FORCE )); then | |
| die "No Algo markers found. Re-run with --force if you still want cleanup." | |
| fi | |
| confirm_action "$marker_count" | |
| create_backup | |
| stop_disable_units | |
| remove_cron_entries | |
| reset_sshd_if_algo_managed | |
| restore_pam_motd_entries | |
| remove_privacy_bashrc_lines | |
| remove_generated_bash_logout | |
| restore_journald_conf | |
| remove_af_key_persistence | |
| remove_sysctl_keys | |
| remove_algo_paths | |
| restore_logrotate_default_if_needed | |
| remove_algo_users_and_groups | |
| purge_algo_packages | |
| flush_firewall_rules | |
| reset_iptables_alternatives | |
| cleanup_empty_dropin_dirs | |
| reload_runtime | |
| log "Cleanup complete." | |
| log "Backup directory: ${BACKUP_DIR}" | |
| log "Review SSH/cloud firewall rules before disconnecting if sshd settings changed." | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment