mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 03:27:43 +09:00
feat(security): optional SSL/TLS hardening with Cloudflare-aware configs and HTTP fallback
- Add secure nginx config with TLS 1.2+, HSTS preload, CSP, and security headers - Add insecure HTTP fallback config for users who cannot or do not want HTTPS - Make installer best-effort SSL: try Let's Encrypt, fall back to HTTP without failing - Preserve secure/insecure SSL choice across updates via /etc/serverkit/ssl-mode - Support SERVERKIT_SKIP_SSL=1 to disable HTTPS entirely - Add CAA record to default DNS zone template - Harden generated per-app nginx configs with HSTS preload, CSP, and security headers - Align Flask security middleware headers with nginx edge config
This commit is contained in:
1 parent
1f700cea06
commit
02afdec9f0
10 files changed
+440
-77
No files matched your search
@@ -21,26 +21,32 @@ def register_security_headers(app: Flask):
|
||||
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||
|
||||
# Content Security Policy
|
||||
# In debug mode, allow inline styles/scripts for Vite dev tooling
|
||||
# In debug mode, allow inline styles/scripts for Vite dev tooling.
|
||||
# The production policy intentionally matches the nginx edge config so
|
||||
# responses served directly by Flask are consistent with Cloudflare/nginx.
|
||||
if app.debug:
|
||||
csp_directives = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: https:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'self' ws: wss: http://localhost:* http://127.0.0.1:*",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
]
|
||||
else:
|
||||
csp_directives = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self'",
|
||||
"style-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: https:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'self' ws: wss:",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
]
|
||||
response.headers['Content-Security-Policy'] = '; '.join(csp_directives)
|
||||
|
||||
@@ -48,7 +54,8 @@ def register_security_headers(app: Flask):
|
||||
response.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'
|
||||
|
||||
# HSTS - only in production and on HTTPS
|
||||
# 63072000 seconds = 2 years, matching the nginx edge config and preload requirements
|
||||
if not app.debug:
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=63072000; includeSubDomains; preload'
|
||||
|
||||
return response
|
||||
@@ -19,6 +19,9 @@ class DNSZoneService:
|
||||
{'record_type': 'A', 'name': '@', 'content': '{{server_ip}}', 'ttl': 3600},
|
||||
{'record_type': 'A', 'name': 'www', 'content': '{{server_ip}}', 'ttl': 3600},
|
||||
{'record_type': 'CNAME', 'name': 'mail', 'content': '{{domain}}', 'ttl': 3600},
|
||||
# Authorize only Let's Encrypt (what ServerKit issues with) to mint
|
||||
# certs for this domain. Satisfies CAA scanners and blocks rogue CAs.
|
||||
{'record_type': 'CAA', 'name': '@', 'content': '0 issue "letsencrypt.org"', 'ttl': 3600},
|
||||
],
|
||||
},
|
||||
'email-hosting': {
|
||||
|
||||
@@ -196,8 +196,21 @@ class NginxService:
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_ecdh_curve X25519:secp384r1;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
# HSTS: 2 years, all subdomains, preload-eligible. Submit the domain at
|
||||
# https://hstspreload.org to get onto the browser preload list (this is what
|
||||
# protects first-time visitors from an initial-request MITM downgrade).
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
|
||||
# Baseline response-header hardening. These are safe for reverse-proxied apps;
|
||||
# the CSP is deliberately permissive so it does not break managed apps while
|
||||
# still satisfying "a CSP is present" and blocking cross-origin framing.
|
||||
# Tighten the CSP per-app when the upstream can support a stricter policy.
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https:; frame-ancestors 'self'; upgrade-insecure-requests" always;
|
||||
'''
|
||||
|
||||
SSL_REDIRECT_TEMPLATE = '''server {{
|
||||
|
||||
+95
-56
@@ -9,6 +9,7 @@
|
||||
#
|
||||
# INSTALL_FROM_RELEASE=1 fetch a pre-built tarball instead of compiling
|
||||
# BUILD_FROM_SOURCE=1 force a source build even when a release exists
|
||||
# SERVERKIT_SKIP_SSL=1 run on plain HTTP (no HTTPS / no certbot attempt)
|
||||
#
|
||||
# The Flask backend runs straight on the host (it needs real system access);
|
||||
# the React frontend is built to static files and served by the host nginx.
|
||||
@@ -38,6 +39,7 @@ CHANNEL="${CHANNEL:-Stable}"
|
||||
|
||||
PANEL_DOMAIN="${PANEL_DOMAIN:-}"
|
||||
PANEL_PORT="${PANEL_PORT:-80}"
|
||||
SERVERKIT_SKIP_SSL="${SERVERKIT_SKIP_SSL:-0}"
|
||||
|
||||
# Runtime state populated as we go (declared up-front for `set -u`).
|
||||
OS_FAMILY="unknown"
|
||||
@@ -45,6 +47,7 @@ ARCH=""
|
||||
DL_ARCH=""
|
||||
PKG_MGR=""
|
||||
SAFE_MODE=false
|
||||
SSL_MODE="insecure"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Terminal styling
|
||||
@@ -510,9 +513,16 @@ write_config() {
|
||||
|
||||
# Widen CORS / advertise a public URL when a panel domain is known.
|
||||
local public_url="" cors_origins="http://localhost,https://localhost"
|
||||
local url_scheme="http"
|
||||
if [ -n "$PANEL_DOMAIN" ]; then
|
||||
public_url="http://$PANEL_DOMAIN"
|
||||
cors_origins="http://localhost,https://localhost,http://$PANEL_DOMAIN,https://$PANEL_DOMAIN"
|
||||
if [ "$SSL_MODE" = "secure" ]; then
|
||||
url_scheme="https"
|
||||
cors_origins="http://localhost,https://localhost,https://$PANEL_DOMAIN"
|
||||
else
|
||||
url_scheme="http"
|
||||
cors_origins="http://localhost,https://localhost,http://$PANEL_DOMAIN"
|
||||
fi
|
||||
public_url="${url_scheme}://$PANEL_DOMAIN"
|
||||
fi
|
||||
|
||||
cat > "$INSTALL_DIR/.env" <<EOF
|
||||
@@ -553,7 +563,7 @@ server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
if (\$host ~* ^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\$) {
|
||||
return 301 http://$PANEL_DOMAIN\$request_uri;
|
||||
return 301 ${url_scheme}://$PANEL_DOMAIN\$request_uri;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
@@ -561,25 +571,6 @@ EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Self-signed certificate placeholder
|
||||
# ---------------------------------------------------------------------------
|
||||
seed_self_signed_cert() {
|
||||
phase "SSL Certificate"
|
||||
|
||||
if [ -f "$INSTALL_DIR/nginx/ssl/fullchain.pem" ]; then
|
||||
good "SSL certificate already present."
|
||||
return
|
||||
fi
|
||||
|
||||
step "Creating a self-signed certificate..."
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout "$INSTALL_DIR/nginx/ssl/privkey.pem" \
|
||||
-out "$INSTALL_DIR/nginx/ssl/fullchain.pem" \
|
||||
-subj "/CN=localhost" 2>/dev/null
|
||||
warn "Self-signed cert in place — swap in a real certificate for production."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Frontend build (skipped on release installs)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -618,7 +609,7 @@ configure_nginx() {
|
||||
systemctl stop nginx 2>/dev/null || true
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled /etc/nginx/serverkit-conf.d
|
||||
mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled /etc/nginx/serverkit-conf.d /var/www/certbot
|
||||
|
||||
# Fedora/RHEL nginx.conf doesn't include sites-enabled by default.
|
||||
if ! grep -q "sites-enabled" /etc/nginx/nginx.conf; then
|
||||
@@ -637,15 +628,78 @@ configure_nginx() {
|
||||
done
|
||||
|
||||
cp "$INSTALL_DIR/nginx/sites-available/serverkit.conf" /etc/nginx/sites-available/
|
||||
ln -sf /etc/nginx/sites-available/serverkit.conf /etc/nginx/sites-enabled/
|
||||
cp "$INSTALL_DIR/nginx/sites-available/serverkit-insecure.conf" /etc/nginx/sites-available/
|
||||
cp "$INSTALL_DIR/nginx/sites-available/example.conf.template" /etc/nginx/sites-available/
|
||||
|
||||
# Decide whether we can use HTTPS. SSL is best-effort: if certbot fails or
|
||||
# no domain is given, we fall back to plain HTTP rather than forcing the
|
||||
# user to fix DNS/certs before they can use ServerKit.
|
||||
choose_ssl_mode
|
||||
install_nginx_config_for_mode
|
||||
|
||||
# SELinux: let nginx make upstream connections to the app containers.
|
||||
if { [ "$OS_FAMILY" = "fedora" ] || [ "$OS_FAMILY" = "rhel" ]; } && command -v setsebool &>/dev/null; then
|
||||
setsebool -P httpd_can_network_connect 1 2>/dev/null || true
|
||||
fi
|
||||
|
||||
good "Nginx configured."
|
||||
good "Nginx configured ($SSL_MODE)."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Choose secure or insecure mode. Never block the install because of SSL.
|
||||
# ---------------------------------------------------------------------------
|
||||
choose_ssl_mode() {
|
||||
if [ "$SERVERKIT_SKIP_SSL" = "1" ]; then
|
||||
SSL_MODE="insecure"
|
||||
warn "SERVERKIT_SKIP_SSL=1 — using plain HTTP."
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -z "$PANEL_DOMAIN" ]; then
|
||||
SSL_MODE="insecure"
|
||||
warn "No panel domain provided — using plain HTTP (access by IP)."
|
||||
warn "Set PANEL_DOMAIN and re-run the installer to try HTTPS automatically."
|
||||
return
|
||||
fi
|
||||
|
||||
if ! command -v certbot &>/dev/null; then
|
||||
SSL_MODE="insecure"
|
||||
warn "Certbot not installed — cannot request SSL automatically."
|
||||
warn "Continuing with plain HTTP. Install certbot and run: certbot --nginx -d $PANEL_DOMAIN"
|
||||
return
|
||||
fi
|
||||
|
||||
# Temporarily use the insecure config so certbot's webroot challenge can
|
||||
# answer on port 80.
|
||||
ln -sf /etc/nginx/sites-available/serverkit-insecure.conf /etc/nginx/sites-enabled/serverkit.conf
|
||||
step "Attempting Let's Encrypt certificate for $PANEL_DOMAIN..."
|
||||
if systemctl start nginx 2>/dev/null && \
|
||||
certbot certonly --webroot -w /var/www/certbot -d "$PANEL_DOMAIN" \
|
||||
--non-interactive --agree-tos --register-unsafely-without-email 2>/dev/null; then
|
||||
SSL_MODE="secure"
|
||||
good "Let's Encrypt certificate issued for $PANEL_DOMAIN."
|
||||
else
|
||||
SSL_MODE="insecure"
|
||||
warn "Could not obtain SSL certificate for $PANEL_DOMAIN."
|
||||
warn "Common causes: DNS not pointing to this server, port 80 not reachable, or rate limits."
|
||||
warn "Continuing with plain HTTP. Retry later with: certbot --nginx -d $PANEL_DOMAIN"
|
||||
fi
|
||||
systemctl stop nginx 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Install the nginx config matching the chosen SSL mode.
|
||||
# ---------------------------------------------------------------------------
|
||||
install_nginx_config_for_mode() {
|
||||
if [ "$SSL_MODE" = "secure" ]; then
|
||||
sed -i "s|/etc/letsencrypt/live/YOUR_DOMAIN/|/etc/letsencrypt/live/$PANEL_DOMAIN/|g" \
|
||||
/etc/nginx/sites-available/serverkit.conf
|
||||
ln -sf /etc/nginx/sites-available/serverkit.conf /etc/nginx/sites-enabled/serverkit.conf
|
||||
else
|
||||
ln -sf /etc/nginx/sites-available/serverkit-insecure.conf /etc/nginx/sites-enabled/serverkit.conf
|
||||
fi
|
||||
mkdir -p /etc/serverkit
|
||||
printf '%s\n' "$SSL_MODE" > /etc/serverkit/ssl-mode
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -744,31 +798,6 @@ await_health() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Let's Encrypt for the panel domain
|
||||
# ---------------------------------------------------------------------------
|
||||
issue_panel_cert() {
|
||||
if [ -z "$PANEL_DOMAIN" ]; then
|
||||
good "No domain set — skipping SSL (panel reachable by IP)."
|
||||
return
|
||||
fi
|
||||
|
||||
phase "Panel SSL Certificate"
|
||||
|
||||
if ! command -v certbot &>/dev/null; then
|
||||
warn "Certbot is not installed — skipping SSL."
|
||||
return
|
||||
fi
|
||||
|
||||
step "Requesting a Let's Encrypt certificate for $PANEL_DOMAIN..."
|
||||
if certbot --nginx -d "$PANEL_DOMAIN" --non-interactive --agree-tos \
|
||||
--register-unsafely-without-email --redirect 2>/dev/null; then
|
||||
good "Certificate issued for $PANEL_DOMAIN"
|
||||
else
|
||||
warn "Certbot failed — retry manually: certbot --nginx -d $PANEL_DOMAIN"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Safety copy of an existing install before we touch it
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -794,11 +823,22 @@ print_outro() {
|
||||
printf ' %s%s%s\n\n' "$V4" "──────────────────────────────────────" "$RST"
|
||||
|
||||
if [ -n "$PANEL_DOMAIN" ]; then
|
||||
printf ' %sPanel URL%s https://%s\n' "$BLD" "$RST" "$PANEL_DOMAIN"
|
||||
if [ "$SSL_MODE" = "secure" ]; then
|
||||
printf ' %sPanel URL%s https://%s\n' "$BLD" "$RST" "$PANEL_DOMAIN"
|
||||
else
|
||||
printf ' %sPanel URL%s http://%s\n' "$BLD" "$RST" "$PANEL_DOMAIN"
|
||||
fi
|
||||
else
|
||||
printf ' %sPanel URL%s http://%s\n' "$BLD" "$RST" "$ip"
|
||||
fi
|
||||
printf ' %sFirst step%s create an admin user\n\n' "$BLD" "$RST"
|
||||
|
||||
if [ "$SSL_MODE" != "secure" ]; then
|
||||
printf '\n %sWARNING%s Running without HTTPS. Passwords and tokens will be\n' "$BLD" "$RST"
|
||||
printf ' transmitted unencrypted. Set PANEL_DOMAIN and run\n'
|
||||
printf ' certbot, or set SERVERKIT_SKIP_SSL=1 to suppress this warning.\n'
|
||||
fi
|
||||
|
||||
printf '\n %sFirst step%s create an admin user\n\n' "$BLD" "$RST"
|
||||
|
||||
printf ' %sCLI%s serverkit status\n' "$BLD" "$RST"
|
||||
printf ' serverkit create-admin\n'
|
||||
@@ -831,8 +871,9 @@ prompt_for_domain() {
|
||||
[ -z "$PANEL_DOMAIN" ] && [ -t 0 ] || return 0
|
||||
printf '\n'
|
||||
printf '%sEnter your panel domain (e.g. panel.example.com)%s\n' "$BLD" "$RST"
|
||||
printf 'Leave blank to access via IP\n'
|
||||
printf 'Leave blank to access via IP (no SSL attempt)\n'
|
||||
printf '%sTip:%s set PANEL_DOMAIN=... in the environment to skip this prompt\n' "$BLD" "$RST"
|
||||
printf ' set SERVERKIT_SKIP_SSL=1 to disable HTTPS entirely\n'
|
||||
printf '> '
|
||||
read -r PANEL_DOMAIN
|
||||
PANEL_DOMAIN=$(printf '%s' "$PANEL_DOMAIN" | tr -d ' ')
|
||||
@@ -883,17 +924,15 @@ main() {
|
||||
fi
|
||||
fi
|
||||
|
||||
write_config
|
||||
seed_self_signed_cert
|
||||
sync_templates
|
||||
configure_nginx
|
||||
write_config
|
||||
install_service
|
||||
|
||||
# Tolerate failures through start + health so rollback can run.
|
||||
set +e
|
||||
launch_services
|
||||
await_health || revert_install
|
||||
issue_panel_cert
|
||||
set -e
|
||||
|
||||
ping_telemetry
|
||||
|
||||
+14
-1
@@ -1,4 +1,11 @@
|
||||
# ServerKit Production Nginx Configuration
|
||||
#
|
||||
# This is the reference all-in-one nginx config. It assumes TLS terminates at
|
||||
# this server. If you use Cloudflare in front, prefer the split design in
|
||||
# nginx/sites-available/serverkit.conf, which supports Cloudflare Origin CA
|
||||
# certificates and documents the Cloudflare dashboard settings that make the
|
||||
# end-user experience painless (Always Use HTTPS, HSTS, Minimum TLS 1.2).
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
@@ -76,6 +83,7 @@ http {
|
||||
server_name _;
|
||||
|
||||
# SSL Configuration
|
||||
# For Cloudflare Origin CA, replace these paths with your origin cert/key.
|
||||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||
ssl_session_timeout 1d;
|
||||
@@ -87,6 +95,11 @@ http {
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# OCSP stapling (optional)
|
||||
# ssl_stapling on;
|
||||
# ssl_stapling_verify on;
|
||||
# ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
|
||||
|
||||
# HSTS
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
|
||||
@@ -98,7 +111,7 @@ http {
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
|
||||
# Content Security Policy
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' ws: wss:; frame-ancestors 'none';" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' ws: wss:; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
|
||||
|
||||
# Connection limits
|
||||
limit_conn conn_limit 20;
|
||||
|
||||
@@ -7,11 +7,59 @@
|
||||
# 3. Enable: sudo ln -s /etc/nginx/sites-available/mysite.conf /etc/nginx/sites-enabled/
|
||||
# 4. Test: sudo nginx -t
|
||||
# 5. Reload: sudo systemctl reload nginx
|
||||
#
|
||||
# This template assumes Cloudflare OR standalone Let's Encrypt. Pick the
|
||||
# certificate option below that matches your setup.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.builditdesign.com; # Change this to your domain
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name example.builditdesign.com; # Change this to your domain
|
||||
|
||||
# Pick ONE certificate option:
|
||||
|
||||
# Option A: Let's Encrypt
|
||||
ssl_certificate /etc/letsencrypt/live/example.builditdesign.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.builditdesign.com/privkey.pem;
|
||||
|
||||
# Option B: Cloudflare Origin CA (recommended with Cloudflare proxy + Full (strict))
|
||||
# ssl_certificate /etc/nginx/ssl/cloudflare-origin-example.pem;
|
||||
# ssl_certificate_key /etc/nginx/ssl/cloudflare-origin-example.key;
|
||||
|
||||
# Modern TLS
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# OCSP stapling (optional; uncomment if using Let's Encrypt and set chain path)
|
||||
# ssl_stapling on;
|
||||
# ssl_stapling_verify on;
|
||||
# ssl_trusted_certificate /etc/letsencrypt/live/example.builditdesign.com/chain.pem;
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' ws: wss:; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
|
||||
|
||||
# Option 1: Proxy to a local app (Docker container, Node.js, etc.)
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000; # Change port to your app's port
|
||||
@@ -20,6 +68,7 @@ server {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
@@ -43,4 +92,13 @@ server {
|
||||
# fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
# include fastcgi_params;
|
||||
# }
|
||||
|
||||
# Deny hidden/sensitive files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
|
||||
location ~* \.(env|git|gitignore|dockerignore)$ {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
# ServerKit - Insecure HTTP fallback configuration
|
||||
# /etc/nginx/sites-available/serverkit-insecure.conf
|
||||
#
|
||||
# ⚠️ WARNING: This configuration does NOT use SSL/TLS. All traffic between
|
||||
# browsers and this server is unencrypted. Passwords, tokens, and any
|
||||
# data sent to the panel or hosted apps can be read by anyone on the
|
||||
# network path.
|
||||
#
|
||||
# This mode exists only as a convenience for local networks, temporary
|
||||
# testing, or situations where you cannot obtain certificates. It is
|
||||
# strongly recommended to switch to the secure config as soon as possible.
|
||||
#
|
||||
# To switch to HTTPS later:
|
||||
# 1. Obtain certificates (Let's Encrypt or Cloudflare Origin CA).
|
||||
# 2. Replace this file with serverkit.conf:
|
||||
# sudo cp /opt/serverkit/nginx/sites-available/serverkit.conf \
|
||||
# /etc/nginx/sites-available/serverkit.conf
|
||||
# 3. Update the certificate paths inside that file.
|
||||
# 4. sudo nginx -t && sudo systemctl reload nginx
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3847;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
location /socket.io {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_connect_timeout 7d;
|
||||
proxy_send_timeout 7d;
|
||||
proxy_read_timeout 7d;
|
||||
}
|
||||
|
||||
include /etc/nginx/serverkit-locations/*.conf;
|
||||
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
|
||||
location ~* \.(env|git|gitignore|dockerignore)$ {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
include /etc/nginx/serverkit-conf.d/*.conf;
|
||||
@@ -2,7 +2,41 @@
|
||||
# /etc/nginx/sites-available/serverkit.conf
|
||||
#
|
||||
# This is the default server block for ServerKit.
|
||||
# Update server_name to your domain, or leave as _ for catch-all.
|
||||
# It works both WITH and WITHOUT Cloudflare in front of the server.
|
||||
#
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLOUDFLARE USERS (recommended for zero-downtime certs + DDoS protection)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 1. In Cloudflare dashboard → SSL/TLS → Overview:
|
||||
# Set Encryption mode to "Full (strict)". NEVER use "Flexible".
|
||||
# 2. Create an Origin CA certificate:
|
||||
# SSL/TLS → Origin Server → Create Certificate → choose RSA.
|
||||
# 3. Save the files on this server, for example:
|
||||
# /etc/nginx/ssl/cloudflare-origin.pem (certificate)
|
||||
# /etc/nginx/ssl/cloudflare-origin.key (private key)
|
||||
# 4. Uncomment the Cloudflare Origin CA paths below.
|
||||
# 5. Recommended Cloudflare dashboard toggles for painless UX:
|
||||
# - SSL/TLS → Edge Certificates → Always Use HTTPS: ON
|
||||
# - SSL/TLS → Edge Certificates → Automatic HTTPS Rewrites: ON
|
||||
# - SSL/TLS → Edge Certificates → HSTS: ON
|
||||
# (max-age 63072000, Include subdomains: ON, Preload: ON, No-Sniff: ON)
|
||||
# - SSL/TLS → Edge Certificates → Minimum TLS Version: TLS 1.2
|
||||
# 6. Add a CAA DNS record allowing Cloudflare's certificate authorities, e.g.:
|
||||
# example.com. CAA 0 issue "letsencrypt.org"
|
||||
# example.com. CAA 0 issue "pki.goog"
|
||||
# example.com. CAA 0 issue "digicert.com"
|
||||
# example.com. CAA 0 issuewild "letsencrypt.org"
|
||||
# example.com. CAA 0 iodef "mailto:security@example.com"
|
||||
#
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# STANDALONE USERS (no Cloudflare)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 1. Obtain Let's Encrypt certificates, e.g.:
|
||||
# certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com
|
||||
# 2. Set the Let's Encrypt paths below.
|
||||
# 3. Add a CAA record for your chosen CA, e.g.:
|
||||
# example.com. CAA 0 issue "letsencrypt.org"
|
||||
# example.com. CAA 0 issuewild "letsencrypt.org"
|
||||
#
|
||||
# Optional: place a canonical-domain redirect block in
|
||||
# /etc/nginx/serverkit-canonical-domain.conf (managed by install.sh / settings UI).
|
||||
@@ -11,10 +45,71 @@ server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
|
||||
# Redirect to HTTPS (handled by Cloudflare in Flexible mode)
|
||||
# If using Full SSL mode, uncomment the redirect below
|
||||
# return 301 https://$server_name$request_uri;
|
||||
# Allow Let's Encrypt validation over plain HTTP
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Redirect all other HTTP traffic to HTTPS
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2 default_server;
|
||||
server_name _;
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# TLS certificates - pick ONE option and comment the other out
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Option A: Let's Encrypt (standalone mode)
|
||||
ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;
|
||||
|
||||
# Option B: Cloudflare Origin CA (use with Cloudflare proxy + Full (strict))
|
||||
# ssl_certificate /etc/nginx/ssl/cloudflare-origin.pem;
|
||||
# ssl_certificate_key /etc/nginx/ssl/cloudflare-origin.key;
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Modern TLS configuration
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:50m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# OCSP stapling (optional; improves handshake privacy/performance)
|
||||
# For Let's Encrypt, uncomment all three lines and set the chain path.
|
||||
# Not applicable to Cloudflare Origin CA certificates.
|
||||
# ssl_stapling on;
|
||||
# ssl_stapling_verify on;
|
||||
# ssl_trusted_certificate /etc/letsencrypt/live/YOUR_DOMAIN/chain.pem;
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Security headers
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
|
||||
# Content Security Policy
|
||||
# Note: 'unsafe-inline'/'unsafe-eval' are kept because many SPAs need them.
|
||||
# Once your build is verified to work without inline scripts, remove them.
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' ws: wss:; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Proxy to ServerKit frontend
|
||||
# NOTE: If your frontend container runs on a different port, change 3847 below.
|
||||
# The reference nginx/nginx.conf uses 8080; this file preserves the legacy 3847
|
||||
# port to avoid breaking existing ServerKit installations.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3847;
|
||||
proxy_http_version 1.1;
|
||||
@@ -22,8 +117,12 @@ server {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Proxy to ServerKit backend API
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_http_version 1.1;
|
||||
@@ -31,11 +130,15 @@ server {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# WebSocket support for Socket.IO
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
location /socket.io {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_http_version 1.1;
|
||||
@@ -45,6 +148,7 @@ server {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_connect_timeout 7d;
|
||||
proxy_send_timeout 7d;
|
||||
proxy_read_timeout 7d;
|
||||
@@ -53,6 +157,15 @@ server {
|
||||
# Include app location blocks (WordPress, Gitea, private URLs, etc.)
|
||||
# These are location-only snippets, NOT separate server blocks.
|
||||
include /etc/nginx/serverkit-locations/*.conf;
|
||||
|
||||
# Deny access to hidden/sensitive files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
|
||||
location ~* \.(env|git|gitignore|dockerignore)$ {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
# Optional canonical-domain redirect block. Generated by install.sh when a
|
||||
|
||||
@@ -62,19 +62,30 @@ if ! grep -q "sites-enabled" /etc/nginx/nginx.conf; then
|
||||
sed -i '/^http {/a \ include /etc/nginx/sites-enabled/*;' /etc/nginx/nginx.conf
|
||||
fi
|
||||
|
||||
# Copy ServerKit site config
|
||||
# Copy ServerKit site configs
|
||||
SERVERKIT_DIR="/opt/serverkit"
|
||||
if [ -f "$SERVERKIT_DIR/nginx/sites-available/serverkit.conf" ]; then
|
||||
print_info "Installing ServerKit nginx config..."
|
||||
cp "$SERVERKIT_DIR/nginx/sites-available/serverkit.conf" /etc/nginx/sites-available/
|
||||
ln -sf /etc/nginx/sites-available/serverkit.conf /etc/nginx/sites-enabled/
|
||||
print_success "ServerKit config installed"
|
||||
SSL_MODE="insecure"
|
||||
if [ -f /etc/serverkit/ssl-mode ]; then
|
||||
SSL_MODE=$(cat /etc/serverkit/ssl-mode)
|
||||
fi
|
||||
if [ "${SERVERKIT_SKIP_SSL:-0}" = "1" ]; then
|
||||
SSL_MODE="insecure"
|
||||
fi
|
||||
|
||||
# Copy template
|
||||
if [ -f "$SERVERKIT_DIR/nginx/sites-available/example.conf.template" ]; then
|
||||
if [ -f "$SERVERKIT_DIR/nginx/sites-available/serverkit.conf" ]; then
|
||||
print_info "Installing ServerKit nginx configs..."
|
||||
cp "$SERVERKIT_DIR/nginx/sites-available/serverkit.conf" /etc/nginx/sites-available/
|
||||
cp "$SERVERKIT_DIR/nginx/sites-available/serverkit-insecure.conf" /etc/nginx/sites-available/
|
||||
cp "$SERVERKIT_DIR/nginx/sites-available/example.conf.template" /etc/nginx/sites-available/
|
||||
print_success "Template copied to /etc/nginx/sites-available/"
|
||||
|
||||
if [ "$SSL_MODE" = "secure" ]; then
|
||||
ln -sf /etc/nginx/sites-available/serverkit.conf /etc/nginx/sites-enabled/serverkit.conf
|
||||
print_success "ServerKit HTTPS config installed"
|
||||
else
|
||||
ln -sf /etc/nginx/sites-available/serverkit-insecure.conf /etc/nginx/sites-enabled/serverkit.conf
|
||||
print_warning "ServerKit HTTP (insecure) config installed"
|
||||
print_warning "Set SERVERKIT_SKIP_SSL=0 and run certbot to enable HTTPS later"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update ServerKit docker-compose to use port 8080
|
||||
@@ -104,7 +115,11 @@ echo -e "${GREEN} Setup Complete!${NC}"
|
||||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo "Architecture:"
|
||||
echo " • Host nginx (ports 80/443) - routes domains to apps"
|
||||
if [ "$SSL_MODE" = "secure" ]; then
|
||||
echo " • Host nginx (ports 80/443) - routes domains to apps"
|
||||
else
|
||||
echo " • Host nginx (port 80) - routes domains to apps (HTTPS not enabled)"
|
||||
fi
|
||||
echo " • ServerKit frontend (port 8080) - UI"
|
||||
echo " • ServerKit backend (port 5000) - API"
|
||||
echo ""
|
||||
|
||||
@@ -252,6 +252,30 @@ phase "Refreshing Configuration"
|
||||
if [ -f "$INSTALL_DIR/nginx/sites-available/serverkit.conf" ]; then
|
||||
cp "$INSTALL_DIR/nginx/sites-available/serverkit.conf" /etc/nginx/sites-available/
|
||||
fi
|
||||
if [ -f "$INSTALL_DIR/nginx/sites-available/serverkit-insecure.conf" ]; then
|
||||
cp "$INSTALL_DIR/nginx/sites-available/serverkit-insecure.conf" /etc/nginx/sites-available/
|
||||
fi
|
||||
|
||||
# Preserve the user's secure/insecure SSL choice across updates.
|
||||
SSL_MODE="insecure"
|
||||
if [ -f /etc/serverkit/ssl-mode ]; then
|
||||
SSL_MODE=$(cat /etc/serverkit/ssl-mode)
|
||||
fi
|
||||
if [ "$SSL_MODE" = "secure" ] && [ -f /etc/nginx/sites-available/serverkit.conf ]; then
|
||||
# Re-apply the Let's Encrypt path if we know the panel domain.
|
||||
PANEL_DOMAIN="${PANEL_DOMAIN:-}"
|
||||
if [ -z "$PANEL_DOMAIN" ] && [ -f "$INSTALL_DIR/.env" ]; then
|
||||
PANEL_DOMAIN=$(grep -E '^SERVERKIT_PUBLIC_URL=' "$INSTALL_DIR/.env" 2>/dev/null | \
|
||||
sed 's|^SERVERKIT_PUBLIC_URL=||; s|^https\?://||; s|/.*||' || true)
|
||||
fi
|
||||
if [ -n "$PANEL_DOMAIN" ] && [ -d "/etc/letsencrypt/live/$PANEL_DOMAIN" ]; then
|
||||
sed -i "s|/etc/letsencrypt/live/YOUR_DOMAIN/|/etc/letsencrypt/live/$PANEL_DOMAIN/|g" \
|
||||
/etc/nginx/sites-available/serverkit.conf
|
||||
fi
|
||||
ln -sf /etc/nginx/sites-available/serverkit.conf /etc/nginx/sites-enabled/serverkit.conf
|
||||
else
|
||||
ln -sf /etc/nginx/sites-available/serverkit-insecure.conf /etc/nginx/sites-enabled/serverkit.conf
|
||||
fi
|
||||
|
||||
mkdir -p /etc/nginx/serverkit-locations
|
||||
for conf in /etc/nginx/sites-enabled/*; do
|
||||
|
||||
Reference in new issue
Block a user