RouteForge v0.5.3-beta
Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR.
From 60242bd529d01710af12ff205c1ecd155d6002b6 Mon Sep 17 00:00:00 2001
From: DeepZone
- Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR. Modernes read-only Operator-Tool für Preflight Checks von ASN, Prefix, RPKI und Registry/IRR. {systemStatusError} RouteForge liefert nachvollziehbare Routing-Preflightchecks für technische Operator-Workflows. Version: v0.5.3-beta RouteForge liefert nachvollziehbare Routing-Preflightchecks für technische Operator-Workflows. Version: v0.5.4-beta
+
@@ -43,7 +43,7 @@ Routing changes often require fast but traceable checks across multiple external
## Current Alpha Status
-RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.5.3-beta**.
+RouteForge is a **functional beta** release with production-like workflows for read-only validation and demo usage. Current release target: **v0.5.4-beta**.
## Quickstart with Docker Compose
@@ -248,3 +248,13 @@ npm run build
## License
RouteForge is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).
+
+
+## Security baseline
+
+For production polish and selfhosting hardening guidance, see:
+
+- `docs/operations/security.md`
+- `docs/operations/release-checklist.md`
+
+In the standard Docker setup, API calls are same-origin via frontend nginx (`/api` proxy). CORS is primarily needed for split frontend/backend deployments.
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 7902cfc..7321812 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,29 @@
# Release Notes
+## v0.5.4-beta
+
+**Production Polish & Security Baseline**
+
+### Highlights
+
+- Security headers for frontend Nginx
+- Improved same-origin proxy security guidance
+- Container hardening review
+- Safer environment and secrets documentation
+- Logging baseline improvements
+- Security operations documentation
+- Release checklist documentation
+- Optional security warnings in system status
+
+### Known limitations
+
+- No authentication yet
+- No multi-user support yet
+- No advanced RBAC yet
+- No automated release pipeline yet
+
+---
+
## Licensing
diff --git a/backend/Dockerfile b/backend/Dockerfile
index b454f49..f9f5fd0 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,9 +1,20 @@
FROM python:3.12-slim
+
WORKDIR /app
+
+RUN useradd --create-home --shell /usr/sbin/nologin routeforge
+
COPY pyproject.toml ./
COPY app ./app
COPY alembic ./alembic
COPY alembic.ini ./alembic.ini
-RUN pip install --no-cache-dir .
+
+RUN pip install --no-cache-dir . \
+ && mkdir -p /app/data \
+ && chown -R routeforge:routeforge /app
+
+USER routeforge
+
EXPOSE 8000
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/backend/app/api/routes_health.py b/backend/app/api/routes_health.py
index a884714..66fbd54 100644
--- a/backend/app/api/routes_health.py
+++ b/backend/app/api/routes_health.py
@@ -8,4 +8,4 @@
@router.get('/health')
def health() -> dict:
- return {"status": "ok", "version": "v0.5.3-beta", "database": get_database_status(engine).get("status", "unknown")}
+ return {"status": "ok", "version": "v0.5.4-beta", "database": get_database_status(engine).get("status", "unknown")}
diff --git a/backend/app/api/routes_system.py b/backend/app/api/routes_system.py
index 5bf99d3..de8514d 100644
--- a/backend/app/api/routes_system.py
+++ b/backend/app/api/routes_system.py
@@ -11,7 +11,7 @@
def system_info():
return {
'name': 'RouteForge',
- 'version': 'v0.5.3-beta',
+ 'version': 'v0.5.4-beta',
'demo_mode': settings.demo_mode,
'read_only': True,
'data_sources': ['RIPEstat', 'RIPEstat Whois/Registry'],
diff --git a/backend/app/config.py b/backend/app/config.py
index 851dd0c..8e0adff 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -13,8 +13,10 @@ class Settings(BaseSettings):
ripestat_retry_backoff_seconds: float = 0.5
ripestat_use_stale_cache_on_error: bool = True
cache_ttl_seconds: int = Field(default=900, validation_alias="RIPESTAT_CACHE_TTL_SECONDS")
- cors_origins: str = "http://192.168.58.167:3000,http://127.0.0.1:3000"
+ cors_origins: str = "http://localhost:3000,http://127.0.0.1:3000"
demo_mode: bool = Field(default=False, validation_alias="ROUTEFORGE_DEMO_MODE")
+ log_level: str = "INFO"
+ postgres_password: str = Field(default="", validation_alias="POSTGRES_PASSWORD")
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
diff --git a/backend/app/core/system_status.py b/backend/app/core/system_status.py
index c7ba5cd..38e3460 100644
--- a/backend/app/core/system_status.py
+++ b/backend/app/core/system_status.py
@@ -102,11 +102,18 @@ def get_database_status(engine: Engine | None) -> dict:
return payload
+def _security_warnings() -> list[str]:
+ warnings: list[str] = []
+ if settings.postgres_password == "change-me":
+ warnings.append("POSTGRES_PASSWORD uses the default example value. Change it before production use.")
+ return warnings
+
+
def build_system_status(engine: Engine | None) -> dict:
return {
"status": "ok",
"name": settings.app_name,
- "version": "v0.5.3-beta",
+ "version": "v0.5.4-beta",
"read_only": True,
"mode": "demo" if settings.demo_mode else "live",
"demo_mode": settings.demo_mode,
@@ -119,6 +126,7 @@ def build_system_status(engine: Engine | None) -> dict:
"retry_backoff_seconds": settings.ripestat_retry_backoff_seconds,
"use_stale_cache_on_error": settings.ripestat_use_stale_cache_on_error,
},
+ "security_warnings": _security_warnings(),
"features": {
"asn_check": True,
"prefix_check": True,
diff --git a/backend/app/main.py b/backend/app/main.py
index 01ca29c..46c4e77 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -1,4 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
+import logging
+
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@@ -7,9 +9,13 @@
from app.api.routes_reports import router as reports_router
from app.api.routes_system import router as system_router
from app.config import settings
+from app.core.system_status import database_type_from_url
from app.database import Base, engine
-app = FastAPI(title="RouteForge", version="0.5.3")
+logging.basicConfig(level=getattr(logging, settings.log_level.upper(), logging.INFO))
+logger = logging.getLogger("routeforge")
+
+app = FastAPI(title="RouteForge", version="0.5.4")
app.add_middleware(
CORSMiddleware,
@@ -25,6 +31,17 @@ def startup() -> None:
if settings.database_url.startswith("sqlite"):
Base.metadata.create_all(bind=engine)
+ logger.info(
+ "RouteForge startup: version=%s mode=%s database_type=%s read_only=%s ripestat_cache_ttl=%s ripestat_timeout=%s ripestat_retries=%s",
+ app.version,
+ "DEMO" if settings.demo_mode else "LIVE",
+ database_type_from_url(settings.database_url),
+ True,
+ settings.cache_ttl_seconds,
+ settings.ripestat_timeout_seconds,
+ settings.ripestat_max_retries,
+ )
+
app.include_router(health_router)
app.include_router(checks_router)
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 1a6846f..b6d164b 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "routeforge-backend"
-version = "0.5.3"
+version = "0.5.4"
description = "RouteForge backend"
license = "AGPL-3.0-or-later"
requires-python = ">=3.12"
diff --git a/backend/tests/test_api_smoke.py b/backend/tests/test_api_smoke.py
index bee9d36..11c623b 100644
--- a/backend/tests/test_api_smoke.py
+++ b/backend/tests/test_api_smoke.py
@@ -157,7 +157,7 @@ def test_system_status_endpoint() -> None:
response = client.get('/api/system/status')
assert response.status_code == 200
payload = response.json()
- assert payload.get('version') == 'v0.5.3-beta'
+ assert payload.get('version') == 'v0.5.4-beta'
assert payload.get('read_only') is True
assert payload.get('database', {}).get('status')
assert payload.get('ripestat', {}).get('cache_ttl_seconds') is not None
diff --git a/docs/operations/release-checklist.md b/docs/operations/release-checklist.md
new file mode 100644
index 0000000..43c83bc
--- /dev/null
+++ b/docs/operations/release-checklist.md
@@ -0,0 +1,30 @@
+# Release Checklist
+
+## Before release
+
+- `git pull`
+- Run backend tests (`cd backend && pytest -q`)
+- Run frontend production build (`cd frontend && npm run build`)
+- Validate Compose (`docker compose config` and production config)
+- Check system status endpoint (`/api/system/status`)
+- Check migration status visibility (`database.schema_version`, `migration_head`, `migration_status`)
+
+## Tagging
+
+- `git tag -a vX.Y.Z[-beta] -m "RouteForge vX.Y.Z[-beta]"`
+- `git push origin vX.Y.Z[-beta]`
+
+## GitHub Release
+
+- Use release title matching the version and sprint scope
+- Enable **prerelease** checkbox for beta versions
+- Include structured release notes with highlights and known limitations
+
+## Post-release smoke test
+
+- `GET /health`
+- `GET /api/system/info`
+- `GET /api/system/status`
+- Basic ASN check
+- Basic Prefix check
+- Export summary from report history
diff --git a/docs/operations/security.md b/docs/operations/security.md
new file mode 100644
index 0000000..9156458
--- /dev/null
+++ b/docs/operations/security.md
@@ -0,0 +1,51 @@
+# Security Baseline
+
+## Read-only safety model
+
+RouteForge is read-only by design. It validates routing state and preflight conditions but does not push configuration changes to external systems.
+
+## Reverse proxy / HTTPS recommendation
+
+Use RouteForge behind a reverse proxy with TLS termination in production (for example Caddy, Traefik, or Nginx). Keep plain HTTP only for local lab usage.
+
+## Secrets and .env handling
+
+- Copy `.env.example` to `.env` and set real values before production use.
+- Never commit `.env` files to git.
+- Change `POSTGRES_PASSWORD` from the example default.
+
+## Default password warning
+
+`/api/system/status` exposes `security_warnings` when a risky default is detected (currently: `POSTGRES_PASSWORD=change-me`). No secret values are returned.
+
+## CORS guidance
+
+In the standard Docker setup, frontend and API are same-origin via nginx (`/api` proxy). CORS is mainly relevant for split deployments where frontend and backend are served from different origins.
+
+## Security headers
+
+Frontend nginx sets baseline headers:
+
+- `X-Content-Type-Options: nosniff`
+- `X-Frame-Options: DENY`
+- `Referrer-Policy: no-referrer-when-downgrade`
+- `Permissions-Policy: geolocation=(), microphone=(), camera=()`
+- A conservative initial Content-Security-Policy for SPA usage
+
+## Container hardening
+
+- Backend container runs as non-root user `routeforge`.
+- Backend image keeps only runtime-relevant files.
+- Frontend image is multi-stage (build + runtime).
+- Nginx container still runs with default upstream behavior; strict non-root nginx runtime can be added later as a dedicated hardening step.
+
+## What RouteForge does not do
+
+- no ROA creation
+- no RIPE DB writes
+- no router deployment
+
+## Current limitations
+
+- no authentication yet
+- no multi-user support yet
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
index 3682c14..51c3ab1 100644
--- a/frontend/nginx.conf
+++ b/frontend/nginx.conf
@@ -5,6 +5,12 @@ server {
root /usr/share/nginx/html;
index index.html;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-Frame-Options "DENY" always;
+ add_header Referrer-Policy "no-referrer-when-downgrade" always;
+ add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
+ add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none';" always;
+
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_http_version 1.1;
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 8b0e1e0..8080088 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,13 @@
{
"name": "routeforge-frontend",
- "version": "0.5.3",
+ "version": "0.5.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "routeforge-frontend",
- "version": "0.5.3",
+ "version": "0.5.4",
+ "license": "AGPL-3.0-or-later",
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
diff --git a/frontend/package.json b/frontend/package.json
index 3d672fb..7a4c616 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "routeforge-frontend",
- "version": "0.5.3",
+ "version": "0.5.4",
"private": true,
"license": "AGPL-3.0-or-later",
"type": "module",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 975c18a..5375fff 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -22,14 +22,14 @@ export default function App() {
getSystemStatus().then((payload) => { setSystemStatus(payload); setSystemStatusError('') }).catch(() => setSystemStatusError('System status could not be loaded.'))
}, [])
- const systemLine = useMemo(() => system ? `${system.name} ${system.version} · mode=${system.demo_mode ? 'DEMO' : 'LIVE'} · read_only=${String(system.read_only)}` : 'RouteForge v0.5.3-beta · read-only preflight checks', [system])
+ const systemLine = useMemo(() => system ? `${system.name} ${system.version} · mode=${system.demo_mode ? 'DEMO' : 'LIVE'} · read_only=${String(system.read_only)}` : 'RouteForge v0.5.4-beta · read-only preflight checks', [system])
const title = { dashboard: 'Dashboard', asn: 'ASN Check', prefix: 'Prefix Check', preflight: 'Preflight Check', reports: 'Reports', system: 'System Status', about: 'About RouteForge' }[active]
const proxyStatus = systemStatusError ? 'ERROR' : 'OK'
const migrationStatus = systemStatus?.database?.migration_status || 'unknown'
return
RouteForge v0.5.3-beta
RouteForge v0.5.4-beta
System Health
{systemStatusError ? Security Warnings
{systemStatus.security_warnings.map((warning) =>
RIPEstat Settings
{JSON.stringify(systemStatus.ripestat, null, 2)}Features
{JSON.stringify(systemStatus.features, null, 2)}