Secret test at startup
Build LinkLog Development Image / development-image (push) Successful in 11s

This commit is contained in:
2026-08-26 20:54:00 +02:00
parent 04b8a5a8b9
commit ffb12a36b5
7 changed files with 64 additions and 5 deletions
+2
View File
@@ -179,6 +179,8 @@ cp .env.example .env
Edit `.env` and replace `LINKLOG_SECRET_KEY` with a long random value. Docker Compose automatically reads `.env` from the repository root. The committed `.env.example` contains safe defaults and placeholders; the real `.env` is ignored by Git.
When `APP_ENV=production`, application startup fails closed unless `LINKLOG_SECRET_KEY` is a non-default high-entropy value of at least 32 characters and `LINKLOG_DATA_ENCRYPTION_KEY` is a valid Fernet key. Development mode may use local defaults, but production secrets should come from a protected secret mechanism.
The main configurable values are:
| Variable | Purpose | Default |
+8 -4
View File
@@ -90,14 +90,18 @@ The application should remain behind the production reverse proxy, with real DNS
### SA-005: Production secret fallback is not fail-closed
**Severity:** Medium
**Evidence:** `Settings.secret_key` defaults to `dev-secret-key-change-me`, and `LINKLOG_DATA_ENCRYPTION_KEY` is validated when encryption is used rather than fully validated during startup.
**Severity:** Medium, remediated in current worktree
**Evidence before remediation:** `Settings.secret_key` defaulted to `dev-secret-key-change-me`, and `LINKLOG_DATA_ENCRYPTION_KEY` was validated when encryption was used rather than fully validated during startup.
**Impact:** A deployment that omits required configuration can start with a known development secret or fail only when a protected feature is exercised. This creates dangerous configuration drift and complicates incident response.
**Recommendation:** During startup, reject a missing or known development `LINKLOG_SECRET_KEY` when `APP_ENV=production`; validate minimum length and entropy. Require and validate the encryption key before startup when encrypted data exists or production requires it. Add configuration tests for fail-closed production behavior.
**Current state:** `validate_configuration()` runs before FastAPI app construction. In production it rejects a missing or known development `LINKLOG_SECRET_KEY`, application secrets shorter than 32 characters or with insufficient character diversity, and missing `LINKLOG_DATA_ENCRYPTION_KEY`. Any supplied encryption key is checked as a valid Fernet key. Focused tests cover rejection and acceptance paths.
**Priority:** Medium.
**Residual impact:** Secret strength checks are pragmatic length/diversity checks rather than a full entropy estimator. Secret provisioning, rotation, and protected storage remain operational requirements.
**Recommendation:** Keep production startup fail-closed, provision secrets through a protected secret manager, rotate them after suspected disclosure, and consider a stronger entropy policy if deployment requirements warrant it.
**Priority:** Completed in code; secret provisioning and rotation remain.
### SA-006: Login and reset-mail throttling are not distributed or atomic
+6
View File
@@ -1,5 +1,11 @@
# Chat Log
### User
Implement SA-005: during startup reject missing or known development `LINKLOG_SECRET_KEY` in production, validate minimum length and entropy, require and validate the encryption key, and add fail-closed configuration tests.
### Assistant outcome
Added startup configuration validation before FastAPI app construction. Production now rejects missing/default/weak application secrets and missing or invalid Fernet encryption keys, while development defaults remain available. Added configuration tests for rejection and valid production settings, and updated SA-005 documentation.
### User
For SA-002, log technical details server-side with a request/correlation ID, return a stable public message with a short reference ID, redact secrets, and test that exception text is absent from responses.
+1
View File
@@ -202,6 +202,7 @@
195. Update SA-012 and README for the implemented refresh-token lifecycle, revocation behavior, and refresh endpoint.
196. Remedy SA-001: migrate logout from JSON token transport to the Authorization bearer header.
197. Implement SA-002: replace raw infrastructure errors with redacted server-side logging, request IDs, and stable public reference messages.
198. Implement SA-005: reject missing/default/weak production secrets at startup and validate the Fernet encryption key, with configuration tests.
## Future entries
+19
View File
@@ -5,6 +5,8 @@ from dataclasses import dataclass
import os
from pathlib import Path
from cryptography.fernet import Fernet
BASE_DIR = Path(__file__).resolve().parent.parent.parent
DB_PATH = BASE_DIR / 'data' / 'linklog.db'
@@ -20,6 +22,7 @@ def normalize_public_url(value: str) -> str:
@dataclass
class Settings:
app_env: str = os.getenv('APP_ENV', 'development').lower()
app_name: str = os.getenv('LINKLOG_APP_NAME', 'LinkLog')
version: str = os.getenv('LINKLOG_VERSION', '0.1.0')
database_url: str = os.getenv('LINKLOG_DATABASE_URL', f'sqlite:///{DB_PATH}')
@@ -59,3 +62,19 @@ class Settings:
settings = Settings()
def validate_configuration(values: Settings) -> None:
if values.app_env == 'production':
if not values.secret_key or values.secret_key == 'dev-secret-key-change-me':
raise RuntimeError('LINKLOG_SECRET_KEY must be configured in production')
if len(values.secret_key) < 32 or len(set(values.secret_key)) < 12:
raise RuntimeError('LINKLOG_SECRET_KEY must be at least 32 characters with sufficient entropy')
if not values.data_encryption_key:
raise RuntimeError('LINKLOG_DATA_ENCRYPTION_KEY must be configured in production')
if values.data_encryption_key:
try:
Fernet(values.data_encryption_key.encode('ascii'))
except (ValueError, UnicodeEncodeError) as error:
raise RuntimeError('LINKLOG_DATA_ENCRYPTION_KEY must be a valid Fernet key') from error
+2 -1
View File
@@ -18,11 +18,12 @@ from backend.app.api.public import router as public_router
from backend.app.api.setup import router as setup_router
from backend.app.api.setup import has_administrator
from backend.app.api.user_config import router as user_config_router
from backend.app.core.config import settings
from backend.app.core.config import settings, validate_configuration
from backend.app.database import AVATARS_DIR
from backend.app.services.link_service import get_public_profile, list_public_links
logging.basicConfig(level=getattr(logging, settings.log_level, logging.INFO))
validate_configuration(settings)
app = FastAPI(title='LinkLog API', version=settings.version)
+26
View File
@@ -1,10 +1,36 @@
import re
from pathlib import Path
import pytest
from backend.app.core.config import Settings, validate_configuration
ROOT = Path(__file__).resolve().parents[2]
def test_production_configuration_rejects_missing_or_default_secret():
with pytest.raises(RuntimeError, match='LINKLOG_SECRET_KEY'):
validate_configuration(Settings(app_env='production', secret_key='', data_encryption_key=''))
with pytest.raises(RuntimeError, match='LINKLOG_SECRET_KEY'):
validate_configuration(Settings(app_env='production', secret_key='dev-secret-key-change-me', data_encryption_key=''))
def test_production_configuration_rejects_weak_or_missing_encryption_key():
with pytest.raises(RuntimeError, match='entropy'):
validate_configuration(Settings(app_env='production', secret_key='A' * 32, data_encryption_key=''))
with pytest.raises(RuntimeError, match='DATA_ENCRYPTION_KEY'):
validate_configuration(Settings(app_env='production', secret_key='A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6', data_encryption_key='invalid'))
def test_production_configuration_accepts_strong_secrets():
values = Settings(
app_env='production',
secret_key='A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6',
data_encryption_key='L5M4sQYVjD1N7pT2Xk8R0aBcDeFgHiJkLmNoPqRsTuV=',
)
validate_configuration(values)
def test_production_compose_configuration_matches_settings_environment_keys():
compose = (ROOT / 'docker-compose.yml').read_text()
settings = (ROOT / 'backend' / 'app' / 'core' / 'config.py').read_text()