Features: - REST API for text/binary pastes with MIME detection - Client certificate auth via X-SSL-Client-SHA1 header - SQLite with WAL mode for concurrent access - Automatic paste expiry with LRU cleanup Security: - HSTS, CSP, X-Frame-Options, X-Content-Type-Options - Cache-Control: no-store for sensitive responses - X-Request-ID tracing for log correlation - X-Proxy-Secret validation for defense-in-depth - Parameterized queries, input validation - Size limits (3 MiB anon, 50 MiB auth) Includes /health endpoint, container support, and 70 tests.
306 lines
6.5 KiB
Markdown
306 lines
6.5 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
|
|
|
|
Create `/etc/systemd/system/flaskpaste.service`:
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=FlaskPaste Pastebin Service
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=notify
|
|
User=flaskpaste
|
|
Group=flaskpaste
|
|
RuntimeDirectory=flaskpaste
|
|
WorkingDirectory=/opt/flaskpaste
|
|
Environment="FLASK_ENV=production"
|
|
Environment="FLASKPASTE_DB=/var/lib/flaskpaste/pastes.db"
|
|
ExecStart=/opt/flaskpaste/venv/bin/gunicorn \
|
|
--workers 4 \
|
|
--bind unix:/run/flaskpaste/gunicorn.sock \
|
|
wsgi:app
|
|
ExecReload=/bin/kill -s HUP $MAINPID
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
```bash
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now flaskpaste
|
|
```
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
| 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` | `432000` | Paste expiry (seconds) |
|
|
| `FLASKPASTE_PROXY_SECRET` | (empty) | Shared secret for proxy trust validation |
|
|
|
|
---
|
|
|
|
## Security Checklist
|
|
|
|
- [ ] Run behind reverse proxy with TLS
|
|
- [ ] Use non-root user in containers
|
|
- [ ] 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`
|
|
- [ ] Set up log rotation
|
|
- [ ] Enable automatic backups
|
|
- [ ] Monitor disk usage (paste storage)
|