security: harden production configuration and routes
Run Tests on Branches / Detect Changes (push) Successful in 12s
Run Tests on Branches / Frontend Tests (push) Successful in 2m12s
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Successful in 2m2s
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Backend Tests (push) Successful in 2m8s

This commit is contained in:
Antoni Nuñez Romeu
2026-07-22 17:24:54 +02:00
parent f60af5f6b2
commit 849763896d
21 changed files with 638 additions and 68 deletions
+1
View File
@@ -1,6 +1,7 @@
APP_NAME=PIP - Pharmacy Integration Platform
APP_VERSION=0.1.0
DEBUG=false
NODE_ENV=development
DATABASE_URL=postgresql+asyncpg://pip:pip-secret@localhost:5432/pip
DATABASE_POOL_SIZE=20
+7 -14
View File
@@ -7,14 +7,14 @@ services:
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql+asyncpg://pip:${PG_PASSWORD:-pip-secret}@postgres:5432/pip
DATABASE_URL: postgresql+asyncpg://pip:${PG_PASSWORD:?PG_PASSWORD must be set}@postgres:5432/pip
REDIS_URL: redis://redis:6379/0
RABBITMQ_URL: amqp://pip:${RABBITMQ_PASSWORD:-pip-secret}@rabbitmq:5672/pip
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-change-me-in-production}
RABBITMQ_URL: amqp://pip:${RABBITMQ_PASSWORD:?RABBITMQ_PASSWORD must be set}@rabbitmq:5672/pip
JWT_SECRET_KEY: ${JWT_SECRET_KEY:?JWT_SECRET_KEY must be set}
NODE_ENV: production
LOG_LEVEL: INFO
LOG_JSON_FORMAT: "true"
CORS_ORIGINS: '["*"]'
CORS_ORIGINS: ${CORS_ORIGINS:?CORS_ORIGINS must be set}
OTEL_EXPORTER_OTLP_ENDPOINT: http://host.docker.internal:4317
OTEL_TRACES_ENABLED: "true"
depends_on:
@@ -45,9 +45,7 @@ services:
environment:
POSTGRES_DB: pip
POSTGRES_USER: pip
POSTGRES_PASSWORD: ${PG_PASSWORD:-pip-secret}
ports:
- "5432:5432"
POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD must be set}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
@@ -73,8 +71,6 @@ services:
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6380:6379"
volumes:
- redis_data:/data
healthcheck:
@@ -94,11 +90,8 @@ services:
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: pip
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-pip-secret}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?RABBITMQ_PASSWORD must be set}
RABBITMQ_DEFAULT_VHOST: pip
ports:
- "5672:5672"
- "15672:15672"
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
@@ -111,4 +104,4 @@ services:
volumes:
postgres_data:
redis_data:
rabbitmq_data:
rabbitmq_data:
+6 -13
View File
@@ -7,14 +7,14 @@ services:
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql+asyncpg://pip:${PG_PASSWORD:-pip-secret}@postgres:5432/pip
DATABASE_URL: postgresql+asyncpg://pip:${PG_PASSWORD:?PG_PASSWORD must be set}@postgres:5432/pip
REDIS_URL: redis://redis:6379/0
RABBITMQ_URL: amqp://pip:${RABBITMQ_PASSWORD:-pip-secret}@rabbitmq:5672/pip
JWT_SECRET_KEY: ${JWT_SECRET_KEY:-change-me-in-production}
RABBITMQ_URL: amqp://pip:${RABBITMQ_PASSWORD:?RABBITMQ_PASSWORD must be set}@rabbitmq:5672/pip
JWT_SECRET_KEY: ${JWT_SECRET_KEY:?JWT_SECRET_KEY must be set}
NODE_ENV: production
LOG_LEVEL: INFO
LOG_JSON_FORMAT: "true"
CORS_ORIGINS: '["*"]'
CORS_ORIGINS: ${CORS_ORIGINS:?CORS_ORIGINS must be set}
# Re-routed to the shared Grafana Alloy collector on srv84-macos.
# host.docker.internal resolves to the Docker host gateway from
# inside the container — works because this stack runs on the same
@@ -53,9 +53,7 @@ services:
environment:
POSTGRES_DB: pip
POSTGRES_USER: pip
POSTGRES_PASSWORD: ${PG_PASSWORD:-pip-secret}
ports:
- "5432:5432"
POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD must be set}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
@@ -81,8 +79,6 @@ services:
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "6380:6379"
volumes:
- redis_data:/data
healthcheck:
@@ -102,11 +98,8 @@ services:
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: pip
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-pip-secret}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?RABBITMQ_PASSWORD must be set}
RABBITMQ_DEFAULT_VHOST: pip
ports:
- "5672:5672"
- "15672:15672"
volumes:
- rabbitmq_data:/var/lib/rabbitmq
healthcheck:
@@ -1,3 +1,4 @@
from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -12,6 +13,7 @@ class Settings(BaseSettings):
APP_NAME: str = "PIP - Pharmacy Integration Platform"
APP_VERSION: str = "0.1.0"
DEBUG: bool = False
NODE_ENV: str = "development"
DATABASE_URL: str = "postgresql+asyncpg://pip:pip@localhost:5432/pip"
DATABASE_POOL_SIZE: int = 20
@@ -50,6 +52,23 @@ class Settings(BaseSettings):
HEALTH_CHECK_CACHE_TTL: int = 10
@model_validator(mode="after")
def validate_production_security(self):
if self.NODE_ENV.lower() != "production":
return self
if self.JWT_SECRET_KEY == "change-me-in-production":
raise ValueError("JWT_SECRET_KEY must be changed in production")
default_credentials = ("pip:pip@", "pip-secret")
if any(value in self.DATABASE_URL or value in self.RABBITMQ_URL for value in default_credentials):
raise ValueError("default database or broker credentials are not allowed in production")
if self.CORS_ALLOW_CREDENTIALS and "*" in self.CORS_ORIGINS:
raise ValueError("wildcard CORS_ORIGINS are not allowed with credentials in production")
return self
@property
def DATABASE_URL_SYNC(self) -> str:
return self.DATABASE_URL.replace("+asyncpg", "+psycopg2", 1)
@@ -0,0 +1,41 @@
import pytest
from src.infrastructure.config.settings import Settings
def test_production_rejects_placeholder_jwt_secret():
with pytest.raises(ValueError, match="JWT_SECRET_KEY"):
Settings(
NODE_ENV="production",
JWT_SECRET_KEY="change-me-in-production",
_env_file=None,
)
def test_production_rejects_default_database_and_broker_credentials():
with pytest.raises(ValueError, match="credentials"):
Settings(
NODE_ENV="production",
JWT_SECRET_KEY="a-real-test-secret",
DATABASE_URL="postgresql+asyncpg://pip:pip@localhost:5432/pip",
RABBITMQ_URL="amqp://pip:pip@localhost:5672/pip",
_env_file=None,
)
def test_production_rejects_wildcard_credentialed_cors():
with pytest.raises(ValueError, match="CORS"):
Settings(
NODE_ENV="production",
JWT_SECRET_KEY="a-real-test-secret",
DATABASE_URL="postgresql+asyncpg://pip:real-password@db:5432/pip",
RABBITMQ_URL="amqp://pip:real-password@rabbitmq:5672/pip",
CORS_ORIGINS=["*"],
CORS_ALLOW_CREDENTIALS=True,
_env_file=None,
)
def test_development_keeps_local_defaults_usable():
settings = Settings(_env_file=None)
assert settings.NODE_ENV == "development"