security: remove config.json from tracking, add example configs and nginx guard

- config.json now in .gitignore — it contained admin password hash and
  secret_key placeholder which should never be in version control
- Add config.example.json as a safe, secrets-free template for new deployments
- Add nginx.conf.example with proper reverse-proxy guards:
  - Static files served by Nginx directly (not Flask)
  - Explicit deny on .py, .json, .db, .log, .env and other sensitive extensions
  - Block dot-files and __pycache__ paths
  - No directory listing (autoindex off)
  - HTTPS enforcement with HSTS
This commit is contained in:
Antigravity 2026-05-16 12:20:01 +01:00
parent 24d70aeb08
commit 9aeed2af50
3 changed files with 120 additions and 7 deletions

8
.gitignore vendored
View File

@ -60,5 +60,9 @@ Thumbs.db
*.tmp
*.temp
# User uploads (if you add file upload functionality)
uploads/gunicorn.pid
# Gunicorn
gunicorn.pid
# Runtime config — contains secrets (SECRET_KEY, admin password hash).
# Copy config.example.json → config.json and fill in your values.
config.json

View File

@ -1,5 +1,5 @@
{
"_comment": "Bastebin configuration. Restart the server after changing values.",
"_comment": "Bastebin configuration. Copy this file to config.json, fill in your values, and restart the server.",
"site": {
"name": "Bastebin",
@ -13,11 +13,13 @@
"host": "0.0.0.0",
"port": 5500,
"debug": false,
"secret_key": "ce81867ddcc16729b42d7ae0564140e6bcc83bcf5dbbe77b7e5b6c5aa4199347"
"use_https": true,
"_secret_key_note": "Prefer the SECRET_KEY environment variable. This fallback is used only if the env var is not set.",
"secret_key": "CHANGE-ME-use-a-long-random-string-at-least-32-chars"
},
"database": {
"path": "pastebin.db"
"path": "bastebin.db"
},
"pastes": {
@ -133,9 +135,12 @@
{"value": "toml", "name": "TOML"},
{"value": "ini", "name": "INI / Config"}
],
"admin": {
"enabled": true,
"user": "admin",
"pass": "scrypt:32768:8:1$WKz9I6qE4hh0paUQ$6fd34e7f0195280f81301a92f5bac26d247f95d64744cb2c6e44108a3d8420eba5343b7b2ba657f39404f4ef102ce2e62a689e7797a43f3169fd69dca7b5b3c7"
"_user_note": "Set a strong username — not 'admin'.",
"user": "your-admin-username",
"_pass_note": "Generate with: python generate_hash.py yourpassword",
"pass": "REPLACE-WITH-SCRYPT-HASH-FROM-generate_hash.py"
}
}

104
nginx.conf.example Normal file
View File

@ -0,0 +1,104 @@
# ── Bastebin — Nginx reverse-proxy configuration ─────────────────────────────
#
# Copy to /etc/nginx/sites-available/bastebin and symlink:
# sudo ln -s /etc/nginx/sites-available/bastebin /etc/nginx/sites-enabled/
# sudo nginx -t && sudo systemctl reload nginx
#
# Assumes:
# - Gunicorn is bound to a Unix socket at /run/bastebin/gunicorn.sock
# - Your TLS certificates are managed by Certbot / Let's Encrypt
# - Static files live at /path/to/bastebin/static/
#
# Replace every occurrence of:
# bastebin.com → your actual domain
# /path/to/bastebin → absolute path to your project directory
server {
listen 80;
listen [::]:80;
server_name bastebin.com www.bastebin.com;
# Redirect all HTTP → HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name bastebin.com www.bastebin.com;
# ── TLS (managed by Certbot — do not edit manually) ──────────────────────
ssl_certificate /etc/letsencrypt/live/bastebin.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bastebin.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# ── Security headers (supplement Flask's own headers) ────────────────────
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Robots-Tag "noindex, nofollow" always;
# ── Block access to sensitive paths ──────────────────────────────────────
# These should NEVER be served, even accidentally.
location ~* \.(py|json|db|sqlite|sqlite3|log|cfg|ini|toml|env|sh|conf|bak|orig)$ {
deny all;
return 404;
}
# Block dot-files / hidden files (e.g. .env, .git)
location ~ /\. {
deny all;
return 404;
}
# Block __pycache__ and similar Python internals
location ~* /__pycache__/ {
deny all;
return 404;
}
# ── Static files served directly by Nginx (fast path) ───────────────────
# Nginx serves these without hitting Gunicorn at all.
location /static/ {
alias /path/to/bastebin/static/;
# No directory listings
autoindex off;
# Cache busting — serve with long expiry; filename includes hash in prod
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
# Only allow known file extensions; block anything else
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# Passthrough — let the alias serve it
}
location / {
# Anything under /static/ that doesn't match above is denied
deny all;
return 404;
}
}
# ── Gunicorn (Flask application) ─────────────────────────────────────────
location / {
proxy_pass http://unix:/run/bastebin/gunicorn.sock;
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;
# Timeouts
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# Body size limit — match config.json max_size_bytes (2 MB) + overhead
client_max_body_size 3m;
}
# ── Logging ───────────────────────────────────────────────────────────────
access_log /var/log/nginx/bastebin.access.log;
error_log /var/log/nginx/bastebin.error.log warn;
}