Forked from rameerez/kamal-production-server-setup.sh
Created
November 21, 2024 11:48
-
-
Save johan--/b2523ef77dd92bd963417652c20fece9 to your computer and use it in GitHub Desktop.
Set up a Ubuntu server to deploy Kamal 2.x Docker containers to, hardened security and production ready
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Production Docker Host Hardening Script | |
| # Suitable for both Kamal deployment and builder hosts | |
| # For Ubuntu Server 24.04 LTS | |
| set -euo pipefail | |
| IFS=$'\n\t' | |
| # --- Aesthetics and Error Handling --- | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| RED='\033[0;31m' | |
| ALIEN='\xF0\x9F\x91\xBD' | |
| NC='\033[0m' | |
| print_message() { | |
| local color=$1 | |
| local message=$2 | |
| echo -e "${color}${ALIEN} ${message}${NC}" | |
| } | |
| handle_error() { | |
| local line_number=$1 | |
| print_message "${RED}" "Error on line ${line_number}" | |
| exit 1 | |
| } | |
| trap 'handle_error ${LINENO}' ERR | |
| # Check if script is run as root | |
| if [[ $EUID -ne 0 ]]; then | |
| print_message "${RED}" "This script must be run as root" | |
| exit 1 | |
| fi | |
| # --- System Updates and Essential Packages --- | |
| print_message "${YELLOW}" "Updating system packages..." | |
| apt-get update | |
| DEBIAN_FRONTEND=noninteractive apt-get upgrade -y | |
| print_message "${YELLOW}" "Installing essential packages..." | |
| DEBIAN_FRONTEND=noninteractive apt-get install -y \ | |
| ufw \ | |
| fail2ban \ | |
| curl \ | |
| wget \ | |
| gnupg \ | |
| lsb-release \ | |
| ca-certificates \ | |
| apt-transport-https \ | |
| software-properties-common \ | |
| sysstat \ | |
| auditd \ | |
| audispd-plugins \ | |
| unattended-upgrades \ | |
| acl \ | |
| openssh-server \ | |
| apparmor \ | |
| apparmor-utils \ | |
| chrony \ | |
| aide \ | |
| rkhunter \ | |
| logwatch \ | |
| git | |
| # --- System Hardening --- | |
| print_message "${YELLOW}" "Configuring system security..." | |
| # AppArmor | |
| systemctl enable apparmor | |
| systemctl start apparmor | |
| # AIDE initialization | |
| aideinit | |
| # Configure chrony for secure time sync | |
| cat <<EOF > /etc/chrony/chrony.conf | |
| pool pool.ntp.org iburst maxsources 4 | |
| keyfile /etc/chrony/chrony.keys | |
| driftfile /var/lib/chrony/chrony.drift | |
| logdir /var/log/chrony | |
| maxupdateskew 100.0 | |
| rtcsync | |
| makestep 1 3 | |
| leapsectz right/UTC | |
| EOF | |
| # Configure system limits | |
| cat <<EOF > /etc/security/limits.d/docker.conf | |
| * soft nproc 10000 | |
| * hard nproc 10000 | |
| * soft nofile 1048576 | |
| * hard nofile 1048576 | |
| * soft core 0 | |
| * hard core 0 | |
| * soft stack 8192 | |
| * hard stack 8192 | |
| EOF | |
| # Kernel hardening | |
| cat <<EOF > /etc/sysctl.d/99-security.conf | |
| # Network security | |
| net.ipv4.conf.all.send_redirects = 0 | |
| net.ipv4.conf.default.send_redirects = 0 | |
| net.ipv4.conf.all.accept_redirects = 0 | |
| net.ipv4.conf.default.accept_redirects = 0 | |
| net.ipv4.conf.all.rp_filter = 1 | |
| net.ipv4.conf.default.rp_filter = 1 | |
| net.ipv4.icmp_echo_ignore_broadcasts = 1 | |
| net.ipv4.icmp_ignore_bogus_error_responses = 1 | |
| net.ipv4.conf.all.accept_source_route = 0 | |
| net.ipv4.conf.default.accept_source_route = 0 | |
| net.ipv6.conf.all.accept_source_route = 0 | |
| net.ipv6.conf.default.accept_source_route = 0 | |
| net.ipv4.tcp_syncookies = 1 | |
| net.ipv4.tcp_max_syn_backlog = 2048 | |
| net.ipv4.tcp_synack_retries = 2 | |
| net.ipv4.tcp_syn_retries = 5 | |
| # Docker needs IPv4 forwarding | |
| net.ipv4.ip_forward = 1 | |
| # System limits | |
| fs.file-max = 1048576 | |
| kernel.pid_max = 65536 | |
| net.ipv4.ip_local_port_range = 1024 65000 | |
| net.ipv4.tcp_tw_reuse = 1 | |
| vm.max_map_count = 262144 | |
| kernel.kptr_restrict = 2 | |
| kernel.dmesg_restrict = 1 | |
| kernel.perf_event_paranoid = 3 | |
| kernel.unprivileged_bpf_disabled = 1 | |
| net.core.bpf_jit_harden = 2 | |
| kernel.yama.ptrace_scope = 2 | |
| # File system hardening | |
| fs.protected_hardlinks = 1 | |
| fs.protected_symlinks = 1 | |
| fs.suid_dumpable = 0 | |
| # Additional network hardening | |
| net.ipv4.conf.all.log_martians = 1 | |
| net.ipv4.conf.default.log_martians = 1 | |
| net.ipv6.conf.all.accept_ra = 0 | |
| net.ipv6.conf.default.accept_ra = 0 | |
| EOF | |
| sysctl -p /etc/sysctl.d/99-security.conf | |
| # --- Docker Installation and Configuration --- | |
| print_message "${YELLOW}" "Installing Docker..." | |
| curl -fsSL https://get.docker.com -o get-docker.sh | |
| sh get-docker.sh | |
| rm get-docker.sh | |
| print_message "${YELLOW}" "Configuring Docker..." | |
| mkdir -p /etc/docker | |
| cat <<EOF > /etc/docker/daemon.json | |
| { | |
| "log-driver": "json-file", | |
| "log-opts": { | |
| "max-size": "10m", | |
| "max-file": "3" | |
| }, | |
| "icc": false, | |
| "live-restore": true, | |
| "userland-proxy": false, | |
| "no-new-privileges": true, | |
| "userns-remap": "default", | |
| "default-ulimits": { | |
| "nofile": { | |
| "Name": "nofile", | |
| "Hard": 64000, | |
| "Soft": 64000 | |
| } | |
| }, | |
| "features": { | |
| "buildkit": true | |
| }, | |
| "experimental": false, | |
| "default-runtime": "runc", | |
| "seccomp-profile": "/etc/docker/seccomp.json", | |
| "selinux-enabled": true, | |
| "userns-remap": "default", | |
| "storage-driver": "overlay2", | |
| "storage-opts": ["overlay2.override_kernel_check=true"], | |
| "metrics-addr": "127.0.0.1:9323", | |
| "builder": { | |
| "gc": { | |
| "enabled": true, | |
| "defaultKeepStorage": "20GB" | |
| } | |
| } | |
| } | |
| EOF | |
| # --- User Setup --- | |
| print_message "${YELLOW}" "Creating docker user..." | |
| adduser --system --group --shell /bin/bash --home /home/docker --disabled-password docker | |
| usermod -aG docker docker | |
| # --- SSH Hardening --- | |
| print_message "${YELLOW}" "Configuring SSH..." | |
| mkdir -p /home/docker/.ssh | |
| chmod 700 /home/docker/.ssh | |
| cat <<EOF > /etc/ssh/sshd_config | |
| Include /etc/ssh/sshd_config.d/*.conf | |
| Port 22 | |
| AddressFamily inet | |
| Protocol 2 | |
| HostKey /etc/ssh/ssh_host_ed25519_key | |
| HostKey /etc/ssh/ssh_host_ecdsa_key | |
| HostKey /etc/ssh/ssh_host_rsa_key | |
| SyslogFacility AUTH | |
| LogLevel VERBOSE | |
| LoginGraceTime 30 | |
| PermitRootLogin prohibit-password | |
| StrictModes yes | |
| MaxAuthTries 3 | |
| MaxSessions 5 | |
| PubkeyAuthentication yes | |
| HostbasedAuthentication no | |
| IgnoreRhosts yes | |
| PasswordAuthentication no | |
| PermitEmptyPasswords no | |
| ChallengeResponseAuthentication no | |
| UsePAM yes | |
| AllowAgentForwarding no | |
| AllowTcpForwarding yes # Required for Docker forwarding | |
| X11Forwarding no | |
| PermitTTY yes | |
| PrintMotd no | |
| ClientAliveInterval 300 | |
| ClientAliveCountMax 2 | |
| TCPKeepAlive no | |
| AllowUsers docker root | |
| KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256 | |
| Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr | |
| MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com | |
| Subsystem sftp internal-sftp | |
| EOF | |
| # --- Firewall Configuration --- | |
| print_message "${YELLOW}" "Configuring firewall..." | |
| ufw --force reset | |
| ufw default deny incoming | |
| ufw default allow outgoing | |
| ufw allow ssh | |
| ufw allow http | |
| ufw allow https | |
| ufw --force enable | |
| # --- Fail2ban Configuration --- | |
| print_message "${YELLOW}" "Configuring fail2ban..." | |
| cat <<EOF > /etc/fail2ban/jail.local | |
| [DEFAULT] | |
| bantime = 3600 | |
| findtime = 600 | |
| maxretry = 3 | |
| banaction = ufw | |
| banaction_allports = ufw | |
| [sshd] | |
| enabled = true | |
| port = ssh | |
| filter = sshd | |
| logpath = /var/log/auth.log | |
| maxretry = 3 | |
| bantime = 3600 | |
| [docker] | |
| enabled = true | |
| filter = docker | |
| logpath = /var/log/auth.log | |
| maxretry = 5 | |
| bantime = 3600 | |
| [docker-api] | |
| enabled = true | |
| port = 2375,2376 | |
| filter = docker-api | |
| logpath = /var/log/docker.log | |
| maxretry = 3 | |
| bantime = 3600 | |
| EOF | |
| # Create custom Docker fail2ban filter | |
| cat <<EOF > /etc/fail2ban/filter.d/docker.conf | |
| [Definition] | |
| failregex = failed login attempt from <HOST> | |
| ignoreregex = | |
| EOF | |
| # --- Logging and Monitoring --- | |
| print_message "${YELLOW}" "Configuring logging and monitoring..." | |
| # Docker log rotation | |
| cat <<EOF > /etc/logrotate.d/docker | |
| /var/lib/docker/containers/*/*.log { | |
| rotate 7 | |
| daily | |
| compress | |
| missingok | |
| delaycompress | |
| copytruncate | |
| size 100M | |
| } | |
| EOF | |
| # Configure auditd | |
| cat <<EOF > /etc/audit/rules.d/docker.rules | |
| # Docker daemon configuration | |
| -w /usr/bin/dockerd -k docker | |
| -w /var/lib/docker -k docker | |
| -w /etc/docker -k docker | |
| -w /usr/lib/systemd/system/docker.service -k docker | |
| -w /etc/systemd/system/docker.service -k docker | |
| -w /usr/lib/systemd/system/docker.socket -k docker | |
| -w /etc/default/docker -k docker | |
| -w /etc/docker/daemon.json -k docker | |
| EOF | |
| # --- Automatic Updates --- | |
| print_message "${YELLOW}" "Configuring automatic updates..." | |
| cat <<EOF > /etc/apt/apt.conf.d/50unattended-upgrades | |
| Unattended-Upgrade::Allowed-Origins { | |
| "\${distro_id}:\${distro_codename}"; | |
| "\${distro_id}:\${distro_codename}-security"; | |
| "\${distro_id}ESMApps:\${distro_codename}-apps-security"; | |
| "\${distro_id}ESM:\${distro_codename}-infra-security"; | |
| }; | |
| Unattended-Upgrade::Remove-Unused-Dependencies "true"; | |
| Unattended-Upgrade::Remove-New-Unused-Dependencies "true"; | |
| Unattended-Upgrade::AutoFixInterruptedDpkg "true"; | |
| Unattended-Upgrade::MinimalSteps "true"; | |
| Unattended-Upgrade::InstallOnShutdown "false"; | |
| EOF | |
| # --- System Maintenance --- | |
| print_message "${YELLOW}" "Setting up maintenance tasks..." | |
| cat <<EOF > /etc/cron.daily/docker-cleanup | |
| #!/bin/bash | |
| docker system prune -af --volumes | |
| docker builder prune -af --keep-storage=20GB | |
| EOF | |
| chmod +x /etc/cron.daily/docker-cleanup | |
| # Enable and start services | |
| systemctl enable docker fail2ban auditd chronyd | |
| systemctl restart docker fail2ban auditd chronyd | |
| # Final cleanup | |
| apt-get autoremove -y | |
| apt-get clean | |
| print_message "${GREEN}" "Setup complete! System hardening successful." | |
| print_message "${YELLOW}" "Important next steps:" | |
| print_message "${YELLOW}" "1. Add your SSH public key to /home/docker/.ssh/authorized_keys" | |
| print_message "${YELLOW}" "2. Configure your deployment tools (Kamal, etc.)" | |
| print_message "${YELLOW}" "3. REBOOT THE SYSTEM to apply all security settings" | |
| print_message "${YELLOW}" "4. Review logs in /var/log/ regularly" | |
| print_message "${YELLOW}" "5. Set up external monitoring and alerting" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment