-
-
Save ibehnam/1b1c6a725807d1a8263cfdc26baeee7f to your computer and use it in GitHub Desktop.
cloud-init script for VPS
This file contains 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
#cloud-config | |
# Enable automatic package updates and upgrades during cloud-init execution | |
package_update: true | |
package_upgrade: true | |
packages: | |
# Security and Hardening | |
- ufw | |
- fail2ban | |
- apparmor | |
- apparmor-utils | |
- auditd | |
- audispd-plugins | |
- unattended-upgrades | |
- acl | |
- aide | |
- rkhunter | |
# Utilities | |
- curl | |
- wget | |
- gnupg | |
- ca-certificates | |
- apt-transport-https | |
- software-properties-common | |
- git | |
- chrony | |
- vim | |
- jq | |
# System Monitoring | |
- sysstat | |
# Email Notification (Postfix / Gmail) | |
- postfix | |
- mailutils | |
- libsasl2-modules | |
# Create a non-root user with sudo privileges for secure server access | |
users: | |
- name: deploy | |
groups: sudo | |
shell: /bin/bash | |
sudo: ["ALL=(ALL) NOPASSWD:ALL"] | |
ssh_authorized_keys: | |
- ${ssh_public_key} | |
# Create configuration files with secure settings | |
write_files: | |
# Secure SSH configuration to prevent unauthorized access | |
- path: /etc/ssh/sshd_config | |
content: | | |
Include /etc/ssh/sshd_config.d/*.conf | |
Port 22 | |
AddressFamily any | |
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 | |
PermitRootLogin no | |
StrictModes yes | |
MaxAuthTries 5 | |
MaxSessions 5 | |
PubkeyAuthentication yes | |
HostbasedAuthentication no | |
IgnoreRhosts yes | |
PasswordAuthentication no | |
PermitEmptyPasswords no | |
ChallengeResponseAuthentication no | |
UsePAM yes | |
# Security configuration: | |
# - Agent forwarding is disabled to prevent potential agent hijacking attacks | |
# - TCP forwarding is enabled to allow SSH tunneling for secure access to services | |
# (e.g., Docker API, databases, or internal services) | |
# - X11 forwarding is disabled as it's not needed on servers | |
AllowAgentForwarding no | |
AllowTcpForwarding yes | |
X11Forwarding no | |
PermitTTY yes | |
PrintMotd no | |
ClientAliveInterval 300 | |
ClientAliveCountMax 2 | |
TCPKeepAlive no | |
AllowUsers deploy | |
# Kernel security parameters to harden against common attacks | |
- path: /etc/sysctl.d/99-security.conf | |
content: | | |
# Disable IP forwarding and redirects for 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 | |
# Enable reverse path filtering to prevent IP spoofing | |
net.ipv4.conf.all.rp_filter = 1 | |
net.ipv4.conf.default.rp_filter = 1 | |
# Ignore broadcast ICMP and bogus error responses | |
net.ipv4.icmp_echo_ignore_broadcasts = 1 | |
net.ipv4.icmp_ignore_bogus_error_responses = 1 | |
# Disable source routing which can be used in packet spoofing | |
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 | |
# Enable SYN cookies to protect against SYN flood attacks | |
net.ipv4.tcp_syncookies = 1 | |
net.ipv4.tcp_max_syn_backlog = 2048 | |
net.ipv4.tcp_synack_retries = 2 | |
net.ipv4.tcp_syn_retries = 5 | |
# Enable IP forwarding for Docker | |
net.ipv4.ip_forward = 1 | |
# System resource limits and performance tuning | |
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 hardening settings | |
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 | |
# Filesystem security | |
fs.protected_hardlinks = 1 | |
fs.protected_symlinks = 1 | |
fs.suid_dumpable = 0 | |
# Log suspicious packets | |
net.ipv4.conf.all.log_martians = 1 | |
net.ipv4.conf.default.log_martians = 1 | |
# IPv6 router advertisement settings | |
net.ipv6.conf.all.accept_ra = 2 | |
net.ipv6.conf.default.accept_ra = 2 | |
# System resource limits for Docker containers | |
- path: /etc/security/limits.d/docker.conf | |
content: | | |
# Increase process and file limits for Docker containers | |
* soft nproc 10000 | |
* hard nproc 10000 | |
* soft nofile 1048576 | |
* hard nofile 1048576 | |
* soft core 0 | |
* hard core 0 | |
* soft stack 8192 | |
* hard stack 8192 | |
# Docker daemon configuration with IPv6 ULA (Unique Local Address) | |
# We use fd00::/64 (ULA) instead of 2001:db8::/64 (documentation prefix) because: | |
# 1. ULA addresses are designed for internal network use, similar to IPv4 private addresses | |
# 2. They work reliably for Docker Swarm clustering without exposing containers directly to the internet | |
# 3. Unlike the documentation prefix (2001:db8), ULA addresses won't be filtered by network equipment | |
# 4. This approach provides IPv6 functionality while maintaining security by default | |
- path: /etc/docker/daemon.json | |
content: | | |
{ | |
"log-driver": "json-file", | |
"log-opts": { | |
"max-size": "10m", | |
"max-file": "3" | |
}, | |
"icc": true, | |
"live-restore": true, | |
"userland-proxy": false, | |
"no-new-privileges": true, | |
"default-ulimits": { | |
"nofile": { | |
"Name": "nofile", | |
"Hard": 64000, | |
"Soft": 64000 | |
} | |
}, | |
"features": { | |
"buildkit": true | |
}, | |
"experimental": false, | |
"default-runtime": "runc", | |
"storage-driver": "overlay2", | |
"metrics-addr": "127.0.0.1:9323", | |
"ipv6": true, | |
"fixed-cidr-v6": "fd00::/64", | |
"ip6tables": true, | |
"builder": { | |
"gc": { | |
"enabled": true, | |
"defaultKeepStorage": "20GB" | |
} | |
} | |
} | |
# Custom fail2ban filter for Docker authentication failures | |
- path: /etc/fail2ban/filter.d/docker.conf | |
content: | | |
[Definition] | |
# More comprehensive patterns for Docker Swarm authentication failures | |
failregex = ^.*level=warning msg=".*: Password authentication failed for user.*" remote="<HOST>:.*"$ | |
^.*level=error msg=".*: authentication failed.*" remote="<HOST>:.*"$ | |
^.*level=warning msg=".*: failed authentication.*" remote="<HOST>:.*"$ | |
ignoreregex = | |
# Configure fail2ban jails for SSH and Docker | |
- path: /etc/fail2ban/jail.local | |
content: | | |
# Global fail2ban settings | |
[DEFAULT] | |
bantime = 3600 | |
findtime = 600 | |
maxretry = 5 | |
banaction = ufw | |
banaction_allports = ufw | |
# SSH protection | |
[sshd] | |
enabled = true | |
port = ssh | |
filter = sshd | |
logpath = /var/log/auth.log | |
maxretry = 10 | |
bantime = 3600 | |
# Docker authentication protection | |
[docker] | |
enabled = true | |
filter = docker | |
# Docker uses json-file logging driver by default, not journald | |
# This configuration targets the default Docker log files directly | |
backend = auto | |
logpath = /var/lib/docker/containers/*/*.log | |
maxretry = 5 | |
bantime = 3600 | |
# Log rotation for Docker container logs to prevent disk space issues | |
- path: /etc/logrotate.d/docker-logs | |
content: | | |
/var/lib/docker/containers/*/*.log { | |
rotate 7 | |
daily | |
compress | |
size=100M | |
missingok | |
delaycompress | |
copytruncate | |
} | |
# Weekly Docker cleanup script to manage disk space | |
- path: /etc/cron.weekly/docker-cleanup | |
permissions: "0755" | |
content: | | |
#!/bin/bash | |
# Weekly cleanup of Docker resources | |
# Only removes unused resources (dangling images, stopped containers, unused networks and volumes) | |
# Keeps images used within the last 30 days | |
docker system prune -f --filter "until=720h" | |
# Keep at least 20GB of builder cache for faster builds | |
docker builder prune -f --keep-storage=20GB | |
# Helper script for Docker Swarm initialization | |
- path: /usr/local/bin/initialize-swarm.sh | |
permissions: "0755" | |
content: | | |
#!/bin/bash | |
# Helper script to initialize Docker Swarm with IPv4 for management and IPv6 enabled | |
# Get the primary IPv4 address | |
IPV4_ADDR=$(hostname -I | awk '{print $1}') | |
# Initialize the swarm with IPv4 for management traffic | |
echo "Initializing Docker Swarm with IPv4 address: $IPV4_ADDR" | |
docker swarm init --advertise-addr $IPV4_ADDR | |
echo "" | |
echo "Swarm initialized successfully!" | |
echo "" | |
echo "To create an IPv6-enabled overlay network, run:" | |
echo "docker network create --driver overlay --ipv6 --subnet=fd00::/80 --subnet=172.20.0.0/16 my-overlay-network" | |
echo "" | |
echo "Worker join token:" | |
docker swarm join-token worker | grep docker | |
echo "" | |
echo "Manager join token:" | |
docker swarm join-token manager | grep docker | |
# Audit rules to monitor Docker-related files and commands | |
- path: /etc/audit/rules.d/audit.rules | |
content: | | |
# Monitor Docker binaries, configuration files, and service files for changes | |
-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/default/docker -k docker | |
-w /etc/docker/daemon.json -k docker | |
-w /usr/bin/docker -k docker-bin | |
-w /var/lib/docker/swarm -k docker-swarm | |
# Configure automatic security updates | |
- path: /etc/apt/apt.conf.d/50unattended-upgrades | |
content: | | |
# Define which updates to automatically install (security updates only) | |
Unattended-Upgrade::Allowed-Origins { | |
"${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::Automatic-Reboot "true"; | |
Unattended-Upgrade::Automatic-Reboot-Time "02:00"; | |
# Postfix configuration for outbound email alerts | |
- path: /etc/postfix/main.cf | |
content: | | |
# Basic Postfix settings | |
smtpd_banner = $myhostname ESMTP $mail_name | |
biff = no | |
append_dot_mydomain = no | |
readme_directory = no | |
# TLS settings for secure mail transmission | |
smtp_tls_security_level = encrypt | |
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt | |
smtp_use_tls = yes | |
# Placeholder value, replaced in rumcmd section | |
myhostname = localhost | |
# Mail routing configuration | |
alias_maps = hash:/etc/aliases | |
alias_database = hash:/etc/aliases | |
myorigin = /etc/mailname | |
mydestination = $myhostname, localhost.$mydomain, localhost | |
relayhost = [smtp.gmail.com]:587 | |
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 | |
mailbox_size_limit = 0 | |
recipient_delimiter = + | |
inet_interfaces = loopback-only | |
inet_protocols = all | |
# SASL authentication for Gmail relay | |
smtp_sasl_auth_enable = yes | |
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd | |
smtp_sasl_security_options = noanonymous | |
smtp_sasl_tls_security_options = noanonymous | |
smtp_sasl_mechanism_filter = plain | |
# Gmail SMTP credentials for outbound email | |
- path: /etc/postfix/sasl_passwd | |
permissions: "0600" | |
content: | | |
# Gmail SMTP authentication credentials (replace with your actual credentials) | |
[smtp.gmail.com]:587 [email protected]:your-app-password | |
# Commands to run after file creation to configure the system | |
runcmd: | |
# Apply kernel parameters | |
- sysctl --system | |
# Disable default time synchronization in favor of chrony | |
- systemctl stop systemd-timesyncd || true | |
- systemctl disable systemd-timesyncd || true | |
# Configure UFW firewall with secure defaults | |
- ufw --force reset | |
- ufw default deny incoming | |
- ufw default allow outgoing | |
- ufw allow ssh | |
- ufw allow http | |
- ufw allow https | |
# Docker Swarm ports | |
- ufw allow 2376/tcp # Docker TLS port | |
- ufw allow 2377/tcp # Docker Swarm cluster management | |
- ufw allow 7946/tcp # Container network discovery | |
- ufw allow 7946/udp # Container network discovery | |
- ufw allow 4789/udp # Container overlay network | |
- ufw --force enable | |
# Install Docker from official repo | |
- apt-get remove -y docker docker-engine docker.io containerd runc || true | |
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor | tee /usr/share/keyrings/docker-archive-keyring.gpg > /dev/null | |
- echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null | |
- apt-get update | |
- apt-get install -y docker-ce docker-ce-cli containerd.io | |
# Enable and start security services | |
- systemctl enable apparmor | |
- systemctl start apparmor | |
- aide --config=/etc/aide/aide.conf --init | |
- mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db | |
- systemctl enable docker fail2ban auditd chrony | |
- systemctl restart docker fail2ban auditd chrony | |
- systemctl reload ssh | |
- apt-get autoremove -y | |
- apt-get clean | |
# Configure Postfix for outbound email | |
- postmap /etc/postfix/sasl_passwd | |
- chmod 600 /etc/postfix/sasl_passwd* | |
# Try to use FQDN for Postfix hostname, which is better for mail delivery | |
# hostname -f attempts to get the fully-qualified domain name | |
# If that fails (returns non-zero exit code), fall back to the short hostname | |
# This ensures proper mail headers regardless of cloud provider hostname configuration | |
- | | |
if hostname -f >/dev/null 2>&1; then | |
sed -i "s/myhostname = .*/myhostname = $(hostname -f)/" /etc/postfix/main.cf | |
else | |
sed -i "s/myhostname = .*/myhostname = $(hostname)/" /etc/postfix/main.cf | |
fi | |
- systemctl restart postfix | |
- echo "Server security setup complete for $(hostname)" | mail -s "Server Setup: $(hostname)" [email protected] | |
# Remove any scheduled report jobs in favor of event-based alerting | |
- rm -f /etc/cron.d/security-report || true | |
# Enable system activity reporting | |
- sed -i 's/ENABLED=no/ENABLED=yes/' /etc/default/sysstat | |
- systemctl restart sysstat |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment