forked from claw/flaskpaste
Update README.md, api.md, and error hints to reflect: - encryption is now default (no -e flag needed) - use -E/--no-encrypt to disable - file path shortcut (fpaste file.txt)
985 lines
21 KiB
Markdown
985 lines
21 KiB
Markdown
# FlaskPaste API Reference
|
|
|
|
## Overview
|
|
|
|
FlaskPaste provides a RESTful API for creating, retrieving, and deleting text and binary pastes.
|
|
|
|
**Base URL:** `http://your-server:5000/`
|
|
|
|
**Content Types:**
|
|
- Requests: `application/json`, `text/plain`, `application/octet-stream`, or any binary type
|
|
- Responses: `application/json` for metadata, original MIME type for raw content
|
|
|
|
## Authentication
|
|
|
|
Authentication is optional and uses client certificate fingerprints passed via the `X-SSL-Client-SHA1` header.
|
|
|
|
```http
|
|
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
|
```
|
|
|
|
The fingerprint must be exactly 40 lowercase hexadecimal characters (SHA1).
|
|
|
|
**Benefits of authentication:**
|
|
- Larger upload limit (50 MiB vs 3 MiB)
|
|
- Ability to delete owned pastes
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
### GET /challenge
|
|
|
|
Get a proof-of-work challenge for paste creation. Required when PoW is enabled.
|
|
|
|
**Request:**
|
|
```http
|
|
GET /challenge HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Response (PoW disabled):**
|
|
```json
|
|
{
|
|
"enabled": false,
|
|
"difficulty": 0
|
|
}
|
|
```
|
|
|
|
**Response (PoW enabled):**
|
|
```json
|
|
{
|
|
"enabled": true,
|
|
"nonce": "a1b2c3d4e5f6a7b8a1b2c3d4e5f6a7b8",
|
|
"difficulty": 20,
|
|
"expires": 1700000300,
|
|
"token": "a1b2c3d4...:1700000300:20:signature"
|
|
}
|
|
```
|
|
|
|
The client must find a number `N` such that `SHA256(nonce + ":" + N)` has at least `difficulty` leading zero bits.
|
|
|
|
---
|
|
|
|
### GET /health
|
|
|
|
Health check endpoint for load balancers and monitoring.
|
|
|
|
**Request:**
|
|
```http
|
|
GET /health HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"status": "healthy",
|
|
"database": "ok"
|
|
}
|
|
```
|
|
|
|
**Response (503 Service Unavailable):**
|
|
```json
|
|
{
|
|
"status": "unhealthy",
|
|
"database": "error"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### GET /
|
|
|
|
Returns API information and usage instructions.
|
|
|
|
**Request:**
|
|
```http
|
|
GET / HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"name": "FlaskPaste",
|
|
"version": "1.2.0",
|
|
"endpoints": {
|
|
"GET /": "API information",
|
|
"GET /health": "Health check",
|
|
"POST /": "Create paste",
|
|
"GET /<id>": "Retrieve paste metadata",
|
|
"GET /<id>/raw": "Retrieve raw paste content",
|
|
"DELETE /<id>": "Delete paste"
|
|
},
|
|
"usage": {
|
|
"raw": "curl --data-binary @file.txt http://host/",
|
|
"pipe": "cat file.txt | curl --data-binary @- http://host/",
|
|
"json": "curl -H 'Content-Type: application/json' -d '{\"content\":\"...\"}' http://host/"
|
|
},
|
|
"note": "Use --data-binary (not -d) to preserve newlines"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### POST /
|
|
|
|
Create a new paste.
|
|
|
|
**Request (Raw Binary):**
|
|
```http
|
|
POST / HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: application/octet-stream
|
|
|
|
```
|
|
|
|
**Request (JSON):**
|
|
```http
|
|
POST / HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: application/json
|
|
|
|
```
|
|
|
|
**Request (with Proof-of-Work):**
|
|
|
|
When PoW is enabled, include the challenge token and solution:
|
|
|
|
```http
|
|
POST / HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: text/plain
|
|
X-PoW-Token: a1b2c3d4...:1700000300:20:signature
|
|
X-PoW-Solution: 12345678
|
|
|
|
```
|
|
|
|
**Request (Burn-After-Read):**
|
|
|
|
Create a paste that deletes itself after first retrieval:
|
|
|
|
```http
|
|
POST / HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: text/plain
|
|
X-Burn-After-Read: true
|
|
|
|
```
|
|
|
|
**Request (Custom Expiry):**
|
|
|
|
Create a paste with custom expiry time (in seconds):
|
|
|
|
```http
|
|
POST / HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: text/plain
|
|
X-Expiry: 3600
|
|
|
|
```
|
|
|
|
**Request (Password Protected):**
|
|
|
|
Create a paste that requires a password to access:
|
|
|
|
```http
|
|
POST / HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: text/plain
|
|
X-Paste-Password: secretpassword
|
|
|
|
```
|
|
|
|
**Response (201 Created):**
|
|
```json
|
|
{
|
|
"id": "abc12345",
|
|
"url": "/abc12345",
|
|
"raw": "/abc12345/raw",
|
|
"mime_type": "text/plain",
|
|
"created_at": 1700000000,
|
|
"owner": "a1b2c3...", // Only present if authenticated
|
|
"burn_after_read": true, // Only present if enabled
|
|
"expires_at": 1700003600, // Only present if custom expiry set
|
|
"password_protected": true // Only present if password set
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | No content provided |
|
|
| 400 | Password too long (max 1024 chars) |
|
|
| 400 | Proof-of-work required (when PoW enabled) |
|
|
| 400 | Proof-of-work failed (invalid/expired challenge) |
|
|
| 400 | Paste too small (below minimum size) |
|
|
| 413 | Paste too large |
|
|
| 429 | Duplicate content rate limit exceeded |
|
|
|
|
**Size Limits:**
|
|
- Minimum: disabled by default (`FLASKPASTE_MIN_SIZE`, e.g. 64 bytes for encryption enforcement)
|
|
- Anonymous: 3 MiB (configurable via `FLASKPASTE_MAX_ANON`)
|
|
- Authenticated: 50 MiB (configurable via `FLASKPASTE_MAX_AUTH`)
|
|
|
|
---
|
|
|
|
### GET /{id}
|
|
|
|
### HEAD /{id}
|
|
|
|
Retrieve paste metadata. HEAD returns headers only (no body).
|
|
|
|
**Request:**
|
|
```http
|
|
GET /abc12345 HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Request (Password Protected):**
|
|
```http
|
|
GET /abc12345 HTTP/1.1
|
|
Host: localhost:5000
|
|
X-Paste-Password: secretpassword
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"id": "abc12345",
|
|
"mime_type": "text/plain",
|
|
"size": 1234,
|
|
"created_at": 1700000000,
|
|
"raw": "/abc12345/raw",
|
|
"password_protected": true // Only present if protected
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | Invalid paste ID format |
|
|
| 401 | Password required |
|
|
| 403 | Invalid password |
|
|
| 404 | Paste not found |
|
|
|
|
---
|
|
|
|
### GET /{id}/raw
|
|
|
|
### HEAD /{id}/raw
|
|
|
|
Retrieve raw paste content with correct MIME type. HEAD returns headers only (useful for checking MIME type and size without downloading content).
|
|
|
|
**Request:**
|
|
```http
|
|
GET /abc12345/raw HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Request (Password Protected):**
|
|
```http
|
|
GET /abc12345/raw HTTP/1.1
|
|
Host: localhost:5000
|
|
X-Paste-Password: secretpassword
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```http
|
|
HTTP/1.1 200 OK
|
|
Content-Type: image/png
|
|
Content-Disposition: inline
|
|
|
|
<binary content>
|
|
```
|
|
|
|
- `Content-Disposition: inline` is set for `image/*` and `text/*` types
|
|
- Content-Type matches the detected/stored MIME type
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | Invalid paste ID format |
|
|
| 401 | Password required |
|
|
| 403 | Invalid password |
|
|
| 404 | Paste not found |
|
|
|
|
---
|
|
|
|
### DELETE /{id}
|
|
|
|
Delete a paste. Requires authentication and ownership.
|
|
|
|
**Request:**
|
|
```http
|
|
DELETE /abc12345 HTTP/1.1
|
|
Host: localhost:5000
|
|
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"message": "Paste deleted"
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | Invalid paste ID format |
|
|
| 401 | Authentication required |
|
|
| 403 | Permission denied (not owner) |
|
|
| 404 | Paste not found |
|
|
|
|
---
|
|
|
|
## MIME Type Detection
|
|
|
|
FlaskPaste automatically detects MIME types using:
|
|
|
|
1. **Magic byte signatures** (highest priority)
|
|
- PNG: `\x89PNG\r\n\x1a\n`
|
|
- JPEG: `\xff\xd8\xff`
|
|
- GIF: `GIF87a` or `GIF89a`
|
|
- WebP: `RIFF....WEBP`
|
|
- ZIP: `PK\x03\x04`
|
|
- PDF: `%PDF`
|
|
- GZIP: `\x1f\x8b`
|
|
|
|
2. **Explicit Content-Type header** (if not generic)
|
|
|
|
3. **UTF-8 detection** (falls back to `text/plain`)
|
|
|
|
4. **Binary fallback** (`application/octet-stream`)
|
|
|
|
---
|
|
|
|
## Paste Expiry
|
|
|
|
Pastes expire based on last access time (default: 5 days).
|
|
|
|
- Every `GET /{id}` or `GET /{id}/raw` updates the last access timestamp
|
|
- Cleanup runs automatically (hourly, throttled)
|
|
- Configurable via `FLASKPASTE_EXPIRY` environment variable
|
|
|
|
**Custom Expiry:**
|
|
|
|
Pastes can have custom expiry times using the `X-Expiry` header:
|
|
|
|
```bash
|
|
# Paste expires in 1 hour
|
|
curl -H "X-Expiry: 3600" --data-binary @file.txt http://host/
|
|
```
|
|
|
|
**Configuration:**
|
|
```bash
|
|
export FLASKPASTE_EXPIRY=432000 # Default expiry: 5 days
|
|
export FLASKPASTE_MAX_EXPIRY=2592000 # Max custom expiry: 30 days
|
|
```
|
|
|
|
**Notes:**
|
|
- Custom expiry is capped at `FLASKPASTE_MAX_EXPIRY`
|
|
- Invalid or negative values are ignored (uses default)
|
|
- Response includes `expires_at` timestamp when custom expiry is set
|
|
|
|
---
|
|
|
|
## Burn-After-Read
|
|
|
|
Single-access pastes that delete themselves after first retrieval.
|
|
|
|
**How it works:**
|
|
- Set `X-Burn-After-Read: true` header on creation
|
|
- First `GET /{id}/raw` returns content and deletes paste
|
|
- Subsequent requests return 404
|
|
- Metadata `GET /{id}` does not trigger burn
|
|
- `HEAD` requests do not trigger burn
|
|
|
|
**Usage:**
|
|
```bash
|
|
# Create burn-after-read paste
|
|
curl -H "X-Burn-After-Read: true" --data-binary @secret.txt http://host/
|
|
|
|
# Response indicates burn is enabled
|
|
{
|
|
"id": "abc12345",
|
|
"burn_after_read": true,
|
|
...
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Response includes `X-Burn-After-Read: true` header when content is retrieved
|
|
- Can be combined with custom expiry (paste expires OR burns, whichever first)
|
|
- Accepted values: `true`, `1`, `yes` (case-insensitive)
|
|
|
|
---
|
|
|
|
## Password Protection
|
|
|
|
Pastes can be protected with a password using PBKDF2-HMAC-SHA256 hashing.
|
|
|
|
**Creating a protected paste:**
|
|
```http
|
|
POST / HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: text/plain
|
|
X-Paste-Password: mysecretpassword
|
|
|
|
```
|
|
|
|
**Response (201 Created):**
|
|
```json
|
|
{
|
|
"id": "abc12345",
|
|
"url": "/abc12345",
|
|
"raw": "/abc12345/raw",
|
|
"mime_type": "text/plain",
|
|
"created_at": 1700000000,
|
|
"password_protected": true
|
|
}
|
|
```
|
|
|
|
**Accessing protected paste:**
|
|
```http
|
|
GET /abc12345 HTTP/1.1
|
|
Host: localhost:5000
|
|
X-Paste-Password: mysecretpassword
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | Password too long (max 1024 chars) |
|
|
| 401 | Password required |
|
|
| 403 | Invalid password |
|
|
|
|
**Response (401 Unauthorized):**
|
|
```json
|
|
{
|
|
"error": "Password required",
|
|
"password_protected": true
|
|
}
|
|
```
|
|
|
|
**Response (403 Forbidden):**
|
|
```json
|
|
{
|
|
"error": "Invalid password"
|
|
}
|
|
```
|
|
|
|
**Security Implementation:**
|
|
- PBKDF2-HMAC-SHA256 with 600,000 iterations (OWASP 2023)
|
|
- 32-byte random salt per password
|
|
- Constant-time comparison prevents timing attacks
|
|
- Passwords never logged or stored in plaintext
|
|
- Maximum 1024 characters to prevent DoS via expensive hashing
|
|
|
|
**CLI Usage:**
|
|
```bash
|
|
# Create protected paste
|
|
./fpaste create -p "mypassword" secret.txt
|
|
|
|
# Retrieve protected paste
|
|
./fpaste get -p "mypassword" abc12345
|
|
```
|
|
|
|
**Notes:**
|
|
- Password protection can be combined with burn-after-read and custom expiry
|
|
- Unicode passwords are supported
|
|
- Special characters are allowed
|
|
|
|
---
|
|
|
|
## Abuse Prevention
|
|
|
|
FlaskPaste includes content-hash based deduplication to prevent spam and abuse.
|
|
|
|
**How it works:**
|
|
- Each paste's SHA256 content hash is tracked
|
|
- Repeated submissions of identical content are throttled
|
|
- After exceeding the threshold, further duplicates are rejected with 429
|
|
|
|
**Default limits:**
|
|
- Window: 1 hour (`FLASKPASTE_DEDUP_WINDOW`)
|
|
- Maximum: 3 identical submissions per window (`FLASKPASTE_DEDUP_MAX`)
|
|
|
|
**Response (429 Too Many Requests):**
|
|
```json
|
|
{
|
|
"error": "Duplicate content rate limit exceeded",
|
|
"count": 3,
|
|
"window_seconds": 3600
|
|
}
|
|
```
|
|
|
|
**Configuration:**
|
|
```bash
|
|
export FLASKPASTE_DEDUP_WINDOW=3600 # Window in seconds (default: 1 hour)
|
|
export FLASKPASTE_DEDUP_MAX=3 # Max duplicates per window (default: 3)
|
|
```
|
|
|
|
**Notes:**
|
|
- Different content is not affected by other content's limits
|
|
- Counter resets after the window expires
|
|
- Hash records are cleaned up periodically
|
|
|
|
---
|
|
|
|
## Entropy Enforcement
|
|
|
|
FlaskPaste can require minimum content entropy to enforce client-side encryption.
|
|
|
|
**How it works:**
|
|
- Shannon entropy is calculated for submitted content (bits per byte)
|
|
- Encrypted/random data: ~7.5-8.0 bits/byte
|
|
- English text: ~4.0-5.0 bits/byte
|
|
- Content below threshold is rejected with 400
|
|
|
|
**Configuration:**
|
|
```bash
|
|
export FLASKPASTE_MIN_ENTROPY=6.0 # Require encryption-level entropy (0=disabled)
|
|
export FLASKPASTE_MIN_ENTROPY_SIZE=256 # Only check content >= this size (default: 256)
|
|
```
|
|
|
|
**Response (400 Bad Request):**
|
|
```json
|
|
{
|
|
"error": "Content entropy too low",
|
|
"entropy": 4.12,
|
|
"min_entropy": 7.0,
|
|
"hint": "Encrypt content before uploading (fpaste encrypts by default)"
|
|
}
|
|
```
|
|
|
|
**Caveats:**
|
|
- Small data is exempt (configurable via `MIN_ENTROPY_SIZE`, default 256 bytes)
|
|
- Compressed data (gzip, zip) also has high entropy — not distinguishable from encrypted
|
|
- This is a heuristic, not cryptographic proof of encryption
|
|
|
|
**Recommended thresholds:**
|
|
| Threshold | Effect |
|
|
|-----------|--------|
|
|
| 0 | Disabled (default) |
|
|
| 5.0 | Blocks most plaintext |
|
|
| 6.0 | Requires encryption or compression |
|
|
| 7.0 | Requires encryption + sufficient size |
|
|
|
|
---
|
|
|
|
### Binary Content Requirement
|
|
|
|
FlaskPaste can require unrecognizable binary content (MIME-based encryption enforcement).
|
|
|
|
**How it works:**
|
|
- Content is checked for magic bytes (PNG, JPEG, PDF, ZIP, etc.)
|
|
- Content is checked for valid UTF-8 text
|
|
- Recognized formats are rejected with 400
|
|
- Only application/octet-stream (unrecognizable binary) is allowed
|
|
|
|
**Configuration:**
|
|
```bash
|
|
export FLASKPASTE_REQUIRE_BINARY=1 # Reject recognizable formats (0=disabled)
|
|
```
|
|
|
|
**Response (400 Bad Request):**
|
|
```json
|
|
{
|
|
"error": "Recognizable format not allowed",
|
|
"detected": "text/plain",
|
|
"hint": "Encrypt content before uploading (fpaste encrypts by default)"
|
|
}
|
|
```
|
|
|
|
**Detected formats:**
|
|
- `text/plain` (valid UTF-8 text)
|
|
- `image/png`, `image/jpeg`, `image/gif`, `image/webp`
|
|
- `application/pdf`, `application/zip`, `application/gzip`
|
|
|
|
**vs Entropy enforcement:**
|
|
| Method | Detects | False positives |
|
|
|--------|---------|-----------------|
|
|
| Entropy | Random-looking data | Compressed files pass |
|
|
| Binary | No magic bytes + invalid UTF-8 | Minimal |
|
|
|
|
Use both together for maximum encryption enforcement:
|
|
```bash
|
|
export FLASKPASTE_REQUIRE_BINARY=1
|
|
export FLASKPASTE_MIN_ENTROPY=6.0
|
|
export FLASKPASTE_MIN_SIZE=64
|
|
```
|
|
|
|
---
|
|
|
|
## Proof-of-Work
|
|
|
|
FlaskPaste includes an optional proof-of-work system to prevent automated spam.
|
|
|
|
**How it works:**
|
|
1. Client requests a challenge via `GET /challenge`
|
|
2. Server returns a nonce, difficulty, expiry time, and signed token
|
|
3. Client computes SHA256 hashes until finding one with enough leading zero bits
|
|
4. Client submits paste with `X-PoW-Token` and `X-PoW-Solution` headers
|
|
|
|
**Algorithm:**
|
|
```
|
|
For N = 0, 1, 2, ...:
|
|
hash = SHA256(nonce + ":" + N)
|
|
if leading_zero_bits(hash) >= difficulty:
|
|
return N as solution
|
|
```
|
|
|
|
**Configuration:**
|
|
```bash
|
|
export FLASKPASTE_POW_DIFFICULTY=20 # Leading zero bits required (0=disabled)
|
|
export FLASKPASTE_POW_TTL=300 # Challenge validity in seconds
|
|
export FLASKPASTE_POW_SECRET="..." # Optional signing key (auto-generated if empty)
|
|
```
|
|
|
|
**Headers:**
|
|
| Header | Description |
|
|
|--------|-------------|
|
|
| `X-PoW-Token` | The full token from `/challenge` response |
|
|
| `X-PoW-Solution` | The computed solution number |
|
|
|
|
**Error responses:**
|
|
```json
|
|
{
|
|
"error": "Proof-of-work required",
|
|
"hint": "GET /challenge for a new challenge"
|
|
}
|
|
```
|
|
|
|
```json
|
|
{
|
|
"error": "Proof-of-work failed: Challenge expired"
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Difficulty 20 requires approximately 1 million hash attempts on average
|
|
- Challenges are signed to prevent tampering
|
|
- Each challenge can only be used once (nonce uniqueness)
|
|
|
|
---
|
|
|
|
## Error Response Format
|
|
|
|
All errors return JSON:
|
|
|
|
```json
|
|
{
|
|
"error": "Description of the error"
|
|
}
|
|
```
|
|
|
|
For size limit errors (413):
|
|
```json
|
|
{
|
|
"error": "Paste too large",
|
|
"size": 5000000,
|
|
"max_size": 3145728,
|
|
"authenticated": false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Security Headers
|
|
|
|
All responses include the following security headers:
|
|
|
|
| Header | Value |
|
|
|--------|-------|
|
|
| `X-Content-Type-Options` | `nosniff` |
|
|
| `X-Frame-Options` | `DENY` |
|
|
| `X-XSS-Protection` | `1; mode=block` |
|
|
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
|
|
| `Content-Security-Policy` | `default-src 'none'; frame-ancestors 'none'` |
|
|
| `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` |
|
|
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` |
|
|
| `Cache-Control` | `no-store, no-cache, must-revalidate, private` |
|
|
| `Pragma` | `no-cache` |
|
|
|
|
---
|
|
|
|
## Request Tracing
|
|
|
|
All requests include an `X-Request-ID` header for log correlation:
|
|
|
|
- If the client provides `X-Request-ID`, it is passed through
|
|
- If not provided, a UUID is generated
|
|
- The ID is echoed back in the response
|
|
- All log entries include `[rid=<request-id>]`
|
|
|
|
**Example:**
|
|
```bash
|
|
# Client-provided ID
|
|
curl -H "X-Request-ID: my-trace-123" https://paste.example.com/health
|
|
|
|
# Response includes:
|
|
# X-Request-ID: my-trace-123
|
|
```
|
|
|
|
---
|
|
|
|
## Proxy Trust Validation
|
|
|
|
When `FLASKPASTE_PROXY_SECRET` is configured, the application validates that requests come from a trusted reverse proxy by checking the `X-Proxy-Secret` header.
|
|
|
|
This provides defense-in-depth against header spoofing if an attacker bypasses the reverse proxy.
|
|
|
|
**Configuration:**
|
|
```bash
|
|
export FLASKPASTE_PROXY_SECRET="your-secret-value"
|
|
```
|
|
|
|
**Proxy Configuration (HAProxy):**
|
|
```
|
|
http-request set-header X-Proxy-Secret your-secret-value
|
|
```
|
|
|
|
If the secret doesn't match, authentication headers (`X-SSL-Client-SHA1`) are ignored and the request is treated as anonymous.
|
|
|
|
---
|
|
|
|
## PKI (Certificate Authority)
|
|
|
|
FlaskPaste includes an optional minimal PKI for issuing client certificates.
|
|
|
|
### Configuration
|
|
|
|
Enable PKI via environment variables:
|
|
|
|
```bash
|
|
export FLASKPASTE_PKI_ENABLED=1 # Enable PKI endpoints
|
|
export FLASKPASTE_PKI_CA_PASSWORD="secret" # Required: CA password
|
|
export FLASKPASTE_PKI_CERT_DAYS=365 # Client certificate validity (days)
|
|
export FLASKPASTE_PKI_CA_DAYS=3650 # CA certificate validity (days)
|
|
```
|
|
|
|
### Certificate Revocation
|
|
|
|
When PKI is enabled, certificates issued by the CA are tracked. Revoked certificates are rejected during authentication (treated as anonymous).
|
|
|
|
---
|
|
|
|
### GET /pki
|
|
|
|
Get PKI status and CA information.
|
|
|
|
**Request:**
|
|
```http
|
|
GET /pki HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Response (PKI disabled):**
|
|
```json
|
|
{
|
|
"enabled": false
|
|
}
|
|
```
|
|
|
|
**Response (PKI enabled, no CA):**
|
|
```json
|
|
{
|
|
"enabled": true,
|
|
"ca_exists": false,
|
|
"hint": "POST /pki/ca to generate CA"
|
|
}
|
|
```
|
|
|
|
**Response (PKI enabled with CA):**
|
|
```json
|
|
{
|
|
"enabled": true,
|
|
"ca_exists": true,
|
|
"common_name": "FlaskPaste CA",
|
|
"fingerprint_sha1": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
|
|
"created_at": 1700000000,
|
|
"expires_at": 2015000000,
|
|
"key_algorithm": "ec:secp384r1"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### POST /pki/ca
|
|
|
|
Generate a new Certificate Authority. Only works once (first-run bootstrap).
|
|
|
|
**Request:**
|
|
```http
|
|
POST /pki/ca HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: application/json
|
|
|
|
```
|
|
|
|
**Response (201 Created):**
|
|
```json
|
|
{
|
|
"message": "CA generated",
|
|
"common_name": "My CA",
|
|
"fingerprint_sha1": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
|
|
"created_at": 1700000000,
|
|
"expires_at": 2015000000,
|
|
"download": "/pki/ca.crt"
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 404 | PKI not enabled |
|
|
| 409 | CA already exists |
|
|
| 500 | PKI_CA_PASSWORD not configured |
|
|
|
|
---
|
|
|
|
### GET /pki/ca.crt
|
|
|
|
Download CA certificate in PEM format (for trust store).
|
|
|
|
**Request:**
|
|
```http
|
|
GET /pki/ca.crt HTTP/1.1
|
|
Host: localhost:5000
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```
|
|
-----BEGIN CERTIFICATE-----
|
|
MIICxDCCAaygAwIBAgIUY...
|
|
-----END CERTIFICATE-----
|
|
```
|
|
|
|
Content-Type: `application/x-pem-file`
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 404 | PKI not enabled or CA not initialized |
|
|
|
|
---
|
|
|
|
### POST /pki/issue
|
|
|
|
Issue a new client certificate (open registration).
|
|
|
|
**Request:**
|
|
```http
|
|
POST /pki/issue HTTP/1.1
|
|
Host: localhost:5000
|
|
Content-Type: application/json
|
|
|
|
```
|
|
|
|
**Response (201 Created):**
|
|
```json
|
|
{
|
|
"message": "Certificate issued",
|
|
"serial": "00000000000000000000000000000001",
|
|
"common_name": "alice",
|
|
"fingerprint_sha1": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3",
|
|
"created_at": 1700000000,
|
|
"expires_at": 1731536000,
|
|
"certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
|
|
"private_key_pem": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 400 | common_name required |
|
|
| 404 | PKI not enabled or CA not initialized |
|
|
| 500 | Certificate issuance failed |
|
|
|
|
**Security Notes:**
|
|
- Private key is generated server-side and returned in response
|
|
- Store the private key securely; it is not recoverable
|
|
- The certificate can be used with nginx, HAProxy, or curl for mTLS
|
|
|
|
---
|
|
|
|
### GET /pki/certs
|
|
|
|
List certificates (own certificates only).
|
|
|
|
**Request (authenticated):**
|
|
```http
|
|
GET /pki/certs HTTP/1.1
|
|
Host: localhost:5000
|
|
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"certificates": [
|
|
{
|
|
"serial": "00000000000000000000000000000001",
|
|
"common_name": "alice",
|
|
"fingerprint_sha1": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3",
|
|
"created_at": 1700000000,
|
|
"expires_at": 1731536000,
|
|
"status": "valid",
|
|
"issued_to": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
|
}
|
|
],
|
|
"count": 1
|
|
}
|
|
```
|
|
|
|
**Notes:**
|
|
- Anonymous users receive an empty list
|
|
- Users see certificates they issued or certificates that match their fingerprint
|
|
|
|
---
|
|
|
|
### POST /pki/revoke/{serial}
|
|
|
|
Revoke a certificate by serial number. Requires authentication and ownership.
|
|
|
|
**Request:**
|
|
```http
|
|
POST /pki/revoke/00000000000000000000000000000001 HTTP/1.1
|
|
Host: localhost:5000
|
|
X-SSL-Client-SHA1: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"message": "Certificate revoked",
|
|
"serial": "00000000000000000000000000000001"
|
|
}
|
|
```
|
|
|
|
**Errors:**
|
|
| Code | Description |
|
|
|------|-------------|
|
|
| 401 | Authentication required |
|
|
| 403 | Permission denied (not owner) |
|
|
| 404 | Certificate not found or PKI not enabled |
|
|
| 409 | Certificate already revoked |
|
|
|
|
**Authorization:**
|
|
- Must be authenticated
|
|
- Can revoke certificates you issued (issued_to matches your fingerprint)
|
|
- Can revoke your own certificate (fingerprint matches)
|