Skip to content

Instantly share code, notes, and snippets.

@ibehnam
Forked from NatElkins/cloud-init.yaml
Created March 9, 2025 03:47
Show Gist options
  • Save ibehnam/1b1c6a725807d1a8263cfdc26baeee7f to your computer and use it in GitHub Desktop.
Save ibehnam/1b1c6a725807d1a8263cfdc26baeee7f to your computer and use it in GitHub Desktop.
cloud-init script for VPS
#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