forked from claw/flaskpaste
PROXY-001: Add startup warning when TRUSTED_PROXY_SECRET empty in production - validate_security_config() checks for missing proxy secret - Additional warning when PKI enabled without proxy secret - Tests for security configuration validation BURN-001: HEAD requests now trigger burn-after-read deletion - Prevents attacker from probing paste existence before retrieval - Updated test to verify new behavior RATE-001: Add RATE_LIMIT_MAX_ENTRIES to cap memory usage - Default 10000 unique IPs tracked - Prunes oldest entries when limit exceeded - Protects against memory exhaustion DoS Test count: 284 -> 291 (7 new security tests)
320 lines
7.6 KiB
Markdown
320 lines
7.6 KiB
Markdown
# FlaskPaste Deployment Guide
|
|
|
|
## Overview
|
|
|
|
FlaskPaste can be deployed in several ways:
|
|
- Development server (for testing only)
|
|
- WSGI server (Gunicorn, uWSGI)
|
|
- Container (Podman/Docker)
|
|
|
|
## Prerequisites
|
|
|
|
- Python 3.11+
|
|
- SQLite 3
|
|
- Reverse proxy for TLS termination (nginx, Apache, Caddy)
|
|
|
|
---
|
|
|
|
## Development Server
|
|
|
|
**For testing only - not for production!**
|
|
|
|
```bash
|
|
# Setup
|
|
python3 -m venv venv
|
|
source venv/bin/activate
|
|
pip install -r requirements.txt
|
|
|
|
# Run
|
|
python run.py
|
|
# or
|
|
flask --app app run --debug
|
|
```
|
|
|
|
---
|
|
|
|
## Production: WSGI Server
|
|
|
|
### Gunicorn
|
|
|
|
```bash
|
|
pip install gunicorn
|
|
|
|
# Basic usage
|
|
gunicorn -w 4 -b 0.0.0.0:5000 wsgi:app
|
|
|
|
# With Unix socket (recommended for nginx)
|
|
gunicorn -w 4 --bind unix:/run/flaskpaste/gunicorn.sock wsgi:app
|
|
|
|
# Production settings
|
|
gunicorn \
|
|
--workers 4 \
|
|
--bind unix:/run/flaskpaste/gunicorn.sock \
|
|
--access-logfile /var/log/flaskpaste/access.log \
|
|
--error-logfile /var/log/flaskpaste/error.log \
|
|
--capture-output \
|
|
wsgi:app
|
|
```
|
|
|
|
### Systemd Service
|
|
|
|
Use the provided examples in `examples/`:
|
|
|
|
```bash
|
|
# Create service user
|
|
sudo useradd -r -s /sbin/nologin flaskpaste
|
|
|
|
# Copy application
|
|
sudo mkdir -p /opt/flaskpaste/data
|
|
sudo cp -r . /opt/flaskpaste/
|
|
sudo chown -R flaskpaste:flaskpaste /opt/flaskpaste
|
|
|
|
# Copy service unit (with security hardening)
|
|
sudo cp examples/flaskpaste.service /etc/systemd/system/
|
|
|
|
# Copy and secure environment file
|
|
sudo mkdir -p /etc/flaskpaste
|
|
sudo cp examples/flaskpaste.env /etc/flaskpaste/env
|
|
sudo chmod 600 /etc/flaskpaste/env
|
|
|
|
# Enable and start
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now flaskpaste
|
|
```
|
|
|
|
The provided service unit includes security hardening:
|
|
- `NoNewPrivileges`, `PrivateTmp`, `ProtectSystem=strict`
|
|
- `MemoryDenyWriteExecute`, `RestrictNamespaces`
|
|
- Resource limits and restart policies
|
|
|
|
See `examples/flaskpaste.service` for the full configuration.
|
|
|
|
---
|
|
|
|
## Production: Container Deployment
|
|
|
|
### Build the Container
|
|
|
|
```bash
|
|
# Using Podman
|
|
podman build -t flaskpaste .
|
|
|
|
# Using Docker
|
|
docker build -t flaskpaste .
|
|
```
|
|
|
|
### Run the Container
|
|
|
|
```bash
|
|
# Basic run
|
|
podman run -d \
|
|
--name flaskpaste \
|
|
-p 5000:5000 \
|
|
-v flaskpaste-data:/app/data \
|
|
flaskpaste
|
|
|
|
# With environment configuration
|
|
podman run -d \
|
|
--name flaskpaste \
|
|
-p 5000:5000 \
|
|
-v flaskpaste-data:/app/data \
|
|
-e FLASKPASTE_EXPIRY=604800 \
|
|
-e FLASKPASTE_MAX_ANON=1048576 \
|
|
flaskpaste
|
|
```
|
|
|
|
### Podman Quadlet (systemd integration)
|
|
|
|
Create `/etc/containers/systemd/flaskpaste.container`:
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=FlaskPaste Container
|
|
After=local-fs.target
|
|
|
|
[Container]
|
|
Image=localhost/flaskpaste:latest
|
|
ContainerName=flaskpaste
|
|
PublishPort=5000:5000
|
|
Volume=flaskpaste-data:/app/data:Z
|
|
Environment=FLASK_ENV=production
|
|
Environment=FLASKPASTE_EXPIRY=432000
|
|
|
|
[Service]
|
|
Restart=on-failure
|
|
RestartSec=10
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target default.target
|
|
```
|
|
|
|
```bash
|
|
systemctl --user daemon-reload
|
|
systemctl --user start flaskpaste
|
|
```
|
|
|
|
---
|
|
|
|
## Reverse Proxy Configuration
|
|
|
|
### nginx
|
|
|
|
```nginx
|
|
upstream flaskpaste {
|
|
server unix:/run/flaskpaste/gunicorn.sock;
|
|
# or for container: server 127.0.0.1:5000;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name paste.example.com;
|
|
|
|
ssl_certificate /etc/ssl/certs/paste.example.com.crt;
|
|
ssl_certificate_key /etc/ssl/private/paste.example.com.key;
|
|
|
|
# Optional: Client certificate authentication
|
|
ssl_client_certificate /etc/ssl/certs/ca.crt;
|
|
ssl_verify_client optional;
|
|
|
|
# Size limit (should match FLASKPASTE_MAX_AUTH)
|
|
client_max_body_size 50M;
|
|
|
|
location / {
|
|
proxy_pass http://flaskpaste;
|
|
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;
|
|
|
|
# Pass client certificate fingerprint
|
|
proxy_set_header X-SSL-Client-SHA1 $ssl_client_fingerprint;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Caddy
|
|
|
|
```caddyfile
|
|
paste.example.com {
|
|
reverse_proxy localhost:5000
|
|
|
|
# Client certificate auth (if needed)
|
|
tls {
|
|
client_auth {
|
|
mode request
|
|
trusted_ca_cert_file /etc/ssl/certs/ca.crt
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### HAProxy
|
|
|
|
```haproxy
|
|
frontend https
|
|
bind *:443 ssl crt /etc/haproxy/certs/
|
|
|
|
# Route to flaskpaste backend
|
|
acl is_paste path_beg /paste
|
|
use_backend backend-flaskpaste if is_paste
|
|
|
|
backend backend-flaskpaste
|
|
mode http
|
|
|
|
# Strip /paste prefix before forwarding
|
|
http-request replace-path /paste(.*) \1
|
|
http-request set-path / if { path -m len 0 }
|
|
|
|
# Request tracing - generate X-Request-ID if not present
|
|
http-request set-header X-Request-ID %[uuid()] unless { req.hdr(X-Request-ID) -m found }
|
|
http-request set-var(txn.request_id) req.hdr(X-Request-ID)
|
|
http-response set-header X-Request-ID %[var(txn.request_id)]
|
|
|
|
# Proxy trust secret - proves request came through HAProxy
|
|
http-request set-header X-Proxy-Secret your-secret-value-here
|
|
|
|
# Pass client certificate fingerprint (if using mTLS)
|
|
http-request set-header X-SSL-Client-SHA1 %[ssl_c_sha1,hex,lower]
|
|
|
|
server flaskpaste 127.0.0.1:5000 check
|
|
```
|
|
|
|
---
|
|
|
|
## Data Persistence
|
|
|
|
### Database Location
|
|
|
|
Default: `./data/pastes.db`
|
|
|
|
Configure via `FLASKPASTE_DB`:
|
|
```bash
|
|
export FLASKPASTE_DB=/var/lib/flaskpaste/pastes.db
|
|
```
|
|
|
|
### Backup
|
|
|
|
```bash
|
|
# SQLite online backup
|
|
sqlite3 /var/lib/flaskpaste/pastes.db ".backup '/backup/pastes-$(date +%Y%m%d).db'"
|
|
```
|
|
|
|
### Container Volume
|
|
|
|
```bash
|
|
# Create named volume
|
|
podman volume create flaskpaste-data
|
|
|
|
# Backup from volume
|
|
podman run --rm \
|
|
-v flaskpaste-data:/app/data:ro \
|
|
-v ./backup:/backup \
|
|
alpine cp /app/data/pastes.db /backup/
|
|
```
|
|
|
|
---
|
|
|
|
## Environment Variables
|
|
|
|
See `examples/flaskpaste.env` for a complete template.
|
|
|
|
| Variable | Default | Description |
|
|
|----------|---------|-------------|
|
|
| `FLASK_ENV` | `development` | Set to `production` in production |
|
|
| `FLASKPASTE_DB` | `./data/pastes.db` | Database file path |
|
|
| `FLASKPASTE_ID_LENGTH` | `12` | Paste ID length (hex chars) |
|
|
| `FLASKPASTE_MAX_ANON` | `3145728` | Max anonymous paste (bytes) |
|
|
| `FLASKPASTE_MAX_AUTH` | `52428800` | Max authenticated paste (bytes) |
|
|
| `FLASKPASTE_EXPIRY_ANON` | `86400` | Anonymous paste expiry (1 day) |
|
|
| `FLASKPASTE_EXPIRY_UNTRUSTED` | `604800` | Untrusted cert expiry (7 days) |
|
|
| `FLASKPASTE_EXPIRY_TRUSTED` | `2592000` | PKI-registered expiry (30 days) |
|
|
| `FLASKPASTE_PROXY_SECRET` | (empty) | Shared secret for proxy trust |
|
|
| `FLASKPASTE_POW_DIFFICULTY` | `20` | PoW difficulty (0=disabled) |
|
|
| `FLASKPASTE_RATE_LIMIT` | `1` | Enable IP-based rate limiting |
|
|
| `FLASKPASTE_RATE_MAX` | `10` | Max requests per window (anon) |
|
|
| `FLASKPASTE_RATE_AUTH_MULT` | `5` | Multiplier for authenticated users |
|
|
| `FLASKPASTE_RATE_MAX_ENTRIES` | `10000` | Max tracked IPs (memory DoS protection) |
|
|
| `FLASKPASTE_ANTIFLOOD` | `1` | Enable dynamic PoW under attack |
|
|
| `FLASKPASTE_PKI_ENABLED` | `0` | Enable built-in PKI |
|
|
|
|
---
|
|
|
|
## Security Checklist
|
|
|
|
- [ ] Run behind reverse proxy with TLS
|
|
- [ ] Use non-root user (or systemd security hardening)
|
|
- [ ] Set appropriate file permissions on database
|
|
- [ ] Configure firewall (only expose reverse proxy)
|
|
- [ ] Set `FLASK_ENV=production`
|
|
- [ ] Configure client certificate auth (if needed)
|
|
- [ ] Set `FLASKPASTE_PROXY_SECRET` for defense-in-depth
|
|
- [ ] Configure proxy to send `X-Proxy-Secret` header
|
|
- [ ] Configure proxy to pass/generate `X-Request-ID`
|
|
- [ ] Enable rate limiting (`FLASKPASTE_RATE_LIMIT=1`)
|
|
- [ ] Enable anti-flood (`FLASKPASTE_ANTIFLOOD=1`)
|
|
- [ ] Enable PoW (`FLASKPASTE_POW_DIFFICULTY=20`)
|
|
- [ ] Set up log rotation
|
|
- [ ] Enable automatic backups
|
|
- [ ] Monitor disk usage (paste storage)
|
|
- [ ] Monitor rate limit headers (`X-RateLimit-*`)
|