722 lines
30 KiB
Go
722 lines
30 KiB
Go
package templates
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"sslh-multiplex-lab/internal/services"
|
|
)
|
|
|
|
type CloudInitConfig struct {
|
|
Users []User `yaml:"users"`
|
|
Packages []string `yaml:"packages"`
|
|
PackageUpdate bool `yaml:"package_update"`
|
|
PackageUpgrade bool `yaml:"package_upgrade"`
|
|
WriteFiles []WriteFile `yaml:"write_files,omitempty"`
|
|
RunCmd []string `yaml:"runcmd"`
|
|
}
|
|
|
|
type User struct {
|
|
Name string `yaml:"name"`
|
|
Groups []string `yaml:"groups,omitempty"`
|
|
Sudo string `yaml:"sudo,omitempty"`
|
|
Shell string `yaml:"shell,omitempty"`
|
|
SSHAuthorizedKeys []string `yaml:"ssh_authorized_keys,omitempty"`
|
|
Passwd string `yaml:"passwd,omitempty"`
|
|
LockPasswd bool `yaml:"lock_passwd,omitempty"`
|
|
}
|
|
|
|
type WriteFile struct {
|
|
Path string `yaml:"path"`
|
|
Content string `yaml:"content"`
|
|
Permissions string `yaml:"permissions,omitempty"`
|
|
Owner string `yaml:"owner,omitempty"`
|
|
}
|
|
|
|
func GenerateCloudInit(sshPublicKey, sslhConfig, username, testuserPassword, letsencryptEmail, domain string, svcList []services.Service) (string, error) {
|
|
users := []User{
|
|
{
|
|
Name: username,
|
|
Groups: []string{"users", "admin"},
|
|
Sudo: "ALL=(ALL) NOPASSWD:ALL",
|
|
Shell: "/bin/bash",
|
|
SSHAuthorizedKeys: []string{strings.TrimSpace(sshPublicKey)},
|
|
},
|
|
{
|
|
Name: "testuser",
|
|
Groups: []string{"users"},
|
|
Shell: "/bin/bash",
|
|
LockPasswd: false,
|
|
},
|
|
}
|
|
|
|
config := CloudInitConfig{
|
|
Users: users,
|
|
Packages: []string{
|
|
"python3-systemd",
|
|
"fail2ban",
|
|
"ufw",
|
|
"git",
|
|
"vim",
|
|
"sendmail",
|
|
"swaks",
|
|
"python3",
|
|
"python3-pip",
|
|
"golang",
|
|
"build-essential",
|
|
"gcc",
|
|
"gdb",
|
|
"g++",
|
|
"mingw-w64",
|
|
"unattended-upgrades",
|
|
"apt-listchanges",
|
|
"sslh",
|
|
// Note: Ensure SSLH v2.2.4+ is installed to fix CVE-2025-46807 and CVE-2025-46806
|
|
// If apt version is < 2.2.4, we'll build from source in runcmd
|
|
"nginx",
|
|
"samba",
|
|
"openssh-server",
|
|
"wireguard",
|
|
"wireguard-tools",
|
|
"certbot",
|
|
"python3-certbot-nginx",
|
|
},
|
|
PackageUpdate: true,
|
|
PackageUpgrade: true,
|
|
WriteFiles: []WriteFile{
|
|
{
|
|
Path: "/etc/sslh.cfg",
|
|
Content: sslhConfig,
|
|
Permissions: "0644",
|
|
Owner: "root:root",
|
|
},
|
|
{
|
|
Path: "/etc/sslh/sslh.cfg",
|
|
Content: sslhConfig,
|
|
Permissions: "0644",
|
|
Owner: "root:root",
|
|
},
|
|
{
|
|
Path: "/etc/sslh.cfg.backup",
|
|
Content: sslhConfig,
|
|
Permissions: "0644",
|
|
Owner: "root:root",
|
|
},
|
|
{
|
|
Path: fmt.Sprintf("/home/%s/.ssh/authorized_keys", username),
|
|
Content: strings.TrimSpace(sshPublicKey) + "\n",
|
|
Permissions: "0600",
|
|
Owner: fmt.Sprintf("%s:%s", username, username),
|
|
},
|
|
{
|
|
Path: "/etc/nginx/sites-available/sslh-proxy",
|
|
Content: generateNginxSSLHProxyConfig(),
|
|
Permissions: "0644",
|
|
Owner: "root:root",
|
|
},
|
|
{
|
|
Path: "/etc/nginx/sites-available/acme-challenge",
|
|
Content: generateNginxACMEConfig(),
|
|
Permissions: "0644",
|
|
Owner: "root:root",
|
|
},
|
|
{
|
|
Path: "/etc/systemd/system/sslh.service.d/override.conf",
|
|
Content: "[Service]\nEnvironmentFile=\nExecStart=\nExecStart=/usr/sbin/sslh --foreground -F /etc/sslh.cfg\n",
|
|
Permissions: "0644",
|
|
Owner: "root:root",
|
|
},
|
|
{
|
|
Path: "/var/www/demo/index.html",
|
|
Content: generateDemoPageHTML(),
|
|
Permissions: "0644",
|
|
Owner: "www-data:www-data",
|
|
},
|
|
},
|
|
RunCmd: generateRunCommands(username, strings.TrimSpace(sshPublicKey), testuserPassword, letsencryptEmail, domain, svcList),
|
|
}
|
|
|
|
yamlData, err := yaml.Marshal(&config)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal cloud-init config: %w", err)
|
|
}
|
|
|
|
// Hetzner requires #cloud-config on the first line for cloud-init to process the file
|
|
return "#cloud-config\n" + string(yamlData), nil
|
|
}
|
|
|
|
func generateRunCommands(username, sshPublicKey, testuserPassword, letsencryptEmail, domain string, svcList []services.Service) []string {
|
|
commands := []string{
|
|
`cat > /etc/fail2ban/jail.local <<'EOF'
|
|
[DEFAULT]
|
|
allowipv6 = false
|
|
bantime = 86400
|
|
bantime.increment = true
|
|
bantime.maxtime = 604800
|
|
bantime.rndtime = 3600
|
|
|
|
[sshd]
|
|
enabled = true
|
|
port = ssh
|
|
banaction = iptables-multiport
|
|
filter = sshd
|
|
backend = auto
|
|
maxretry = 5
|
|
findtime = 600
|
|
bantime = 86400
|
|
|
|
[recidive]
|
|
enabled = true
|
|
filter = recidive
|
|
logpath = /var/log/fail2ban.log
|
|
bantime = 604800
|
|
findtime = 86400
|
|
maxretry = 5
|
|
EOF`,
|
|
"systemctl enable fail2ban",
|
|
"dpkg-reconfigure -f noninteractive unattended-upgrades",
|
|
`cat > /etc/apt/apt.conf.d/20auto-upgrades <<EOF
|
|
APT::Periodic::Update-Package-Lists "1";
|
|
APT::Periodic::Unattended-Upgrade "1";
|
|
EOF`,
|
|
`cat > /etc/apt/apt.conf.d/50unattended-upgrades <<EOF
|
|
Unattended-Upgrade::Origins-Pattern {
|
|
"origin=Debian,codename=\${distro_codename},label=Debian-Security";
|
|
"origin=Ubuntu,codename=\${distro_codename},label=Ubuntu";
|
|
};
|
|
Unattended-Upgrade::Automatic-Reboot "true";
|
|
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
|
EOF`,
|
|
"ufw allow OpenSSH",
|
|
"ufw allow 80/tcp",
|
|
"ufw allow 443/tcp",
|
|
"ufw --force enable",
|
|
"getent group admin >/dev/null 2>&1 || groupadd admin",
|
|
fmt.Sprintf("usermod -a -G admin %s 2>/dev/null || true", username),
|
|
fmt.Sprintf("echo '%s ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/%s", username, username),
|
|
"chmod 440 /etc/sudoers.d/*",
|
|
"echo '%admin ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/admin-group",
|
|
"chmod 440 /etc/sudoers.d/admin-group",
|
|
fmt.Sprintf("mkdir -p /home/%s/.ssh", username),
|
|
fmt.Sprintf("chmod 700 /home/%s/.ssh", username),
|
|
fmt.Sprintf("chown -R %s:%s /home/%s/.ssh", username, username, username),
|
|
fmt.Sprintf("cat > /tmp/ssh_key_temp <<'KEYEOF'\n%s\nKEYEOF", strings.TrimSpace(sshPublicKey)),
|
|
fmt.Sprintf("mv /tmp/ssh_key_temp /home/%s/.ssh/authorized_keys", username),
|
|
fmt.Sprintf("chmod 600 /home/%s/.ssh/authorized_keys", username),
|
|
fmt.Sprintf("chown %s:%s /home/%s/.ssh/authorized_keys", username, username, username),
|
|
"cloud-init-per once ssh-keygen -A",
|
|
"sed -i -e '/^#*PubkeyAuthentication/s/^.*$/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
|
"sed -i -e '/^#*PasswordAuthentication/s/^.*$/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
|
"sed -i -e '/^#*PermitRootLogin/s/^.*$/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
|
"sed -i -e '/^#*AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\\/authorized_keys/' /etc/ssh/sshd_config",
|
|
"sed -i -e '/^#*StrictModes/s/^.*$/StrictModes yes/' /etc/ssh/sshd_config",
|
|
"sed -i '/^AllowUsers/d' /etc/ssh/sshd_config",
|
|
"systemctl restart sshd",
|
|
fmt.Sprintf("echo 'testuser:%s' | chpasswd", testuserPassword),
|
|
"mkdir -p /home/testuser",
|
|
"chown testuser:testuser /home/testuser",
|
|
fmt.Sprintf("(echo '%s'; echo '%s') | smbpasswd -a -s testuser", testuserPassword, testuserPassword),
|
|
"smbpasswd -e testuser",
|
|
`cat >> /etc/samba/smb.conf <<'SMBEOF'
|
|
|
|
[testuser]
|
|
comment = Test user share for demonstration
|
|
path = /home/testuser
|
|
browseable = yes
|
|
read only = no
|
|
valid users = testuser
|
|
create mask = 0644
|
|
directory mask = 0755
|
|
SMBEOF`,
|
|
"sed -i 's/; interfaces = 127.0.0.0\\/8/ interfaces = 127.0.0.1/' /etc/samba/smb.conf",
|
|
"sed -i 's/; bind interfaces only = yes/ bind interfaces only = yes/' /etc/samba/smb.conf",
|
|
"sed -i 's/^ interfaces = 127.0.0.0\\/8/ interfaces = 127.0.0.1/' /etc/samba/smb.conf || true",
|
|
"sed -i 's/^ bind interfaces only = no/ bind interfaces only = yes/' /etc/samba/smb.conf || true",
|
|
"systemctl enable smbd",
|
|
"systemctl restart smbd",
|
|
"mkdir -p /var/www/demo",
|
|
"chown -R www-data:www-data /var/www/demo 2>/dev/null || chown -R nginx:nginx /var/www/demo 2>/dev/null || true",
|
|
"test -f /var/www/demo/index.html || { cat > /var/www/demo/index.html <<'HTML'",
|
|
"<!DOCTYPE html><html><head><title>Demo</title><meta charset=utf-8><style>body{font-family:Arial;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white}.container{text-align:center;padding:2rem;background:rgba(255,255,255,0.1);border-radius:10px;backdrop-filter:blur(10px)}h1{margin:0;font-size:3rem}</style></head><body><div class=container><h1>Demo app page</h1></div></body></html>",
|
|
"HTML",
|
|
"chown www-data:www-data /var/www/demo/index.html 2>/dev/null || chown nginx:nginx /var/www/demo/index.html 2>/dev/null || true; }",
|
|
"rm -f /usr/share/nginx/html/index.html /var/www/html/index.html /usr/share/nginx/html/*.html 2>/dev/null || true",
|
|
"echo '=== Ensuring SSL certificates exist ==='",
|
|
`bash -c 'if [ ! -f /etc/ssl/certs/ssl-cert-snakeoil.pem ] || [ ! -f /etc/ssl/private/ssl-cert-snakeoil.key ]; then
|
|
echo "Generating self-signed SSL certificates..."
|
|
if command -v make-ssl-cert >/dev/null 2>&1; then
|
|
make-ssl-cert generate-default-snakeoil --force-overwrite 2>&1 || {
|
|
echo "make-ssl-cert failed, using openssl fallback..."
|
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/ssl-cert-snakeoil.key -out /etc/ssl/certs/ssl-cert-snakeoil.pem -subj "/CN=localhost" 2>&1
|
|
}
|
|
else
|
|
echo "make-ssl-cert not available, using openssl..."
|
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/ssl-cert-snakeoil.key -out /etc/ssl/certs/ssl-cert-snakeoil.pem -subj "/CN=localhost" 2>&1
|
|
fi
|
|
groupadd -f ssl-cert 2>/dev/null || true
|
|
chmod 644 /etc/ssl/certs/ssl-cert-snakeoil.pem 2>/dev/null || true
|
|
chmod 640 /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || true
|
|
chown root:ssl-cert /etc/ssl/certs/ssl-cert-snakeoil.pem /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || true
|
|
usermod -a -G ssl-cert www-data 2>/dev/null || usermod -a -G ssl-cert nginx 2>/dev/null || true
|
|
echo "SSL certificates generated successfully"
|
|
if [ ! -f /etc/ssl/certs/ssl-cert-snakeoil.pem ] || [ ! -f /etc/ssl/private/ssl-cert-snakeoil.key ]; then
|
|
echo "ERROR: SSL certificates still missing after generation attempt!"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "SSL certificates already exist"
|
|
fi'`,
|
|
"echo '=== Preventing nginx from auto-starting during package install ==='",
|
|
"systemctl mask nginx 2>/dev/null || true",
|
|
"systemctl stop nginx 2>/dev/null || true",
|
|
"echo '=== Creating nginx configuration files ==='",
|
|
"mkdir -p /etc/nginx/sites-available",
|
|
"test -f /etc/nginx/sites-available/sslh-proxy || { cat > /etc/nginx/sites-available/sslh-proxy <<'EOF'",
|
|
"server {",
|
|
" listen 127.0.0.1:8444 ssl http2 default_server;",
|
|
" server_name _;",
|
|
" ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;",
|
|
" ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;",
|
|
" ssl_protocols TLSv1.2 TLSv1.3;",
|
|
" ssl_ciphers HIGH:!aNULL:!MD5;",
|
|
" root /var/www/demo;",
|
|
" index index.html;",
|
|
" location / {",
|
|
" try_files $uri $uri/ =404;",
|
|
" }",
|
|
"}",
|
|
"EOF",
|
|
"chmod 644 /etc/nginx/sites-available/sslh-proxy; }",
|
|
"test -f /etc/nginx/sites-available/acme-challenge || { cat > /etc/nginx/sites-available/acme-challenge <<'EOF'",
|
|
"server {",
|
|
" listen 0.0.0.0:80 default_server;",
|
|
" listen [::]:80 default_server;",
|
|
" server_name _;",
|
|
" location /.well-known/acme-challenge/ {",
|
|
" root /var/www/html;",
|
|
" default_type text/plain;",
|
|
" access_log off;",
|
|
" }",
|
|
" location / {",
|
|
" root /var/www/demo;",
|
|
" try_files $uri $uri/ /index.html;",
|
|
" }",
|
|
"}",
|
|
"EOF",
|
|
"chmod 644 /etc/nginx/sites-available/acme-challenge; }",
|
|
"test -f /etc/nginx/sites-available/sslh-proxy || exit 1",
|
|
"test -f /etc/nginx/sites-available/acme-challenge || exit 1",
|
|
"echo '=== Removing ALL default nginx configs BEFORE enabling our sites ==='",
|
|
"rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-enabled/default.conf /etc/nginx/sites-enabled/000-default /etc/nginx/sites-enabled/000-default.conf 2>/dev/null || true",
|
|
"rm -f /etc/nginx/conf.d/default.conf 2>/dev/null || true",
|
|
"rm -f /usr/share/nginx/html/index.html /var/www/html/index.html 2>/dev/null || true",
|
|
"echo '=== Enabling our nginx sites ==='",
|
|
"ln -sf /etc/nginx/sites-available/acme-challenge /etc/nginx/sites-enabled/acme-challenge",
|
|
"ln -sf /etc/nginx/sites-available/sslh-proxy /etc/nginx/sites-enabled/sslh-proxy",
|
|
"echo '=== Verifying nginx config is valid ==='",
|
|
"nginx -t || exit 1",
|
|
"echo '=== Unmasking and starting nginx with correct config ==='",
|
|
"systemctl unmask nginx",
|
|
"systemctl enable nginx",
|
|
"rm -f /etc/sslh/sslh.cfg /etc/default/sslh 2>/dev/null || true",
|
|
"echo '=== Checking SSLH version (must be >= 2.2.4 for security fixes) ==='",
|
|
"SSLH_VERSION=$(sslh -V 2>&1 | grep -oP 'version \\K[0-9.]+' || sslh -V 2>&1 | grep -oE '[0-9]+\\.[0-9]+\\.[0-9]+' | head -1 || echo '0.0.0')",
|
|
"echo \"Installed SSLH version: $SSLH_VERSION\"",
|
|
"if [ -n \"$SSLH_VERSION\" ] && [ \"$SSLH_VERSION\" != '0.0.0' ]; then",
|
|
" if [ \"$(printf '%s\\n' '2.2.4' '$SSLH_VERSION' | sort -V | head -n1)\" != '2.2.4' ]; then",
|
|
" echo 'WARNING: SSLH version < 2.2.4 detected. This version has known DoS vulnerabilities (CVE-2025-46807, CVE-2025-46806).'",
|
|
" echo 'Consider building from source or using a PPA with patched version.'",
|
|
" echo 'See: https://security.opensuse.org/2025/06/13/sslh-denial-of-service-vulnerabilities.html'",
|
|
" else",
|
|
" echo 'SSLH version is >= 2.2.4 (security fixes included)'",
|
|
" fi",
|
|
"fi",
|
|
"mkdir -p /etc/systemd/system/sslh.service.d",
|
|
"test -f /etc/systemd/system/sslh.service.d/override.conf || { cat > /etc/systemd/system/sslh.service.d/override.conf <<'EOF'",
|
|
"[Service]",
|
|
"EnvironmentFile=",
|
|
"ExecStart=",
|
|
"ExecStart=/usr/sbin/sslh --foreground -F /etc/sslh.cfg",
|
|
"LimitNOFILE=4096",
|
|
"EOF",
|
|
"chmod 644 /etc/systemd/system/sslh.service.d/override.conf; }",
|
|
"echo '=== Configuring systemd service limits for SSLH (DoS protection) ==='",
|
|
"echo 'File descriptor limit set to 4096 via systemd LimitNOFILE'",
|
|
"systemctl daemon-reload",
|
|
"setcap 'cap_net_bind_service=+ep' /usr/sbin/sslh /usr/sbin/sslh-select 2>/dev/null || true",
|
|
"echo '=== Configuring systemd service limits for SSLH (DoS protection) ==='",
|
|
"mkdir -p /etc/systemd/system/sslh.service.d",
|
|
"cat >> /etc/systemd/system/sslh.service.d/override.conf <<'LIMITEOF'",
|
|
"LimitNOFILE=4096",
|
|
"LIMITEOF",
|
|
"systemctl daemon-reload",
|
|
"echo '=== Starting backend services ==='",
|
|
"systemctl enable sshd",
|
|
"systemctl restart sshd",
|
|
"if ! systemctl is-active --quiet sshd; then",
|
|
" echo 'ERROR: SSH service failed to start!'",
|
|
" systemctl status sshd --no-pager || true",
|
|
" exit 1",
|
|
"fi",
|
|
"systemctl start nginx",
|
|
"if ! systemctl is-active --quiet nginx; then",
|
|
" echo 'ERROR: Nginx service failed to start!'",
|
|
" systemctl status nginx --no-pager || true",
|
|
" nginx -t || true",
|
|
" echo 'Checking enabled sites:'",
|
|
" ls -la /etc/nginx/sites-enabled/ || true",
|
|
" echo 'Checking for default configs:'",
|
|
" [ -f /etc/nginx/sites-enabled/default ] && echo 'ERROR: default still enabled!' || echo 'default removed (good)'",
|
|
" [ -f /etc/nginx/conf.d/default.conf ] && echo 'ERROR: conf.d/default.conf exists!' || echo 'conf.d/default.conf removed (good)'",
|
|
" exit 1",
|
|
"fi",
|
|
"sleep 3",
|
|
"echo '=== Verifying nginx is listening on port 8444 ==='",
|
|
"for i in 1 2 3 4 5; do",
|
|
" if ss -tlnp | grep -q ':8444 '; then",
|
|
" echo 'Nginx is listening on port 8444'",
|
|
" break",
|
|
" fi",
|
|
" echo 'Waiting for nginx to listen on 8444... (attempt $i/5)'",
|
|
" sleep 2",
|
|
" if [ $i -eq 5 ]; then",
|
|
" echo 'ERROR: Nginx failed to listen on port 8444 after multiple attempts!'",
|
|
" systemctl status nginx --no-pager || true",
|
|
" nginx -t || true",
|
|
" ss -tlnp | grep nginx || true",
|
|
" exit 1",
|
|
" fi",
|
|
"done",
|
|
"echo '=== Verifying HTTP serves demo page (not default nginx) ==='",
|
|
"HTTP_VERIFIED=false",
|
|
"for i in 1 2 3 4 5; do",
|
|
" HTTP_CONTENT=$(curl -s http://127.0.0.1:80/ 2>/dev/null)",
|
|
" if echo \"$HTTP_CONTENT\" | grep -q 'Demo app page'; then",
|
|
" echo 'SUCCESS: HTTP serves demo page'",
|
|
" HTTP_VERIFIED=true",
|
|
" break",
|
|
" elif echo \"$HTTP_CONTENT\" | grep -qi 'Welcome to nginx'; then",
|
|
" echo 'ERROR: HTTP still serving default nginx page!'",
|
|
" echo 'Enabled sites:'",
|
|
" ls -la /etc/nginx/sites-enabled/ || true",
|
|
" [ -f /etc/nginx/sites-enabled/default ] && echo 'ERROR: default still enabled!' || echo 'default removed'",
|
|
" [ -f /etc/nginx/conf.d/default.conf ] && echo 'ERROR: conf.d/default.conf exists!' || echo 'conf.d/default.conf removed'",
|
|
" exit 1",
|
|
" fi",
|
|
" sleep 2",
|
|
"done",
|
|
"[ \"$HTTP_VERIFIED\" = false ] && { echo 'ERROR: HTTP demo page verification failed!'; exit 1; }",
|
|
"echo '=== Verifying HTTPS serves demo page ==='",
|
|
"HTTPS_VERIFIED=false",
|
|
"for i in 1 2 3 4 5; do",
|
|
" HTTPS_CONTENT=$(curl -k -s https://127.0.0.1:8444/ 2>/dev/null)",
|
|
" if echo \"$HTTPS_CONTENT\" | grep -q 'Demo app page'; then",
|
|
" echo 'SUCCESS: HTTPS serves demo page'",
|
|
" HTTPS_VERIFIED=true",
|
|
" break",
|
|
" elif echo \"$HTTPS_CONTENT\" | grep -qi 'Welcome to nginx'; then",
|
|
" echo 'ERROR: HTTPS still serving default nginx page!'",
|
|
" exit 1",
|
|
" fi",
|
|
" sleep 2",
|
|
"done",
|
|
"[ \"$HTTPS_VERIFIED\" = false ] && { echo 'ERROR: HTTPS demo page verification failed!'; exit 1; }",
|
|
"echo '=== Obtaining Let's Encrypt certificates (if email provided) ==='",
|
|
func() string {
|
|
if letsencryptEmail == "" {
|
|
return "echo 'No Let's Encrypt email provided, skipping certificate generation'"
|
|
}
|
|
|
|
// Build domain list for certbot - include root domain and ALL subdomains
|
|
// This ensures certificates cover all services (SSH, SMB, LDAP, etc.)
|
|
domains := []string{domain} // Always include root domain
|
|
domainSet := make(map[string]bool)
|
|
domainSet[domain] = true
|
|
|
|
// Add all subdomains from services
|
|
for _, svc := range svcList {
|
|
if svc.Subdomain != "" {
|
|
fqdn := svc.Subdomain + "." + domain
|
|
if !domainSet[fqdn] {
|
|
domains = append(domains, fqdn)
|
|
domainSet[fqdn] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build certbot command with all domains
|
|
certbotDomains := ""
|
|
domainList := ""
|
|
for i, d := range domains {
|
|
if i > 0 {
|
|
certbotDomains += " "
|
|
domainList += ", "
|
|
}
|
|
certbotDomains += "-d " + d
|
|
domainList += d
|
|
}
|
|
|
|
// Debug: Show which domains will be included
|
|
domainDebugList := ""
|
|
for i, d := range domains {
|
|
if i > 0 {
|
|
domainDebugList += ", "
|
|
}
|
|
domainDebugList += d
|
|
}
|
|
|
|
return fmt.Sprintf(`if [ -n '%s' ]; then
|
|
echo '=== Obtaining Let's Encrypt certificate ==='
|
|
echo 'Domains to include in certificate: %s'
|
|
echo 'Certbot command will be: certbot certonly --webroot -d %s'
|
|
echo 'Verifying nginx is running before certbot...'
|
|
systemctl is-active --quiet nginx || systemctl start nginx || exit 1
|
|
sleep 2
|
|
mkdir -p /var/www/html/.well-known/acme-challenge
|
|
chown -R www-data:www-data /var/www/html/.well-known 2>/dev/null || chown -R nginx:nginx /var/www/html/.well-known 2>/dev/null || true
|
|
chmod -R 755 /var/www/html/.well-known 2>/dev/null || true
|
|
nginx -t || exit 1
|
|
echo 'Waiting for DNS propagation (30 seconds)...'
|
|
sleep 30
|
|
echo 'Running certbot for ALL domains: %s'
|
|
echo 'Command: certbot certonly --webroot -n --agree-tos -m '%s' %s -w /var/www/html'
|
|
sudo certbot certonly --webroot -n --agree-tos -m '%s' %s -w /var/www/html --keep-until-expiring 2>&1 | tee /tmp/certbot-output.log
|
|
CERTBOT_EXIT=$?
|
|
echo 'Certbot exit code: $CERTBOT_EXIT'
|
|
if [ $CERTBOT_EXIT -eq 0 ]; then
|
|
CERT_DIR=$(sudo certbot certificates 2>/dev/null | grep -A 5 'Certificate Name:' | grep 'Certificate Path:' | head -1 | awk '{print $3}' | xargs dirname 2>/dev/null || echo '')
|
|
if [ -z "$CERT_DIR" ]; then
|
|
CERT_DIR="/etc/letsencrypt/live/%s"
|
|
fi
|
|
echo "Certificate directory: $CERT_DIR"
|
|
if [ -f "$CERT_DIR/fullchain.pem" ] || [ -f /etc/letsencrypt/live/%s/fullchain.pem ]; then
|
|
CERT_PATH="$CERT_DIR/fullchain.pem"
|
|
KEY_PATH="$CERT_DIR/privkey.pem"
|
|
if [ ! -f "$CERT_PATH" ]; then
|
|
CERT_PATH="/etc/letsencrypt/live/%s/fullchain.pem"
|
|
KEY_PATH="/etc/letsencrypt/live/%s/privkey.pem"
|
|
fi
|
|
echo "Using certificate: $CERT_PATH"
|
|
echo "Using key: $KEY_PATH"
|
|
cat > /tmp/sslh-proxy-letsencrypt.conf <<EOF
|
|
server {
|
|
listen 127.0.0.1:8444 ssl http2 default_server;
|
|
server_name _;
|
|
ssl_certificate $CERT_PATH;
|
|
ssl_certificate_key $KEY_PATH;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
root /var/www/demo;
|
|
index index.html;
|
|
location / {
|
|
try_files $uri $uri/ =404;
|
|
}
|
|
}
|
|
EOF
|
|
mv /tmp/sslh-proxy-letsencrypt.conf /etc/nginx/sites-available/sslh-proxy
|
|
chmod 644 /etc/nginx/sites-available/sslh-proxy
|
|
chmod 644 "$CERT_PATH" 2>/dev/null || true
|
|
chmod 640 "$KEY_PATH" 2>/dev/null || true
|
|
chown root:root "$CERT_PATH" "$KEY_PATH" 2>/dev/null || true
|
|
usermod -a -G ssl-cert www-data 2>/dev/null || usermod -a -G ssl-cert nginx 2>/dev/null || true
|
|
chmod 755 "$(dirname "$CERT_PATH")" /etc/letsencrypt/live/ 2>/dev/null || true
|
|
echo 'Verifying certificate details...'
|
|
openssl x509 -in "$CERT_PATH" -noout -text 2>/dev/null | grep -A 1 'Subject Alternative Name' || true
|
|
nginx -t && systemctl reload nginx && sleep 2 || systemctl restart nginx
|
|
echo 'Let's Encrypt certificate installed successfully'
|
|
else
|
|
echo 'ERROR: Certificate file not found after certbot success!'
|
|
echo 'Checking certbot certificates:'
|
|
sudo certbot certificates 2>&1 | head -30
|
|
echo 'Checking Let's Encrypt directories:'
|
|
ls -la /etc/letsencrypt/live/ 2>/dev/null || echo 'No live certificates found'
|
|
fi
|
|
else
|
|
echo 'ERROR: Certbot failed!'
|
|
echo 'Certbot output:'
|
|
cat /tmp/certbot-output.log 2>/dev/null || echo 'No certbot output log found'
|
|
echo 'This may be due to DNS propagation delays or ACME challenge failures.'
|
|
echo 'Certificate will need to be obtained manually or retried later.'
|
|
fi
|
|
fi`, letsencryptEmail, domainDebugList, certbotDomains, domainDebugList, letsencryptEmail, letsencryptEmail, certbotDomains, domain, domain, domain, domain)
|
|
}(),
|
|
"echo '=== Waiting for all backend services to be ready ==='",
|
|
"for i in 1 2 3 4 5; do ss -tlnp | grep -q ':22 ' && ss -tlnp | grep -q ':8444 ' && ss -tlnp | grep -q ':445 ' && break || sleep 2; done",
|
|
"echo '=== Verifying nginx is ready for SSLH ==='",
|
|
"for i in 1 2 3 4 5; do",
|
|
" if ss -tlnp | grep ':8444 ' | grep -q nginx && curl -k -s https://127.0.0.1:8444/ 2>/dev/null | grep -q 'Demo app page'; then",
|
|
" echo 'Nginx is ready on 8444'",
|
|
" break",
|
|
" fi",
|
|
" if [ $i -eq 5 ]; then",
|
|
" echo 'ERROR: Nginx not ready on 8444!'",
|
|
" ss -tlnp | grep ':8444 ' || echo 'Port 8444 not listening'",
|
|
" systemctl status nginx --no-pager | head -10",
|
|
" exit 1",
|
|
" fi",
|
|
" sleep 2",
|
|
"done",
|
|
"echo '=== Verifying SSLH config file exists and is valid ==='",
|
|
"test -f /etc/sslh.cfg || { echo 'ERROR: SSLH config file missing!'; ls -la /etc/sslh* 2>/dev/null; exit 1; }",
|
|
"test -s /etc/sslh.cfg || { echo 'ERROR: SSLH config file is empty!'; exit 1; }",
|
|
"echo '=== SSLH Config File Size ==='",
|
|
"wc -l /etc/sslh.cfg",
|
|
"echo '=== SSLH Config File Contents (first 50 lines) ==='",
|
|
"head -50 /etc/sslh.cfg",
|
|
"echo '=== Validating SSLH config syntax ==='",
|
|
"sslh -F /etc/sslh.cfg -v 1 -f -t 2>&1 || { echo 'ERROR: SSLH config validation failed!'; echo 'Full config:'; cat /etc/sslh.cfg; exit 1; }",
|
|
"echo '=== Verifying TLS route in SSLH config ==='",
|
|
"grep -q 'name: \"tls\"' /etc/sslh.cfg || { echo 'ERROR: TLS route not found in SSLH config!'; exit 1; }",
|
|
"TLS_ROUTE_COUNT=$(grep -c 'name: \"tls\"' /etc/sslh.cfg || echo '0')",
|
|
"echo \"Found $TLS_ROUTE_COUNT TLS route(s) in config\"",
|
|
"grep -A 10 'name: \"tls\"' /etc/sslh.cfg | grep -q 'port: \"8444\"' || { echo 'ERROR: TLS route not pointing to port 8444!'; echo 'TLS routes found:'; grep -A 10 'name: \"tls\"' /etc/sslh.cfg; exit 1; }",
|
|
"echo '=== Verifying protocol order (TLS before SSH) ==='",
|
|
"TLS_LINE=$(grep -n 'name: \"tls\"' /etc/sslh.cfg | head -1 | cut -d: -f1)",
|
|
"SSH_LINE=$(grep -n 'name: \"ssh\"' /etc/sslh.cfg | head -1 | cut -d: -f1)",
|
|
"if [ -n \"$TLS_LINE\" ] && [ -n \"$SSH_LINE\" ] && [ \"$TLS_LINE\" -lt \"$SSH_LINE\" ]; then",
|
|
" echo 'Protocol order correct: TLS before SSH'",
|
|
"else",
|
|
" echo 'WARNING: Protocol order may be incorrect. TLS should come before SSH.'",
|
|
"fi",
|
|
"echo '=== Starting SSLH service ==='",
|
|
"systemctl stop sslh 2>/dev/null || true",
|
|
"pkill -9 sslh sslh-select 2>/dev/null || true",
|
|
"ss -tlnp | grep -q ':443 ' && fuser -k 443/tcp 2>/dev/null || true",
|
|
"sleep 2",
|
|
"systemctl daemon-reload",
|
|
"systemctl enable sslh",
|
|
"systemctl restart sslh",
|
|
"sleep 5",
|
|
"systemctl is-active --quiet sslh || { echo 'ERROR: SSLH service failed to start!'; systemctl status sslh --no-pager; journalctl -u sslh -n 20 --no-pager; exit 1; }",
|
|
"ss -tlnp | grep -q ':443 ' || { echo 'ERROR: SSLH not listening on port 443!'; ss -tlnp | grep ':443 '; exit 1; }",
|
|
"echo '=== Verifying SSLH can reach nginx ==='",
|
|
"for i in 1 2 3 4 5; do",
|
|
" if timeout 2 bash -c '</dev/tcp/127.0.0.1/8444' 2>/dev/null; then",
|
|
" echo 'SSLH can reach nginx on 8444'",
|
|
" break",
|
|
" fi",
|
|
" if [ $i -eq 5 ]; then",
|
|
" echo 'ERROR: SSLH cannot reach nginx on 8444!'",
|
|
" ss -tlnp | grep ':8444 '",
|
|
" systemctl status nginx --no-pager | head -10",
|
|
" exit 1",
|
|
" fi",
|
|
" sleep 1",
|
|
"done",
|
|
"echo '=== Testing SSLH TLS routing ==='",
|
|
"echo 'Testing direct connection to SSLH on port 443...'",
|
|
"timeout 3 bash -c '</dev/tcp/127.0.0.1/443' 2>/dev/null && echo 'Port 443 is reachable' || echo 'WARNING: Port 443 not reachable'",
|
|
"TLS_ROUTING_WORKING=false",
|
|
"for i in 1 2 3 4 5; do",
|
|
" echo \"TLS routing test attempt $i/5...\"",
|
|
" TLS_TEST=$(timeout 10 bash -c 'echo | openssl s_client -connect 127.0.0.1:443 -servername chaosengineering.cc 2>&1' 2>/dev/null)",
|
|
" if echo \"$TLS_TEST\" | grep -q 'CONNECTED'; then",
|
|
" if echo \"$TLS_TEST\" | grep -q 'Verify return code: 0'; then",
|
|
" echo 'SSLH TLS routing test passed (certificate valid)'",
|
|
" elif echo \"$TLS_TEST\" | grep -q 'Verify return code:'; then",
|
|
" echo 'SSLH TLS routing test passed (certificate self-signed, but connection works)'",
|
|
" else",
|
|
" echo 'SSLH TLS routing test passed (connection established)'",
|
|
" fi",
|
|
" TLS_ROUTING_WORKING=true",
|
|
" break",
|
|
" fi",
|
|
" sleep 2",
|
|
"done",
|
|
"[ \"$TLS_ROUTING_WORKING\" = false ] && { echo 'ERROR: SSLH TLS routing test failed!'; echo 'Testing direct nginx connection:'; timeout 3 bash -c 'echo | openssl s_client -connect 127.0.0.1:8444 -servername localhost 2>&1' | head -20; echo 'SSLH config TLS route:'; grep -A 10 'name: \"tls\"' /etc/sslh.cfg | head -15; echo 'SSLH logs:'; journalctl -u sslh -n 30 --no-pager; exit 1; }",
|
|
"systemctl is-active --quiet nginx && ss -tlnp | grep -q ':8444 ' && systemctl is-active --quiet sslh && ss -tlnp | grep -q ':443 ' && curl -s http://127.0.0.1:80/ 2>/dev/null | grep -q 'Demo app page' && curl -k -s https://127.0.0.1:8444/ 2>/dev/null | grep -q 'Demo app page' && echo 'Deployment ready' || echo 'Some verifications failed'",
|
|
}
|
|
|
|
return commands
|
|
}
|
|
|
|
func generateNginxSSLHProxyConfig() string {
|
|
return `# Default server for root domain (HTTPS on port 443 via SSLH)
|
|
# Certbot will update this configuration with Let's Encrypt certificates if email is provided
|
|
server {
|
|
listen 127.0.0.1:8444 ssl http2 default_server;
|
|
server_name _;
|
|
|
|
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
|
|
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
|
|
|
root /var/www/demo;
|
|
index index.html;
|
|
|
|
location / {
|
|
try_files $uri $uri/ =404;
|
|
}
|
|
}
|
|
`
|
|
}
|
|
|
|
func generateNginxACMEConfig() string {
|
|
return `# HTTP server for Let's Encrypt ACME challenge
|
|
# This must be the default_server on port 80 to handle all HTTP requests
|
|
server {
|
|
listen 0.0.0.0:80 default_server;
|
|
listen [::]:80 default_server;
|
|
server_name _;
|
|
|
|
# Serve ACME challenge for Let's Encrypt
|
|
# Certbot webroot plugin writes files here for HTTP-01 validation
|
|
location /.well-known/acme-challenge/ {
|
|
root /var/www/html;
|
|
default_type "text/plain";
|
|
# Allow certbot to write files here
|
|
access_log off;
|
|
}
|
|
|
|
# For root domain, serve demo page on HTTP (before HTTPS redirect)
|
|
# This allows initial access while certificates are being obtained
|
|
location / {
|
|
root /var/www/demo;
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
}
|
|
`
|
|
}
|
|
|
|
func generateDemoPageHTML() string {
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Demo App Page</title>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100vh;
|
|
margin: 0;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
}
|
|
.container {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 10px;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
h1 {
|
|
margin: 0;
|
|
font-size: 3rem;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Demo app page</h1>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`
|
|
}
|