Files
2026-01-29 00:03:02 +00:00

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>
`
}