fix: share PoW HMAC secret across gunicorn workers
Some checks failed
CI / Lint & Format (push) Failing after 29s
CI / Unit Tests (push) Has been skipped
CI / Memory Leak Check (push) Has been skipped
CI / Fuzz Testing (push) Has been skipped
CI / SBOM Generation (push) Has been skipped
CI / Security Scan (push) Successful in 34s
CI / Security Tests (push) Has been skipped
CI / Advanced Security Tests (push) Has been skipped
CI / Build & Push Image (push) Has been skipped
CI / Harbor Vulnerability Scan (push) Has been skipped

get_pow_secret() generated a random secret per process, so challenges
signed by worker A failed verification on worker B (~90% failure rate
with 2 workers).  Persist a file-backed secret to data/.pow_secret
using O_EXCL for atomic creation.  FLASKPASTE_POW_SECRET env var
still takes priority when configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-23 21:32:06 +01:00
parent ca1cbd6e73
commit e7c278be0d
3 changed files with 33 additions and 12 deletions

View File

@@ -1,12 +1,35 @@
"""Application configuration."""
import os
import secrets as _secrets
from pathlib import Path
# Application version
VERSION = "1.5.2"
def _get_pow_secret(data_dir: Path) -> str:
"""Read or create a shared PoW signing secret.
Ensures all gunicorn workers use the same HMAC key by persisting
the generated secret to a file in the data directory. Uses O_EXCL
for atomic creation so concurrent workers converge on one secret.
"""
secret_file = data_dir / ".pow_secret"
try:
return secret_file.read_text().strip()
except FileNotFoundError:
data_dir.mkdir(parents=True, exist_ok=True)
secret = _secrets.token_hex(32)
try:
fd = os.open(str(secret_file), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
os.write(fd, secret.encode())
os.close(fd)
return secret
except FileExistsError:
return secret_file.read_text().strip()
class Config:
"""Base configuration."""
@@ -70,8 +93,12 @@ class Config:
# Difficulty is number of leading zero bits required in hash (0 = disabled).
POW_DIFFICULTY = int(os.environ.get("FLASKPASTE_POW_DIFFICULTY", "20"))
POW_CHALLENGE_TTL = int(os.environ.get("FLASKPASTE_POW_TTL", "300")) # 5 minutes
# Secret key for signing challenges (auto-generated if not set)
POW_SECRET = os.environ.get("FLASKPASTE_POW_SECRET", "")
# Secret key for signing challenges.
# When not set via env, a shared secret is persisted to data/.pow_secret
# so all gunicorn workers use the same HMAC key.
POW_SECRET = os.environ.get("FLASKPASTE_POW_SECRET", "") or _get_pow_secret(
Path(os.environ.get("FLASKPASTE_DB", Path(__file__).parent.parent / "data" / "pastes.db")).parent
)
# Registration PoW difficulty (higher than paste creation for security)
REGISTER_POW_DIFFICULTY = int(os.environ.get("FLASKPASTE_REGISTER_POW", "24"))
@@ -159,6 +186,7 @@ class TestingConfig(Config):
TESTING = True
DATABASE = ":memory:"
POW_SECRET = "test-pow-secret"
# Relaxed dedup for testing (100 per second window)
CONTENT_DEDUP_WINDOW = 1