├── .gitignore ├── README.md ├── hardening ├── automated_hardening.sh ├── configs │ └── config.sh └── standalones │ ├── apparmor │ └── setup_apparmor.sh │ ├── auditd │ └── setup_auditd.sh │ ├── fail2ban │ └── setup_fail2ban.sh │ ├── firewall │ └── setup_firewall.sh │ ├── lynis │ └── setup_lynis.sh │ ├── memory │ └── setup_shared_memory_hardening.sh │ ├── networking │ └── setup_network_hardening_parameters.sh │ ├── password │ └── setup_password_policy.sh │ ├── rkhunter │ └── setup_rkhunter.sh │ ├── sshd │ ├── configure_ssh_hardening.sh │ └── generate_ssh_intrusion_detection.sh │ └── upgrades │ └── setup_auto_upgrades.sh ├── mail ├── create_aws_dkim_identity.sh ├── generate_dkim_verification_dmarc_records.sh └── postfix_forwarding_service_installer.sh └── security ├── create_administrative_user.sh └── ssh_setup_and_deploy_key.sh /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/error-try-again/UbuntuHardeningFramework/fb286e592bfc29e691b30664a4b971cadf47ab12/.gitignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An Automated Hardening Framework for Ubuntu 🔐 2 | 3 | The following bash framework stands up and installs several bespoke security toolkits, developed to provide seamless automatic security controls for Ubuntu servers. 4 | 5 | ### Primary Toolkits 6 | - [x] SSH Intrusion Detection (Custom Daemon) /w Email Alerts & Logs 7 | - [x] Auditd (Hardened Configuration) - /w Email Alerts & Logs 8 | - [x] Automated Fail2Ban Deployment (Hardened Restrictive Configuration with incremental bans) - /w Email Alerts, Logs & Whitelisting 9 | - [x] Automated Upgrading Installation (Security & Critical) Deployment - Cron Jobs, Email Alerts & Logs 10 | - [x] Automated RKHunter Deployment - Scans, Email Alerts, and Cron Job (Daily) (Weekly) /w Email Reports & Logs 11 | - [x] Automated Lynis Deployment - Scans, Source Builds, Fingerprinting, Email Alerts, and Cron Job (Daily) (Weekly) /w Email Reports & Logs 12 | - [x] Firewall (UFW) Deployment - Default Deny, pre-deployment testing, and Email Alerts & Logs 13 | - [x] Automated Shared Memory Hardening & Restriction Deployment (/run/shm) 14 | - [x] Automated Network Kernel Hardening Deployment (TCP/IP Stack Hardening) (E.g., DDoS Mitigation, ICMP, SYN Flood, etc.) 15 | - [x] Automated AppArmor Deployment - Customized Enforcing Profiles & Logging 16 | - [x] Automated Password Policy Deployment - Highly Conservative Configuration & Logging (e.g., Password Complexity, Length, etc.) 17 | - [x] Automated SSH Hardening Deployment - Includes MFA, Key-Based Authentication & Hardened Configuration (e.g., AllowUsers, AllowGroups, etc.) 18 | 19 | ### Extra Toolkits 20 | - [x] SSH Key generation, keyscan and deployment Scripts (optional) 21 | - [x] Administrative user account creation and hardening (optional) 22 | - [x] AWS DKIM & DMARC Configuration Deployment Toolkits (optional) 23 | - [x] Automated Postfix Forwarding Service Deployment (optional) 24 | 25 | ### Pre-Installation 26 | 27 | There are a couple of important steps to the installation process. You'll first need to ensure your system is up to date and has the necessary packages installed. 28 | 29 | ```bash 30 | apt update && apt upgrade -y 31 | apt install -y git 32 | ``` 33 | 34 | # Initial Setup 35 | 36 | Then, we can clone the repository and navigate to the hardening directory. 37 | 38 | ```bash 39 | git clone https://github.com/error-try-again/auto-harden-ubuntu.git 40 | cd auto-harden-ubuntu/ 41 | ``` 42 | 43 | If this is a new installation, and you haven't yet created separate user account that will be used to log in to the server now is the time. It will also be used for administrative access. 44 | The following script automates this process. 45 | ```bash 46 | ./security/create_administrative_user.sh 47 | ``` 48 | 49 | ### SSH Key Preparation 50 | 51 | Once this is done, we can go ahead and create a new SSH key for the user account (on your local machine) or use an existing one/multiple. 52 | ```bash 53 | 0 % ssh-keygen -t ed25519 -C "void@null" 54 | Generating public/private ed25519 key pair. 55 | Enter file in which to save the key (/home/void/.ssh/id_ed25519): /home/void/.ssh/void-ed25519 56 | Enter passphrase (empty for no passphrase): 57 | Enter same passphrase again: 58 | Your identification has been saved in /home/void/.ssh/void-ed25519 59 | Your public key has been saved in /home/void/.ssh/void-ed25519.pub 60 | The key fingerprint is: 61 | SHA256:0pOxo... void@null 62 | void@null ~/.ssh 63 | 1 % cat void-ed25519.pub # Copy this key 64 | ``` 65 | 66 | ### Configuration 67 | 68 | Next, it's important to note that the the project can be executed directly via a single script or as a series of individual standalone scripts. 69 | If you're executing it as a single script, you can modify the `config.sh` file to include your SSH key mappings, allowed SSH users, Fail2Ban white-listed ips, and email alerts. 70 | - SSH key mappings can be configured to automatically allow PKE access to certain user accounts via SSH. 71 | - Access to the server can be restricted to specific users by modifying the `allowed_ssh_users` csv list. 72 | - Whitelisted IPs can be added to the `ip_whitelist` csv list to prevent them from being banned by Fail2Ban. 73 | - Email alerts can be configured by modifying the `sender` and `recipients` variables respectively. 74 | 75 | By default, the installer will try to install and configure a mail forwarding service to your mail transfer authority so that the toolkits can send alerts and logs. 76 | To skip this, just ignore the `sender` and `recipients` variables in the `config.sh` file. In this case, mail will be from and to the localhost. 77 | 78 | Modify the `config.sh` file so that it looks similar to the following: 79 | 80 | ```bash 81 | #!/usr/bin/env bash 82 | 83 | # Users are separated by a new line and keys are separated by a comma. 84 | # You can add as many keys or users as you'd like. 85 | allowed_ssh_pk_user_mappings="admin:ssh-rsa AAAAB3NzwnBnmkSBpiBsqQ== void@null 86 | void:ssh-rsa AAAAB3NzwnBnmkSBpiBsqQ== void@null,ssh-ed2551 AAIDk7VFe example.eg@example.com" 87 | 88 | # Users are separated by a comma. These are the users that are allowed to SSH into the server. 89 | allowed_ssh_users="your_user,another_user" 90 | 91 | # Whitelisted IPs are separated by a comma. These IPs will not be banned by Fail2Ban. 92 | ip_whitelist="your_ip/32" 93 | 94 | # Email alerts are sent from the sender to the recipients. If you want to add multiple recipients, separate them by a comma. 95 | sender="void@your-domain.com.au" 96 | recipients="your-main-email@another-domain.com" 97 | 98 | # SSH Port for UFW and SSHD 99 | ssh_port=2783 100 | 101 | export allowed_ssh_pk_user_mappings allowed_ssh_users ip_whitelist sender recipients ssh_port 102 | ``` 103 | 104 | ### Mailing & Alerts 105 | 106 | If you already have a mail transfer authority or relay with TLS enabled and would like to - the following script will help you configure a mail forwarding service to your mail transfer authority so that the toolkits can send alerts and logs. Otherwise, you can skip this step. 107 | It's set up to install and configure Postfix to forward all mail to your mail transfer authority over SMTP/TLS on port 587. The script will also install the necessary SASL authentication packages and configure the relay with password authentication. Passwords are hashed and stored in `/etc/postfix/sasl_passwd`. All configurations are validated, backed up and timestamped. 108 | 109 | ```bash 110 | /mail# ./postfix_forwarding_service_installer.sh public.void@mydomain.com.au email-smtp.ap-southeast-2.amazonaws.com 111 | ``` 112 | 113 | The script will prompt you for your SMTP username and password. Once you've entered these, the script will validate the configuration and restart the Postfix service. 114 | ```bash 115 | Reading package lists... Done 116 | Building dependency tree... Done 117 | Reading state information... Done 118 | Package 'sendmail' is not installed, so not removed 119 | 0 to upgrade, 0 to newly install, 0 to remove and 0 not to upgrade. 120 | Reading package lists... Done 121 | Building dependency tree... Done 122 | Reading state information... Done 123 | postfix is already the newest version (3.6.4-1ubuntu1.3). 124 | libsasl2-modules is already the newest version (2.1.27+dfsg2-3ubuntu1.2). 125 | 0 to upgrade, 0 to newly install, 0 to remove and 0 not to upgrade. 126 | Updated sendmail_path in /etc/postfix/main.cf. 127 | Updated mailq_path in /etc/postfix/main.cf. 128 | Updated newaliases_path in /etc/postfix/main.cf. 129 | Updated html_directory in /etc/postfix/main.cf. 130 | Updated manpage_directory in /etc/postfix/main.cf. 131 | Updated sample_directory in /etc/postfix/main.cf. 132 | Updated readme_directory in /etc/postfix/main.cf. 133 | Enter SMTP username: TEST 134 | Enter SMTP password: postmap: name_mask: ipv4 135 | postmap: inet_addr_local: configured 4 IPv4 addresses 136 | postmap: open hash /etc/postfix/sasl_passwd 137 | postmap: Compiled against Berkeley DB: 5.3.28? 138 | postmap: Run-time linked against Berkeley DB: 5.3.28? 139 | ● postfix.service - Postfix Mail Transport Agent 140 | Loaded: loaded (/lib/systemd/system/postfix.service; enabled; vendor preset: enabled) 141 | Active: active (exited) since Fri 2024-03-01 22:41:53 AEST; 26ms ago 142 | Docs: man:postfix(1) 143 | Process: 1469773 ExecStart=/bin/true (code=exited, status=0/SUCCESS) 144 | Main PID: 1469773 (code=exited, status=0/SUCCESS) 145 | CPU: 940us 146 | 147 | Mar 01 22:41:53 null systemd[1]: Starting Postfix Mail Transport Agent... 148 | Mar 01 22:41:53 null systemd[1]: Finished Postfix Mail Transport Agent. 149 | Validating Postfix configuration... 150 | Postfix configuration is valid. 151 | ● postfix.service - Postfix Mail Transport Agent 152 | Loaded: loaded (/lib/systemd/system/postfix.service; enabled; vendor preset: enabled) 153 | Active: active (exited) since Fri 2024-03-01 22:41:55 AEST; 22ms ago 154 | Docs: man:postfix(1) 155 | Process: 1470205 ExecStart=/bin/true (code=exited, status=0/SUCCESS) 156 | Main PID: 1470205 (code=exited, status=0/SUCCESS) 157 | CPU: 931us 158 | 159 | Mar 01 22:41:55 null systemd[1]: Starting Postfix Mail Transport Agent... 160 | Mar 01 22:41:55 null systemd[1]: Finished Postfix Mail Transport Agent. 161 | Postfix configuration successfully completed. 162 | ``` 163 | 164 | # Installation 165 | Directly as a single script 166 | ```bash 167 | pwd # /hardening 168 | chmod +x automated_hardening.sh 169 | ./automated_hardening.sh 170 | ``` 171 | 172 | Or as a series of individual standalone scripts - note that running this will not configure proper email alerts. 173 | ```bash 174 | cd /hardening/standalones 175 | find . -type f -name '*.sh' -exec sudo bash {} \; 176 | ``` 177 | 178 | The exception here is the SSH-IDS toolkit, which first has to be generated and then installed. 179 | ```bash 180 | sudo ./standalones/sshd/generate_ssh_intrusion_detection.sh 181 | sudo bash /opt/ssh_monitor/ssh_monitor.sh install optional-email@example.com 182 | ``` 183 | 184 | To skip the installation of a specific toolkit, you can use the following command: 185 | ```bash 186 | cd /hardening/standalones 187 | find . -type f -name '*.sh' ! -name 'setup_ssh_intrusion_detection.sh' -exec sudo bash {} \; # Skip SSH Intrusion Detection 188 | ```** 189 | 190 | Or individually - note certain toolkits use commandline arguments to enable email alerts and more specialized use cases. 191 | ```bash 192 | cd /hardening/standalones 193 | chmod +x *.sh 194 | ./setup_auditd.sh 195 | # etc... 196 | ``` 197 | 198 | # Post-Installation 199 | 200 | Some cron jobs are set up to run daily and weekly scans for RKHunter and Lynis respectively. Additionally, unattended upgrades are set to run daily. You can check the cron jobs by running the following commands: 201 | 202 | ```bash 203 | ❯ sudo crontab -l 204 | 205 | 0 0 * * * /usr/local/bin/audit-report.sh >/var/log/audit-report.log 2>&1 206 | 0 0 * * * /usr/bin/unattended-upgrade -d >/var/log/unattended-upgrades.log 2>&1 207 | 0 0 * * * /usr/local/bin/lynis_installer.sh >/var/log/lynis_cron.log 2>&1 208 | ``` 209 | 210 | ```bash 211 | ❯ ls /etc/cron.weekly/rkhunter 212 | /etc/cron.weekly/rkhunter 213 | ❯ ls /etc/cron.daily/rkhunter 214 | /etc/cron.daily/rkhunter 215 | ``` 216 | 217 | # Roadmap 218 | - [ ] Add precise configuration examples & documentation 219 | - [ ] Additional toolkits (e.g., further kernel hardening, automatic audit remediation, etc.) 220 | - [ ] Additional controls for alerts and logs 221 | - [ ] Streamline configuration for easy deployment 222 | - [ ] Email digests for alerts 223 | - [ ] Update SSH IDS alerting to use pam_exec based system rather than rotating timer 224 | 225 | # Notes 226 | If for some reason you need to reinstall apt installed configuration files to their defaults, e.g. pam.d, you can use the following command: 227 | 228 | ```bash 229 | sudo apt install --reinstall -o Dpkg::Options::="--force-confmiss" $(dpkg -S /etc/pam.d/\* | cut -d ':' -f 1) 230 | ``` -------------------------------------------------------------------------------- /hardening/automated_hardening.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Enforce strict mode for bash scripting to catch errors early 4 | set -euo pipefail 5 | 6 | # Executes a series of hardening scripts that require preconfiguration. 7 | # SC2154 (variable undefined) is a false positive. The variables are defined in the specified configuration file. 8 | # shellcheck disable=SC2154 9 | preconfigured_hardening_scripts() { 10 | # Setup auditd for system auditing 11 | source standalones/auditd/setup_auditd.sh "${recipients}" "${sender}" 12 | 13 | # Setup AppArmor in enforcing mode - requires auditd to be installed 14 | source standalones/apparmor/setup_apparmor.sh 15 | 16 | # Enable and configure fail2ban 17 | source standalones/fail2ban/setup_fail2ban.sh "${ssh_port}" "${recipients}" "${sender}" "${ip_whitelist}" 18 | 19 | # Setup automatic system updates 20 | source standalones/upgrades/setup_auto_upgrades.sh "${recipients}" "${sender}" 21 | 22 | # Configure SSH hardening 23 | source standalones/sshd/configure_ssh_hardening.sh "${allowed_ssh_pk_user_mappings}" "${allowed_ssh_users}" "${ssh_port}" 24 | 25 | # Setup firewall using UFW (Uncomplicated Firewall) 26 | source standalones/firewall/setup_firewall.sh "${ssh_port}" "${port_list_csv}" "${firewall_action}" 27 | 28 | # Setup SSH intrusion detection 29 | source standalones/sshd/generate_ssh_intrusion_detection.sh 30 | source /opt/ssh_monitor/ssh_monitor.sh install "${recipients}" 31 | 32 | # Setup rkhunter for rootkit detection 33 | source standalones/rkhunter/setup_rkhunter.sh "${recipients}" "${sender}" 34 | 35 | # Setup lynis for comprehensive system auditing 36 | source standalones/lynis/setup_lynis.sh "${recipients}" "${sender}" 37 | } 38 | 39 | # Executes a series of standalone hardening scripts that do not require preconfiguration. 40 | standalone_hardening_scripts() { 41 | # Harden shared memory configuration 42 | source standalones/memory/setup_shared_memory_hardening.sh 43 | 44 | # Harden network parameters for the system 45 | source standalones/networking/setup_network_hardening_parameters.sh 46 | 47 | # Configure password policy using libpam-pwquality 48 | source standalones/password/setup_password_policy.sh 49 | } 50 | 51 | # Calls functions to execute all preconfigured and standalone hardening scripts. 52 | all_hardening_scripts() { 53 | preconfigured_hardening_scripts 54 | standalone_hardening_scripts 55 | } 56 | 57 | configuration_warning() { 58 | echo "Error: No configuration file found. Running standalone hardening scripts only..." 59 | echo "Preconfigured hardening scripts that require additional configuration are skipped:" 60 | echo " - auditd" 61 | echo " - apparmor" 62 | echo " - fail2ban" 63 | echo " - upgrades" 64 | echo " - sshd" 65 | echo " - rkhunter" 66 | echo " - lynis" 67 | } 68 | 69 | # Sources the configuration file if present, then runs all preconfigured and standalone hardening scripts. 70 | # If the configuration file is missing, it skips preconfigured scripts and only runs standalone scripts. 71 | # TODO: Adjust cases to handle being run from arbitrary directories 72 | main() { 73 | if [[ -f configs/config.sh ]]; then 74 | echo "Configuration file found. Running all hardening scripts..." 75 | source configs/config.sh 76 | all_hardening_scripts 77 | echo "All hardening scripts completed." 78 | else 79 | configuration_warning 80 | standalone_hardening_scripts 81 | echo "Standalone hardening scripts completed." 82 | echo "Please restart the system to apply all changes as certain hardening scripts require a reboot to take effect." 83 | fi 84 | } 85 | 86 | # Invoke the main function and pass all positional parameters to it 87 | main "$@" -------------------------------------------------------------------------------- /hardening/configs/config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # User mappings for SSH public keys 4 | # Format: "UserName:Algorithm Public_Key Comment" 5 | # Example A: "admin:ssh-rsa AAAAB3NzwnBnmkSBpiBsqQ== void@null" 6 | allowed_ssh_pk_user_mappings="admin:ssh-rsa AAAAB3NzwnBnmkSBpiBsqQ== void@null 7 | void:ssh-rsa AAAAB3NzwnBnmkSBpiBsqQ== void@null,ssh-ed2551 AAIDk7VFe example.eg@example.com" 8 | 9 | # Users allowed to login via SSH 10 | # Format: "user1,user2" 11 | # Example A: "admin" 12 | # Example B: "admin,void" 13 | allowed_ssh_users="admin,void" 14 | 15 | # IP Whitelist for Fail2Ban 16 | # Format: "IP/CIDR" 17 | # Example A: "1.1.1.1/32" 18 | # Example B: "1.1.1.1/32,2.2.2.2/32" 19 | ip_whitelist="1.1.1.1/32,2.2.2.2/32" 20 | 21 | # Email settings for sending notifications 22 | # Sender format: "@" 23 | # Example: "example@domain.com" 24 | # Recipients format: "@,@" 25 | # Example A: "example@domain.com" 26 | # Example B: "example@domain.com,example1@domain.com" 27 | sender="void@ex.com.au" 28 | recipients="void.recip@ex.com,example1@domain.com" 29 | 30 | # SSH Port for UFW and SSHD 31 | ssh_port=2783 32 | 33 | # Port list - used for mass allow or mass deny 34 | # Format: "port1,port2" 35 | # Example A: "22" 36 | # Example B: "22,2222" 37 | port_list_csv="8081,8080,443,80" 38 | 39 | # Port Action - used for mass allow or mass deny 40 | # Format: "allow" or "deny" 41 | # Example A: "allow" 42 | # Example B: "deny" 43 | firewall_action="allow" 44 | 45 | export allowed_ssh_pk_user_mappings allowed_ssh_users ip_whitelist sender recipients ssh_port port_list_csv firewall_action 46 | -------------------------------------------------------------------------------- /hardening/standalones/apparmor/setup_apparmor.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Ensures the script is executed with root privileges. Exits if not. 6 | check_root() { 7 | local uuid 8 | uuid=$(id -u) 9 | if [[ ${uuid} -ne 0 ]]; then 10 | echo "This script must be run as root. Exiting..." >&2 11 | exit 1 12 | fi 13 | } 14 | 15 | # Installs and configures AppArmor if it is not already active. 16 | install_and_configure_apparmor() { 17 | if systemctl is-active --quiet apparmor; then 18 | echo "AppArmor is already active and running. Skipping apt installation." 19 | else 20 | echo "Installing and activating AppArmor..." 21 | if apt-get update -y \ 22 | && apt-get install -y apparmor \ 23 | apparmor-utils \ 24 | apparmor-notify \ 25 | apparmor-profiles \ 26 | apparmor-profiles-extra \ 27 | && systemctl enable apparmor \ 28 | && systemctl start apparmor; then 29 | echo "AppArmor installed and activated successfully." 30 | else 31 | echo "Failed to install or activate AppArmor." 32 | exit 1 33 | fi 34 | fi 35 | } 36 | 37 | # Enforces AppArmor profiles and adds more coverage 38 | enforce_apparmor_profiles() { 39 | echo "Setting all AppArmor profiles to enforce mode..." 40 | if aa-enforce /etc/apparmor.d/* &> /dev/null; then 41 | echo "All AppArmor profiles set to enforce mode." 42 | else 43 | echo "Failed to set some profiles to enforce mode." 44 | fi 45 | echo "Current status of AppArmor profiles:" 46 | aa-status 47 | } 48 | 49 | # Enable Auditing for AppArmor 50 | enable_apparmor_auditing() { 51 | local auditd_installed 52 | auditd_installed=$(command -v auditd) 53 | 54 | if [[ -n ${auditd_installed} ]]; then 55 | echo "Auditd is already installed. Skipping installation." 56 | else 57 | echo "Installing auditd..." 58 | if apt-get update -y && apt-get install -y auditd; then 59 | echo "Auditd installed successfully." 60 | else 61 | echo "Failed to install auditd." 62 | exit 1 63 | fi 64 | fi 65 | 66 | echo "Enabling auditd for AppArmor..." 67 | { 68 | echo "-w /etc/apparmor/ -p wa -k apparmor" 69 | echo "-w /etc/apparmor.d/ -p wa -k apparmor" 70 | } >> /etc/audit/audit.rules && systemctl restart auditd && echo "Auditing for AppArmor enabled." 71 | } 72 | 73 | # Main function 74 | main() { 75 | check_root 76 | install_and_configure_apparmor 77 | enforce_apparmor_profiles 78 | enable_apparmor_auditing 79 | } 80 | 81 | main "$@" 82 | -------------------------------------------------------------------------------- /hardening/standalones/auditd/setup_auditd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Checks if the script is being run as root 6 | check_root() { 7 | local uuid 8 | uuid=$(id -u) 9 | if [[ ${uuid} -ne 0 ]]; then 10 | echo "This script must be run as root. Exiting..." >&2 11 | exit 1 12 | fi 13 | } 14 | 15 | ####################################### 16 | # description 17 | # Arguments: 18 | # 0 19 | ####################################### 20 | usage() { 21 | echo "Usage: $0 " 22 | echo "Example: $0 recipient1@ex.com,recipient2@ex.com sender@ex.com" 23 | exit 1 24 | } 25 | 26 | # Log messages to a log file 27 | log() { 28 | local message="$1" 29 | local date 30 | date=$(date +'%Y-%m-%d %H:%M:%S') 31 | echo "${date} - ${message}" 32 | } 33 | 34 | # Enable and start a service 35 | enable_service() { 36 | local service_name="$1" 37 | systemctl enable "${service_name}" --no-pager 38 | } 39 | 40 | # Restart a service 41 | restart_service() { 42 | local service_name="$1" 43 | systemctl restart "${service_name}" --no-pager 44 | } 45 | 46 | # Start a service 47 | start_service() { 48 | local service_name="$1" 49 | systemctl start "${service_name}" --no-pager 50 | } 51 | 52 | # Retrieve service status 53 | status_service() { 54 | local service_name="$1" 55 | systemctl status "${service_name}" --no-pager 56 | } 57 | 58 | # Show aureport 59 | show_auditd_aureport() { 60 | log "Showing aureport..." 61 | aureport --summary 62 | } 63 | 64 | # View Auditd configuration 65 | view_auditd_config() { 66 | log "Viewing Auditd configuration..." 67 | auditctl -l 68 | 69 | log "Viewing Auditd rules..." 70 | cat /etc/audit/rules.d/custom-audit-rules.rules 71 | 72 | log "Viewing Auditd configuration..." 73 | cat /etc/audit/auditd.conf 74 | } 75 | 76 | # Write an audit email reporting script to the specified location 77 | write_auditd_reporting() { 78 | local script_location="$1" 79 | local email_recipients="$2" 80 | local sender="$3" 81 | 82 | cat << EOF > "${script_location}" 83 | #!/usr/bin/env bash 84 | 85 | # Log a message to the log file 86 | log() { 87 | local message="\$1" 88 | local date 89 | date=\$(date +'%Y-%m-%d %H:%M:%S') 90 | echo "\${date} - \${message}" >> "\${log_file}" 91 | } 92 | 93 | # Generate and output an audit report 94 | generate_aurp_report() { 95 | local report 96 | report=\$(aureport --summary -i -ts today) 97 | echo "\${report}" 98 | } 99 | 100 | # Send a recipient with the audit report using sendmail 101 | send_auditd_report() { 102 | # Set the recipient address where the report will be sent 103 | local recipient="\$1" 104 | local mail_tool="\$2" 105 | local subject="\$3" 106 | local report="\$4" 107 | 108 | # Check if the report contains relevant information 109 | if [[ -n \${report} ]]; then 110 | # Send a recipient with the report using sendmail 111 | if ! echo -e "Subject: \${subject}\nTo: \${recipient}\nFrom: ${sender}\n\n\${report}" | \${mail_tool} -f "${sender}" -t "\${recipient}"; then 112 | log "Error: Failed to send email." 113 | else 114 | log "Email sent: \${subject}" 115 | fi 116 | log "Audit report sent." 117 | else 118 | # If no relevant information found, log a message 119 | log "No relevant audit information found." 120 | fi 121 | } 122 | 123 | # Main function 124 | main() { 125 | log_file="/var/log/audit-report.log" 126 | local mail_tool="sendmail" 127 | 128 | subject="[$(hostname -f)] - [Auditd Review Report] - [\$(date +'%Y-%m-%d')]" 129 | 130 | local report 131 | report="\$(generate_aurp_report)" 132 | 133 | # Add Comma separated email addresses of the recipients to the variable 134 | local recipients="${email_recipients}" # Directly inserting the variable value 135 | send_auditd_report "\${recipients}" "\${mail_tool}" "\${subject}" "\${report}" 136 | } 137 | 138 | main "\$@" 139 | EOF 140 | 141 | chmod +x "${script_location}" 142 | chown root:root "${script_location}" 143 | chown root:root "${script_location}" 144 | 145 | log "Audit reporting script copied to ${script_location}" 146 | } 147 | 148 | # Configure Auditd rules 149 | configure_audit_rules() { 150 | log "Configuring Auditd rules..." 151 | 152 | # Define custom audit rules 153 | # TODO - Extend ruleset 154 | cat << EOL > /etc/audit/rules.d/custom-audit-rules.rules 155 | # Monitor system changes 156 | -w /etc/passwd -p wa 157 | -w /etc/shadow -p wa 158 | -w /etc/sudoers -p wa 159 | -w /etc/group -p wa 160 | 161 | # Monitor login and authentication events 162 | -w /var/log/auth.log -p wa 163 | -w /var/log/secure -p wa 164 | 165 | # Monitor executable files 166 | -a always,exit -F arch=b64 -S execve 167 | -a always,exit -F arch=b32 -S execve 168 | EOL 169 | 170 | log "Custom Auditd rules configured." 171 | } 172 | 173 | # Configure Auditd logging 174 | configure_auditd_logging() { 175 | log "Configuring Auditd logging..." 176 | # Look for the log_format and log_file settings in the auditd.conf file and update them if necessary 177 | sed -i 's/^log_format =.*/log_format = RAW/g' /etc/audit/auditd.conf 178 | sed -i 's/^log_file =.*/log_file = \/var\/log\/audit\/audit.log/g' /etc/audit/auditd.conf 179 | log "Auditd logging configured." 180 | } 181 | 182 | # Updates the cron job for a script 183 | # shellcheck disable=SC2091 184 | update_cron_job() { 185 | if [[ -z "$1" ]] || [[ -z "$2" ]]; then 186 | log "Usage: update_cron_job " 187 | return 1 188 | fi 189 | 190 | local script="$1" 191 | local log_file="$2" 192 | local cron_entry="0 0 * * * ${script} >${log_file} 2>&1" 193 | 194 | # Attempt to read existing cron jobs, suppressing errors about no existing crontab 195 | local current_cron_jobs 196 | if ! current_cron_jobs=$(crontab -l 2> /dev/null); then 197 | log "No existing crontab for user. Creating new crontab..." 198 | fi 199 | 200 | # Check if the cron job already exists and is up-to-date 201 | if echo "${current_cron_jobs}" | grep -Fxq -- "${cron_entry}"; then 202 | log "Cron job already up to date." 203 | return 0 204 | else 205 | log "Cron job for script not found or not up-to-date. Adding new job..." 206 | fi 207 | 208 | # Add the cron job to the crontab 209 | if (echo "${current_cron_jobs}"; echo "${cron_entry}") | crontab -; then 210 | log "Cron job added successfully." 211 | else 212 | log "Failed to add cron job." 213 | fi 214 | 215 | } 216 | 217 | # Installs a list of apt packages 218 | install_apt_packages() { 219 | local package_list=("${@}") # Capture all arguments as an array of packages 220 | log "Starting package installation process." 221 | 222 | # Verify that there are no apt locks 223 | while fuser /var/lib/dpkg/lock > /dev/null 2>&1 || fuser /var/lib/apt/lists/lock > /dev/null 2>&1 || fuser /var/cache/apt/archives/lock > /dev/null 2>&1; do 224 | log "Waiting for other software managers to finish..." 225 | sleep 1 226 | done 227 | 228 | if apt update -y; then 229 | log "Package lists updated successfully." 230 | else 231 | log "Failed to update package lists. Continuing with installation..." 232 | fi 233 | 234 | local package 235 | local failed_packages=() 236 | for package in "${package_list[@]}"; do 237 | if dpkg -l | grep -qw "${package}"; then 238 | log "${package} is already installed." 239 | else 240 | # Sleep to avoid "E: Could not get lock /var/lib/dpkg/lock-frontend" error 241 | sleep 1 242 | if apt install -y "${package}"; then 243 | log "Successfully installed ${package}." 244 | else 245 | log "Failed to install ${package}." 246 | failed_packages+=("${package}") 247 | fi 248 | fi 249 | done 250 | 251 | if [[ ${#failed_packages[@]} -eq 0 ]]; then 252 | log "All packages were installed successfully." 253 | else 254 | log "Failed to install the following packages: ${failed_packages[*]}" 255 | fi 256 | } 257 | 258 | # Main function 259 | main() { 260 | check_root 261 | echo "Initializing Auditd..." 262 | 263 | local recipients="${1:-root@$(hostname -f)}" # Default recipient email if not provided 264 | local sender="${2:-root@$(hostname -f)}" # Default sender email if not provided 265 | 266 | install_apt_packages "auditd" 267 | configure_audit_rules 268 | configure_auditd_logging 269 | 270 | status_service "auditd" 271 | start_service "auditd" 272 | enable_service "auditd" 273 | restart_service "auditd" 274 | 275 | # view_auditd_config 276 | show_auditd_aureport 277 | 278 | local script_location="/usr/local/bin/audit-report.sh" 279 | local log_file="/var/log/audit-report.log" 280 | 281 | write_auditd_reporting "${script_location}" "${recipients}" "${sender}" 282 | update_cron_job "${script_location}" "${log_file}" 283 | 284 | echo "Auditd initialization complete." 285 | } 286 | 287 | main "$@" -------------------------------------------------------------------------------- /hardening/standalones/fail2ban/setup_fail2ban.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Log messages to a log file 6 | log() { 7 | local message="$1" 8 | local date 9 | date=$(date +'%Y-%m-%d %H:%M:%S') 10 | echo "${date} - ${message}" 11 | } 12 | 13 | usage() { 14 | echo "Usage: $0 " 15 | echo "Example A: $0 22 recipient1@ex.com,recipient2@ex.com sender@ex.com 5.5.5.5/32" 16 | echo "Example B: $0 22 recipient1@ex.com,recipient2@ex.com sender@ex.com 5.5.5.5/32,6.6.6.6/32" 17 | exit 1 18 | } 19 | 20 | # Checks if the script is being run as root 21 | check_root() { 22 | local uuid 23 | uuid=$(id -u) 24 | if [[ ${uuid} -ne 0 ]]; then 25 | echo "This script must be run as root. Exiting..." >&2 26 | exit 1 27 | fi 28 | } 29 | 30 | # Enable and start a service 31 | enable_service() { 32 | local service_name="$1" 33 | systemctl enable "${service_name}" --no-pager && { 34 | log "Enabled ${service_name} service." && { 35 | start_service "${service_name}" 36 | } 37 | } || { 38 | log "Failed to enable ${service_name} service." 39 | } 40 | } 41 | 42 | # Restart a service 43 | restart_service() { 44 | local service_name="$1" 45 | systemctl restart "${service_name}" --no-pager 46 | } 47 | 48 | # Start a service 49 | start_service() { 50 | local service_name="$1" 51 | systemctl start "${service_name}" --no-pager 52 | } 53 | 54 | # Retrieve service status 55 | status_service() { 56 | local service_name="$1" 57 | systemctl status "${service_name}" --no-pager 58 | } 59 | 60 | # Backup the existing Fail2ban jail configuration 61 | backup_jail_config() { 62 | local backup_dir="/etc/fail2ban/backup" 63 | local current_config="/etc/fail2ban/jail.local" 64 | local timestamp 65 | timestamp=$(date +"%Y%m%d%H%M%S") 66 | 67 | log "Creating a backup of Fail2ban jail configuration..." 68 | 69 | # Create the backup directory if it doesn't exist 70 | mkdir -p "${backup_dir}" 71 | 72 | # Copy the current jail configuration to the backup directory 73 | cp "${current_config}" "${backup_dir}/jail.local_${timestamp}.bak" || { 74 | log "Failed to create a backup of Fail2ban jail configuration." 75 | exit 1 76 | } && { 77 | log "Backup of Fail2ban jail configuration created." 78 | } 79 | } 80 | 81 | # Create a custom Fail2ban jail configuration for SSH protection 82 | create_jail_config() { 83 | local ssh_port="${1}" 84 | local custom_jail="${2}" # Path to the custom jail configuration 85 | local recipients="${3:-example.eg@example.com}" # Default recipient email if not provided 86 | local sender="${4:-example1.eg@example.com}" # Default sender email if not provided 87 | local ip_list="${5:-""}" # Default IP list to empty if not provided 88 | local ignore_ips 89 | 90 | if [[ ${ip_list} == "" ]]; then 91 | echo "No IP list provided. Proceeding without ignoring any IPs." 92 | ignore_ips="" 93 | else 94 | ignore_ips="ignoreip = ${ip_list}" 95 | fi 96 | 97 | # Check if parameters are provided 98 | if [[ -z ${custom_jail} ]]; then 99 | echo "Error: No file path provided for the jail configuration." 100 | return 1 101 | fi 102 | 103 | # Aggressive jail configuration for SSH 104 | cat <<- EOF > "${custom_jail}" 105 | [sshd] 106 | enabled = true 107 | port = ${ssh_port} 108 | filter = sshd 109 | logpath = /var/log/auth.log 110 | maxretry = 3 111 | findtime = 300 112 | bantime = 604800 113 | ${ignore_ips} 114 | destemail = ${recipients} 115 | sender = ${sender} 116 | sendername = Fail2Ban 117 | mta = sendmail 118 | action = %(action_mwl)s 119 | bantime.increment = true 120 | EOF 121 | } 122 | 123 | # Create a generic file if it doesn't exist 124 | create_file_if_not_exists() { 125 | local file="$1" 126 | local error_log_file="$2" 127 | if [[ ! -f ${file} ]]; then 128 | echo "attempting to create file: ${file}" 129 | touch "${file}" || { 130 | system_log "ERROR" "Unable to create file: ${file}" "${error_log_file}" 131 | exit 1 132 | } 133 | fi 134 | } 135 | 136 | # Ensure a file is writable 137 | ensure_file_is_writable() { 138 | local file="$1" 139 | local error_log_file="$2" 140 | if [[ ! -w ${file} ]]; then 141 | echo "attempting to make file writable: ${file}" 142 | system_log "ERROR" "File is not writable: ${file}" "${error_log_file}" 143 | exit 1 144 | fi 145 | } 146 | 147 | # Installs a list of apt packages 148 | install_apt_packages() { 149 | local package_list=("${@}") # Capture all arguments as an array of packages 150 | 151 | log "Starting package installation process." 152 | 153 | # Verify that there are no apt locks 154 | while fuser /var/lib/dpkg/lock > /dev/null 2>&1 || fuser /var/lib/apt/lists/lock > /dev/null 2>&1 || fuser /var/cache/apt/archives/lock > /dev/null 2>&1; do 155 | log "Waiting for other software managers to finish..." 156 | sleep 1 157 | done 158 | 159 | if apt update -y; then 160 | log "Package lists updated successfully." 161 | else 162 | log "Failed to update package lists. Continuing with installation..." 163 | fi 164 | 165 | local package 166 | local failed_packages=() 167 | for package in "${package_list[@]}"; do 168 | if dpkg -l | grep -qw "${package}"; then 169 | log "${package} is already installed." 170 | else 171 | # Sleep to avoid "E: Could not get lock /var/lib/dpkg/lock-frontend" error 172 | sleep 1 173 | if apt install -y "${package}"; then 174 | log "Successfully installed ${package}." 175 | else 176 | log "Failed to install ${package}." 177 | failed_packages+=("${package}") 178 | fi 179 | fi 180 | done 181 | 182 | if [[ ${#failed_packages[@]} -eq 0 ]]; then 183 | log "All packages were installed successfully." 184 | else 185 | log "Failed to install the following packages: ${failed_packages[*]}" 186 | fi 187 | } 188 | 189 | # Main function 190 | main() { 191 | check_root 192 | echo "Initializing Fail2Ban..." 193 | 194 | local custom_jail="/etc/fail2ban/jail.local" 195 | local fail2ban_log_file="/var/log/fail2ban-setup.log" 196 | 197 | if [[ $# -ne 4 ]]; then 198 | usage 199 | fi 200 | 201 | local ssh_port="${1:-22}" # Default SSH port if not provided 202 | local recipients="${2:-root@$(hostname -f)}" # Default recipient email if not provided 203 | local sender="${3:-root@$(hostname -f)}" # Default sender email if not provided 204 | local ip_list="${4:-""}" # Default IP list to empty if not provided 205 | 206 | install_apt_packages "fail2ban" 207 | 208 | create_file_if_not_exists "${fail2ban_log_file}" "${fail2ban_log_file}" 209 | create_file_if_not_exists "${custom_jail}" "${fail2ban_log_file}" 210 | 211 | ensure_file_is_writable "${fail2ban_log_file}" "${fail2ban_log_file}" 212 | ensure_file_is_writable "${custom_jail}" "${fail2ban_log_file}" 213 | 214 | enable_service "fail2ban" 215 | 216 | backup_jail_config 217 | 218 | # Create a custom Fail2ban jail configuration for SSH protection 219 | create_jail_config "${ssh_port}" "${custom_jail}" "${recipients}" "${sender}" "${ip_list}" 220 | 221 | # Restart Fail2ban service to apply the new configuration 222 | restart_service "fail2ban" 223 | 224 | # Verify the status of the Fail2ban service 225 | status_service "fail2ban" 226 | } 227 | 228 | main "$@" -------------------------------------------------------------------------------- /hardening/standalones/firewall/setup_firewall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Checks if the script is being run as root 6 | check_root() { 7 | local uuid 8 | uuid=$(id -u) 9 | if [[ ${uuid} -ne 0 ]]; then 10 | echo "This script must be run as root. Exiting..." >&2 11 | exit 1 12 | fi 13 | } 14 | 15 | # Prints informational messages to stdout. 16 | info() { 17 | echo "[INFO] $1" # Echoes the input message prefixed with [INFO]. 18 | } 19 | 20 | backup_file() { 21 | local file_path=$1 22 | local date 23 | date=$(date +%Y-%m-%d-%H-%M-%S) 24 | local backup_path="${file_path}.bak.${date}" 25 | 26 | if [[ -f "${file_path}" ]]; then 27 | info "Backing up ${file_path} to ${backup_path}..." 28 | cp "${file_path}" "${backup_path}" || { info "Failed to back up ${file_path}"; exit 1; } 29 | else 30 | info "${file_path} does not exist. No need to back it up." 31 | fi 32 | } 33 | 34 | # Update ICMP rules in the specified UFW before.rules file 35 | update_icmp_iptables() { 36 | local ufw_before_rules="$1" 37 | 38 | # Ensure the UFW rules file exists 39 | if [[ ! -f "${ufw_before_rules}" ]]; then 40 | info "File ${ufw_before_rules} does not exist. Skipping ICMP blocking..." 41 | return 1 42 | fi 43 | 44 | info "File ${ufw_before_rules} exists. Modifying to block ICMP..." 45 | 46 | # Define the types of ICMP messages to be modified 47 | local icmp_types=("destination-unreachable" "source-quench" "time-exceeded" "parameter-problem" "echo-request") 48 | 49 | # Backup the original file before making changes 50 | cp "${ufw_before_rules}" "${ufw_before_rules}.bak" 51 | 52 | local icmp_type 53 | for icmp_type in "${icmp_types[@]}"; do 54 | # Check if an ACCEPT rule exists for the current ICMP type 55 | if grep -qE -- "--icmp-type ${icmp_type} -j ACCEPT" "${ufw_before_rules}"; then 56 | # Replace ACCEPT with DROP for the found rule 57 | sed -i "s/--icmp-type ${icmp_type} -j ACCEPT/--icmp-type ${icmp_type} -j DROP/g" "${ufw_before_rules}" 58 | info "Replaced ACCEPT with DROP for ICMP type: ${icmp_type}." 59 | else 60 | # If no specific ACCEPT rule exists, check for a DROP rule before adding 61 | if ! grep -qE -- "--icmp-type ${icmp_type} -j DROP" "${ufw_before_rules}"; then 62 | # Insert the DROP rule before COMMIT 63 | sed -i "/^COMMIT/i -A ufw-before-input -p icmp --icmp-type ${icmp_type} -j DROP" "${ufw_before_rules}" 64 | info "Inserted DROP rule for ICMP type: ${icmp_type}." 65 | fi 66 | fi 67 | done 68 | 69 | info "ICMP blocking rules have been updated in ${ufw_before_rules}." 70 | } 71 | 72 | # Checks if UFW is installed and installs it if it is not. 73 | install_ufw() { 74 | if ! command -v ufw &> /dev/null; then 75 | info "UFW is not installed. Installing..." 76 | apt-get update -y || { info "Failed to update package list"; exit 1; } 77 | apt-get install -y ufw || { info "Failed to install UFW"; exit 1; } 78 | else 79 | info "UFW is already installed." 80 | fi 81 | } 82 | 83 | # Enable a strict policy for UFW 84 | enable_strict_policy() { 85 | local ssh_port="$1" 86 | 87 | info "Enabling default deny policy and allowing SSH on a non-standard port..." 88 | 89 | # Specify the policy to deny all incoming connections by default 90 | ufw default deny incoming || { info "Failed to set default deny policy"; exit 1; } 91 | 92 | # Specify the policy to allow all outgoing connections by default 93 | ufw default allow outgoing || { info "Failed to set default allow policy"; exit 1; } 94 | 95 | # Enable logging to help diagnose issues 96 | ufw logging on || { info "Failed to enable logging"; exit 1; } 97 | 98 | # Increase the logging level to help diagnose issues 99 | ufw logging high || { info "Failed to set logging level"; exit 1; } 100 | 101 | ## Allow SSH connections on a non-standard port 102 | ufw allow "${ssh_port}"/tcp || { info "Failed to allow SSH on port ${ssh_port}"; exit 1; } 103 | 104 | # Rate limit SSH connections on the non-standard port to prevent brute force attacks 105 | ufw limit "${ssh_port}"/tcp || { info "Failed to rate limit SSH on port ${ssh_port}"; exit 1; } 106 | 107 | # Disable connections on the default SSH port 108 | ufw deny 22/tcp || { info "Failed to deny SSH on port 22"; exit 1; } 109 | 110 | # Enable UFW with --force to avoid being prompted to confirm the changes 111 | ufw --force enable || { info "Failed to enable UFW"; exit 1; } 112 | 113 | systemctl enable ufw || { info "Failed to enable UFW on boot"; exit 1; } 114 | } 115 | 116 | handle_ports() { 117 | local port_list_csv="$1" 118 | local state_type="$2" 119 | local ports port_list 120 | 121 | IFS=',' read -r -a port_list <<< "${port_list_csv}" 122 | for ports in "${port_list[@]}"; do 123 | if [[ ${state_type} == "allow" ]]; then 124 | ufw allow "${ports}" 125 | elif [[ ${state_type} == "deny" ]]; then 126 | ufw deny "${ports}" 127 | else 128 | echo "Invalid state type: ${state_type}" 129 | exit 1 130 | fi 131 | done 132 | 133 | ufw reload 134 | ufw status 135 | } 136 | 137 | usage() { 138 | echo "Usage: ./setup_firewall.sh " >&2 139 | exit 1 140 | } 141 | 142 | main() { 143 | check_root 144 | 145 | if [[ "$#" -ne 3 ]]; then 146 | usage 147 | fi 148 | 149 | local ssh_port="${1}" 150 | local allow_list_csv="${2}" 151 | local action="${3}" 152 | 153 | install_ufw 154 | local ufw_before_rules="/etc/ufw/before.rules" 155 | 156 | backup_file "${ufw_before_rules}" 157 | update_icmp_iptables "${ufw_before_rules}" 158 | enable_strict_policy "${ssh_port}" 159 | 160 | handle_ports "${allow_list_csv}" "${action}" 161 | } 162 | 163 | main "$@" -------------------------------------------------------------------------------- /hardening/standalones/lynis/setup_lynis.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Checks if the script is being run as root 6 | check_root() { 7 | local uuid 8 | uuid=$(id -u) 9 | if [[ ${uuid} -ne 0 ]]; then 10 | echo "This script must be run as root. Exiting..." >&2 11 | exit 1 12 | fi 13 | } 14 | 15 | usage() { 16 | echo "Usage: $0 " 17 | echo "Example: $0 recipient1@ex.com,recipient2@ex.com sender@ex.com" 18 | exit 1 19 | } 20 | 21 | # Updates the cron job for a script 22 | update_cron_job() { 23 | if [[ -z "$1" ]] || [[ -z "$2" ]]; then 24 | log "Usage: update_cron_job " 25 | return 1 26 | fi 27 | 28 | local script="$1" 29 | local log_file="$2" 30 | local cron_entry="0 0 * * * ${script} >${log_file} 2>&1" 31 | 32 | # Attempt to read existing cron jobs, suppressing errors about no existing crontab 33 | local current_cron_jobs 34 | if ! current_cron_jobs=$(crontab -l 2> /dev/null); then 35 | log "No existing crontab for user. Creating new crontab..." 36 | fi 37 | 38 | # Check if the cron job already exists and is up-to-date 39 | if echo "${current_cron_jobs}" | grep -Fxq -- "${cron_entry}"; then 40 | log "Cron job already up to date." 41 | return 0 42 | else 43 | log "Cron job for script not found or not up-to-date. Adding new job..." 44 | fi 45 | 46 | # Add the cron job to the crontab 47 | if (echo "${current_cron_jobs}"; echo "${cron_entry}") | crontab -; then 48 | log "Cron job added successfully." 49 | else 50 | log "Failed to add cron job." 51 | fi 52 | 53 | } 54 | 55 | # Writes out the Lynis installer script 56 | # shellcheck disable=SC1091 57 | write_out_lynis_installer() { 58 | 59 | local installer_location="$1" 60 | local email_recipient_list="${2:-root@localhost}" 61 | local email_sender="${3:-root@localhost}" 62 | 63 | cat << EOF > "${installer_location}" 64 | #!/usr/bin/env bash 65 | 66 | set -euo pipefail 67 | 68 | # Email variables dynamically inserted 69 | email_recipient_list="${email_recipient_list}" 70 | email_sender="${email_sender}" 71 | 72 | # Cleans up old versions of Lynis 73 | cleanup_old_lynis() { 74 | local lynis_dir="\$1" 75 | local lynis_version="\$2" 76 | echo "Cleaning up old versions of Lynis..." 77 | find "\${lynis_dir}" -maxdepth 1 -type d -name 'lynis-*' -not -name "lynis-\${lynis_version}" -exec rm -rf {} \; 78 | [[ -d "\${lynis_dir}/.git" ]] && echo "Removing existing Lynis Git installation..." && rm -rf "\${lynis_dir}" 79 | echo "Old versions of Lynis cleaned up." 80 | } 81 | 82 | # Creates the Lynis directory 83 | create_directory() { 84 | local lynis_dir="\$1" 85 | echo "Creating Lynis directory..." 86 | mkdir -p "\${lynis_dir}" 87 | } 88 | 89 | # Downloads the Lynis tarball and signature file 90 | download_lynis() { 91 | local lynis_tarball="\$1" 92 | local lynis_tarball_url="\$2" 93 | local download_path="\$3" 94 | 95 | echo "Downloading Lynis from \${lynis_tarball}..." 96 | 97 | wget -q "\${lynis_tarball_url}" -O "\${download_path}/\${lynis_tarball}" || { 98 | echo "Failed to download Lynis tarball" 99 | exit 1 100 | } 101 | 102 | wget -q "\${lynis_tarball_url}.asc" -O "\${download_path}/\${lynis_tarball}.asc" || { 103 | echo "Failed to download Lynis signature file" 104 | exit 1 105 | } 106 | 107 | [[ -s "\${download_path}/\${lynis_tarball}.asc" ]] || { 108 | echo "Lynis signature file is missing or empty" 109 | exit 1 110 | } 111 | } 112 | 113 | # Unpacks the Lynis tarball 114 | unpack_tarball() { 115 | local lynis_tarball="\$1" 116 | local lynis_dir="\$2" 117 | echo "Unpacking Lynis tarball..." 118 | tar xfvz "\${lynis_tarball}" -C "\${lynis_dir}" || { 119 | echo "Failed to unpack Lynis tarball" 120 | exit 1 121 | } 122 | echo "Contents of \${lynis_dir}:" 123 | ls -la "\${lynis_dir}" 124 | } 125 | 126 | # Verifies the downloaded Lynis tarball against the checksum 127 | verify_download() { 128 | local lynis_tarball="\$1" 129 | echo "Verifying Lynis tarball..." 130 | sha256sum "\${lynis_tarball}" || { 131 | echo "sha256sum checksum verification failed" 132 | exit 1 133 | } 134 | } 135 | 136 | # Imports the GPG key used to sign the Lynis tarball and checks the fingerprint 137 | import_and_retrieve_gpg_key_fingerprint() { 138 | local lynis_gpg_key_url="\$1" 139 | wget "\${lynis_gpg_key_url}" -O cisofy-software.pub || { 140 | echo "Failed to download GPG key" 141 | exit 1 142 | } 143 | gpg --import cisofy-software.pub || { 144 | echo "Failed to import GPG key" 145 | exit 1 146 | } 147 | 148 | local fingerprint 149 | fingerprint=\$(gpg --list-keys --with-fingerprint | grep -oP '([A-F0-9]{4}\\s*){10}' | tail -1 | tr -d '[:space:]') 150 | 151 | echo "\${fingerprint}" 152 | } 153 | 154 | # Verifies the signature of the Lynis tarball 155 | verify_signature() { 156 | local lynis_tarball="\$1" 157 | local lynis_tarball_signature="\$2" 158 | gpg --verify "\${lynis_tarball_signature}" "\${lynis_tarball}" || { 159 | echo "GPG signature verification failed" 160 | exit 1 161 | } 162 | echo "GPG signature verified successfully." 163 | } 164 | 165 | # Compares the GPG key fingerprint with the DNS TXT record 166 | compare_fingerprint_with_dns() { 167 | local expected_fingerprint="\$1" 168 | 169 | local dns_fingerprint 170 | dns_fingerprint=\$(host -t txt cisofy-software-key.cisofy.com | grep -oP 'Key fingerprint = \\K.*' | tr -d '[:space:]' | tr -d '"') 171 | 172 | [[ \${dns_fingerprint} == "\${expected_fingerprint}" ]] || { 173 | echo "DNS fingerprint mismatch" 174 | exit 1 175 | } 176 | echo "DNS fingerprint verification successful." 177 | } 178 | 179 | # Sets the trust level of the GPG key 180 | set_gpg_key_trust() { 181 | # Set the trust level of the GPG key to "ultimate/5" without user interaction to avoid the prompt during the Lynis installation 182 | echo -e "5\\ny\\n" | gpg --command-fd 0 --edit-key "CISOfy software signing" trust 183 | } 184 | 185 | # Add a timestamp to the log message and print it to stdout and the log file 186 | log_message() { 187 | local log_file="/var/log/lynis.log" 188 | local date 189 | date=\$(date '+%Y-%m-%d %H:%M:%S') 190 | echo "[\${date}] \$1" | tee -a "\${log_file}" 191 | } 192 | 193 | # Sends an email 194 | send_email() { 195 | local subject="\$1" 196 | local content="\$2" 197 | local recipient="\$email_recipient_list" 198 | 199 | local sender="\$email_sender" 200 | 201 | local mail_tool="sendmail" 202 | 203 | local hostname 204 | hostname=\$(hostname -f) 205 | 206 | subject="[\$1] - [\${hostname}] - \$(date '+%Y-%m-%d %H:%M')" 207 | 208 | if [[ -z \${recipient} ]]; then 209 | log_message "Error: Email recipient not specified." 210 | return 1 211 | fi 212 | 213 | if ! echo -e "Subject: \${subject}\\nTo: \${recipient}\\nFrom: \${sender}\\n\\n\$(cat "\$2")" | \${mail_tool} -f "\${sender}" -t "\${recipient}"; then 214 | log_message "Error: Failed to send email." 215 | else 216 | log_message "Email sent: \${subject}" 217 | fi 218 | } 219 | 220 | # Controls script flow for installing Lynis 221 | lynis_installer() { 222 | local lynis_version="3.0.9" 223 | local lynis_dir="/usr/local" 224 | local log_dir="/var/log" 225 | local lynis_tarball_url="https://downloads.cisofy.com/lynis/lynis-3.0.9.tar.gz" 226 | local lynis_gpg_key_url="https://cisofy.com/files/cisofy-software.pub" 227 | 228 | cd "\${lynis_dir}" || { 229 | echo "Failed to change directory to \${lynis_dir}" 230 | exit 1 231 | } 232 | 233 | cleanup_old_lynis "\${lynis_dir}" "\${lynis_version}" 234 | create_directory "\${lynis_dir}" 235 | download_lynis "lynis-\${lynis_version}.tar.gz" "\${lynis_tarball_url}" "\${lynis_dir}" 236 | unpack_tarball "lynis-\${lynis_version}.tar.gz" "\${lynis_dir}" 237 | verify_download "\${lynis_dir}/lynis-\${lynis_version}.tar.gz" 238 | 239 | local fingerprint 240 | fingerprint=\$(import_and_retrieve_gpg_key_fingerprint "\${lynis_gpg_key_url}") 241 | 242 | verify_signature "\${lynis_dir}/lynis-\${lynis_version}.tar.gz" "\${lynis_dir}/lynis-\${lynis_version}.tar.gz.asc" 243 | compare_fingerprint_with_dns "\${fingerprint}" 244 | set_gpg_key_trust 245 | 246 | # Ensure the Lynis script is r/w/x by root only 247 | local lynis_executable_path="\${lynis_dir}/lynis/lynis" 248 | chown root:root "\${lynis_executable_path}" 249 | chmod 700 "\${lynis_executable_path}" 250 | 251 | # Ensure the Lynis script is executable 252 | if [[ ! -x \${lynis_executable_path} ]]; then 253 | echo "Lynis script is not executable" 254 | exit 1 255 | fi 256 | 257 | echo "Executing the Lynis script..." 258 | cd "\${lynis_dir}/lynis" && ls -la 259 | 260 | ./lynis show version 261 | # Run the Lynis audit and pentest 262 | ./lynis audit system --quiet 263 | ./lynis --pentest --quiet 264 | ./lynis --forensics --quiet 265 | ./lynis --devops --quiet 266 | ./lynis --developer --quiet 267 | 268 | # Send the audit report via email 269 | send_email "Lynis Audit Report" "\${log_dir}/lynis-report.dat" "\${email_recipient_list}" "\${email_sender}" 270 | } 271 | 272 | # Main function to control script flow 273 | main() { 274 | lynis_installer 275 | } 276 | 277 | main "\$@" 278 | 279 | EOF 280 | 281 | # Modify the permissions of the installer script to be executable by root only 282 | chmod 700 "${installer_location}" 283 | echo "Lynis installer script written to ${installer_location}" 284 | 285 | # Run the installer 286 | "${installer_location}" 287 | } 288 | 289 | # Main function to control script flow 290 | main() { 291 | check_root 292 | 293 | echo "Initializing Lynis..." 294 | local lynis_script_path="/usr/local/bin/lynis_installer.sh" 295 | 296 | local recipients="${1:-root@$(hostname -f)}" 297 | local sender="${2:-root@$(hostname -f)}" 298 | 299 | # Dynamically generate the installer 300 | write_out_lynis_installer "${lynis_script_path}" "${recipients}" "${sender}" 301 | 302 | # Update the cron job 303 | update_cron_job "${lynis_script_path}" "/var/log/lynis_cron.log" 304 | 305 | } 306 | 307 | main "$@" -------------------------------------------------------------------------------- /hardening/standalones/memory/setup_shared_memory_hardening.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Enhances the security of /run/shm by setting 'noexec', 'nosuid', 'nodev' options. 6 | # Ensures these settings persist after reboot by updating /etc/fstab. Requires root privileges. 7 | 8 | # Checks if the script is being run as root 9 | check_root() { 10 | local uuid 11 | uuid=$(id -u) 12 | if [[ ${uuid} -ne 0 ]]; then 13 | echo "This script must be run as root. Exiting..." >&2 14 | exit 1 15 | fi 16 | } 17 | 18 | # Backs up /etc/fstab with a timestamp. 19 | backup_fstab() { 20 | local backup_path 21 | backup_path="/etc/fstab.backup.$(date +%Y%m%d-%H%M%S)" 22 | echo "Backing up /etc/fstab to ${backup_path}..." 23 | cp /etc/fstab "${backup_path}" || { 24 | echo "Failed to back up /etc/fstab." >&2 25 | exit 1 26 | } 27 | echo "Backup created successfully." 28 | } 29 | 30 | # Updates or adds the fstab entry for /run/shm with secure options. 31 | update_fstab() { 32 | local entry="tmpfs /run/shm tmpfs defaults,noexec,nosuid,nodev 0 0" 33 | echo "Ensuring secure options for /run/shm in /etc/fstab..." 34 | 35 | # Replace existing entry with secure options or add if not present. 36 | if grep -qE "^tmpfs /run/shm tmpfs" /etc/fstab; then 37 | sed -i "\|^tmpfs /run/shm tmpfs|c\\${entry}" /etc/fstab 38 | else 39 | echo "${entry}" >> /etc/fstab 40 | fi 41 | 42 | echo "/etc/fstab updated." 43 | } 44 | 45 | # Remounts /run/shm to apply the new options. 46 | remount_shared_memory() { 47 | if mount -o remount /run/shm; then 48 | echo "Shared memory remounted successfully." 49 | else 50 | echo "Failed to remount /run/shm." >&2 51 | exit 1 52 | fi 53 | } 54 | 55 | # Validates the secure configuration of /run/shm. 56 | validate_config() { 57 | echo "Validating shared memory configuration..." 58 | local mount_options 59 | mount_options=$(findmnt -n -o OPTIONS /run/shm) 60 | 61 | local option 62 | for option in noexec nosuid nodev; do 63 | if [[ ! "${mount_options}" = *"${option}"* ]]; then 64 | echo "Validation failed: /run/shm missing ${option} option." >&2 65 | exit 1 66 | fi 67 | done 68 | 69 | echo "Shared memory is secured correctly." 70 | } 71 | 72 | main() { 73 | check_root 74 | echo "Initializing secure /run/shm configuration..." 75 | backup_fstab 76 | update_fstab 77 | remount_shared_memory 78 | validate_config 79 | } 80 | 81 | main "$@" 82 | -------------------------------------------------------------------------------- /hardening/standalones/networking/setup_network_hardening_parameters.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Checks if the script is being run as root 6 | check_root() { 7 | local uuid 8 | uuid=$(id -u) 9 | if [[ ${uuid} -ne 0 ]]; then 10 | echo "This script must be run as root. Exiting..." >&2 11 | exit 1 12 | fi 13 | } 14 | 15 | # This script hardens the network configuration to improve security. It includes 16 | # backup of existing configurations, application of strict networking rules, 17 | # and adjustments to system parameters to mitigate various network-based attacks. 18 | harden_networking_stack_kernel_params() { 19 | echo "Starting network configuration hardening process..." 20 | 21 | # Create a backup of the existing sysctl configuration with a timestamp 22 | # to allow easy restoration if needed. The script exits if the backup fails. 23 | local backup_file 24 | backup_file="/etc/sysctl.conf.backup.$(date +%Y%m%d%H%M%S)" 25 | echo "Creating backup of current sysctl configuration..." 26 | cp /etc/sysctl.conf "${backup_file}" && echo "Backup saved as ${backup_file}" || { echo "Backup failed. Exiting."; exit 1; } 27 | 28 | # Specifies the file where hardening configurations will be written. 29 | # This approach keeps our custom configurations separate for easy management. 30 | local hardening_config="/etc/sysctl.d/99-hardening.conf" 31 | echo "Applying hardening configurations to ${hardening_config}..." 32 | 33 | { 34 | echo "# Custom hardening network settings" 35 | 36 | # Prevent IP spoofing by enabling reverse path filtering on all interfaces. 37 | echo "net.ipv4.conf.all.rp_filter=1" 38 | echo "net.ipv4.conf.default.rp_filter=1" 39 | 40 | # Disable IP forwarding to prevent the machine from routing packets. 41 | echo "net.ipv4.ip_forward=0" 42 | 43 | # Block source routed packets to prevent direct routing control. 44 | echo "net.ipv4.conf.all.accept_source_route=0" 45 | echo "net.ipv4.conf.default.accept_source_route=0" 46 | 47 | # Disable ICMP redirects to prevent misrouting and MITM attacks. 48 | echo "net.ipv4.conf.all.accept_redirects=0" 49 | echo "net.ipv4.conf.default.accept_redirects=0" 50 | echo "net.ipv4.conf.all.secure_redirects=1" 51 | echo "net.ipv4.conf.default.secure_redirects=1" 52 | 53 | # Mitigate smurf attacks by ignoring ICMP broadcasts. 54 | echo "net.ipv4.icmp_echo_ignore_broadcasts=1" 55 | 56 | # Protect against bogus ICMP responses which can lead to DOS attacks. 57 | echo "net.ipv4.icmp_ignore_bogus_error_responses=1" 58 | 59 | # Use TCP SYN cookies to protect against SYN flood attacks. 60 | echo "net.ipv4.tcp_syncookies=1" 61 | 62 | # Log packets with impossible source addresses to detect spoofing. 63 | echo "net.ipv4.conf.all.log_martians=1" 64 | echo "net.ipv4.conf.default.log_martians=1" 65 | 66 | # Disable sending of ICMP redirects to prevent potential misuse. 67 | echo "net.ipv4.conf.all.send_redirects=0" 68 | echo "net.ipv4.conf.default.send_redirects=0" 69 | 70 | # Adjust port and buffer settings to mitigate denial-of-service attacks. 71 | echo "net.ipv4.ip_local_port_range=1024 65535" 72 | echo "net.ipv4.tcp_fin_timeout=30" 73 | echo "net.ipv4.tcp_keepalive_time=300" 74 | echo "net.ipv4.tcp_keepalive_probes=5" 75 | echo "net.ipv4.tcp_keepalive_intvl=15" 76 | echo "net.ipv4.tcp_rmem=4096 87380 6291456" 77 | echo "net.ipv4.tcp_wmem=4096 16384 4194304" 78 | echo "net.ipv4.udp_rmem_min=16384" 79 | echo "net.ipv4.udp_wmem_min=16384" 80 | 81 | # Additional protections against TCP TIME-WAIT assassination and ARP spoofing. 82 | echo "net.ipv4.tcp_rfc1337=1" 83 | echo "net.ipv4.conf.all.drop_gratuitous_arp=1" 84 | 85 | # Limit SYN backlog and retries to mitigate SYN flood attacks. 86 | echo "net.ipv4.tcp_max_syn_backlog=2048" 87 | echo "net.ipv4.tcp_synack_retries=2" 88 | 89 | # IPv6 specific settings to disable redirects and autoconfiguration 90 | # if IPv6 is not used. This helps in simplifying network security posture. 91 | echo "net.ipv6.conf.all.accept_redirects=0" 92 | echo "net.ipv6.conf.default.accept_redirects=0" 93 | echo "net.ipv6.conf.all.disable_ipv6=1" 94 | echo "net.ipv6.conf.default.disable_ipv6=1" 95 | 96 | # Disable IPv6 features that may not be needed and could pose security risks. 97 | echo "net.ipv6.conf.all.accept_ra=0" 98 | echo "net.ipv6.conf.default.accept_ra=0" 99 | 100 | echo "net.ipv6.conf.all.proxy_ndp=0" 101 | echo "net.ipv6.conf.default.proxy_ndp=0" 102 | 103 | } > "${hardening_config}" 104 | 105 | # Apply the new sysctl parameters from the configuration file. 106 | echo "Applying sysctl parameters..." 107 | sysctl --system 108 | 109 | echo "Network configuration hardening completed." 110 | } 111 | 112 | # Main function 113 | main() { 114 | check_root 115 | echo "Initializing network configuration hardening..." 116 | harden_networking_stack_kernel_params 117 | } 118 | 119 | main "$@" -------------------------------------------------------------------------------- /hardening/standalones/password/setup_password_policy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Ensures the script is executed with root privileges. Exits if not. 6 | check_root() { 7 | if [[ ${EUID} -ne 0 ]]; then 8 | echo "This script must be run as root. Exiting." 9 | exit 1 10 | fi 11 | } 12 | 13 | # Backup a file before modification 14 | backup_file() { 15 | local file_path="$1" 16 | if [[ -f ${file_path} ]]; then 17 | cp "${file_path}" "${file_path}.bak" 18 | echo "Backup of ${file_path} created." 19 | else 20 | echo "File ${file_path} does not exist. Skipping backup." 21 | fi 22 | } 23 | 24 | # Set a strong password policy 25 | configure_password_policy() { 26 | local pwquality_config="$1" 27 | local login_defs="$2" 28 | local common_password="$3" 29 | 30 | echo "Configuring password policy..." 31 | apt-get install libpam-pwquality libpam-cracklib -y || { 32 | echo "Failed to install libpam-pwquality libpam-cracklib" 33 | exit 1 34 | } 35 | 36 | # Back up configuration files 37 | backup_file "${pwquality_config}" 38 | backup_file "${login_defs}" 39 | backup_file "${common_password}" 40 | 41 | # Define a helper function for setting configuration options without duplicating them 42 | set_config_option() { 43 | local key="$1" 44 | local value="$2" 45 | local file="$3" 46 | 47 | if ! grep -q "^${key} = ${value}" "$file"; then 48 | if grep -q "^${key} " "$file"; then 49 | sed -i "/^${key} /c\\${key} = ${value}" "$file" 50 | else 51 | echo "${key} = ${value}" >> "$file" 52 | fi 53 | fi 54 | } 55 | 56 | # Password length, requirements for digits, uppercase, lowercase, special characters, repeat characters, and classes 57 | # Negative integer values imposes 'at least' requirements (e.g. -1 means at least 1 digit that matches the dcredit requirement) 58 | # Positive integer values imposes 'at most' requirements (e.g. maxrepeat=3 means no more than 3 repeating characters) 59 | set_config_option "minlen" "24" "${pwquality_config}" 60 | set_config_option "retry" "3" "${pwquality_config}" 61 | set_config_option "dcredit" "-1" "${pwquality_config}" 62 | set_config_option "ucredit" "-1" "${pwquality_config}" 63 | set_config_option "lcredit" "-1" "${pwquality_config}" 64 | set_config_option "ocredit" "-1" "${pwquality_config}" 65 | set_config_option "maxrepeat" "3" "${pwquality_config}" 66 | set_config_option "maxclassrepeat" "3" "${pwquality_config}" 67 | 68 | # Enforce SHA512 for password hashing 69 | set_config_option "ENCRYPT_METHOD" "SHA512" "/etc/login.defs" 70 | 71 | # Configure PAM to enforce the password policy 72 | local pam_pwquality_line="password requisite pam_pwquality.so retry=3 minlen=24 ucredit=-1 dcredit=-1 ocredit=-1 lcredit=-1 maxrepeat=3 maxclassrepeat=3" 73 | 74 | # Ensure no duplicate pam_pwquality.so line 75 | if ! grep -qF -- "$pam_pwquality_line" "${common_password}"; then 76 | sed -i "/^password.*requisite.*pam_pwquality.so/c\\$pam_pwquality_line" "${common_password}" 77 | fi 78 | 79 | # Ensure no duplicate pam_pwhistory.so line 80 | # Password history enforcement - remember 5 last passwords, use_authtok - use the current password when changing the password (not prompting for the old password) 81 | if ! grep -q "pam_pwhistory.so" "${common_password}"; then 82 | echo "password required pam_pwhistory.so remember=5 use_authtok" >> "${common_password}" 83 | fi 84 | 85 | # Password aging settings 86 | { 87 | echo "PASS_MAX_DAYS 90" 88 | echo "PASS_MIN_DAYS 7" 89 | echo "PASS_WARN_AGE 7" 90 | } >> "${login_defs}" 91 | 92 | echo "Password policy configured." 93 | } 94 | 95 | configure_faillock() { 96 | local common_auth="$1" 97 | local common_account="$2" 98 | 99 | backup_file "$common_auth" 100 | backup_file "$common_account" 101 | 102 | echo "Configuring faillock..." 103 | # Remove existing lines to avoid duplicates 104 | sed -i "/pam_faillock.so/d" "${common_auth}" 105 | 106 | # Pre-auth lines 107 | { 108 | echo "auth required pam_faillock.so preauth silent deny=5 unlock_time=1800" 109 | echo "auth [success=1 default=bad] pam_unix.so" 110 | echo "auth [default=die] pam_faillock.so authfail deny=5 unlock_time=1800" 111 | echo "auth optional pam_permit.so" 112 | } >> "${common_auth}" 113 | 114 | # Account lines 115 | sed -i "/pam_faillock.so/d" "${common_account}" 116 | echo "account required pam_faillock.so" >> "${common_account}" 117 | 118 | echo "Faillock configured." 119 | } 120 | 121 | # Main function 122 | main() { 123 | 124 | local pwquality_config="/etc/security/pwquality.conf" 125 | local login_defs="/etc/login.defs" 126 | local common_password="/etc/pam.d/common-password" 127 | local common_auth="/etc/pam.d/common-auth" 128 | local common_account="/etc/pam.d/common-account" 129 | 130 | check_root 131 | configure_password_policy "${pwquality_config}" "${login_defs}" "${common_password}" 132 | configure_faillock "${common_auth}" "${common_account}" 133 | } 134 | 135 | main "$@" 136 | -------------------------------------------------------------------------------- /hardening/standalones/rkhunter/setup_rkhunter.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Checks if the script is being run as root 6 | check_root() { 7 | local uuid 8 | uuid=$(id -u) 9 | if [[ ${uuid} -ne 0 ]]; then 10 | echo "This script must be run as root. Exiting..." >&2 11 | exit 1 12 | fi 13 | } 14 | 15 | # Log a message to the log file 16 | log() { 17 | local message="$1" 18 | local date 19 | date=$(date +'%Y-%m-%d %H:%M:%S') 20 | echo "${date} - ${message}" 21 | } 22 | 23 | # Usage example message for the user 24 | usage() { 25 | echo "Usage: $0 " 26 | echo "Example: $0 recipient1@ex.com,recipient2@ex.com sender@ex.com" 27 | exit 1 28 | } 29 | 30 | # Installs a list of apt packages 31 | install_apt_packages() { 32 | local package_list=("${@}") # Capture all arguments as an array of packages 33 | 34 | log "Starting package installation process." 35 | 36 | # Verify that there are no apt locks 37 | while fuser /var/lib/dpkg/lock >/dev/null 2>&1 || fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || fuser /var/cache/apt/archives/lock >/dev/null 2>&1; do 38 | log "Waiting for other software managers to finish..." 39 | sleep 1 40 | done 41 | 42 | if apt update -y; then 43 | log "Package lists updated successfully." 44 | else 45 | log "Failed to update package lists. Continuing with installation..." 46 | fi 47 | 48 | local package 49 | local failed_packages=() 50 | for package in "${package_list[@]}"; do 51 | if dpkg -l | grep -qw "${package}"; then 52 | log "${package} is already installed." 53 | else 54 | # Sleep to avoid "E: Could not get lock /var/lib/dpkg/lock-frontend" error 55 | sleep 1 56 | if apt install -y "${package}"; then 57 | log "Successfully installed ${package}." 58 | else 59 | log "Failed to install ${package}." 60 | failed_packages+=("${package}") 61 | fi 62 | fi 63 | done 64 | 65 | if [[ ${#failed_packages[@]} -eq 0 ]]; then 66 | log "All packages were installed successfully." 67 | else 68 | log "Failed to install the following packages: ${failed_packages[*]}" 69 | fi 70 | } 71 | 72 | # Configures rkhunter to run daily and email results 73 | configure_rkhunter() { 74 | local recipients="$1" 75 | local sender="$2" 76 | 77 | log "Configuring rkhunter..." 78 | 79 | # Modify the UPDATE_MIRRORS and MIRRORS_MODE settings in /etc/rkhunter.conf to enable the use of mirrors 80 | sed -i -e 's|^[#]*[[:space:]]*UPDATE_MIRRORS=.*|UPDATE_MIRRORS=1|' /etc/rkhunter.conf 81 | sed -i -e 's|^[#]*[[:space:]]*MIRRORS_MODE=.*|MIRRORS_MODE=0|' /etc/rkhunter.conf 82 | 83 | # Modify the APPEND_LOG setting in /etc/rkhunter.conf to ensure that logs are appended rather than overwritten 84 | sed -i -e 's|^[#]*[[:space:]]*APPEND_LOG=.*|APPEND_LOG=1|' /etc/rkhunter.conf 85 | 86 | # Modify the USE_SYSLOG setting in /etc/rkhunter.conf to enable the use of syslog integration 87 | sed -i 's|^USE_SYSLOG=.*|USE_SYSLOG=authpriv.warning|' /etc/rkhunter.conf 88 | 89 | # Modify the WEB_CMD setting in /etc/rkhunter.conf to disable the use of the web command 90 | sed -i "s|^WEB_CMD=.*|WEB_CMD=|" /etc/rkhunter.conf 91 | 92 | # Use GLOBSTAR to improve comprehensive file pattern matching. 93 | # sed -i -e 's|^[#]*[[:space:]]*GLOBSTAR=.*|GLOBSTAR=1|' /etc/rkhunter.conf 94 | 95 | # Adjust ENABLE_TESTS and DISABLE_TESTS to enable all tests 96 | sed -i -e 's|^[#]*[[:space:]]*ENABLE_TESTS=.*|ENABLE_TESTS=ALL|' /etc/rkhunter.conf 97 | sed -i -e 's|^[#]*[[:space:]]*DISABLE_TESTS=.*|DISABLE_TESTS=None|' /etc/rkhunter.conf 98 | 99 | # set the default installer values so that 100 | echo "rkhunter rkhunter/apt_autogen boolean true" | debconf-set-selections 101 | echo "rkhunter rkhunter/cron_daily_run boolean true" | debconf-set-selections 102 | echo "rkhunter rkhunter/cron_db_update boolean true" | debconf-set-selections 103 | 104 | # use dpkg-reconfigure using debian standard packaging with non-interactive mode 105 | dpkg-reconfigure debconf -f noninteractive 106 | 107 | # Modify /etc/default/rkhunter to enable daily runs and database updates 108 | sed -i -e 's|^[#]*[[:space:]]*CRON_DAILY_RUN=.*|CRON_DAILY_RUN="true"|' /etc/default/rkhunter # enable daily runs 109 | sed -i -e 's|^[#]*[[:space:]]*CRON_DB_UPDATE=.*|CRON_DB_UPDATE="true"|' /etc/default/rkhunter # enable database updates 110 | sed -i -e 's|^[#]*[[:space:]]*APT_AUTOGEN=.*|APT_AUTOGEN="true"|' /etc/default/rkhunter # enable automatic updates 111 | 112 | # Email settings for rkhunter 113 | sed -i -e 's|^[#]*[[:space:]]*REPORT_EMAIL=.*|REPORT_EMAIL="'"${recipients}"'"|' /etc/default/rkhunter # set the email recipient 114 | sed -i -e 's|^[#]*[[:space:]]*DB_UPDATE_EMAIL=.*|DB_UPDATE_EMAIL="true"|' /etc/default/rkhunter # enable database update emails 115 | 116 | # Set the sender email for rkhunter if it doesn't exist 117 | grep -qE "^REPORT_SENDER=" /etc/default/rkhunter || { echo "# Report sender email" >> /etc/default/rkhunter && echo "REPORT_SENDER=\"${sender}\"" >> /etc/default/rkhunter; } 118 | 119 | # Replace the sender if it exists in the file 120 | sed -i -e 's|^[[:space:]]*#*\s*REPORT_SENDER=.*|REPORT_SENDER="'"${sender}"'"|' /etc/default/rkhunter # Replace the sender email if it exists 121 | 122 | log "rkhunter has been configured successfully at /etc/default/rkhunter and /etc/rkhunter.conf" 123 | } 124 | 125 | write_rkhunter_weekly() { 126 | echo "Writing rkhunter weekly cron job at /etc/cron.weekly/rkhunter" 127 | 128 | local rk_weekly="/etc/cron.weekly/rkhunter" 129 | 130 | cat <<'EOF' > "${rk_weekly}" 131 | #!/usr/bin/env bash 132 | 133 | # Log messages with appropriate prefixes 134 | system_log() { 135 | local log_type="$1" 136 | local message="$2" 137 | local log_file="$3" 138 | local date 139 | date=$(date '+%Y-%m-%d %H:%M:%S') 140 | echo "${date} - ${log_type}: ${message}" | tee -a "${log_file}" 141 | } 142 | 143 | # Checks if the script is being run as root 144 | check_root() { 145 | local uuid 146 | uuid=$(id -u) 147 | if [[ ${uuid} -ne 0 ]]; then 148 | echo "This script must be run as root. Exiting..." >&2 149 | exit 1 150 | fi 151 | } 152 | 153 | # Ensure command can execute 154 | ensure_command_is_executable() { 155 | local command="$1" 156 | if ! [[ -x "$(command -v "${command}")" ]]; then 157 | system_log "ERROR" "${command} not found or not executable." "${log_file}" 158 | exit 1 159 | fi 160 | } 161 | 162 | # Ensure that a file exists 163 | create_file_if_not_exists() { 164 | local file="$1" 165 | if [[ ! -f ${file} ]]; then 166 | touch "${file}" || { 167 | system_log "ERROR" "Unable to create file: ${file}" "${log_file}" 168 | exit 1 169 | } 170 | fi 171 | } 172 | 173 | # Send an email with the given subject and content to the given recipient 174 | send_email() { 175 | local subject="$1" 176 | local content="$2" 177 | local recipient="$3" 178 | 179 | local mail_tool="sendmail" 180 | 181 | local hostname 182 | hostname=$(hostname -f) 183 | 184 | if [[ -z ${recipient} ]]; then 185 | system_log "ERROR" "Email recipient not specified." "${log_file}" 186 | return 1 187 | fi 188 | 189 | if ! echo -e "Subject: ${subject}\nTo: ${recipient}\nFrom: ${REPORT_SENDER}\n\n${content}" | ${mail_tool} -f "${REPORT_SENDER}" -t "${recipient}"; then 190 | system_log "ERROR" "Failed to send email." "${log_file}" 191 | else 192 | system_log "INFO" "Email sent: ${subject}" "${log_file}" 193 | fi 194 | } 195 | 196 | # Check for the presence of web tools required for downloading updates 197 | check_for_web_tools() { 198 | local web_tools=(wget curl links elinks lynx) 199 | local tool 200 | for tool in "${web_tools[@]}"; do 201 | if [[ -x "$(command -v "${tool}")" ]]; then 202 | return 0 203 | fi 204 | done 205 | echo "Error: No tool to download rkhunter updates was found. Please install wget, curl, (e)links or lynx." 206 | return 1 207 | } 208 | 209 | # Handle rkhunter database updates 210 | handle_updates() { 211 | local rkhunter="$1" 212 | local recipient="$2" 213 | local report_file="$3" 214 | 215 | check_for_web_tools || exit 1 216 | 217 | if [[ ${DB_UPDATE_EMAIL} =~ [YyTt] ]]; then 218 | system_log "INFO" "Weekly rkhunter database update started" "${log_file}" 219 | "${rkhunter}" --versioncheck --nocolors --appendlog > "${report_file}" 2>&1 220 | "${rkhunter}" --update --nocolors --appendlog > "${report_file}" 2>&1 221 | 222 | local report_content 223 | local hostname 224 | 225 | report_content=$(cat "${report_file}") 226 | hostname=$(hostname -f) 227 | 228 | local report_date 229 | report_date=$(date '+%Y-%m-%d') 230 | 231 | local subject 232 | subject="[rkhunter] - [${hostname}] - [Weekly Report] - [$(date '+%Y-%m-%d %H:%M')]" 233 | 234 | send_email "${subject}" "${report_content}" "${recipient}" 235 | system_log "INFO" "Weekly report email sent." "${log_file}" 236 | else 237 | system_log "ERROR" "DB_UPDATE_EMAIL is not set to true. Exiting." "${log_file}" 238 | exit 0 239 | fi 240 | } 241 | 242 | # Main function 243 | main() { 244 | check_root 245 | 246 | # Source config 247 | . /etc/default/rkhunter 248 | 249 | log_file="/var/log/rkhunter.log" 250 | report_file="/tmp/rkhunter_weekly_report.log" 251 | 252 | local rkhunter="rkhunter" 253 | local mta="sendmail" 254 | 255 | # Check if rkhunter is installed and executable 256 | ensure_command_is_executable "${rkhunter}" 257 | ensure_command_is_executable "${mta}" 258 | 259 | create_file_if_not_exists "${log_file}" 260 | create_file_if_not_exists "${report_file}" 261 | 262 | # Run handle_updates if configured to do so in cron 263 | [[ ${CRON_DB_UPDATE} =~ [YyTt] ]] && handle_updates "${rkhunter}" "${REPORT_EMAIL}" "${report_file}" 264 | } 265 | 266 | main "$@" 267 | 268 | EOF 269 | 270 | chmod +x /etc/cron.weekly/rkhunter 271 | 272 | log "rkhunter cron jobs have been written successfully at /etc/cron.weekly/rkhunter" 273 | log "Attempting to perform a weekly rkhunter database update..." 274 | 275 | "${rk_weekly}" 276 | 277 | } 278 | 279 | write_rkhunter_daily() { 280 | echo "Writing rkhunter daily cron job at /etc/cron.daily/rkhunter" 281 | 282 | local rk_daily="/etc/cron.daily/rkhunter" 283 | 284 | cat <<'EOF' > "${rk_daily}" 285 | #!/usr/bin/env bash 286 | 287 | # Log messages with appropriate prefixes 288 | system_log() { 289 | local log_type="$1" 290 | local message="$2" 291 | local log_file="$3" 292 | local date 293 | date=$(date '+%Y-%m-%d %H:%M:%S') 294 | echo "${date} - ${log_type}: ${message}" | tee -a "${log_file}" 295 | } 296 | 297 | # Checks if the script is being run as root 298 | check_root() { 299 | local uuid 300 | uuid=$(id -u) 301 | if [[ ${uuid} -ne 0 ]]; then 302 | echo "This script must be run as root. Exiting..." >&2 303 | exit 1 304 | fi 305 | } 306 | 307 | # Ensure command can execute 308 | ensure_command_is_executable() { 309 | local command="$1" 310 | if ! [[ -x "$(command -v "${command}")" ]]; then 311 | system_log "ERROR" "${command} not found or not executable." "${log_file}" 312 | exit 1 313 | fi 314 | } 315 | 316 | # Ensure that a file exists 317 | create_file_if_not_exists() { 318 | local file="$1" 319 | if [[ ! -f ${file} ]]; then 320 | touch "${file}" || { 321 | system_log "ERROR" "Unable to create file: ${file}" "${log_file}" 322 | exit 1 323 | } 324 | fi 325 | } 326 | 327 | # Send an email with the given subject and content to the given recipient 328 | send_email() { 329 | local subject="$1" 330 | local content="$2" 331 | local recipient="$3" 332 | 333 | local mail_tool="sendmail" 334 | 335 | local hostname 336 | hostname=$(hostname -f) 337 | 338 | if [[ -z ${recipient} ]]; then 339 | system_log "ERROR" "Email recipient not specified." "${log_file}" 340 | return 1 341 | fi 342 | 343 | if ! echo -e "Subject: ${subject}\nTo: ${recipient}\nFrom: ${REPORT_SENDER}\n\n${content}" | ${mail_tool} -f "${REPORT_SENDER}" -t "${recipient}"; then 344 | system_log "ERROR" "Failed to send email." "${log_file}" 345 | else 346 | system_log "INFO" "Email sent: ${subject}" "${log_file}" 347 | fi 348 | } 349 | 350 | # Handle the daily rkhunter run and send an email if there are warnings 351 | handle_updates() { 352 | local rkhunter="$1" 353 | local recipient="$2" 354 | local report_file="$3" 355 | 356 | # Set default nice value if not set 357 | local nice=${nice:-0} 358 | 359 | # Check if CRON_DAILY_RUN is set and proceed if it's true 360 | if [[ ${CRON_DAILY_RUN} =~ [YyTt]* ]]; then 361 | system_log "INFO" "Starting rkhunter daily run..." "${log_file}" 362 | /usr/bin/nice -n "${nice}" "${rkhunter}" --cronjob --report-warnings-only --appendlog > "${report_file}" 2>&1 363 | 364 | local report_content 365 | local hostname 366 | 367 | report_content=$(cat "${report_file}") 368 | hostname=$(hostname -f) 369 | 370 | local report_date 371 | report_date=$(date '+%Y-%m-%d') 372 | 373 | local subject 374 | subject="[rkhunter] - [${hostname}] - [Daily Report] - [$(date '+%Y-%m-%d %H:%M')]" 375 | 376 | send_email "${subject}" "${report_content}" "${recipient}" 377 | system_log "INFO" "Daily report email sent." "${log_file}" 378 | else 379 | system_log "ERROR" "CRON_DAILY_RUN is not set to true. Exiting." "${log_file}" 380 | exit 0 381 | fi 382 | } 383 | 384 | # Main function 385 | main() { 386 | check_root 387 | 388 | # Source config 389 | . /etc/default/rkhunter 390 | 391 | log_file="/var/log/rkhunter.log" 392 | report_file="/tmp/rkhunter_report.log" 393 | 394 | local rkhunter="rkhunter" 395 | local mta="sendmail" 396 | 397 | # Check if rkhunter is installed and executable 398 | ensure_command_is_executable "${rkhunter}" 399 | ensure_command_is_executable "${mta}" 400 | 401 | create_file_if_not_exists "${log_file}" 402 | create_file_if_not_exists "${report_file}" 403 | 404 | # Handle the daily rkhunter run 405 | handle_updates "${rkhunter}" "${REPORT_EMAIL}" "${report_file}" 406 | } 407 | 408 | main "$@" 409 | EOF 410 | 411 | chmod +x /etc/cron.daily/rkhunter 412 | 413 | log "rkhunter cron jobs have been written successfully at /etc/cron.weekly/rkhunter and /etc/cron.daily/rkhunter" 414 | log "Attempting to perform a daily rkhunter run..." 415 | ${rk_daily} 416 | } 417 | 418 | # Main function 419 | main() { 420 | check_root 421 | echo "Initializing rkhunter..." 422 | 423 | local recipients="${1:-root@$(hostname -f)}" # Default recipient email if not provided 424 | local sender="${2:-root@$(hostname -f)}" # Default sender email if not provided 425 | 426 | install_apt_packages "debconf-utils" "rkhunter" 427 | configure_rkhunter "${recipients}" "${sender}" 428 | write_rkhunter_weekly 429 | write_rkhunter_daily 430 | } 431 | 432 | main "$@" -------------------------------------------------------------------------------- /hardening/standalones/sshd/configure_ssh_hardening.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Ensure script is run with root privileges 6 | check_root() { 7 | local uuid 8 | uuid=$(id -u) 9 | if [[ ${uuid} -ne 0 ]]; then 10 | echo "This script must be run as root. Exiting..." >&2 11 | exit 1 12 | fi 13 | } 14 | 15 | # Usage message 16 | usage() { 17 | cat << EOF 18 | # Usage 19 | ${0} 20 | 21 | # Formats 22 | (username:ssh-rsa key1,key2,key3) 23 | void:ssh-rsa AAAAB3Nzwn...BnmkSBpiBsqQ== void@null 24 | admin:ssh-rsa AAAAB3Nzwn...BnmkSBpiBsqQ== void@null,ssh-ed2551 ...AAIDk7VFe example.eg@example.com 25 | 26 | (user1,user2,user3) 27 | void,admin 28 | 29 | # Example 30 | ${0} "void:ssh-rsa AAAAB3Nzwn...BnmkSBpiBsqQ== void@null,admin:ssh-rsa AAAAB3Nzwn...BnmkSBpiBsqQ== void@null" "void,admin" 2222 31 | 32 | # Note 33 | SSH port must be a valid number between 1 and 65535, and enabled separately in the firewall. 34 | 35 | EOF 36 | } 37 | 38 | # Backup sshd_config file with dynamic naming 39 | backup_config() { 40 | local timestamp 41 | timestamp=$(date +%Y%m%d%H%M) 42 | cp "${1}" "${1}.bak.${timestamp}" || { 43 | echo "Failed to back-up ${1}. Exiting..." >&2 44 | exit 1 45 | } 46 | echo "Backup created at ${1}.bak.${timestamp}" 47 | } 48 | 49 | # Validate SSH port number 50 | validate_port() { 51 | if ! [[ ${1} =~ ^[0-9]+$ ]] || ((${1} < 1 || ${1} > 65535)); then 52 | echo "Invalid SSH port specified: ${1}. Exiting..." >&2 53 | exit 1 54 | fi 55 | } 56 | 57 | # Restart SSHD to apply changes 58 | restart_sshd() { 59 | if systemctl restart sshd; then 60 | echo "SSHD restarted successfully." 61 | elif service ssh restart; then 62 | echo "SSH restarted successfully." 63 | else 64 | echo "Failed to restart SSH/D. Check manually." >&2 65 | exit 1 66 | fi 67 | } 68 | 69 | # Verify the SSHD configuration file 70 | verify_sshd_config() { 71 | local sshd_config_file="$1" 72 | if ! sshd -t -f "${sshd_config_file}"; then 73 | echo "Failed to verify the SSHD configuration. Check manually." >&2 74 | exit 1 75 | fi 76 | } 77 | 78 | # Update Issue.net file 79 | update_issue_net() { 80 | echo "Updating /etc/issue.net..." 81 | cat << 'EOF' > /etc/issue.net 82 | +----------------------------------------------------+ 83 | | This is a controlled access system. The activities | 84 | | on this system are heavily monitored. | 85 | | Evidence of unauthorised activities will be | 86 | | disclosed to the appropriate authorities. | 87 | +----------------------------------------------------+ 88 | EOF 89 | } 90 | 91 | # Parse users and their keys from the multiline string 92 | parse_user_ssh_keys() { 93 | local allowed_ssh_pk_user_mappings="$1" 94 | 95 | echo "Parsing users and their keys..." 96 | local line 97 | while IFS= read -r line; do 98 | local username="${line%%:*}" 99 | local keys="${line#*:}" 100 | if [[ -z ${username} || -z ${keys} ]]; then 101 | echo "Invalid user or keys. Skipping..." >&2 102 | continue 103 | fi 104 | # Add the user and their keys to the top level associative array 105 | user_ssh_keys_map["${username}"]="${keys}" 106 | done <<< "${allowed_ssh_pk_user_mappings}" 107 | } 108 | 109 | # Adds specified SSH keys to a user's authorized_keys. 110 | inject_public_keys() { 111 | local username="$1" 112 | local keys="$2" 113 | local home_directory 114 | home_directory=$(getent passwd "${username}" | cut -d: -f6) 115 | 116 | if [[ -z ${home_directory} || ! -d ${home_directory} ]]; then 117 | echo "Unable to find or access home directory for ${username}. Skipping..." >&2 118 | return 119 | fi 120 | 121 | local ssh_dir="${home_directory}/.ssh" 122 | local auth_keys="${ssh_dir}/authorized_keys" 123 | 124 | # Ensure .ssh directory and authorized_keys file exist 125 | mkdir -p "${ssh_dir}" && chmod 700 "${ssh_dir}" 126 | touch "${auth_keys}" && chmod 600 "${auth_keys}" 127 | 128 | # Add keys if they do not already exist 129 | local key_added=0 130 | local keys_array 131 | IFS=',' read -ra keys_array <<< "${keys}" # Convert keys list into an array 132 | local key 133 | for key in "${keys_array[@]}"; do 134 | if ! grep -qF -- "${key}" "${auth_keys}"; then 135 | echo "${key}" >> "${auth_keys}" 136 | key_added=1 137 | fi 138 | done 139 | 140 | if [[ ${key_added} -eq 1 ]]; then 141 | echo "New keys added for ${username}." 142 | else 143 | echo "No new keys to add for ${username}." 144 | fi 145 | } 146 | 147 | # Injects public SSH keys into the authorized_keys file for each user in the map. 148 | inject_keys_for_all_users() { 149 | echo "Injecting public keys for all users..." 150 | local user 151 | for user in "${!user_ssh_keys_map[@]}"; do 152 | if ! id -u "${user}" &> /dev/null; then 153 | echo "Moving to the next user..." >&2 154 | continue 155 | fi 156 | inject_public_keys "${user}" "${user_ssh_keys_map[${user}]}" 157 | done 158 | } 159 | 160 | # Parse allowed SSH users from a comma-separated string 161 | parse_allowed_ssh_users() { 162 | echo "Parsing allowed SSH users..." 163 | # If command-line list is provided 164 | if [[ -n $1 ]]; then 165 | local users_array 166 | IFS=',' read -r -a users_array <<< "$1" 167 | # Add the users to the allowed SSH users array with a space separator for the configuration file 168 | allowed_ssh_users=$(printf "%s " "${users_array[@]}") 169 | fi 170 | } 171 | 172 | # Update or append a configuration setting in a specified file. 173 | # Searches for a key in the given file, and if it exists, it updates the line. 174 | # If the key does not exist, it appends a new line with the key and value. 175 | update_config() { 176 | local key="$1" 177 | local value="$2" 178 | local file="$3" 179 | 180 | # Check if the key exists and is not commented out, then replace or append the value. 181 | if grep -qE "^#*${key} " "${file}"; then 182 | sed -i "/^#*${key} /c\\${key} ${value}" "${file}" 183 | else 184 | echo "${key} ${value}" >> "${file}" 185 | fi 186 | } 187 | 188 | # Install the Google Authenticator PAM module 189 | install_google_authenticator() { 190 | echo "Updating system packages..." 191 | apt-get update -y 192 | echo "Installing Google Authenticator PAM module..." 193 | apt-get install libpam-google-authenticator -y 194 | } 195 | 196 | # Append TOTP profile script execution to /etc/profile 197 | append_totp_to_profile() { 198 | mkdir -p /etc/ssh_login_scripts 199 | 200 | local totp_script="/etc/ssh_login_scripts/google_authenticator_totp_check.sh" 201 | 202 | if [[ -f ${totp_script} ]]; then 203 | echo "The TOTP profile script already exists." 204 | else 205 | cat << 'EOF' > "${totp_script}" 206 | #!/usr/bin/env bash 207 | 208 | # Logging function with error handling 209 | log_event() { 210 | local current_time 211 | current_time=$(date '+%Y-%m-%d %H:%M:%S') 212 | echo "[${current_time}] $1" >> "${log_file}" 2>/dev/null 213 | } 214 | 215 | # Check for all required commands at the start 216 | check_dependencies() { 217 | local missing_commands=() 218 | local cmd 219 | for cmd in "$@"; do 220 | if ! command -v "${cmd}" &>/dev/null; then 221 | missing_commands+=("${cmd}") 222 | fi 223 | done 224 | 225 | if [[ ${#missing_commands[@]} -ne 0 ]]; then 226 | local cmd 227 | for cmd in "${missing_commands[@]}"; do 228 | echo "Required command '${cmd}' not found. Please install it." >&2 229 | done 230 | log_event "Missing required commands: ${missing_commands[*]}" 231 | exit 1 232 | fi 233 | } 234 | 235 | # Skip TOTP setup, log the event and exit 236 | skip_totp_setup() { 237 | local user 238 | user=$(whoami) 239 | echo "Skipping TOTP setup." 240 | log_event "Skipping TOTP setup for ${user}." 241 | exit 0 242 | } 243 | 244 | # Checks if the script is being run as root 245 | check_root() { 246 | local uuid 247 | uuid=$(id -u) 248 | if [[ ${uuid} -eq 0 ]]; then 249 | skip_totp_setup 250 | fi 251 | } 252 | 253 | # Set up TOTP using Google Authenticator with improved error handling 254 | configure_totp() { 255 | if [[ -f "${HOME}/.google_authenticator" ]]; then 256 | echo "TOTP is already configured." 257 | log_event "TOTP configuration found. No action needed." 258 | else 259 | echo "TOTP is not configured. Setting up TOTP..." 260 | echo -e "To complete setup:\n1. Install the Google Authenticator app.\n2. Select 'Begin setup' > 'Scan a barcode'.\n3. Scan the QR code displayed.\n4. Follow the app instructions." 261 | if google-authenticator -t -d -f -r 3 -R 30 -W; then 262 | log_event "TOTP setup completed." 263 | else 264 | log_event "Error creating TOTP token." 265 | exit 2 266 | fi 267 | fi 268 | } 269 | 270 | # Main function 271 | main() { 272 | check_root 273 | log_file="${HOME}/google_auth_setup.log" 274 | check_dependencies "google-authenticator" 275 | configure_totp 276 | } 277 | 278 | main "$@" 279 | EOF 280 | chmod +x "${totp_script}" 281 | fi 282 | 283 | local totp_script_execution="bash ${totp_script}" 284 | local totp_script_execution_grep_query="^#*bash ${totp_script}" 285 | 286 | if [[ -f /etc/zsh/zprofile ]]; then 287 | if grep -qE "${totp_script_execution_grep_query}" /etc/zsh/zprofile; then 288 | echo "TOTP profile script execution already exists in /etc/zsh/zprofile." 289 | else 290 | # Append script execution to /etc/zsh/zprofile 291 | echo "${totp_script_execution}" >> /etc/zsh/zprofile 292 | echo "TOTP profile script execution added to /etc/zsh/zprofile." 293 | fi 294 | fi 295 | 296 | if [[ -f /etc/profile ]]; then 297 | if grep -qE "${totp_script_execution_grep_query}" /etc/profile; then 298 | echo "TOTP profile script execution already exists in /etc/profile." 299 | else 300 | # Append script execution to /etc/profile 301 | echo "${totp_script_execution}" >> /etc/profile 302 | echo "TOTP profile script execution added to /etc/profile." 303 | fi 304 | fi 305 | } 306 | 307 | # Configure PAM for 2FA 308 | configure_pam_for_2fa() { 309 | local sshd_pam_config="/etc/pam.d/sshd" 310 | local pam_common_auth_string="@include common-auth" 311 | local pam_common_auth_grep_query="^#*@include common-auth" 312 | 313 | local pam_auth_required_permit_login_string="auth required pam_permit.so" 314 | local pam_auth_required_permit_login_grep_query="^#*auth required pam_permit.so" 315 | 316 | local pam_google_authenticator_string="auth required pam_google_authenticator.so nullok" 317 | local pam_google_authenticator_grep_query="^#*auth required pam_google_authenticator.so nullok" 318 | 319 | backup_config "${sshd_pam_config}" 320 | 321 | # Comment out the common-session include line if it exists 322 | sed -i "s/${pam_common_auth_grep_query}/#${pam_common_auth_string}/" "${sshd_pam_config}" 323 | 324 | # Allow login without TOTP for the first time 325 | grep -qE "${pam_auth_required_permit_login_grep_query}" "${sshd_pam_config}" || echo "${pam_auth_required_permit_login_string}" >> "${sshd_pam_config}" 326 | 327 | # Add the Google Authenticator PAM module to the SSHD PAM configuration 328 | grep -qE "${pam_google_authenticator_grep_query}" "${sshd_pam_config}" || echo "${pam_google_authenticator_string}" >> "${sshd_pam_config}" 329 | } 330 | 331 | check_install_host_keys() { 332 | local host_keys=("/etc/ssh/ssh_host_ed25519_key" "/etc/ssh/ssh_host_rsa_key" "/etc/ssh/ssh_host_ecdsa_key") 333 | local key 334 | for key in "${host_keys[@]}"; do 335 | if [[ ! -f ${key} ]]; then 336 | echo "Host key ${key} not found. Generating..." >&2 337 | ssh-keygen -t "${key##*/}" -f "${key}" -N "" -q 338 | fi 339 | done 340 | } 341 | 342 | # Apply multiple security configurations to the sshd_config file. 343 | # This includes setting the SSH port, configuring user access, and securing the SSH daemon. 344 | apply_configurations() { 345 | local ssh_port="$1" 346 | local sshd_config_file="/etc/ssh/sshd_config" 347 | 348 | echo "Applying SSH hardening configurations..." 349 | 350 | # Ensure there's a backup of the original sshd_config before making changes. 351 | backup_config "${sshd_config_file}" 352 | 353 | # General Security Settings 354 | update_config "StrictModes" "yes" "${sshd_config_file}" 355 | update_config "PermitEmptyPasswords" "no" "${sshd_config_file}" 356 | update_config "PermitRootLogin" "no" "${sshd_config_file}" 357 | update_config "UsePAM" "yes" "${sshd_config_file}" 358 | 359 | #2FA Related 360 | update_config "PasswordAuthentication" "no" "${sshd_config_file}" 361 | update_config "ChallengeResponseAuthentication" "yes" "${sshd_config_file}" 362 | update_config "KbdInteractiveAuthentication" "yes" "${sshd_config_file}" 363 | update_config "AuthenticationMethods" "publickey,keyboard-interactive" "${sshd_config_file}" 364 | 365 | # Logging and Monitoring 366 | update_config "SyslogFacility" "AUTH" "${sshd_config_file}" 367 | update_config "LogLevel" "VERBOSE" "${sshd_config_file}" 368 | update_config "PrintLastLog" "yes" "${sshd_config_file}" 369 | update_config "MaxStartups" "10:30:100" "${sshd_config_file}" 370 | 371 | # SSH Banner 372 | update_config "Banner" "/etc/issue.net" "${sshd_config_file}" 373 | 374 | # Network and System Settings 375 | update_config "AddressFamily" "any" "${sshd_config_file}" 376 | update_config "ListenAddress" "0.0.0.0" "${sshd_config_file}" 377 | update_config "PidFile" "/var/run/sshd.pid" "${sshd_config_file}" 378 | update_config "VersionAddendum" "none" "${sshd_config_file}" 379 | 380 | # Authentication and Access Settings 381 | update_config "PubkeyAuthentication" "yes" "${sshd_config_file}" 382 | update_config "AuthorizedKeysFile" ".ssh/authorized_keys" "${sshd_config_file}" 383 | update_config "AllowUsers" "${allowed_ssh_users[*]}" "${sshd_config_file}" 384 | update_config "MaxAuthTries" "3" "${sshd_config_file}" 385 | update_config "MaxSessions" "3" "${sshd_config_file}" 386 | 387 | # Connection and Session Settings 388 | update_config "ClientAliveInterval" "120" "${sshd_config_file}" 389 | update_config "ClientAliveCountMax" "0" "${sshd_config_file}" 390 | update_config "LoginGraceTime" "30s" "${sshd_config_file}" 391 | update_config "TCPKeepAlive" "no" "${sshd_config_file}" 392 | update_config "Port" "${ssh_port}" "${sshd_config_file}" 393 | 394 | # Interactive Shell 395 | update_config "PermitTTY" "yes" "${sshd_config_file}" 396 | 397 | # Feature Restrictions 398 | update_config "X11Forwarding" "no" "${sshd_config_file}" 399 | update_config "AllowTcpForwarding" "no" "${sshd_config_file}" 400 | update_config "GatewayPorts" "no" "${sshd_config_file}" 401 | update_config "UseDNS" "no" "${sshd_config_file}" 402 | update_config "PermitTunnel" "no" "${sshd_config_file}" 403 | 404 | # Additional Security Enhancements 405 | update_config "IgnoreRhosts" "yes" "${sshd_config_file}" 406 | update_config "HostbasedAuthentication" "no" "${sshd_config_file}" 407 | update_config "IgnoreUserKnownHosts" "yes" "${sshd_config_file}" 408 | update_config "PermitUserEnvironment" "no" "${sshd_config_file}" 409 | update_config "AllowAgentForwarding" "no" "${sshd_config_file}" 410 | update_config "Compression" "no" "${sshd_config_file}" 411 | update_config "PrintMotd" "no" "${sshd_config_file}" 412 | update_config "AcceptEnv" "LANG LC_*" "${sshd_config_file}" 413 | 414 | # Host Key Algorithms 415 | update_config "HostKey" "/etc/ssh/ssh_host_ed25519_key" "${sshd_config_file}" 416 | update_config "HostKey" "/etc/ssh/ssh_host_rsa_key" "${sshd_config_file}" 417 | update_config "HostKey" "/etc/ssh/ssh_host_ecdsa_key" "${sshd_config_file}" 418 | 419 | # Update KexAlgorithms, Ciphers, and MACs 420 | update_config "KexAlgorithms" "curve25519-sha256@libssh.org" "${sshd_config_file}" 421 | update_config "Ciphers" "aes256-ctr,aes192-ctr,aes128-ctr" "${sshd_config_file}" 422 | update_config "MACs" "hmac-sha2-512,hmac-sha2-256" "${sshd_config_file}" 423 | 424 | # Extra Rules (Breaking) 425 | # update_config "ListenAddress" "::" "${sshd_config_file}" 426 | # update_config "ChrootDirectory" "/etc/ssh/chroot" "${sshd_config_file}" 427 | 428 | # Remove any duplicate lines from the file 429 | awk '!seen[$0]++' "${sshd_config_file}" > "${sshd_config_file}.tmp" && mv "${sshd_config_file}.tmp" "${sshd_config_file}" 430 | 431 | # Verify the SSHD configuration file 432 | verify_sshd_config "${sshd_config_file}" 433 | } 434 | 435 | # Set the correct ownership and permissions for the .ssh directory and authorized_keys file 436 | set_ssh_directory_and_file_permissions() { 437 | local user="$1" 438 | local home_directory 439 | home_directory=$(getent passwd "${user}" | cut -d: -f6) 440 | 441 | if [[ -z ${home_directory} || ! -d ${home_directory} ]]; then 442 | echo "Unable to find or access home directory for ${user}. Skipping..." >&2 443 | return 444 | fi 445 | 446 | local ssh_dir="${home_directory}/.ssh" 447 | local auth_keys="${ssh_dir}/authorized_keys" 448 | 449 | # Ensure .ssh directory and authorized_keys file exist 450 | mkdir -p "${ssh_dir}" && chmod 700 "${ssh_dir}" 451 | touch "${auth_keys}" && chmod 600 "${auth_keys}" 452 | 453 | # Set the correct ownership and permissions for the .ssh directory and authorized_keys file 454 | chown -R "${user}:${user}" "${ssh_dir}" "${auth_keys}" 455 | } 456 | 457 | # If the user exists, set the correct ownership and permissions for the .ssh directory and authorized_keys file 458 | validate_allowed_user_ssh_permissions() { 459 | local allowed_users_csv="$1" 460 | local user users_array 461 | IFS=',' read -ra users_array <<< "${allowed_users_csv}" 462 | for user in "${users_array[@]}"; do 463 | if ! id -u "${user}" &> /dev/null; then 464 | echo "User ${user} does not exist. Exiting..." >&2 465 | exit 1 466 | fi 467 | 468 | if [[ -z $(getent passwd "${user}") ]]; then 469 | echo "User ${user} does not exist. Exiting..." >&2 470 | exit 1 471 | fi 472 | set_ssh_directory_and_file_permissions "${user}" 473 | done 474 | } 475 | 476 | 477 | # Main function 478 | main() { 479 | check_root 480 | 481 | if [[ $# -ne 3 ]]; then 482 | usage 483 | exit 1 484 | fi 485 | 486 | local allowed_users_ssh_key_mapping="$1" 487 | local allowed_users="$2" 488 | local ssh_port="$3" 489 | 490 | # Initialize associative arrays 491 | declare -Ag user_ssh_keys_map 492 | declare -Ag allowed_ssh_users 493 | 494 | # Parse the SSH user key mapping and the allowed users list. 495 | parse_user_ssh_keys "${allowed_users_ssh_key_mapping}" 496 | parse_allowed_ssh_users "${allowed_users}" 497 | validate_allowed_user_ssh_permissions "${allowed_users}" 498 | 499 | # Proceed with SSH configuration and key injection 500 | inject_keys_for_all_users 501 | 502 | # Check and install host keys 503 | check_install_host_keys 504 | 505 | # Apply SSH hardening configurations 506 | apply_configurations "${ssh_port}" 507 | 508 | # Additional configurations and service restart 509 | update_issue_net 510 | install_google_authenticator 511 | append_totp_to_profile 512 | configure_pam_for_2fa 513 | restart_sshd 514 | 515 | echo "SSH hardening configuration applied successfully." 516 | } 517 | 518 | main "$@" -------------------------------------------------------------------------------- /hardening/standalones/sshd/generate_ssh_intrusion_detection.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | ####################################### 5 | # description 6 | # Arguments: 7 | # None 8 | ####################################### 9 | generate_ssh_intrusion_detection() { 10 | local ssh_monitor_path="/opt/ssh_monitor" 11 | mkdir -p "${ssh_monitor_path}" 12 | 13 | cat <<'EOF' > "${ssh_monitor_path}/ssh_monitor.sh" 14 | #!/usr/bin/env bash 15 | set -euo pipefail 16 | 17 | usage() { 18 | echo "Usage: $0 [install|status|stop|disable|start] [email_recipients (optional)]" 19 | exit 1 20 | } 21 | 22 | # Checks if the script is being run as root 23 | check_root() { 24 | local uuid 25 | uuid=$(id -u) 26 | if [[ ${uuid} -ne 0 ]]; then 27 | echo "This script must be run as root. Exiting..." >&2 28 | exit 1 29 | fi 30 | } 31 | 32 | # Enable and start a service 33 | enable_service() { 34 | local service_name="$1" 35 | systemctl enable "${service_name}" --no-pager 36 | } 37 | 38 | # Start a service 39 | start_service() { 40 | local service_name="$1" 41 | systemctl start "${service_name}" --no-pager 42 | } 43 | 44 | # Ensure that a file is writeable 45 | verify_file_is_writable() { 46 | local file="$1" 47 | local error_log_file="$2" 48 | 49 | if [[ ! -w ${file} ]]; then 50 | ssh_system_log "ERROR" "File is not writable: ${file}" "${error_log_file}" 51 | exit 1 52 | fi 53 | } 54 | 55 | # Ensure that a file exists 56 | check_and_create_file() { 57 | local file="$1" 58 | local error_log_file="$2" 59 | if [[ ! -f ${file} ]]; then 60 | touch "${file}" || { 61 | ssh_system_log "ERROR" "Unable to create file: ${file}" "${error_log_file}" 62 | exit 1 63 | } 64 | fi 65 | } 66 | 67 | # Log messages with appropriate prefixes 68 | ssh_system_log() { 69 | local log_type="$1" 70 | local message="$2" 71 | local log_file="$3" 72 | local date 73 | date=$(date '+%Y-%m-%d %H:%M:%S') 74 | echo "${date} - ${log_type}: ${message}" | tee -a "${log_file}" 75 | } 76 | 77 | # Send email with throttling 78 | send_email_with_throttle() { 79 | local email_subject="${1}" 80 | local message="${2}" 81 | local email_recipients="${3}" 82 | local last_email_sent_file="${4}" 83 | 84 | local current_time 85 | current_time=$(date '+%s') 86 | 87 | # Read the last email sent time from file 88 | local last_email_sent_time 89 | last_email_sent_time=$(cat "${last_email_sent_file}" 2> /dev/null || echo "") 90 | 91 | # Calculate the time elapsed since the last email was sent 92 | local time_elapsed=$((current_time - last_email_sent_time)) 93 | 94 | if [[ -z ${last_email_sent_time} || ${time_elapsed} -ge 150 ]]; then 95 | { 96 | echo -e "Subject: [${email_subject}] - [$(hostname -f)] - [$(date '+%Y-%m-%d %H:%M')] \nTo: ${email_recipients}\n\n${message}" 97 | } | sendmail -t || { 98 | ssh_system_log "ERROR" "Alert mail could not be sent." 99 | } 100 | 101 | # Update the last email sent time 102 | last_email_sent_time="${current_time}" 103 | echo "${last_email_sent_time}" > "${last_email_sent_file}" 104 | ssh_system_log "INFO" "Sent an alert email." 105 | else 106 | ssh_system_log "INFO" "Throttling email. Not sending another alert yet." 107 | fi 108 | } 109 | 110 | # Load configuration from the local config file 111 | # shellcheck disable=SC1091 112 | load_config() { 113 | local local_config_file="$1" 114 | local log_file="$2" 115 | local error_log_file="$3" 116 | local recipients="$4" 117 | local email_alert_threshold="$5" 118 | local email_subject="$6" 119 | 120 | if [[ -f ${local_config_file} ]]; then 121 | ssh_system_log "INFO" "Attempting to load configuration from ${local_config_file}" "${log_file}" 122 | echo "Loading configuration from ${local_config_file}" 123 | # shellcheck source=./$local_config_file 124 | source "${local_config_file}" 125 | echo "Loaded configuration from ${local_config_file}" 126 | else 127 | ssh_system_log "ERROR" "Configuration file not found. Using default settings." "${error_log_file}" 128 | generate_default_config "${local_config_file}" "${log_file}" "${error_log_file}" "${recipients}" "${email_alert_threshold}" "${email_subject}" 129 | fi 130 | } 131 | 132 | # Generate the default configuration if it doesn't exist 133 | generate_default_config() { 134 | local local_config_file="$1" 135 | local log_file="$2" 136 | local error_log_file="$3" 137 | local recipients="$4" 138 | local email_alert_threshold="$5" 139 | local email_subject="$6" 140 | 141 | # Generate the default configuration if it doesn't exist - used to populate the environment variables via source 142 | if [[ ! -f ${local_config_file} ]]; then 143 | echo "log_file=\"${log_file}\"" > "${local_config_file}" 144 | echo "error_log_file=\"${error_log_file}\"" >> "${local_config_file}" 145 | echo "recipients=\"${recipients}\"" >> "${local_config_file}" 146 | echo "email_alert_threshold=\"${email_alert_threshold}\"" >> "${local_config_file}" 147 | echo "email_subject=\"${email_subject}\"" >> "${local_config_file}" 148 | ssh_system_log "INFO" "Default configuration file generated at ${local_config_file}" "${log_file}" 149 | else 150 | ssh_system_log "INFO" "Configuration file already exists. Skipping generation." "${log_file}" 151 | fi 152 | } 153 | 154 | # Create the systemd service file 155 | create_daemon_service() { 156 | local log_file="$1" 157 | local error_log_file="$2" 158 | local script_path="$3" 159 | local service_file="$4" 160 | local description="$5" 161 | local service_name="$6" 162 | 163 | echo "Creating service file at ${service_file}" 164 | 165 | echo "[Unit]" > "${service_file}" || { 166 | ssh_system_log "ERROR" "Unable to create service file." "${error_log_file}" 167 | return 1 168 | } 169 | echo "Description=${description}" >> "${service_file}" 170 | echo "" >> "${service_file}" 171 | echo "[Service]" >> "${service_file}" 172 | echo "ExecStart=bash ${script_path} start" >> "${service_file}" 173 | echo "Restart=always" >> "${service_file}" 174 | echo "" >> "${service_file}" 175 | echo "[Install]" >> "${service_file}" 176 | echo "WantedBy=multi-user.target" >> "${service_file}" 177 | 178 | ssh_system_log "INFO" "Created service file at ${service_file}" "${log_file}" 179 | 180 | enable_service "${service_name}" 181 | start_service "${service_name}" 182 | } 183 | 184 | # Manage the SSH Monitor daemon 185 | manage_daemon() { 186 | local log_file="$1" 187 | local error_log_file="$2" 188 | local script_path="$3" 189 | local service_file="$4" 190 | local service_name="$5" 191 | local description="$6" 192 | 193 | echo "Managing the SSH Monitor daemon..." 194 | case "$7" in 195 | install) 196 | create_daemon_service "${log_file}" "${error_log_file}" "${script_path}" "${service_file}" "${description}" "${service_name}" 197 | ;; 198 | status) 199 | systemctl status "${service_name}" || ssh_system_log "ERROR" "Failed to get status of the ${service_name} daemon." "${error_log_file}" 200 | ;; 201 | stop) 202 | systemctl stop "${service_name}" || ssh_system_log "ERROR" "Failed to stop the ${service_name} daemon." "${error_log_file}" 203 | ;; 204 | disable) 205 | systemctl disable "${service_name}" && systemctl stop "${service_name}" 206 | ssh_system_log "INFO" "${service_name} daemon disabled and stopped." "${log_file}" 207 | ;; 208 | *) 209 | ssh_system_log "ERROR" "Invalid command. Usage: $0 [install|status|stop|disable] [email_recipients (optional)]" "${error_log_file}" 210 | exit 1 211 | ;; 212 | esac 213 | } 214 | 215 | # Check if the session count exceeds the alert threshold and send an alert if necessary 216 | handle_ssh_threshold_exceeds() { 217 | local session_count="$1" 218 | local session_details="$2" 219 | local email_alert_threshold="$3" 220 | local email_subject="$4" 221 | local recipients="$5" 222 | local last_email_sent_file="$6" 223 | 224 | if ((session_count >= email_alert_threshold)); then 225 | local message="Alert: High number of SSH logins detected. Count: ${session_count}\n\nSession Details:\n${session_details}" 226 | send_email_with_throttle "${email_subject}" "${message}" "${recipients}" "${last_email_sent_file}" 227 | ssh_system_log "INFO" "Checked SSH session count (${session_count}) against threshold." "${log_file}" 228 | else 229 | ssh_system_log "INFO" "SSH session count (${session_count}) is below the threshold." "${log_file}" 230 | fi 231 | } 232 | 233 | # Manage SSH sessions 234 | handle_ssh_sessions() { 235 | local email_alert_threshold="${1}" 236 | local email_subject="${2}" 237 | local recipients="${3:-root@$(hostname -f)}" 238 | local last_email_sent_file="${4}" 239 | local error_log_file="${5}" 240 | local last_check_file="/tmp/last_ssh_check" 241 | 242 | # Get the current time in the format: YYYY-MM-DD HH:MM 243 | local now 244 | now=$(date '+%Y-%m-%d %H:%M') 245 | 246 | # Calculate the time 20 seconds ago in the same format 247 | local twenty_seconds_ago 248 | twenty_seconds_ago=$(date '+%Y-%m-%d %H:%M' -d "20 seconds ago") 249 | 250 | local ssh_sessions 251 | ssh_sessions=$(last -s "${twenty_seconds_ago}" -t "${now}" | grep 'pts/' | sort | uniq -c) || { 252 | ssh_system_log "INFO" "No SSH sessions found in this interval." "${log_file}" 253 | } 254 | 255 | echo "${now}" > "${last_check_file}" || { 256 | ssh_system_log "ERROR" "Unable to update last check file." "${error_log_file}" 257 | } 258 | 259 | if [[ -n ${ssh_sessions} ]]; then 260 | log_session_data "${ssh_sessions}" "${error_log_file}" 261 | ssh_system_log "INFO" "Logged SSH sessions data." "${log_file}" 262 | local session_count 263 | session_count=$(echo "${ssh_sessions}" | wc -l) 264 | handle_ssh_threshold_exceeds "${session_count}" "${ssh_sessions}" "${email_alert_threshold}" "${email_subject}" "${recipients}" "${last_email_sent_file}" 265 | 266 | else 267 | ssh_system_log "INFO" "No SSH sessions found in this interval." "${log_file}" 268 | fi 269 | } 270 | 271 | # Log session data to the ssh_system_log file 272 | log_session_data() { 273 | local error_log_file="${1}" 274 | 275 | local line 276 | while read -r line; do 277 | if [[ ! -f ${log_file} ]]; then 278 | touch "${log_file}" || { 279 | ssh_system_log "ERROR" "Unable to create ssh_system_log file." "${error_log_file}" 280 | return 1 281 | } 282 | ssh_system_log "INFO" "Created ssh_system_log file at ${log_file}" "${log_file}" 283 | fi 284 | 285 | local date 286 | date=$(date '+%Y-%m-%d %H:%M:%S') 287 | { 288 | echo "[${date}] New SSH sessions detected:" 289 | echo "${line}" 290 | } >> "${log_file}" || { 291 | ssh_system_log "ERROR" "Unable to write to ssh_system_log file." "${error_log_file}" 292 | return 1 293 | } 294 | done <<< "$2" 295 | } 296 | 297 | # Rotate ssh_system_log files if they exceed the given size 298 | rotate_logs() { 299 | local log_file_to_rotate="$1" 300 | local max_size_kb="$2" 301 | if [[ -f ${log_file_to_rotate} ]]; then 302 | local file_size_kb 303 | file_size_kb=$(du -k "${log_file_to_rotate}" | cut -f1) 304 | if ((file_size_kb >= max_size_kb)); then 305 | local backup_name 306 | backup_name="${log_file_to_rotate}_$(date '+%Y%m%d%H%M%S')" 307 | mv "${log_file_to_rotate}" "${backup_name}" 308 | touch "${log_file_to_rotate}" 309 | ssh_system_log "INFO" "Rotated ssh_system_log file: ${backup_name}" 310 | fi 311 | fi 312 | } 313 | 314 | # Handle file checks 315 | handle_file_checks() { 316 | local last_check_file="${1}" 317 | local last_email_sent_file="${2}" 318 | local error_log_file="${3}" 319 | 320 | local files_to_check 321 | files_to_check=( 322 | "${log_file}" 323 | "${error_log_file}" 324 | "${last_check_file}" 325 | "${last_email_sent_file}" 326 | ) 327 | 328 | local file 329 | for file in "${files_to_check[@]}"; do 330 | ssh_system_log "INFO" "Checking file: ${file}" "${log_file}" 331 | check_and_create_file "${file}" "${log_file}" 332 | verify_file_is_writable "${file}" "${log_file}" 333 | done 334 | } 335 | 336 | # Main function 337 | main() { 338 | check_root 339 | 340 | echo "Initializing SSHD IDS..." 341 | 342 | local service_name="ssh_monitor.service" 343 | local service_file="/etc/systemd/system/${service_name}" 344 | local service_description="SSH Monitor Service" 345 | local last_email_sent_file="/tmp/last_email_sent_time" 346 | local email_alert_threshold=1 347 | local email_subject="SSH Monitor Alert" 348 | local last_check_file="/tmp/last_ssh_check" 349 | 350 | local script_dir script_path local_config_file 351 | script_dir="/opt/ssh_monitor" 352 | script_path="${script_dir}/ssh_monitor.sh" 353 | local_config_file="${script_dir}/ssh_monitor_config" 354 | 355 | local default_log_file="/var/log/ssh_monitor.log" 356 | local error_log_file="/var/log/ssh_monitor_error.log" 357 | log_file="${default_log_file}" 358 | 359 | local recipients="${2:-root@$(hostname -f)}" 360 | 361 | handle_file_checks "${last_check_file}" "${last_email_sent_file}" "${error_log_file}" 362 | load_config "${local_config_file}" "${log_file}" "${error_log_file}" "${recipients}" "${email_alert_threshold}" "${email_subject}" 363 | 364 | if [[ $1 =~ ^(install|status|stop|disable)$ ]]; then 365 | manage_daemon "${log_file}" "${error_log_file}" "${script_path}" "${service_file}" "${service_name}" "${service_description}" "$1" 366 | echo "SSH IDS management complete." 367 | elif [[ $1 =~ ^start$ ]]; then 368 | command -v last >/dev/null 2>&1 || { 369 | ssh_system_log "ERROR" "The 'last' command is not available. Please install the 'sysstat' package." "${error_log_file}" 370 | exit 1 371 | } 372 | 373 | echo "Monitoring SSH sessions..." 374 | 375 | local cycle_duration=20 376 | while true; do 377 | rotate_logs "${log_file}" 1024 378 | rotate_logs "${error_log_file}" 1024 379 | 380 | local start_time 381 | start_time=$(date +%s) 382 | 383 | handle_ssh_sessions "${email_alert_threshold}" "${email_subject}" "${recipients}" "${last_email_sent_file}" "${error_log_file}" 384 | 385 | local end_time elapsed sleep_duration 386 | end_time=$(date +%s) 387 | elapsed=$((end_time - start_time)) 388 | sleep_duration=$((cycle_duration - elapsed)) 389 | 390 | if ((sleep_duration > 0)); then 391 | sleep "${sleep_duration}" 392 | else 393 | ssh_system_log "INFO" "Warning: Script execution time exceeded the cycle duration." "${log_file}" 394 | fi 395 | done 396 | 397 | echo "Finished monitoring SSH sessions." 398 | else 399 | usage 400 | fi 401 | } 402 | 403 | # Execute the main function with command line arguments 404 | main "$@" 405 | EOF 406 | } 407 | 408 | ####################################### 409 | # description 410 | # Arguments: 411 | # None 412 | ####################################### 413 | main() { 414 | generate_ssh_intrusion_detection 415 | echo "SSH Intrusion Detection System installed successfully." 416 | } 417 | 418 | main -------------------------------------------------------------------------------- /hardening/standalones/upgrades/setup_auto_upgrades.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Checks if the script is being run as root 6 | check_root() { 7 | local uuid 8 | uuid=$(id -u) 9 | if [[ ${uuid} -ne 0 ]]; then 10 | echo "This script must be run as root. Exiting..." >&2 11 | exit 1 12 | fi 13 | } 14 | 15 | # Log a message to the log file 16 | log() { 17 | local message="$1" 18 | local date 19 | date=$(date +'%Y-%m-%d %H:%M:%S') 20 | } 21 | 22 | # Append or update a line in a file with a key value pair 23 | write_key() { 24 | local key="$1" 25 | local value="$2" 26 | local config_file="$3" 27 | echo "${key} ${value}" >> "${config_file}" 28 | } 29 | 30 | # Updates the cron job for a script 31 | update_cron_job() { 32 | if [[ -z "$1" ]] || [[ -z "$2" ]]; then 33 | log "Usage: update_cron_job " 34 | return 1 35 | fi 36 | 37 | local script="$1" 38 | local log_file="$2" 39 | local cron_entry="0 0 * * * ${script} >${log_file} 2>&1" 40 | 41 | # Attempt to read existing cron jobs, suppressing errors about no existing crontab 42 | local current_cron_jobs 43 | if ! current_cron_jobs=$(crontab -l 2> /dev/null); then 44 | log "No existing crontab for user. Creating new crontab..." 45 | fi 46 | 47 | # Check if the cron job already exists and is up-to-date 48 | if echo "${current_cron_jobs}" | grep -Fxq -- "${cron_entry}"; then 49 | log "Cron job already up to date." 50 | return 0 51 | else 52 | log "Cron job for script not found or not up-to-date. Adding new job..." 53 | fi 54 | 55 | # Add the cron job to the crontab 56 | if (echo "${current_cron_jobs}"; echo "${cron_entry}") | crontab -; then 57 | log "Cron job added successfully." 58 | else 59 | log "Failed to add cron job." 60 | fi 61 | 62 | } 63 | 64 | # Configures automatic updates 65 | configure_auto_updates() { 66 | # Configure automatic updates 67 | local config_file="/etc/apt/apt.conf.d/50unattended-upgrades" 68 | local recipients="${1}" 69 | 70 | # Define the lines to insert 71 | declare -A updates=( 72 | ["Unattended-Upgrade::DevRelease"]='"auto";' 73 | ["Unattended-Upgrade::MinimalSteps"]='"true";' 74 | ["Unattended-Upgrade::Mail"]="\"${recipients}\";" 75 | ["Unattended-Upgrade::MailReport"]='"always";' 76 | ["Unattended-Upgrade::Remove-Unused-Kernel-Packages"]='"false";' 77 | ["Unattended-Upgrade::Automatic-Reboot-WithUsers"]='"true";' 78 | ["Unattended-Upgrade::Automatic-Reboot-Time"]='"00:00";' 79 | ["Unattended-Upgrade::Automatic-Reboot"]='"true";' 80 | ["Acquire::http::Dl-Limit"]='"10000";' 81 | ["Unattended-Upgrade::SyslogEnable"]='"true";' 82 | ["Unattended-Upgrade::SyslogFacility"]='"daemon";' 83 | ["Unattended-Upgrade::Verbose"]='"true";' 84 | ["Unattended-Upgrade::Debug"]='"true";' 85 | ["Unattended-Upgrade::AutoFixInterruptedDpkg"]='"true";' 86 | ) 87 | 88 | # Clear the contents of the file 89 | echo "" > "${config_file}" 90 | 91 | # Update each entry 92 | local key 93 | for key in "${!updates[@]}"; do 94 | echo "Updating ${key}..." 95 | write_key "${key}" "${updates[${key}]}" "${config_file}" 96 | done 97 | 98 | # Add an Unattended-Upgrade::Allowed-Origins section 99 | cat << EOF | sudo tee -a "${config_file}" > /dev/null 100 | Unattended-Upgrade::Allowed-Origins { 101 | "\${distro_id}:\${distro_codename}"; 102 | "\${distro_id}:\${distro_codename}-security"; 103 | "\${distro_id}ESMApps:\${distro_codename}-apps-security"; 104 | "\${distro_id}ESM:\${distro_codename}-infra-security"; 105 | "\${distro_id}:\${distro_codename}-updates"; 106 | }; 107 | EOF 108 | 109 | update_cron_job "/usr/bin/unattended-upgrade -d" "/var/log/unattended-upgrades.log" 110 | } 111 | 112 | # Installs a list of apt packages 113 | install_apt_packages() { 114 | local package_list=("${@}") # Capture all arguments as an array of packages 115 | 116 | log "Starting package installation process." 117 | 118 | # Verify that there are no apt locks 119 | while fuser /var/lib/dpkg/lock >/dev/null 2>&1 || fuser /var/lib/apt/lists/lock >/dev/null 2>&1 || fuser /var/cache/apt/archives/lock >/dev/null 2>&1; do 120 | log "Waiting for other software managers to finish..." 121 | sleep 1 122 | done 123 | 124 | if apt update -y; then 125 | log "Package lists updated successfully." 126 | else 127 | log "Failed to update package lists. Continuing with installation..." 128 | fi 129 | 130 | local package 131 | local failed_packages=() 132 | for package in "${package_list[@]}"; do 133 | if dpkg -l | grep -qw "${package}"; then 134 | log "${package} is already installed." 135 | else 136 | # Sleep to avoid "E: Could not get lock /var/lib/dpkg/lock-frontend" error 137 | sleep 1 138 | if apt install -y "${package}"; then 139 | log "Successfully installed ${package}." 140 | else 141 | log "Failed to install ${package}." 142 | failed_packages+=("${package}") 143 | fi 144 | fi 145 | done 146 | 147 | if [[ ${#failed_packages[@]} -eq 0 ]]; then 148 | log "All packages were installed successfully." 149 | else 150 | log "Failed to install the following packages: ${failed_packages[*]}" 151 | fi 152 | } 153 | 154 | # Main 155 | main() { 156 | check_root 157 | 158 | echo "Initializing unattended-upgrades..." 159 | local recipients="${1:-root@$(hostname -f)}" # Default recipient email if not provided 160 | 161 | install_apt_packages "debconf-utils" "unattended-upgrades" 162 | configure_auto_updates "${recipients}" 163 | } 164 | 165 | main "$@" -------------------------------------------------------------------------------- /mail/create_aws_dkim_identity.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Create a new email identity in AWS SES with DKIM signing enabled using the AWS CLI 6 | main() { 7 | 8 | # Generate a default private key using 9 | openssl genrsa -f4 -out private.key 2048 10 | 11 | # Set the domain, private key file, selector, and JSON file path for the AWS CLI command 12 | local domain="myawesomedomain.com.au" 13 | local private_key_file="private.key" 14 | local selector="selector1" 15 | local json_file_path="create-identity.json" 16 | 17 | # Read and clean the private key 18 | # Remove BEGIN/END lines, line breaks, and ensure it's base64-encoded without line breaks 19 | local cleaned_private_key 20 | cleaned_private_key=$(sed '1d;$d' "${private_key_file}" | tr -d '\n') 21 | 22 | # Create the JSON payload 23 | cat > "${json_file_path}" << EOF 24 | { 25 | "EmailIdentity": "${domain}", 26 | "DkimSigningAttributes": { 27 | "DomainSigningPrivateKey": "${cleaned_private_key}", 28 | "DomainSigningSelector": "${selector}" 29 | } 30 | } 31 | EOF 32 | 33 | # Execute the AWS CLI command to create the email identity 34 | aws sesv2 create-email-identity --cli-input-json file://"${json_file_path}" 35 | } 36 | 37 | main "$@" 38 | -------------------------------------------------------------------------------- /mail/generate_dkim_verification_dmarc_records.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Cleans the key by removing the first and last lines and all newline characters 6 | clean_key() { 7 | local key_file=$1 8 | local cleaned_key_file="${key_file}.cleaned" 9 | echo "Cleaning the key..." 10 | sed '1d;$d' "${key_file}" | tr -d '\n' > "${cleaned_key_file}" 11 | echo "Cleaned key saved to ${cleaned_key_file}" 12 | } 13 | 14 | # Generate RSA private key with a specified size and format (PKCS#1 or PKCS#8) 15 | generate_private_key() { 16 | local file=$1 17 | local size=$2 18 | local format=$3 19 | echo "Generating RSA private key of size ${size} bits in ${format} format..." 20 | if [[ ${format} == "pkcs8" ]]; then 21 | openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:"$size" -outform PEM -out "${file}.pem" 22 | else # Default to PKCS #1 if not specified or specified as pkcs1 23 | openssl genrsa -out "${file}.pem" "${size}" 24 | fi 25 | clean_key "${file}.pem" 26 | } 27 | 28 | # Generate the corresponding public key from a private key 29 | generate_public_key() { 30 | local private_key_file=$1 31 | local public_key_file=$2 32 | echo "Generating the public key from the private key..." 33 | openssl rsa -in "${private_key_file}" -outform PEM -pubout -out "${public_key_file}" 34 | clean_key "${public_key_file}" 35 | } 36 | 37 | # Prepare the public key for DNS TXT record inclusion by removing headers and concatenating lines 38 | format_public_key_for_dns() { 39 | local public_key_file=$1 40 | local pubkey 41 | pubkey=$(grep -v -- "----" "${public_key_file}" | tr -d '\n') 42 | echo "\"v=DKIM1; k=rsa; p=${pubkey}\"" 43 | } 44 | 45 | # Prepare the DMARC record for DNS TXT record inclusion 46 | format_dmarc_record_for_dns() { 47 | local domain=$1 48 | 49 | # DMARC record with quarantine policy, including both aggregate and forensic reporting. 50 | # Provides options for SPF and DKIM alignment modes, and specifies the failure reporting condition. 51 | # Note: Adjust the 'pct' value to increase or decrease the percentage of messages subjected to the 'quarantine' policy based on your monitoring results and comfort level. 52 | # The 'fo=1' option means forensic reports are generated for any failures, providing detailed insights into issues. 53 | # 'adkim=r' and 'aspf=r' set relaxed alignment for DKIM and SPF, reducing false positives without significantly compromising security. 54 | echo "v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarcreports@${domain}; ruf=mailto:forensic@${domain}; fo=1; adkim=r; aspf=r; rf=afrf" 55 | } 56 | 57 | # Output the DNS TXT record in a user-friendly format 58 | output_dns_record() { 59 | local selector=$1 60 | local domain=$2 61 | local dkim_record_value=$3 62 | local dmarc_record_value=$4 63 | 64 | echo "########################################################" 65 | echo "Enter the following DNS record with your DNS provider:" 66 | echo "########################################################" 67 | echo "DKIM RECORD:" 68 | echo "Type: TXT" 69 | echo "Domain: ${selector}._domainkey.${domain}" 70 | echo "Value: ${dkim_record_value}" 71 | echo "TTL: 86400 (1 day recommended)" 72 | echo "########################################################" 73 | echo "DMARC RECORD:" 74 | echo "Type: TXT" 75 | echo "Domain: _dmarc.${domain}" 76 | echo "Value: ${dmarc_record_value}" 77 | echo "########################################################" 78 | } 79 | 80 | # Main function to orchestrate key generation and formatting for DNS 81 | main() { 82 | local domain="example.com.au" 83 | local selector="selector1" 84 | local key_size=${1:-2048} # Default to 2048 bits, can be overridden with script argument 85 | local key_format=${2:-pkcs1} # Default to PKCS #1, can be overridden with script argument 86 | 87 | # Validate key size 88 | if [[ ${key_size} -lt 2048 ]]; then 89 | echo "For enhanced security, a key size of 2048 bits or higher is recommended." 90 | exit 1 91 | fi 92 | 93 | local private_key_file="${domain}.${selector}.private" 94 | local public_key_file="${private_key_file}.public.pem" 95 | 96 | # Generate private and public keys 97 | generate_private_key "${private_key_file}" "${key_size}" "${key_format}" 98 | generate_public_key "${private_key_file}.pem" "${public_key_file}" 99 | 100 | # Format the public key and DMARC record for DNS 101 | local dkim_record_value dmarc_record_value 102 | dkim_record_value=$(format_public_key_for_dns "${public_key_file}.cleaned") 103 | dmarc_record_value=$(format_dmarc_record_for_dns "${domain}") 104 | output_dns_record "${selector}" "${domain}" "${dkim_record_value}" "${dmarc_record_value}" 105 | } 106 | 107 | # Run the main function with all provided arguments 108 | main "$@" 109 | -------------------------------------------------------------------------------- /mail/postfix_forwarding_service_installer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Checks if the script is run as root and exits if not. 6 | check_root() { 7 | if [[ $(id -u) -ne 0 ]]; then 8 | echo "This script must be run as root." 9 | exit 1 10 | fi 11 | } 12 | 13 | usage() { 14 | echo "Usage: $0 [canonical_sender] [mail_transfer_authority]" 15 | echo "Configures Postfix to use an external mail transfer authority for sending emails." 16 | echo "If canonical_sender is provided, it will be used to rewrite the sender address so that all emails appear to be sent from the provided address." 17 | exit 1 18 | } 19 | 20 | # Updates a configuration setting in the Postfix main.cf file. 21 | update_postfix_config() { 22 | local setting_name="$1" 23 | local setting_value="$2" 24 | local main_cf="$3" 25 | 26 | if [[ -z ${setting_value} ]]; then 27 | echo "Value for ${setting_name} is not provided. Skipping update." 28 | return 29 | fi 30 | 31 | # Update or add the configuration in Postfix main.cf 32 | if grep -q "^${setting_name}" "${main_cf}"; then 33 | sed -i "s|^${setting_name}.*|${setting_name} = ${setting_value}|" "${main_cf}" 34 | echo "Updated ${setting_name} in ${main_cf}." 35 | else 36 | echo "${setting_name} = ${setting_value}" >> "${main_cf}" 37 | echo "Added ${setting_name} to ${main_cf}." 38 | fi 39 | } 40 | 41 | # Installs necessary packages and removes sendmail. 42 | install_packages() { 43 | apt --purge remove sendmail -y && apt install postfix libsasl2-modules -y 44 | if [[ $? -ne 0 ]]; then 45 | echo "Package installation failed. Exiting." 46 | exit 1 47 | fi 48 | } 49 | 50 | # Verifies if the postdrop user exists in the system. 51 | verify_postdrop() { 52 | if ! grep -q postdrop /etc/group; then 53 | echo "Postdrop user does not exist. Exiting." 54 | exit 1 55 | fi 56 | } 57 | 58 | # Configures postfix with required settings. 59 | configure_postfix() { 60 | local main_cf="$1" 61 | local sasl_passwd="$2" 62 | local mail_transfer_authority="$3" 63 | 64 | \cp -f "${main_cf}"{.proto,} 65 | sed -i "s/^setgid_group =.*/setgid_group = postdrop/" "${main_cf}" 66 | postconf -e "relayhost = [${mail_transfer_authority}]:587" \ 67 | "smtp_sasl_auth_enable = yes" \ 68 | "smtp_sasl_security_options = noanonymous" \ 69 | "smtp_sasl_password_maps = hash:${sasl_passwd}" \ 70 | "smtp_use_tls = yes" \ 71 | "smtp_tls_security_level = encrypt" \ 72 | "smtp_tls_note_starttls_offer = yes" \ 73 | "smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt" 74 | enable_aliases 75 | } 76 | 77 | # Configure aliases for root user 78 | enable_aliases() { 79 | local aliases_file="/etc/aliases" 80 | local aliases_db="/etc/aliases.db" 81 | 82 | if [[ ! -f ${aliases_file} ]]; then 83 | touch "${aliases_file}" 84 | else 85 | postalias /etc/aliases 86 | fi 87 | 88 | if [[ ! -f ${aliases_db} ]]; then 89 | newaliases 90 | fi 91 | } 92 | 93 | # Inserts the canonical sender configuration in main.cf to rewrite the sender address. 94 | insert_sender_canonical() { 95 | local new_canonical_sender="$1" 96 | local sender_canonical_config_string="sender_canonical_maps = static:${new_canonical_sender}" 97 | if ! grep -q "${sender_canonical_config_string}" /etc/postfix/main.cf; then 98 | echo "${sender_canonical_config_string}" >> /etc/postfix/main.cf 99 | fi 100 | } 101 | 102 | # Starts postfix service if not already running. 103 | start_postfix() { 104 | if ! systemctl is-active --quiet postfix; then 105 | echo "Starting Postfix service..." 106 | systemctl start postfix 107 | fi 108 | } 109 | 110 | # Configures the SASL password file. 111 | configure_sasl_passwd() { 112 | local sasl_passwd="$1" 113 | local mail_transfer_authority="$2" 114 | local ses_port="${3:-587}" 115 | 116 | local smtp_user smtp_password 117 | read -p "Enter SMTP username: " smtp_user 118 | read -sp "Enter SMTP password: " smtp_password 119 | 120 | echo "[${mail_transfer_authority}]:${ses_port} ${smtp_user}:${smtp_password}" > "${sasl_passwd}" 121 | chown root:root "${sasl_passwd}" 122 | chmod 0600 "${sasl_passwd}" 123 | postmap -v hash:"${sasl_passwd}" 124 | 125 | unset smtp_user smtp_password 126 | } 127 | 128 | # Enables and restarts postfix, and checks its status. 129 | enable_and_restart_postfix() { 130 | systemctl enable postfix --quiet 131 | systemctl restart postfix --quiet 132 | systemctl status postfix --no-pager 133 | } 134 | 135 | # Backup the Postfix configuration file, if it exists. 136 | backup_configs() { 137 | local main_cf="$1" 138 | local backup_dir="/etc/postfix/backup" 139 | local backup_file 140 | backup_file="${backup_dir}/main.cf.$(date +%Y%m%d%H%M%S)" 141 | 142 | if [[ ! -d ${backup_dir} ]]; then 143 | mkdir -p "${backup_dir}" 144 | fi 145 | 146 | if [[ -f ${main_cf} ]]; then 147 | cp -f "${main_cf}" "${backup_file}" 148 | echo "Backup of ${main_cf} created at ${backup_file}." 149 | else 150 | echo "No backup created. ${main_cf} does not exist." 151 | fi 152 | } 153 | 154 | # Validate Postfix configuration. 155 | validate_postfix_config() { 156 | echo "Validating Postfix configuration..." 157 | if ! postfix check; then 158 | echo "Postfix configuration validation failed. Exiting." 159 | exit 1 160 | fi 161 | echo "Postfix configuration is valid." 162 | } 163 | 164 | # Main function to execute script tasks. 165 | main() { 166 | check_root 167 | 168 | local backup=false 169 | local main_cf="/etc/postfix/main.cf" 170 | local sasl_passwd="/etc/postfix/sasl_passwd" 171 | 172 | if [[ $# -ne 2 ]]; then 173 | usage 174 | fi 175 | 176 | local canonical_sender="${1}" 177 | local mail_transfer_authority="${2:-email-smtp.ap-southeast-2.amazonaws.com}" 178 | 179 | install_packages 180 | start_postfix 181 | verify_postdrop 182 | 183 | configure_postfix "${main_cf}" "${sasl_passwd}" "${mail_transfer_authority}" 184 | update_postfix_config "sendmail_path" "$(which sendmail)" "${main_cf}" 185 | update_postfix_config "mailq_path" "$(which mailq)" "${main_cf}" 186 | update_postfix_config "newaliases_path" "$(which newaliases)" "${main_cf}" 187 | update_postfix_config "html_directory" "/usr/share/doc/postfix/html" "${main_cf}" 188 | update_postfix_config "manpage_directory" "/usr/share/man" "${main_cf}" 189 | update_postfix_config "sample_directory" "/usr/share/doc/postfix/samples" "${main_cf}" 190 | update_postfix_config "readme_directory" "/usr/share/doc/postfix/readme" "${main_cf}" 191 | 192 | insert_sender_canonical "${canonical_sender}" 193 | configure_sasl_passwd "${sasl_passwd}" "${mail_transfer_authority}" 194 | 195 | if [[ ${backup} == "true" ]]; then 196 | backup_configs "${main_cf}" 197 | fi 198 | 199 | enable_and_restart_postfix 200 | validate_postfix_config 201 | enable_and_restart_postfix 202 | 203 | echo "Postfix configuration successfully completed." 204 | } 205 | 206 | main "$@" 207 | -------------------------------------------------------------------------------- /security/create_administrative_user.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | usage() { 6 | echo "Usage: $0 " 7 | echo " - The name of the administrative user to create." 8 | exit 1 9 | } 10 | 11 | # Create an administrative user 12 | create_administrative_user() { 13 | local user=${1} 14 | 15 | echo "Creating administrative user '${user}'..." 16 | if sudo -S adduser "${user}" && sudo -S usermod -aG sudo "${user}"; then 17 | echo "${user} has been added to the sudo group." 18 | else 19 | echo "Failed to create administrative user '${user}'. Exiting." 20 | exit 1 21 | fi 22 | } 23 | 24 | # Initialize SSH key-based authentication 25 | setup_ssh_key_authentication() { 26 | local user=${1} 27 | 28 | local user_home 29 | user_home=$(getent passwd "${user}") 30 | user_home=$(echo "${user_home}" | cut -d: -f6) 31 | 32 | echo "Setting up SSH key authentication for '${user}'..." 33 | sudo -S mkdir -p "${user_home}/.ssh" 34 | sudo -S touch "${user_home}/.ssh/authorized_keys" 35 | sudo -S chmod 700 "${user_home}/.ssh" 36 | sudo -S chmod 600 "${user_home}/.ssh/authorized_keys" 37 | sudo -S chown -R "${user}:${user}" "${user_home}/.ssh" 38 | 39 | echo "SSH key authentication setup for ${user}." 40 | } 41 | 42 | # Main function 43 | main() { 44 | 45 | if [ "${#}" -ne 1 ]; then 46 | usage 47 | fi 48 | 49 | local user=${1} 50 | 51 | # Create an administrative user 52 | create_administrative_user "${user}" 53 | 54 | # Setup SSH key-based authentication for the user 55 | setup_ssh_key_authentication "${user}" 56 | } 57 | 58 | main "${@}" 59 | 60 | # To set up SSH access while root access is still enabled perform the following: 61 | # ssh -v -i ~/.ssh/ssd-nodes root@5.5.5.5 "cat >> /home/void/.ssh/authorized_keys" < "/home/void/.ssh/ssd-nodes.pub" 2>/dev/null -------------------------------------------------------------------------------- /security/ssh_setup_and_deploy_key.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # Generates an SSH key pair, updates known_hosts, and copies the public key to the server. 6 | # Usage: ./this_script.sh 7 | 8 | # Generate a new ed25519 SSH key pair for the specified server and user. 9 | generate_key_pair() { 10 | local server="$1" 11 | local user="$2" 12 | local email="$3" 13 | ssh-keygen -t ed25519 -f "${HOME}/.ssh/${server}-${user}" -q -N "" -C "${email}" 14 | } 15 | 16 | # Add the server's public key to the user's known_hosts file. 17 | key_scan() { 18 | local server="$1" 19 | local username="$2" 20 | local user_known_hosts="${HOME}/.ssh/known_hosts_${username}" 21 | 22 | local temp_file 23 | temp_file="$(mktemp)" 24 | 25 | if ssh-keyscan -H "${server}" > "${temp_file}" 2>/dev/null; then 26 | if ! grep -F -f "${temp_file}" "${user_known_hosts}" > /dev/null; then 27 | cat "${temp_file}" >> "${user_known_hosts}" 28 | echo "Added ${server} to ${user_known_hosts}." 29 | else 30 | echo "${server} is already in ${user_known_hosts}." 31 | fi 32 | else 33 | echo "ssh-keyscan failed for ${server}." 34 | return 1 35 | fi 36 | 37 | rm -f "${temp_file}" 38 | } 39 | 40 | # Copy the public key to the server for the specified user. 41 | copy_key_to_server() { 42 | local server="$1" 43 | local user="$2" 44 | local public_key_file="${HOME}/.ssh/${server}-${user}.pub" 45 | 46 | if ssh "${user}@${server}" "mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && cat >> ~/.ssh/authorized_keys" < "${public_key_file}" 2>/dev/null; then 47 | echo "Public key successfully copied to ${server} for user ${user}." 48 | else 49 | echo "Failed to copy public key to ${server} for user ${user}." 50 | return 1 51 | fi 52 | } 53 | 54 | # Main function. 55 | main() { 56 | if [[ "$#" -ne 3 ]]; then 57 | echo "Usage: $0 " 58 | exit 1 59 | fi 60 | 61 | local server="$1" 62 | local user="$2" 63 | local email="$3" 64 | 65 | generate_key_pair "${server}" "${user}" "${email}" 66 | key_scan "${server}" "${user}" 67 | copy_key_to_server "${server}" "${user}" 68 | } 69 | 70 | main "$@" 71 | --------------------------------------------------------------------------------