forked from username/flaskpaste
flaskpaste: initial commit with security hardening
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.
This commit is contained in:
305
documentation/deployment.md
Normal file
305
documentation/deployment.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user