Skip to content

Harden

I have analized a clean installation of Linux Mint OS and discovered some vulnerabilities that can be hardened. During the analysis I created scripts that created reports and for the hardening process. I will also be creating scripts in this hardening phase that people can run on their own Linux Mint OS. So, the scripts created in this hardening phase will become a sort of hardening toolkit for Linux Mint OS.


I will follow the same structure as in the analysis and create scripts that address each vulnerability. I will also create a restore script in case people want to roll back the changes made by the vulnerability hardening script. I will then run the analysis scripts again to see before-and-after comparison results, create a validation report and if I have time I will make a full Bootable FabFutures CyberOS ISO from a hardened clean Linux Mint OS. I will start working in spirals, tackling one issue at a time, not neccesarily in order.

✅ Hardening plan + testing

Hardening Plan + testing

I will create one script per vulnerability. That script will enable/disable/harden the vulnerability but also create a restore point before making any changes in case things go wrong. The restore points will be protected using sha256sum checksums. All scripts will reuse the lib/hardening_common.sh script to avoid repeating code. The user will be asked to confirm all actions before the scripts actually run.

The workflow to create and run each script follows this structure:

  • Create the script

    • nano hardening/enable_automatic_security_updates.sh
  • Make the script executable

    • chmod +x hardening/enable_automatic_security_updates.sh
  • Run the script without creating a report

    • ./run_remote_action.sh hardening/enable_automatic_security_updates.sh
  • Create a new analysis report

    • ./run_remote.sh analysis/check_updates.sh \hardened/updates.txt
  • Then finally a new health report will be generated and compared to the baseline health report

    • ./run_remote.sh analysis/check_health.sh \hardened/health.txt

Here are screenshots after running the first script enable_automatic_security_updates.sh

Run automatic security update script

And here is the health report after running only the enable_automatic_security_updates.sh

health report

The health report shows the health score go from 25/100 in the baseline health check to 37/100

✅ Restore

Restore

All the hardening scripts save a restore point before being executed in case users want to roll back changes made by the hardening scripts, Now to check if the restore script works, this should restore the system to the way it was before the enable_automatic_security_updates.sh script was run. One thing it will not do is roll back any security updates that were installed but the configs and settings should go back to they way they were.

To run the restore script: ./run_remote_action.sh restore/restore.sh. This opens a window to select which restore point to restore.

restore test

The summary clears with no errors.

Now to check the health report again.

health check after restore

This shows the health score go back to 25/100 indicating that the restore was successful.

✅ Pre-installed Software

Pre-installed software

  • ✅ Upgrade packages

  • Remove unnecessary packages.

  • Install security-related tools.

  • (Install FabFutures class recommended packages)


Before I start removing unnecessary packages I want to upgrade the pre-existing packages because some of these upgrades may be security upgrades. Durin analysis the updates.txt report showed me that the system hadthis many pendin upgrades:

Pending upgrades: 379
Estimated security upgrades: 256

After running my apply_package_upgrades.sh script the the updates.txt report prints this:

Pending upgrades: 0
Estimated security upgrades: 0

The script successfully upgraded 635 packages.

apply_package_upgrades.sh

apply_package_upgrades.sh

#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

readonly ACTION_NAME="apply-package-upgrades"
readonly BACKUP_ROOT="/var/backups/linux-hardening/package-upgrades"
readonly SNAPSHOT_ID="$(date -u +%Y%m%dT%H%M%SZ)"
readonly SNAPSHOT_DIR="${BACKUP_ROOT}/${SNAPSHOT_ID}"
readonly CHECKSUM_FILE="${SNAPSHOT_DIR}/SHA256SUMS"
readonly LOCK_FILE="/run/lock/linux-hardening.lock"

PASS_COUNT=0
WARN_COUNT=0
FAIL_COUNT=0
SNAPSHOT_CREATED=0
CHANGES_STARTED=0
HEARTBEAT_PID=""

pass() {
    printf '[PASS] %s\n' "$1"
    PASS_COUNT=$((PASS_COUNT + 1))
}

warn() {
    printf '[WARN] %s\n' "$1"
    WARN_COUNT=$((WARN_COUNT + 1))
}

fail() {
    printf '[FAIL] %s\n' "$1"
    FAIL_COUNT=$((FAIL_COUNT + 1))
}

info() {
    printf '[INFO] %s\n' "$1"
}

fatal() {
    printf '[ERROR] %s\n' "$1" >&2
    exit 1
}

stop_heartbeat() {
    if [[ -n "$HEARTBEAT_PID" ]]; then
        kill "$HEARTBEAT_PID" >/dev/null 2>&1 || true
        wait "$HEARTBEAT_PID" 2>/dev/null || true
        HEARTBEAT_PID=""
    fi
}

start_heartbeat() {
    local message="$1"
    local interval="${2:-30}"

    stop_heartbeat

    (
        while true; do
            sleep "$interval"
            printf '[INFO] %s Still working at %s...\n' \
                "$message" \
                "$(date --iso-8601=seconds)"
        done
    ) &

    HEARTBEAT_PID=$!
}

run_with_heartbeat() {
    local description="$1"
    shift

    info "$description"
    start_heartbeat "$description"

    set +e
    "$@"
    local status=$?
    set -e

    stop_heartbeat
    return "$status"
}

cleanup() {
    stop_heartbeat
}

on_error() {
    local status=$?
    local line_number="${1:-unknown}"

    stop_heartbeat

    echo >&2
    printf '[ERROR] Package upgrade failed near line %s.\n' \
        "$line_number" >&2

    if (( SNAPSHOT_CREATED == 1 )); then
        echo "[INFO] Pre-upgrade audit snapshot:" >&2
        printf '       %s\n' "$SNAPSHOT_DIR" >&2
    fi

    if (( CHANGES_STARTED == 0 )); then
        echo "[INFO] No package upgrade had started." >&2
    fi

    exit "$status"
}

print_header() {
    echo "========================================="
    echo " Apply Package Upgrades"
    echo "========================================="
    echo
    echo "Date:        $(date --iso-8601=seconds)"
    echo "Hostname:    $(hostname)"
    echo "Snapshot ID: $SNAPSHOT_ID"
    echo
}

require_root() {
    if [[ "$EUID" -ne 0 ]]; then
        fatal "This script must be run as root."
    fi
}

require_commands() {
    local command_name

    for command_name in \
        apt-get \
        apt-mark \
        dpkg \
        dpkg-query \
        sha256sum \
        flock \
        install \
        find \
        sort \
        awk \
        grep \
        hostname \
        date
    do
        if ! command -v "$command_name" >/dev/null 2>&1; then
            fatal "Required command not found: $command_name"
        fi
    done
}

acquire_lock() {
    exec 9>"$LOCK_FILE"

    if ! flock -n 9; then
        fatal "Another hardening or restore operation is running."
    fi

    pass "Acquired the hardening operation lock."
}

confirm_action() {
    local answer=""

    echo "WARNING"
    echo "-----------------------------------------"
    echo "This action will upgrade installed packages on:"
    echo "  $(hostname)"
    echo
    echo "It will:"
    echo "  - Create a root-only pre-upgrade audit snapshot."
    echo "  - Refresh APT package indexes."
    echo "  - Upgrade installed packages."
    echo "  - Install new dependencies when required."
    echo "  - Refuse to remove installed packages."
    echo "  - Check whether a reboot is required."
    echo
    echo "It will not:"
    echo "  - Run apt autoremove."
    echo "  - Perform a release upgrade."
    echo "  - Automatically reboot the VM."
    echo "  - Create an analysis report."
    echo
    echo "Important:"
    echo "  Package upgrades cannot be reliably rolled back using"
    echo "  configuration-file backups. A VM snapshot is the safest"
    echo "  rollback mechanism."
    echo
    echo "Pre-upgrade audit data will be stored under:"
    echo "  $SNAPSHOT_DIR"
    echo

    if [[ ! -r /dev/tty || ! -w /dev/tty ]]; then
        fatal "No interactive terminal is available. No changes were made."
    fi

    printf '%s' \
        "Have you created a VM snapshot, and do you want to continue? [y/N]: " \
        >/dev/tty

    if ! IFS= read -r answer </dev/tty; then
        fatal "Could not read confirmation. No changes were made."
    fi

    case "${answer,,}" in
        y|yes)
            echo
            info "Confirmation accepted."
            ;;
        *)
            echo
            info "Confirmation declined. No changes were made."
            exit 0
            ;;
    esac
}

wait_for_apt_locks() {
    local attempt
    local lock_file
    local locked

    echo
    echo "Checking package-manager availability"
    echo "---------------------------------"

    if ! command -v fuser >/dev/null 2>&1; then
        warn "fuser is unavailable; APT will perform its own lock checks."
        return
    fi

    for ((attempt = 1; attempt <= 60; attempt++)); do
        locked=0

        for lock_file in \
            /var/lib/dpkg/lock-frontend \
            /var/lib/dpkg/lock \
            /var/lib/apt/lists/lock \
            /var/cache/apt/archives/lock
        do
            if [[ -e "$lock_file" ]] &&
               fuser "$lock_file" >/dev/null 2>&1; then
                locked=1
                break
            fi
        done

        if (( locked == 0 )); then
            pass "APT and dpkg locks are available."
            return
        fi

        if (( attempt == 1 )); then
            info "Another package-management process is running."
        fi

        if (( attempt % 15 == 0 )); then
            info "Still waiting for package-manager locks..."
        fi

        sleep 2
    done

    fatal "APT remained locked after approximately two minutes."
}

create_audit_snapshot() {
    echo
    echo "Creating pre-upgrade audit snapshot"
    echo "---------------------------------"

    install -d -o root -g root -m 0700 "$BACKUP_ROOT"

    if [[ -e "$SNAPSHOT_DIR" ]]; then
        fatal "Snapshot directory already exists: $SNAPSHOT_DIR"
    fi

    install -d -o root -g root -m 0700 "$SNAPSHOT_DIR"

    info "Recording installed package versions."
    dpkg-query \
        --show \
        --showformat='${binary:Package}\t${Version}\t${Architecture}\n' \
        2>/dev/null |
        sort >"${SNAPSHOT_DIR}/installed-packages.before.tsv"

    info "Recording manually installed packages."
    apt-mark showmanual |
        sort >"${SNAPSHOT_DIR}/manual-packages.before.txt"

    info "Recording held packages."
    apt-mark showhold |
        sort >"${SNAPSHOT_DIR}/held-packages.before.txt"

    run_with_heartbeat \
        "Simulating the package upgrade plan." \
        apt-get --simulate upgrade --with-new-pkgs \
        >"${SNAPSHOT_DIR}/upgrade-simulation.before.txt" 2>&1

    {
        echo "format_version=1"
        echo "action=$ACTION_NAME"
        echo "snapshot_id=$SNAPSHOT_ID"
        echo "created_utc=$(date -u --iso-8601=seconds)"
        echo "hostname=$(hostname)"
        echo "invoking_user=${SUDO_USER:-root}"
        echo "rollback_type=vm-snapshot-recommended"
        echo "automatic_package_downgrade_supported=no"
    } >"${SNAPSHOT_DIR}/metadata.txt"

    chown root:root "${SNAPSHOT_DIR}"/*
    chmod 0600 "${SNAPSHOT_DIR}"/*

    (
        cd "$SNAPSHOT_DIR"

        find . \
            -maxdepth 1 \
            -type f \
            ! -name SHA256SUMS \
            -print0 |
            sort -z |
            xargs -0 sha256sum >SHA256SUMS

        sha256sum --check --strict SHA256SUMS >/dev/null
    )

    chown root:root "$CHECKSUM_FILE"
    chmod 0600 "$CHECKSUM_FILE"

    SNAPSHOT_CREATED=1

    pass "Pre-upgrade audit snapshot was created and verified."
    echo "Snapshot:"
    echo "  $SNAPSHOT_DIR"
}

refresh_package_indexes() {
    echo
    echo "Refreshing package indexes"
    echo "---------------------------------"

    export DEBIAN_FRONTEND=noninteractive
    CHANGES_STARTED=1

    run_with_heartbeat \
        "Refreshing APT package indexes." \
        apt-get update

    pass "APT package indexes were refreshed."
}

show_upgrade_plan() {
    local upgrade_count
    local removal_count

    echo
    echo "Reviewing upgrade plan"
    echo "---------------------------------"

    run_with_heartbeat \
        "Rechecking the current upgrade plan." \
        apt-get --simulate upgrade --with-new-pkgs \
        >"${SNAPSHOT_DIR}/upgrade-simulation.current.txt" 2>&1

    chown root:root "${SNAPSHOT_DIR}/upgrade-simulation.current.txt"
    chmod 0600 "${SNAPSHOT_DIR}/upgrade-simulation.current.txt"

    upgrade_count="$(
        awk '
            /^Inst / {
                count++
            }

            END {
                print count + 0
            }
        ' "${SNAPSHOT_DIR}/upgrade-simulation.current.txt"
    )"

    removal_count="$(
        awk '
            /^Remv / {
                count++
            }

            END {
                print count + 0
            }
        ' "${SNAPSHOT_DIR}/upgrade-simulation.current.txt"
    )"

    info "Packages planned for upgrade or installation: $upgrade_count"
    info "Packages planned for removal: $removal_count"

    if (( removal_count > 0 )); then
        echo
        echo "Removal entries:"
        grep '^Remv ' \
            "${SNAPSHOT_DIR}/upgrade-simulation.current.txt" ||
            true

        fatal "The upgrade simulation includes package removals."
    fi

    if (( upgrade_count == 0 )); then
        pass "No package upgrades are currently required."
    else
        pass "The upgrade plan contains no package removals."
        info "The upgrade may take a while. A status message will appear every 30 seconds during quiet periods."
    fi
}

apply_upgrades() {
    echo
    echo "Applying package upgrades"
    echo "---------------------------------"

    export DEBIAN_FRONTEND=noninteractive
    CHANGES_STARTED=1

    # --with-new-pkgs allows required new dependencies.
    # apt-get upgrade does not remove installed packages.
    run_with_heartbeat \
        "Applying package upgrades." \
        apt-get upgrade \
            --yes \
            --with-new-pkgs \
            -o Dpkg::Options::="--force-confdef" \
            -o Dpkg::Options::="--force-confold"

    pass "The conservative package-upgrade command completed."
}

capture_after_state() {
    echo
    echo "Capturing post-upgrade state"
    echo "---------------------------------"

    info "Recording installed package versions after the upgrade."
    dpkg-query \
        --show \
        --showformat='${binary:Package}\t${Version}\t${Architecture}\n' \
        2>/dev/null |
        sort >"${SNAPSHOT_DIR}/installed-packages.after.tsv"

    info "Recording any remaining upgrades."
    apt list --upgradable 2>/dev/null \
        >"${SNAPSHOT_DIR}/remaining-upgrades.after.txt"

    chown root:root \
        "${SNAPSHOT_DIR}/installed-packages.after.tsv" \
        "${SNAPSHOT_DIR}/remaining-upgrades.after.txt"

    chmod 0600 \
        "${SNAPSHOT_DIR}/installed-packages.after.tsv" \
        "${SNAPSHOT_DIR}/remaining-upgrades.after.txt"

    (
        cd "$SNAPSHOT_DIR"

        find . \
            -maxdepth 1 \
            -type f \
            ! -name SHA256SUMS \
            -print0 |
            sort -z |
            xargs -0 sha256sum >SHA256SUMS

        sha256sum --check --strict SHA256SUMS >/dev/null
    )

    pass "Post-upgrade package state was recorded."
}

verify_package_state() {
    local remaining_count
    local audit_output

    echo
    echo "Verifying package state"
    echo "---------------------------------"

    remaining_count="$(
        apt list --upgradable 2>/dev/null |
            awk '
                NR > 1 && NF {
                    count++
                }

                END {
                    print count + 0
                }
            '
    )"

    if (( remaining_count == 0 )); then
        pass "No package upgrades remain pending."
    else
        warn "$remaining_count package upgrades remain pending."
        warn "These may require package removals, phased updates, or special handling."
    fi

    audit_output="$(dpkg --audit || true)"

    if [[ -n "$audit_output" ]]; then
        fail "dpkg reported packages requiring attention."
        printf '%s\n' "$audit_output"
    else
        pass "dpkg reports no incomplete package operations."
    fi

    if [[ -f /var/run/reboot-required ]]; then
        warn "A reboot is required to complete one or more upgrades."

        if [[ -f /var/run/reboot-required.pkgs ]]; then
            echo
            echo "Packages requesting a reboot:"
            echo "---------------------------------"
            cat /var/run/reboot-required.pkgs
        fi
    else
        pass "No reboot requirement is currently reported."
    fi
}

print_summary() {
    echo
    echo "Summary"
    echo "---------------------------------"
    printf 'Passed:   %d\n' "$PASS_COUNT"
    printf 'Warnings: %d\n' "$WARN_COUNT"
    printf 'Failed:   %d\n' "$FAIL_COUNT"
    echo
    echo "Audit snapshot:"
    echo "  $SNAPSHOT_DIR"
    echo
    echo "No analysis report was created."
    echo "Run check_updates.sh, check_packages.sh, and check_health.sh separately."
    echo

    if (( FAIL_COUNT > 0 )); then
        echo "Overall status: PACKAGE UPGRADE VERIFICATION FAILED"
        return 1
    fi

    if (( WARN_COUNT > 0 )); then
        echo "Overall status: PACKAGE UPGRADES APPLIED WITH WARNINGS"
        return 0
    fi

    echo "Overall status: PACKAGES SUCCESSFULLY UPGRADED"
}

main() {
    trap cleanup EXIT
    trap 'on_error "$LINENO"' ERR

    print_header
    require_root
    require_commands
    acquire_lock
    confirm_action
    wait_for_apt_locks
    create_audit_snapshot
    refresh_package_indexes
    show_upgrade_plan
    apply_upgrades
    capture_after_state
    verify_package_state
    print_summary
}

main "$@"
Automatically running services

Automatically running services

  • Turn off all unnecessary services that start automatically.

  • Not implemented yet.
✅ Open Ports

Open ports

  • Close all ports that do not need to be open.

The configure_firewall does this, it denies incoming traffic, accepts outgoing traffic, but leaves port 22 open for SSH.

✅ Firewall

Firewall

  • Enable ufw and create firewall rules.

If I run ./run_hardening.sh configure_firewall it starts a small wizard that walkd the user through what is happening. The user needs to input their password to continue.

configure firewall 1

The user is then asked if they would like to deny incoming and allow outgoing traffic.

configure firewall 2

Before making any changes to the firewall configuration a restore point is created. ufw is installed if not already installed, then the firewall is activated and the user sees a summary.

configure firewall 3

✅ configure_firewall.sh

configure_firewall.sh

#!/usr/bin/env bash
# configure_firewall.sh
# Linux Mint Hardening Toolkit

set -Eeuo pipefail
umask 077

readonly SCRIPT_DIR="$(
    cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &&
    pwd
)"
readonly PROJECT_ROOT="$(
    cd -- "$SCRIPT_DIR/.." &&
    pwd
)"
readonly HARDENING_LIB="${PROJECT_ROOT}/lib/hardening_common.sh"

if [[ ! -r "$HARDENING_LIB" ]]; then
    echo "[ERROR] Shared hardening library not found:"
    echo "        $HARDENING_LIB"
    exit 1
fi

# shellcheck disable=SC1090
source "$HARDENING_LIB"

readonly UFW_DEFAULT="/etc/default/ufw"

install_ufw() {
    if hardening_package_installed ufw; then
        hardening_pass "ufw is already installed."
        return
    fi

    export DEBIAN_FRONTEND=noninteractive
    hardening_wait_for_apt_locks
    hardening_mark_changes_started

    hardening_run_with_heartbeat \
        "Installing ufw." \
        apt-get install -y ufw

    hardening_package_installed ufw || hardening_fatal "Failed to install ufw."
}

configure_ufw() {
    hardening_mark_changes_started

    ufw --force default deny incoming
    ufw --force default allow outgoing
    ufw logging low

    systemctl enable ufw

    if ! ufw status | grep -q "^Status: active"; then
        ufw --force enable
    fi
}

verify_ufw() {
    local status
    status="$(ufw status verbose)"

    echo
    echo "Firewall status"
    echo "---------------------------------"
    printf '%s\n' "$status"

    grep -q "Status: active" <<<"$status" \
        && hardening_pass "Firewall is active." \
        || hardening_fail "Firewall is not active."

    grep -q "Default: deny (incoming)" <<<"$status" \
        && hardening_pass "Default incoming policy is deny." \
        || hardening_fail "Incoming policy is not deny."

    grep -q "allow (outgoing)" <<<"$status" \
        && hardening_pass "Default outgoing policy is allow." \
        || hardening_fail "Outgoing policy is not allow."

    grep -qi "Logging: on (low)" <<<"$status" \
        && hardening_pass "Low logging is enabled." \
        || hardening_warn "Logging is not set to low."
}

main() {
    trap hardening_cleanup EXIT
    trap 'hardening_handle_error "$?" "$LINENO"' ERR

    hardening_begin \
        "firewall" \
        "Configure UFW Firewall" \
        "configure"

    hardening_require_commands \
        apt-get grep systemctl ufw

    hardening_confirm \
        "Configure the firewall with deny incoming / allow outgoing?"

    hardening_create_restore_point
    hardening_backup_file "$UFW_DEFAULT"
    hardening_record_package "ufw"
    hardening_record_unit "ufw.service"
    hardening_finalize_restore_point

    install_ufw
    configure_ufw
    verify_ufw

    (( FAIL_COUNT == 0 )) && hardening_mark_completed

    hardening_print_summary

    (( FAIL_COUNT == 0 ))
}

main "$@"
✅ SSH

SSH

  • Create a simple way for users to securely setup SSH.

To make a simple, easy way for non-tech savvy people to setup SSH on their Linux Mint I created a Secure SSH Wizard that runs on the terminal. To start the wizard simply type:

./run_hardening.sh setup_secure_ssh_server

The run_hardening.sh is a helper script that us used by all the hardening scripts. When you run the wizard it starts by explaining what the wizard will do.

SSH first screen

Press y to continue the wizard, n to exit.

The wizard then asks which Linux account will be allowed to login remotely using SSH, and shows the user what the default user is, the user types in the user name.

SSH user and password

The user then can pick options 1-3, to use an existing public/private key pair, create a new public/private key or login with the user password.

ssh_wizard_screen_4

If the user chooses to create a new public/private key then the wizard will explain that the public/private key needs to be created on the client computer and then the user is prompted to paste the public key into the wizard.

The user is then prompted to choose the firewall acces, either make no changes to the ufw firewall, allow remote access or just on a local LAN.

ssh_wizard_screen_5

If the user chooses to allow remote access then the wizard will explain to the user that OpenSSH-server will be installed, if not already installed, explain that port 22 will be opened and a restore point will be created in case the user wants to revert the changes.

ssh_wizard_screen_6

If the user chooses to continue then the SSH settings will be applied, but first a restore point is created, then the SSH firewall access is configured

ssh_wizard_screen_7

The final screen on the wizard just summarizes what the wizard has done.

ssh_wizard_screen_8

ssh_wizard_screen_8_2

SSH has now been setup in an easy way. Users can now SSH into their machine using the login method they chose during the setup.

setup_secure_ssh_server.sh

setup_secure_ssh_server.sh

#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

readonly SCRIPT_DIR="$(
    cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &&
        pwd
)"

readonly PROJECT_ROOT="$(
    cd -- "$SCRIPT_DIR/.." &&
        pwd
)"

readonly HARDENING_LIB="${PROJECT_ROOT}/lib/hardening_common.sh"

if [[ ! -r "$HARDENING_LIB" ]]; then
    echo "[ERROR] Shared hardening library not found:" >&2
    echo "        $HARDENING_LIB" >&2
    exit 1
fi

# shellcheck disable=SC1090
source "$HARDENING_LIB"

readonly SSH_PACKAGE="openssh-server"
readonly SSH_SERVICE="ssh.service"
readonly SSHD_CONFIG="/etc/ssh/sshd_config"
readonly SSHD_DROPIN="/etc/ssh/sshd_config.d/99-linux-mint-hardening.conf"
readonly UFW_USER_RULES="/etc/ufw/user.rules"
readonly UFW_USER6_RULES="/etc/ufw/user6.rules"

TARGET_USER=""
TARGET_HOME=""
TARGET_GROUP=""
AUTHORIZED_KEYS=""

AUTH_MODE="password"
FIREWALL_MODE="none"
FIREWALL_SUBNET=""

read_tty_line() {
    local prompt="$1"
    local value=""

    if [[ ! -r /dev/tty || ! -w /dev/tty ]]; then
        hardening_fatal "An interactive terminal is required."
    fi

    printf '%s' "$prompt" >/dev/tty

    if ! IFS= read -r value </dev/tty; then
        hardening_fatal "Could not read input from the terminal."
    fi

    printf '%s\n' "$value"
}

pause_for_enter() {
    read_tty_line "Press ENTER to continue..." >/dev/null
}

print_step() {
    local number="$1"
    local title="$2"

    echo
    echo "========================================="
    echo " Step ${number} of 5: ${title}"
    echo "========================================="
    echo
}

select_target_user() {
    local default_user
    local requested_user
    local passwd_entry
    local target_uid
    local target_shell

    print_step "1" "Choose SSH Login Account"

    default_user="${SUDO_USER:-}"

    if [[ -z "$default_user" || "$default_user" == "root" ]]; then
        default_user="$(
            getent passwd |
                awk -F: '$3 >= 1000 && $3 != 65534 {print $1; exit}'
        )"
    fi

    if [[ -z "$default_user" ]]; then
        hardening_fatal "No regular local user account was found."
    fi

    echo "Choose the existing Linux account that will be allowed"
    echo "to log in remotely over SSH."
    echo
    echo "This should normally be the same account you use to log"
    echo "in to the Linux Mint desktop."
    echo
    echo "Default account: $default_user"
    echo

    requested_user="$(
        read_tty_line "SSH login account [${default_user}]: "
    )"

    TARGET_USER="${requested_user:-$default_user}"
    passwd_entry="$(getent passwd "$TARGET_USER" || true)"

    if [[ -z "$passwd_entry" ]]; then
        hardening_fatal \
            "The selected local account does not exist: $TARGET_USER"
    fi

    target_uid="$(awk -F: '{print $3}' <<<"$passwd_entry")"
    TARGET_HOME="$(awk -F: '{print $6}' <<<"$passwd_entry")"
    target_shell="$(awk -F: '{print $7}' <<<"$passwd_entry")"
    TARGET_GROUP="$(id -gn "$TARGET_USER")"

    if [[ "$target_uid" -eq 0 ]]; then
        hardening_fatal "Root cannot be selected for remote SSH login."
    fi

    if [[ "$target_shell" =~ (nologin|false)$ ]]; then
        hardening_fatal \
            "The selected account does not have a login-capable shell."
    fi

    if [[ ! -d "$TARGET_HOME" ]]; then
        hardening_fatal \
            "The selected account has no home directory: $TARGET_HOME"
    fi

    AUTHORIZED_KEYS="${TARGET_HOME}/.ssh/authorized_keys"

    echo
    echo "Selected SSH login account: $TARGET_USER"
    echo "Authorized public keys:     $AUTHORIZED_KEYS"
}

authorized_keys_contains_valid_key() {
    [[ -s "$AUTHORIZED_KEYS" ]] || return 1
    ssh-keygen -l -f "$AUTHORIZED_KEYS" >/dev/null 2>&1
}

select_authentication_mode() {
    local choice

    print_step "2" "Configure SSH Authentication"

    echo "SSH public/private key pairs should be generated on the"
    echo "client computer—the computer you will connect FROM."
    echo
    echo "The private key remains on that client computer."
    echo "Only the public key is stored on this Linux Mint server."
    echo

    if authorized_keys_contains_valid_key; then
        echo "A valid authorized_keys file already exists for $TARGET_USER."
        echo
        echo "1) Use the existing key(s) and require key-only login"
        echo "2) Add another public key and require key-only login"
        echo "3) Keep password login enabled"
        echo

        choice="$(read_tty_line "Choose [1-3]: ")"

        case "$choice" in
            1)
                AUTH_MODE="existing"
                ;;
            2)
                AUTH_MODE="import"
                ;;
            3)
                AUTH_MODE="password"
                ;;
            *)
                hardening_fatal "Invalid authentication choice: $choice"
                ;;
        esac
    else
        echo "No valid authorized SSH keys were found for $TARGET_USER."
        echo
        echo "1) I already have a public key and will paste it now"
        echo "2) Show me how to generate a key on the client, then paste it"
        echo "3) Keep password login enabled for now"
        echo

        choice="$(read_tty_line "Choose [1-3]: ")"

        case "$choice" in
            1)
                AUTH_MODE="import"
                ;;
            2)
                AUTH_MODE="guided-import"
                ;;
            3)
                AUTH_MODE="password"
                ;;
            *)
                hardening_fatal "Invalid authentication choice: $choice"
                ;;
        esac
    fi
}

select_firewall_mode() {
    local choice

    print_step "3" "Choose Firewall Access"

    echo "Choose where SSH connections to TCP port 22 may come from."
    echo
    echo "1) Do not change UFW"
    echo "2) Allow SSH from anywhere"
    echo "3) Allow SSH only from one network or subnet"
    echo

    choice="$(read_tty_line "Choose [1-3]: ")"

    case "$choice" in
        1)
            FIREWALL_MODE="none"
            ;;
        2)
            FIREWALL_MODE="anywhere"
            ;;
        3)
            FIREWALL_MODE="subnet"
            FIREWALL_SUBNET="$(
                read_tty_line \
                    "Allowed network (example 192.168.1.0/24): "
            )"

            if ! python3 - "$FIREWALL_SUBNET" <<'PY'
import ipaddress
import sys

try:
    ipaddress.ip_network(sys.argv[1], strict=False)
except ValueError:
    raise SystemExit(1)
PY
            then
                hardening_fatal \
                    "Invalid network or subnet: $FIREWALL_SUBNET"
            fi
            ;;
        *)
            hardening_fatal "Invalid firewall choice: $choice"
            ;;
    esac
}

create_restore_point() {
    echo
    echo "Creating restore point"
    echo "---------------------------------"

    hardening_create_restore_point

    hardening_backup_file "$SSHD_CONFIG"
    hardening_backup_file "$SSHD_DROPIN"
    hardening_backup_file "$AUTHORIZED_KEYS"

    hardening_record_package "$SSH_PACKAGE"
    hardening_record_unit "$SSH_SERVICE"

    if [[ "$FIREWALL_MODE" != "none" ]]; then
        hardening_backup_file "$UFW_USER_RULES"
        hardening_backup_file "$UFW_USER6_RULES"
        hardening_record_unit "ufw.service"
    fi

    hardening_finalize_restore_point
}

install_ssh_server() {
    print_step "4" "Install and Configure OpenSSH"

    if hardening_package_installed "$SSH_PACKAGE"; then
        hardening_pass "$SSH_PACKAGE is already installed."
    else
        export DEBIAN_FRONTEND=noninteractive

        hardening_wait_for_apt_locks
        hardening_mark_changes_started

        hardening_run_with_heartbeat \
            "Installing OpenSSH Server." \
            apt-get install \
                --yes \
                --no-install-recommends \
                "$SSH_PACKAGE"

        if hardening_package_installed "$SSH_PACKAGE"; then
            hardening_pass "$SSH_PACKAGE was installed."
        else
            hardening_fatal "$SSH_PACKAGE installation failed."
        fi
    fi

    if ! command -v sshd >/dev/null 2>&1; then
        hardening_fatal \
            "The sshd command is unavailable after installation."
    fi

    if [[ ! -f "$SSHD_CONFIG" ]]; then
        hardening_fatal \
            "The SSH server configuration file was not created."
    fi
}

validate_public_key() {
    local public_key="$1"
    local temporary_key

    temporary_key="$(mktemp)"
    printf '%s\n' "$public_key" >"$temporary_key"
    chmod 0600 "$temporary_key"

    if ! ssh-keygen -l -f "$temporary_key" >/dev/null 2>&1; then
        rm -f -- "$temporary_key"
        return 1
    fi

    rm -f -- "$temporary_key"
}

prompt_for_public_key() {
    local public_key
    local ssh_directory

    if [[ "$AUTH_MODE" == "guided-import" ]]; then
        echo
        echo "On the client computer that will connect to this server,"
        echo "generate an Ed25519 key pair with:"
        echo
        echo "  ssh-keygen -t ed25519 -a 100"
        echo
        echo "Accept the default location unless you need another path."
        echo "A passphrase is strongly recommended."
        echo
        echo "Then display the public key with:"
        echo
        echo "  cat ~/.ssh/id_ed25519.pub"
        echo
        echo "Copy the complete single line beginning with ssh-ed25519."
        echo
        pause_for_enter
    fi

    echo
    echo "Paste one complete SSH public key line."
    echo "The private key must not be pasted here."
    echo

    public_key="$(read_tty_line "Public key: ")"

    if [[ -z "$public_key" ]]; then
        hardening_fatal "No public key was provided."
    fi

    if ! validate_public_key "$public_key"; then
        hardening_fatal "The supplied SSH public key is invalid."
    fi

    ssh_directory="${TARGET_HOME}/.ssh"

    hardening_mark_changes_started

    install -d \
        -o "$TARGET_USER" \
        -g "$TARGET_GROUP" \
        -m 0700 \
        "$ssh_directory"

    touch "$AUTHORIZED_KEYS"
    chown "$TARGET_USER:$TARGET_GROUP" "$AUTHORIZED_KEYS"
    chmod 0600 "$AUTHORIZED_KEYS"

    if grep -Fxq -- "$public_key" "$AUTHORIZED_KEYS"; then
        hardening_pass "The supplied public key is already authorized."
    else
        printf '%s\n' "$public_key" >>"$AUTHORIZED_KEYS"
        hardening_pass "The supplied public key was authorized."
    fi

    if ! authorized_keys_contains_valid_key; then
        hardening_fatal \
            "The public key could not be verified after installation."
    fi
}

configure_authentication() {
    case "$AUTH_MODE" in
        import | guided-import)
            prompt_for_public_key
            ;;
        existing)
            if ! authorized_keys_contains_valid_key; then
                hardening_fatal \
                    "The existing authorized_keys file is not valid."
            fi

            hardening_pass \
                "Existing authorized SSH key(s) will be used."
            ;;
        password)
            hardening_warn \
                "Password authentication will remain enabled."
            ;;
    esac
}

write_ssh_configuration() {
    local password_authentication
    local authentication_methods

    if [[ -L "$SSHD_DROPIN" ]]; then
        hardening_fatal \
            "Refusing to replace symbolic link: $SSHD_DROPIN"
    fi

    if [[ "$AUTH_MODE" == "password" ]]; then
        password_authentication="yes"
        authentication_methods="any"
    else
        password_authentication="no"
        authentication_methods="publickey"
    fi

    hardening_mark_changes_started

    install -d \
        -o root \
        -g root \
        -m 0755 \
        "$(dirname "$SSHD_DROPIN")"

    cat >"$SSHD_DROPIN" <<EOF
# Managed by the Linux Mint Hardening Toolkit.

Port 22
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication ${password_authentication}
KbdInteractiveAuthentication no
AuthenticationMethods ${authentication_methods}
PermitEmptyPasswords no
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
GatewayPorts no
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
MaxSessions 2
EOF

    chown root:root "$SSHD_DROPIN"
    chmod 0644 "$SSHD_DROPIN"

    if ! grep -Eq \
        '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf' \
        "$SSHD_CONFIG"; then
        hardening_fatal \
            "$SSHD_CONFIG does not include sshd_config.d/*.conf."
    fi

    sshd -t
    hardening_pass "OpenSSH configuration syntax is valid."
}

configure_firewall() {
    echo
    echo "Configuring SSH firewall access"
    echo "---------------------------------"

    if [[ "$FIREWALL_MODE" == "none" ]]; then
        hardening_warn \
            "UFW was not changed. SSH may remain blocked by the firewall."
        return
    fi

    if ! command -v ufw >/dev/null 2>&1; then
        hardening_fatal \
            "UFW is not installed. Run configure_firewall first."
    fi

    hardening_mark_changes_started

    case "$FIREWALL_MODE" in
        anywhere)
            ufw allow 22/tcp \
                comment "Linux Mint Hardening Toolkit SSH"

            hardening_pass \
                "UFW allows SSH on TCP port 22 from anywhere."
            ;;
        subnet)
            ufw allow \
                from "$FIREWALL_SUBNET" \
                to any \
                port 22 \
                proto tcp \
                comment "Linux Mint Hardening Toolkit SSH"

            hardening_pass \
                "UFW allows SSH from $FIREWALL_SUBNET."
            ;;
    esac
}

enable_and_restart_ssh() {
    hardening_mark_changes_started

    systemctl enable "$SSH_SERVICE"
    systemctl restart "$SSH_SERVICE"

    if systemctl is-enabled --quiet "$SSH_SERVICE"; then
        hardening_pass "$SSH_SERVICE is enabled."
    else
        hardening_fail "$SSH_SERVICE is not enabled."
    fi

    if systemctl is-active --quiet "$SSH_SERVICE"; then
        hardening_pass "$SSH_SERVICE is active."
    else
        hardening_fail "$SSH_SERVICE is not active."
    fi
}

verify_setting() {
    local effective_config="$1"
    local setting="$2"
    local expected="$3"

    if grep -Fqx "${setting} ${expected}" <<<"$effective_config"; then
        hardening_pass "${setting} is ${expected}."
    else
        hardening_fail "${setting} is not set to ${expected}."
    fi
}

verify_configuration() {
    local effective_config
    local expected_password_authentication
    local expected_authentication_methods

    print_step "5" "Verify Secure SSH Setup"

    effective_config="$(sshd -T)"

    if [[ "$AUTH_MODE" == "password" ]]; then
        expected_password_authentication="yes"
        expected_authentication_methods="any"
    else
        expected_password_authentication="no"
        expected_authentication_methods="publickey"
    fi

    verify_setting "$effective_config" "port" "22"
    verify_setting "$effective_config" "permitrootlogin" "no"
    verify_setting "$effective_config" "pubkeyauthentication" "yes"
    verify_setting \
        "$effective_config" \
        "passwordauthentication" \
        "$expected_password_authentication"
    verify_setting \
        "$effective_config" \
        "authenticationmethods" \
        "$expected_authentication_methods"
    verify_setting \
        "$effective_config" \
        "kbdinteractiveauthentication" \
        "no"
    verify_setting "$effective_config" "permitemptypasswords" "no"
    verify_setting "$effective_config" "x11forwarding" "no"
    verify_setting "$effective_config" "allowtcpforwarding" "no"
    verify_setting "$effective_config" "allowagentforwarding" "no"
    verify_setting "$effective_config" "maxauthtries" "3"

    if ss -tlnH |
        awk '$4 ~ /:22$/ || $4 ~ /\]:22$/ {found=1} END {exit !found}'; then
        hardening_pass "SSH is listening on TCP port 22."
    else
        hardening_fail "SSH is not listening on TCP port 22."
    fi

    if [[ "$AUTH_MODE" != "password" ]]; then
        if authorized_keys_contains_valid_key; then
            hardening_pass \
                "A valid public key is authorized for $TARGET_USER."
        else
            hardening_fail \
                "No valid public key is authorized for $TARGET_USER."
        fi
    fi

    if [[ "$FIREWALL_MODE" != "none" ]]; then
        echo
        echo "UFW status"
        echo "---------------------------------"
        ufw status numbered
    fi
}

print_connection_guidance() {
    local host_name
    local primary_address
    local connection_target

    host_name="$(hostname)"
    primary_address="$(
        hostname -I 2>/dev/null |
            awk '{print $1}'
    )"
    connection_target="${primary_address:-$host_name}"

    echo
    echo "Connection guidance"
    echo "---------------------------------"
    echo "SSH login account: $TARGET_USER"
    echo "SSH server:        $connection_target"
    echo "SSH port:          22"
    echo

    if [[ "$AUTH_MODE" == "password" ]]; then
        echo "Password login remains enabled."
        echo "Connect from another computer with:"
        echo
        echo "  ssh ${TARGET_USER}@${connection_target}"
    else
        echo "Key-only login is enabled."
        echo "Use the private key matching the installed public key:"
        echo
        echo "  ssh -i ~/.ssh/id_ed25519 ${TARGET_USER}@${connection_target}"
    fi
}

main() {
    trap hardening_cleanup EXIT
    trap 'hardening_handle_error "$?" "$LINENO"' ERR

    hardening_begin \
        "secure-ssh-server" \
        "Linux Mint Secure SSH Wizard" \
        "configure"

    hardening_require_commands \
        apt-get \
        awk \
        getent \
        grep \
        hostname \
        id \
        install \
        mktemp \
        python3 \
        ssh-keygen \
        ss \
        systemctl

    echo "This wizard installs and securely configures OpenSSH Server."
    echo
    echo "It will guide you through:"
    echo "  1. Choosing the existing Linux account used for SSH login."
    echo "  2. Configuring public-key or password authentication."
    echo "  3. Configuring optional UFW access to TCP port 22."
    echo "  4. Installing and hardening OpenSSH Server."
    echo "  5. Verifying the completed SSH configuration."
    echo
    echo "Private SSH keys are never generated or stored on this server."
    echo

    hardening_confirm "Do you want to start the secure SSH wizard?"

    select_target_user
    select_authentication_mode
    select_firewall_mode
    create_restore_point
    install_ssh_server
    configure_authentication
    write_ssh_configuration
    configure_firewall
    enable_and_restart_ssh
    verify_configuration
    print_connection_guidance

    if ((FAIL_COUNT == 0)); then
        hardening_mark_completed
    fi

    hardening_print_summary

    if ((FAIL_COUNT > 0)); then
        echo "Overall status: SECURE SSH SETUP FAILED VERIFICATION"
        exit 1
    fi

    if ((WARN_COUNT > 0)); then
        echo "Overall status: SECURE SSH SETUP COMPLETED WITH WARNINGS"
        exit 0
    fi

    echo "Overall status: SECURE SSH SETUP COMPLETED"
}

main "$@"
✅ Encryption

Encryption

  • Add a helper script for encrypting files with GPG to make it easy for users to encrypt files.

Instead of going for a full-disk encryption in this first spiral I thought it would be nice to make something for non-tech savvy people to encrypt and decrypt files using gpg, for people that don’t know what gpg is. Running ./install_encryptionwizard.sh will install the encryption wizard. The encryption wizard installs shell aliases that let users open the wizard and encrypt or decrypt files using simple commands such as encryptionwizard, encrypt, and decrypt, making it easier and more visual for people not used to gpg.

encryption wizard install

Users can open the wizard by typing encryptionwizard or simply encrypt <FILENAME> or decrypt <FILENAME> to open the wizard which leads you through the encryption/decryption process. Select 1. to encrypt a file.. and so on.

encryption wizard main


Encrypt a file

If you chose to encrypt a file then you can either hand-type the location of your file or drag and drop the file into the terminal which will auto-fill the location for you. Then hit Enter.

encrypt a file 1

You then see a screen asking how to encrypt, with a simple password, encrypt for a friend using a public key from a friend, or use your own public key and encrypt for yourself.

encrypt a file 2

You then see the keys you have available in you gpg keychan, and you pick which identity should be able to decrypt the file.

encrypt a file 3

Then you are asked what the output name of the encrypted file should be.

encrypt a file 4

Then you get a final confirmation before encrypting, hit 1 to encrypt then the file gets encrypted and the wizard goes back to the main menu.

encrypt a file 5

You have successfully created an encrypted file.

encrypted file


Decrypt a file

To decrypt a file you simply type decrypt <FILENAME> to open the encryption wizard, you get asked what the name of the decrypted file should be.

decrypt a file 1

Accept the suggested name or enter a different filename.

decrypt a file 2

The wizard auto-detects if the encrypted file uses one of you keys or if it is simple password protected.

decrypt file 3

You then get a prompt to enter the password you used when you created your key, and then the file gets decrypted, you can then either decrypt another file or exit.

decrypt file 4

You have now successfully decrypted the file.

decryption success

✅ ./install_encryptionwizard.sh

install_encryptionwizard.sh

#!/usr/bin/env bash
set -euo pipefail

INSTALL_DIR="${HOME}/.local/bin"
APP_PATH="${INSTALL_DIR}/encryptionwizard"

say() {
    printf '%s\n' "$*"
}

die() {
    printf 'Error: %s\n' "$*" >&2
    exit 1
}

ask_yes_no() {
    local prompt="$1"
    local answer=""
    read -r -p "${prompt} [y/N]: " answer
    [[ "$answer" =~ ^([yY]|[yY][eE][sS])$ ]]
}

install_gpg_if_needed() {
    if command -v gpg >/dev/null 2>&1; then
        return 0
    fi

    say
    say "GnuPG is not installed."
    say "Encryption Wizard uses GnuPG to protect and open files."
    say

    ask_yes_no "Install GnuPG now?" ||
        die "GnuPG is required. Install it and run this installer again."

    if command -v apt-get >/dev/null 2>&1; then
        sudo apt-get update
        sudo apt-get install -y gnupg
    elif command -v dnf >/dev/null 2>&1; then
        sudo dnf install -y gnupg2
    elif command -v yum >/dev/null 2>&1; then
        sudo yum install -y gnupg2
    elif command -v pacman >/dev/null 2>&1; then
        sudo pacman -S --needed gnupg
    elif command -v zypper >/dev/null 2>&1; then
        sudo zypper --non-interactive install gpg2
    elif command -v apk >/dev/null 2>&1; then
        sudo apk add gnupg
    else
        die "No supported package manager was found. Install GnuPG manually."
    fi

    command -v gpg >/dev/null 2>&1 ||
        die "GnuPG installation did not finish successfully."
}

ensure_path() {
    local rc_file

    case "${SHELL:-}" in
        */zsh)
            rc_file="${HOME}/.zshrc"
            ;;
        */bash)
            rc_file="${HOME}/.bashrc"
            ;;
        *)
            rc_file="${HOME}/.profile"
            ;;
    esac

    if [[ ":${PATH}:" != *":${INSTALL_DIR}:"* ]]; then
        if ! grep -qsF 'export PATH="$HOME/.local/bin:$PATH"' "$rc_file" 2>/dev/null; then
            {
                printf '\n'
                printf '%s\n' '# Encryption Wizard commands'
                printf '%s\n' 'export PATH="$HOME/.local/bin:$PATH"'
            } >> "$rc_file"
        fi

        export PATH="${INSTALL_DIR}:${PATH}"
        say "Added ${INSTALL_DIR} to your command path in ${rc_file}."
    fi
}

write_application() {
    cat > "$APP_PATH" <<'APP_EOF'
#!/usr/bin/env bash
set -u

APP_VERSION="0.1.0"
BACK_CODE=20

die() {
    printf 'Error: %s\n' "$*" >&2
    exit 1
}

clear_screen() {
    if command -v clear >/dev/null 2>&1 && [[ -t 1 ]]; then
        clear
    else
        printf '\033[2J\033[H'
    fi
}

header() {
    clear_screen
    printf '%s\n' "=============================================="
    printf '%s\n' "             ENCRYPTION WIZARD"
    printf '%s\n' "=============================================="

    if [[ -n "${1:-}" ]]; then
        printf '\n%s\n' "$1"
    fi
}

pause_screen() {
    printf '\n'
    read -r -p "Press Enter to continue..." _
}

ask_yes_no() {
    local prompt="$1"
    local answer=""

    read -r -p "${prompt} [y/N]: " answer
    [[ "$answer" =~ ^([yY]|[yY][eE][sS])$ ]]
}

strip_quotes() {
    local value="$1"

    value=${value#\"}
    value=${value%\"}
    value=${value#\'}
    value=${value%\'}

    printf '%s' "$value"
}

prompt_for_file() {
    local title="$1"
    local message="$2"
    local value=""

    while true; do
        header "$title"
        printf '%s\n' "$message"
        printf '%s\n' "You can type a path or drag a file into this terminal."
        printf '\n%s\n' "B. Back"

        read -r -e -p "> " value

        if [[ "$value" =~ ^[Bb]$ ]]; then
            return "$BACK_CODE"
        fi

        value=$(strip_quotes "$value")

        if [[ -f "$value" ]]; then
            SELECTED_FILE="$value"
            return 0
        fi

        printf '\nFile not found:\n  %s\n' "$value"
        pause_screen
    done
}

prompt_for_output() {
    local title="$1"
    local source_file="$2"
    local suggested_output="$3"
    local choice=""
    local output=""

    while true; do
        header "$title"
        printf 'Source file:\n  %s\n\n' "$source_file"
        printf 'Suggested output:\n  %s\n\n' "$suggested_output"
        printf '%s\n' "1. Use the suggested filename"
        printf '%s\n' "2. Choose another filename"
        printf '%s\n' "B. Back"

        read -r -p "> " choice

        case "$choice" in
            1)
                output="$suggested_output"
                ;;
            2)
                header "$title"
                printf '%s\n' "Enter the complete output filename."
                printf '\n%s\n' "B. Back"

                read -r -e -p "> " output

                if [[ "$output" =~ ^[Bb]$ ]]; then
                    continue
                fi

                output=$(strip_quotes "$output")

                if [[ -z "$output" ]]; then
                    continue
                fi
                ;;
            [Bb])
                return "$BACK_CODE"
                ;;
            *)
                continue
                ;;
        esac

        if [[ -e "$output" ]]; then
            header "$title"
            printf 'This file already exists:\n  %s\n\n' "$output"

            if ! ask_yes_no "Replace it?"; then
                continue
            fi
        fi

        OUTPUT_FILE="$output"
        return 0
    done
}

list_keys() {
    local key_type="$1"

    if [[ "$key_type" == "secret" ]]; then
        gpg --batch --with-colons --fingerprint --list-secret-keys 2>/dev/null
    else
        gpg --batch --with-colons --fingerprint --list-keys 2>/dev/null
    fi |
    awk -F: '
        $1 == "sec" || $1 == "pub" {
            waiting_for_fingerprint = 1
            fingerprint = ""
            uid = ""
        }

        waiting_for_fingerprint && $1 == "fpr" {
            fingerprint = $10
            waiting_for_fingerprint = 0
        }

        fingerprint != "" && uid == "" && $1 == "uid" {
            uid = $10
            gsub(/\\x3a/, ":", uid)
            print fingerprint "\t" uid
            fingerprint = ""
            uid = ""
        }
    '
}

choose_key() {
    local key_type="$1"
    local title="$2"
    local explanation="$3"
    local key_data=""
    local count=0
    local selection=""
    local selected_line=""
    local number=1

    while true; do
        key_data=$(list_keys "$key_type")

        header "$title"
        printf '%s\n\n' "$explanation"

        if [[ -z "$key_data" ]]; then
            printf '%s\n' "No suitable keys were found."
            printf '\n%s\n' "B. Back"
            read -r -p "> " selection
            return "$BACK_CODE"
        fi

        number=1

        while IFS=$'\t' read -r fingerprint uid; do
            printf '%d. %s\n' "$number" "$uid"
            printf '   Identity code: %s\n\n' "$fingerprint"
            ((number++))
        done <<< "$key_data"

        printf '%s\n' "B. Back"

        count=$(printf '%s\n' "$key_data" | wc -l)
        read -r -p "> " selection

        if [[ "$selection" =~ ^[Bb]$ ]]; then
            return "$BACK_CODE"
        fi

        if [[ "$selection" =~ ^[0-9]+$ ]] &&
           (( selection >= 1 && selection <= count )); then
            selected_line=$(printf '%s\n' "$key_data" | sed -n "${selection}p")
            SELECTED_FINGERPRINT=${selected_line%%$'\t'*}
            SELECTED_UID=${selected_line#*$'\t'}
            return 0
        fi
    done
}

create_identity() {
    local name=""
    local email=""
    local user_id=""
    local choice=""

    while true; do
        header "Create your personal identity — Step 1 of 3"
        printf '%s\n' "Enter the name your friends will recognize."
        printf '\n%s\n' "B. Back"

        read -r -p "> " name

        if [[ "$name" =~ ^[Bb]$ ]]; then
            return "$BACK_CODE"
        fi

        if [[ -z "$name" ]]; then
            continue
        fi

        while true; do
            header "Create your personal identity — Step 2 of 3"
            printf '%s\n' "Enter your email address."
            printf '\n%s\n' "B. Back"

            read -r -p "> " email

            if [[ "$email" =~ ^[Bb]$ ]]; then
                break
            fi

            if [[ "$email" != *"@"* ]]; then
                printf '\nThat does not look like an email address.\n'
                pause_screen
                continue
            fi

            user_id="${name} <${email}>"

            header "Create your personal identity — Step 3 of 3"
            printf 'Your identity will be:\n\n  %s\n\n' "$user_id"

            cat <<'TEXT'
GPG will ask you to protect your private key with a password.

Your private key:
• Lets you open files sent specifically to you.
• Must never be shared.
• Cannot be recovered if both the key and its backup are lost.

1. Create my identity
B. Back
TEXT

            read -r -p "> " choice

            case "$choice" in
                1)
                    if gpg --quick-generate-key "$user_id" default default 2y; then
                        header "Identity created"
                        printf 'Your personal identity is ready:\n\n  %s\n\n' "$user_id"
                        printf '%s\n' "It is set to expire in two years."
                        pause_screen
                        return 0
                    fi

                    header "Identity creation failed"
                    printf '%s\n' "GPG could not create the identity."
                    pause_screen
                    return 1
                    ;;
                [Bb])
                    continue
                    ;;
            esac
        done
    done
}

ensure_personal_key() {
    local choice=""

    if [[ -n "$(list_keys secret)" ]]; then
        return 0
    fi

    while true; do
        header "Create your personal identity"

        cat <<'TEXT'
You do not have a personal encryption identity yet.

You need one to:
• Receive files encrypted specifically for you.
• Encrypt files so only you can open them.
• Share a public key with friends.

1. Create my identity now
2. Explain this
B. Back
TEXT

        read -r -p "> " choice

        case "$choice" in
            1)
                create_identity

                if [[ -n "$(list_keys secret)" ]]; then
                    return 0
                fi
                ;;
            2)
                header "About personal encryption identities"

                cat <<'TEXT'
GPG creates two connected keys.

PUBLIC KEY
You may share this with friends. They use it to encrypt files for you.

PRIVATE KEY
This stays on your computer. It opens files encrypted to your public key.

A private key is usually protected by a password. Never send your
private key or its password to anyone.
TEXT

                pause_screen
                ;;
            [Bb])
                return "$BACK_CODE"
                ;;
        esac
    done
}

confirm_friend_key() {
    local choice=""

    while true; do
        header "Confirm your friend's identity"

        printf 'This key claims to belong to:\n\n  %s\n\n' "$SELECTED_UID"
        printf 'Identity code:\n  %s\n\n' "$SELECTED_FINGERPRINT"

        cat <<'TEXT'
For important files, compare this complete identity code with your
friend by phone, in person, or through another trusted method.

1. I checked the identity code
2. Use it without checking this time
B. Back
TEXT

        read -r -p "> " choice

        case "$choice" in
            1)
                FRIEND_KEY_VERIFIED="yes"
                return 0
                ;;
            2)
                FRIEND_KEY_VERIFIED="no"
                return 0
                ;;
            [Bb])
                return "$BACK_CODE"
                ;;
        esac
    done
}

import_key_from_file() {
    local key_file=""

    prompt_for_file \
        "Add a friend's public key" \
        "Choose the public-key file your friend sent you." ||
        return "$?"

    key_file="$SELECTED_FILE"

    header "Review public key"

    if ! gpg --show-keys --fingerprint -- "$key_file"; then
        printf '\nThis file does not contain a readable public key.\n'
        pause_screen
        return 1
    fi

    printf '\n'

    if ! ask_yes_no "Import this public key?"; then
        return "$BACK_CODE"
    fi

    if ! gpg --import -- "$key_file"; then
        header "Import failed"
        printf '%s\n' "GPG could not import the public key."
        pause_screen
        return 1
    fi

    choose_key \
        "public" \
        "Choose the imported key" \
        "Choose the public key you just imported." ||
        return "$?"

    confirm_friend_key
}

import_pasted_key() {
    local temporary_file=""

    temporary_file=$(mktemp) || return 1
    trap 'rm -f "$temporary_file"' RETURN

    header "Paste a friend's public key"

    cat <<'TEXT'
Paste the complete public-key text, including:

-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----

After the END line:
1. Press Enter.
2. Press Ctrl-D.
TEXT

    printf '\n'
    cat > "$temporary_file"

    header "Review public key"

    if ! gpg --show-keys --fingerprint -- "$temporary_file"; then
        printf '\nThe pasted text is not a readable public key.\n'
        pause_screen
        return 1
    fi

    printf '\n'

    if ! ask_yes_no "Import this public key?"; then
        return "$BACK_CODE"
    fi

    if ! gpg --import -- "$temporary_file"; then
        header "Import failed"
        printf '%s\n' "GPG could not import the public key."
        pause_screen
        return 1
    fi

    choose_key \
        "public" \
        "Choose the imported key" \
        "Choose the public key you just imported." ||
        return "$?"

    confirm_friend_key
}

choose_friend() {
    local choice=""

    while true; do
        header "Choose a friend"

        cat <<'TEXT'
1. Choose a public key already saved
2. Import a public-key file
3. Paste a public key
B. Back
TEXT

        read -r -p "> " choice

        case "$choice" in
            1)
                if choose_key \
                    "public" \
                    "Choose a saved public key" \
                    "Choose the person who should be able to open the file."; then
                    confirm_friend_key && return 0
                fi
                ;;
            2)
                import_key_from_file && return 0
                ;;
            3)
                import_pasted_key && return 0
                ;;
            [Bb])
                return "$BACK_CODE"
                ;;
        esac
    done
}

encrypt_with_password() {
    local input_file="$1"
    local choice=""

    prompt_for_output \
        "Password encryption — Output" \
        "$input_file" \
        "${input_file}.gpg" ||
        return "$?"

    header "Password encryption — Ready"

    cat <<TEXT
Original file:
  $input_file

Encrypted copy:
  $OUTPUT_FILE

GPG will ask you to create and confirm a password.

Important:
• Anyone with the password can open the encrypted file.
• A forgotten password cannot be recovered.
• Send the password separately from the encrypted file.
• The original file will not be deleted.

1. Encrypt now
B. Back
TEXT

    read -r -p "> " choice

    if [[ "$choice" =~ ^[Bb]$ ]]; then
        return "$BACK_CODE"
    fi

    if [[ "$choice" != "1" ]]; then
        return 1
    fi

    if gpg --yes --output "$OUTPUT_FILE" --symmetric -- "$input_file"; then
        header "Encryption complete"
        printf 'Encrypted file created:\n\n  %s\n\n' "$OUTPUT_FILE"
        printf '%s\n' "Keep or send this encrypted file."
        printf '%s\n' "Share its password separately."
    else
        header "Encryption failed"
        printf '%s\n' "GPG could not encrypt the file."
    fi

    pause_screen
}

encrypt_for_friend() {
    local input_file="$1"
    local friend_fingerprint=""
    local friend_name=""
    local self_choice=""
    local choice=""
    local -a recipients=()

    choose_friend || return "$?"

    friend_fingerprint="$SELECTED_FINGERPRINT"
    friend_name="$SELECTED_UID"
    recipients=(--recipient "$friend_fingerprint")

    while true; do
        header "Keep your own readable copy?"

        printf 'Recipient:\n  %s\n\n' "$friend_name"

        cat <<'TEXT'
Would you also like to encrypt the file to one of your own keys?

1. Yes — I want to be able to open my copy
2. No — only my friend should be able to open it
B. Back
TEXT

        read -r -p "> " self_choice

        case "$self_choice" in
            1)
                if ! ensure_personal_key; then
                    continue
                fi

                if ! choose_key \
                    "secret" \
                    "Choose your personal identity" \
                    "Choose the personal key that should also open this file."; then
                    continue
                fi

                recipients+=(--recipient "$SELECTED_FINGERPRINT")
                break
                ;;
            2)
                break
                ;;
            [Bb])
                return "$BACK_CODE"
                ;;
        esac
    done

    prompt_for_output \
        "Encrypt for a friend — Output" \
        "$input_file" \
        "${input_file}.gpg" ||
        return "$?"

    header "Encrypt for a friend — Ready"

    printf 'Original file:\n  %s\n\n' "$input_file"
    printf 'Recipient:\n  %s\n\n' "$friend_name"
    printf 'Encrypted copy:\n  %s\n\n' "$OUTPUT_FILE"

    if [[ "${FRIEND_KEY_VERIFIED:-no}" == "no" ]]; then
        printf '%s\n\n' "WARNING: You did not verify your friend's identity code."
    fi

    printf '%s\n' "1. Encrypt now"
    printf '%s\n' "B. Back"

    read -r -p "> " choice

    if [[ "$choice" =~ ^[Bb]$ ]]; then
        return "$BACK_CODE"
    fi

    if [[ "$choice" != "1" ]]; then
        return 1
    fi

    if gpg \
        --yes \
        --trust-model always \
        --output "$OUTPUT_FILE" \
        --encrypt \
        "${recipients[@]}" \
        -- "$input_file"; then

        header "Encryption complete"
        printf 'Encrypted file created:\n\n  %s\n\n' "$OUTPUT_FILE"
        printf 'Send this file to:\n  %s\n' "$friend_name"
    else
        header "Encryption failed"
        printf '%s\n' "GPG could not encrypt the file."
    fi

    pause_screen
}

encrypt_for_self() {
    local input_file="$1"
    local fingerprint=""
    local choice=""

    ensure_personal_key || return "$?"

    choose_key \
        "secret" \
        "Encrypt for yourself" \
        "Choose which personal identity should be able to open the file." ||
        return "$?"

    fingerprint="$SELECTED_FINGERPRINT"

    prompt_for_output \
        "Encrypt for yourself — Output" \
        "$input_file" \
        "${input_file}.gpg" ||
        return "$?"

    header "Encrypt for yourself — Ready"

    printf 'Original file:\n  %s\n\n' "$input_file"
    printf 'Encrypted copy:\n  %s\n\n' "$OUTPUT_FILE"
    printf '%s\n' "1. Encrypt now"
    printf '%s\n' "B. Back"

    read -r -p "> " choice

    if [[ "$choice" =~ ^[Bb]$ ]]; then
        return "$BACK_CODE"
    fi

    if [[ "$choice" != "1" ]]; then
        return 1
    fi

    if gpg \
        --yes \
        --output "$OUTPUT_FILE" \
        --encrypt \
        --recipient "$fingerprint" \
        -- "$input_file"; then

        header "Encryption complete"
        printf 'Encrypted file created:\n\n  %s\n' "$OUTPUT_FILE"
    else
        header "Encryption failed"
        printf '%s\n' "GPG could not encrypt the file."
    fi

    pause_screen
}

encrypt_workflow() {
    local input_file="${1:-}"
    local choice=""

    while true; do
        if [[ -z "$input_file" ]]; then
            if ! prompt_for_file \
                "Encrypt a file" \
                "Choose the file you want to protect."; then
                return 0
            fi

            input_file="$SELECTED_FILE"
        fi

        header "Encrypt a file"

        printf 'Selected file:\n  %s\n\n' "$input_file"

        cat <<'TEXT'
How do you want to protect it?

1. Use a password
   Anyone with the password can open it.

2. Encrypt it for a friend
   Only the selected recipient can open it.

3. Encrypt it for myself
   Only one of my personal keys can open it.

4. Choose a different file
B. Back
TEXT

        read -r -p "> " choice

        case "$choice" in
            1)
                encrypt_with_password "$input_file"
                ;;
            2)
                encrypt_for_friend "$input_file"
                ;;
            3)
                encrypt_for_self "$input_file"
                ;;
            4)
                input_file=""
                ;;
            [Bb])
                return 0
                ;;
        esac
    done
}

decrypt_workflow() {
    local input_file="${1:-}"
    local suggested_output=""
    local choice=""

    while true; do
        if [[ -z "$input_file" ]]; then
            if ! prompt_for_file \
                "Decrypt a file" \
                "Choose the encrypted file you want to open."; then
                return 0
            fi

            input_file="$SELECTED_FILE"
        fi

        case "$input_file" in
            *.gpg)
                suggested_output=${input_file%.gpg}
                ;;
            *.pgp)
                suggested_output=${input_file%.pgp}
                ;;
            *.asc)
                suggested_output=${input_file%.asc}
                ;;
            *)
                suggested_output="${input_file}.decrypted"
                ;;
        esac

        if ! prompt_for_output \
            "Decrypt a file — Output" \
            "$input_file" \
            "$suggested_output"; then
            return 0
        fi

        header "Decrypt a file — Ready"

        printf 'Encrypted file:\n  %s\n\n' "$input_file"
        printf 'Decrypted output:\n  %s\n\n' "$OUTPUT_FILE"

        cat <<'TEXT'
GPG will automatically determine whether this file uses:
• A password, or
• One of your private keys.

1. Decrypt now
2. Choose a different encrypted file
B. Back
TEXT

        read -r -p "> " choice

        case "$choice" in
            1)
                if gpg --yes --output "$OUTPUT_FILE" --decrypt -- "$input_file"; then
                    header "Decryption complete"
                    printf 'Decrypted file created:\n\n  %s\n' "$OUTPUT_FILE"
                else
                    header "Decryption failed"
                    cat <<'TEXT'
The password may be wrong, the required private key may be missing,
or the encrypted file may be damaged.
TEXT
                fi

                pause_screen
                ;;
            2)
                input_file=""
                ;;
            [Bb])
                return 0
                ;;
        esac
    done
}

export_public_key() {
    local fingerprint=""
    local uid=""
    local safe_name=""
    local output_file=""
    local choice=""

    ensure_personal_key || return "$?"

    choose_key \
        "secret" \
        "Export your public key" \
        "Choose the identity whose public key you want to share." ||
        return "$?"

    fingerprint="$SELECTED_FINGERPRINT"
    uid="$SELECTED_UID"
    safe_name=$(printf '%s' "$uid" | tr ' /<>' '____' | tr -cd '[:alnum:]_.@-')
    output_file="${HOME}/${safe_name}.public-key.asc"

    header "Export your public key"

    printf 'Identity:\n  %s\n\n' "$uid"
    printf 'Public-key file:\n  %s\n\n' "$output_file"
    printf '%s\n' "1. Export public key"
    printf '%s\n' "B. Back"

    read -r -p "> " choice

    if [[ "$choice" != "1" ]]; then
        return 0
    fi

    if gpg \
        --yes \
        --armor \
        --output "$output_file" \
        --export "$fingerprint"; then

        header "Public key exported"
        printf 'Share this file with friends:\n\n  %s\n\n' "$output_file"
        printf '%s\n' "This is your PUBLIC key and is safe to share."
        printf '%s\n' "Never share your private key or its password."
    else
        header "Export failed"
        printf '%s\n' "GPG could not export the public key."
    fi

    pause_screen
}

identity_menu() {
    local choice=""

    while true; do
        header "My identity"

        cat <<'TEXT'
1. Create a personal identity
2. View my personal identities
3. Export my public key for friends
B. Back
TEXT

        read -r -p "> " choice

        case "$choice" in
            1)
                create_identity
                ;;
            2)
                header "My personal identities"
                gpg --list-secret-keys --fingerprint
                pause_screen
                ;;
            3)
                export_public_key
                ;;
            [Bb])
                return 0
                ;;
        esac
    done
}

friends_menu() {
    local choice=""

    while true; do
        header "Friends and public keys"

        cat <<'TEXT'
1. Import a public-key file
2. Paste a public key
3. View saved public keys
B. Back
TEXT

        read -r -p "> " choice

        case "$choice" in
            1)
                import_key_from_file
                ;;
            2)
                import_pasted_key
                ;;
            3)
                header "Saved public keys"
                gpg --list-keys --fingerprint
                pause_screen
                ;;
            [Bb])
                return 0
                ;;
        esac
    done
}

help_screen() {
    header "Help"

    cat <<'TEXT'
QUICK COMMANDS

  encrypt FILE
      Protect a file with a password or public key.

  decrypt FILE
      Open a password-protected or public-key-encrypted file.

  encryptionwizard
      Open the complete menu.

IMPORTANT SAFETY NOTES

• Encryption does not delete the original file.
• Send passwords separately from encrypted files.
• Share public keys, never private keys.
• Compare a friend's complete identity code before trusting their key.
• Keep backups of important private keys in a secure location.
TEXT

    pause_screen
}

main_menu() {
    local choice=""

    while true; do
        header "Main menu"

        cat <<'TEXT'
What would you like to do?

1. Encrypt a file
2. Decrypt a file
3. Manage my identity
4. Manage friends' public keys
5. Help
6. Exit
TEXT

        read -r -p "> " choice

        case "$choice" in
            1)
                encrypt_workflow
                ;;
            2)
                decrypt_workflow
                ;;
            3)
                identity_menu
                ;;
            4)
                friends_menu
                ;;
            5)
                help_screen
                ;;
            6)
                clear_screen
                exit 0
                ;;
        esac
    done
}

main() {
    local invoked_name=""

    command -v gpg >/dev/null 2>&1 ||
        die "GnuPG is not installed. Run the installer again."

    invoked_name=$(basename "$0")

    case "$invoked_name" in
        encrypt)
            if [[ $# -gt 1 ]]; then
                die "Usage: encrypt [FILE]"
            fi

            if [[ $# -eq 1 && ! -f "$1" ]]; then
                die "File not found: $1"
            fi

            encrypt_workflow "${1:-}"
            ;;
        decrypt)
            if [[ $# -gt 1 ]]; then
                die "Usage: decrypt [FILE]"
            fi

            if [[ $# -eq 1 && ! -f "$1" ]]; then
                die "File not found: $1"
            fi

            decrypt_workflow "${1:-}"
            ;;
        *)
            case "${1:-}" in
                --version)
                    printf 'Encryption Wizard %s\n' "$APP_VERSION"
                    ;;
                --help)
                    help_screen
                    ;;
                *)
                    main_menu
                    ;;
            esac
            ;;
    esac
}

main "$@"
APP_EOF
}

main() {
    say "Installing Encryption Wizard..."

    install_gpg_if_needed

    mkdir -p "$INSTALL_DIR"

    write_application
    chmod +x "$APP_PATH"

    ln -sf "encryptionwizard" "${INSTALL_DIR}/encrypt"
    ln -sf "encryptionwizard" "${INSTALL_DIR}/decrypt"

    ensure_path

    say
    say "Installation complete."
    say
    say "Available commands:"
    say "  encryptionwizard"
    say "  encrypt FILE"
    say "  decrypt FILE"
    say
    say "You may need to open a new terminal before the commands are available."

    if [[ -t 0 ]] && ask_yes_no "Open Encryption Wizard now?"; then
        exec "$APP_PATH"
    fi
}

main "$@"
✅ CPU Velnerabilities

CPU Vulnerabilities

  • Add a script that reports CPU vulnerabilities and mitigation status.

This was achieved in the analysis part with the check_cpu.sh script

✅ Automatic Updates

Automatic Updates

  • Turn on automatic security updates.

In the Hardening plan + testing section I show how I ran the script and restored the changes to the way things were before running the hardening script.

enable_automatic_security_updates.sh

enable_automatic_security_updates.sh

#!/usr/bin/env bash

set -Eeuo pipefail

if [[ -z "${HARDENING_LIB:-}" || ! -r "$HARDENING_LIB" ]]; then
    echo "[ERROR] Shared hardening library is unavailable." >&2
    exit 1
fi

# shellcheck disable=SC1090
source "$HARDENING_LIB"

readonly AUTO_CONFIG="/etc/apt/apt.conf.d/20auto-upgrades"
readonly POLICY_CONFIG="/etc/apt/apt.conf.d/50unattended-upgrades"

readonly APT_DAILY_TIMER="apt-daily.timer"
readonly APT_UPGRADE_TIMER="apt-daily-upgrade.timer"

wait_for_apt_locks() {
    local attempt
    local lock_file
    local locked

    echo
    echo "Checking package-manager availability"
    echo "---------------------------------"

    if ! command -v fuser >/dev/null 2>&1; then
        hardening_warn \
            "fuser is unavailable; APT will perform its own lock checks."
        return
    fi

    for ((attempt = 1; attempt <= 60; attempt++)); do
        locked=0

        for lock_file in \
            /var/lib/dpkg/lock-frontend \
            /var/lib/dpkg/lock \
            /var/lib/apt/lists/lock \
            /var/cache/apt/archives/lock
        do
            if [[ -e "$lock_file" ]] &&
               fuser "$lock_file" >/dev/null 2>&1; then
                locked=1
                break
            fi
        done

        if (( locked == 0 )); then
            hardening_pass "APT and dpkg locks are available."
            return
        fi

        if (( attempt == 1 )); then
            hardening_info \
                "Another package-management process is running."
            hardening_info "Waiting for it to finish."
        fi

        sleep 2
    done

    hardening_fatal \
        "APT remained locked after approximately two minutes."
}

install_unattended_upgrades() {
    echo
    echo "Installing unattended-upgrades"
    echo "---------------------------------"

    export DEBIAN_FRONTEND=noninteractive
    hardening_mark_changes_started

    if hardening_package_installed "unattended-upgrades"; then
        hardening_pass "unattended-upgrades is already installed."
        return
    fi

    apt-get update

    apt-get install \
        --yes \
        --no-install-recommends \
        unattended-upgrades

    if hardening_package_installed "unattended-upgrades"; then
        hardening_pass "unattended-upgrades was installed."
    else
        hardening_fatal \
            "unattended-upgrades could not be installed."
    fi
}

configure_automatic_updates() {
    local temporary_file

    echo
    echo "Configuring automatic updates"
    echo "---------------------------------"

    hardening_mark_changes_started

    if [[ -L "$AUTO_CONFIG" ]]; then
        hardening_fatal \
            "Refusing to replace symbolic link: $AUTO_CONFIG"
    fi

    temporary_file="$(
        mktemp "/etc/apt/apt.conf.d/.20auto-upgrades.XXXXXX"
    )"

    cat >"$temporary_file" <<'EOF'
// Managed by enable_automatic_security_updates.sh.
//
// Numeric values are intervals in days.

APT::Periodic::Enable "1";
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOF

    chown root:root "$temporary_file"
    chmod 0644 "$temporary_file"

    mv -fT -- "$temporary_file" "$AUTO_CONFIG"

    hardening_pass "Configured $AUTO_CONFIG."

    if [[ ! -f "$POLICY_CONFIG" ]]; then
        hardening_fatal \
            "Missing unattended-upgrades policy: $POLICY_CONFIG"
    fi

    hardening_info \
        "Retained the distribution-provided update policy."
    hardening_info "Automatic rebooting was not enabled."
}

enable_apt_timers() {
    echo
    echo "Enabling APT timers"
    echo "---------------------------------"

    hardening_mark_changes_started

    systemctl daemon-reload
    systemctl enable --now "$APT_DAILY_TIMER"
    systemctl enable --now "$APT_UPGRADE_TIMER"

    if systemctl is-enabled --quiet "$APT_DAILY_TIMER"; then
        hardening_pass "$APT_DAILY_TIMER is enabled."
    else
        hardening_fail "$APT_DAILY_TIMER is not enabled."
    fi

    if systemctl is-active --quiet "$APT_DAILY_TIMER"; then
        hardening_pass "$APT_DAILY_TIMER is active."
    else
        hardening_fail "$APT_DAILY_TIMER is not active."
    fi

    if systemctl is-enabled --quiet "$APT_UPGRADE_TIMER"; then
        hardening_pass "$APT_UPGRADE_TIMER is enabled."
    else
        hardening_fail "$APT_UPGRADE_TIMER is not enabled."
    fi

    if systemctl is-active --quiet "$APT_UPGRADE_TIMER"; then
        hardening_pass "$APT_UPGRADE_TIMER is active."
    else
        hardening_fail "$APT_UPGRADE_TIMER is not active."
    fi
}

verify_configuration() {
    local apt_dump
    local dry_run_output
    local dry_run_status

    echo
    echo "Verifying configuration"
    echo "---------------------------------"

    apt_dump="$(apt-config dump)"

    if grep -Fq \
        'APT::Periodic::Enable "1";' \
        <<<"$apt_dump"; then
        hardening_pass "APT periodic processing is enabled."
    else
        hardening_fail "APT periodic processing is not enabled."
    fi

    if grep -Fq \
        'APT::Periodic::Update-Package-Lists "1";' \
        <<<"$apt_dump"; then
        hardening_pass "Daily package-list updates are enabled."
    else
        hardening_fail \
            "Daily package-list updates are not enabled."
    fi

    if grep -Fq \
        'APT::Periodic::Unattended-Upgrade "1";' \
        <<<"$apt_dump"; then
        hardening_pass "Daily unattended upgrades are enabled."
    else
        hardening_fail \
            "Daily unattended upgrades are not enabled."
    fi

    set +e
    dry_run_output="$(
        unattended-upgrade --dry-run --debug 2>&1
    )"
    dry_run_status=$?
    set -e

    if (( dry_run_status == 0 )); then
        hardening_pass \
            "unattended-upgrades completed a successful dry run."
    else
        hardening_fail \
            "unattended-upgrades dry run failed with status ${dry_run_status}."

        printf '%s\n' "$dry_run_output"
    fi
}

main() {
    trap \
        'hardening_handle_error "$?" "$LINENO"' \
        ERR

    hardening_begin \
        "automatic-security-updates" \
        "Enable Automatic Security Updates" \
        "enable"

    hardening_require_commands \
        apt-get apt-config dpkg-query systemctl mktemp

    echo "WARNING"
    echo "-----------------------------------------"
    echo "This action will:"
    echo "  - Create a root-only restore point."
    echo "  - Install unattended-upgrades if needed."
    echo "  - Enable automatic security updates."
    echo "  - Enable the APT update timers."
    echo
    echo "It will not:"
    echo "  - Create an analysis report."
    echo "  - Enable automatic rebooting."
    echo

    hardening_confirm \
        "Are you sure you want to enable automatic security updates?"

    echo
    echo "Creating restore point"
    echo "---------------------------------"

    hardening_create_restore_point
    hardening_backup_file "$AUTO_CONFIG"
    hardening_backup_file "$POLICY_CONFIG"
    hardening_record_package "unattended-upgrades"
    hardening_record_unit "$APT_DAILY_TIMER"
    hardening_record_unit "$APT_UPGRADE_TIMER"
    hardening_finalize_restore_point

    wait_for_apt_locks
    install_unattended_upgrades
    configure_automatic_updates
    enable_apt_timers
    verify_configuration

    hardening_print_summary

    if (( FAIL_COUNT > 0 )); then
        echo "Overall status: AUTOMATIC UPDATES NOT FULLY ENABLED"
        exit 1
    fi

    if (( WARN_COUNT > 0 )); then
        echo "Overall status: ENABLED WITH WARNINGS"
        exit 0
    fi

    echo "Overall status: AUTOMATIC SECURITY UPDATES ENABLED"
}

main "$@"
User Accounts

User Accounts

  • Review user accounts and sudo privileges to ensure they follow the principle of least privilege.

  • Not implemented yet.
✅ AppArmor

AppArmor

  • Review the default AppArmor configuration and improve it where appropriate.

  1. The check_apparmor.sh script analysis report tells me that AppArmor is installed, enabled and active.

  2. There are 109 enabled profiles.

So, no hardening was deemed necessary at this point.

✅ Boot Security

Boot Security

  • Verify whether Secure Boot is enabled and document its status.

The check_boot.sh script from the analysis phase gives a summary of the boot status:

Summary
---------------------------------
Boot mode:              UEFI
EFI variables:          yes
Secure Boot available:  yes
Secure Boot state:      disabled
Kernel lockdown:        [none] integrity confidentiality
✅ Human Security

Human Security

  1. Create a security health report.

  2. Turn on automatic updates.

  3. Activate the firewall.

  4. Make encryption easy to use.


  1. The check_health.sh made in the analysis phase creates a very human understandable summary report to indicate the status of the system

  2. The enable_automatic_security_updates.sh turns on automatic updates.

  3. The configure_firewall.sh script makes it easy to intall ufw if not already installed and activate it.

  4. The install_encryptionwizard.sh script makes aliases that open an encryption wizard so users that dont know gpg can easily and visually encrypt and decrypt files.

The actions taken to increase human security have mostly been to make it easier for non-tech savvy people to learn about the cyber security health of the system and make it easier to configure the system to make it a bit more secure.