Compare commits
4 Commits
3932aa56f7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 597579376f | |||
| f25792b8d8 | |||
| 6093c5dea8 | |||
| 84bc7b0384 |
@@ -12,6 +12,7 @@ LOG_LEVEL=INFO
|
||||
# Core Database (internal metadata DB)
|
||||
# ------------------------------
|
||||
# Database that stores users, targets, metrics, query stats, and audit logs.
|
||||
# DEV default only. Use strong unique credentials in production.
|
||||
DB_NAME=nexapg
|
||||
DB_USER=nexapg
|
||||
DB_PASSWORD=nexapg
|
||||
@@ -23,7 +24,7 @@ DB_PORT=5433
|
||||
# ------------------------------
|
||||
# Host port mapped to backend container port 8000.
|
||||
BACKEND_PORT=8000
|
||||
# JWT signing secret. Change this in every non-local environment.
|
||||
# JWT signing secret. Never hardcode in source. Rotate regularly.
|
||||
JWT_SECRET_KEY=change_this_super_secret
|
||||
JWT_ALGORITHM=HS256
|
||||
# Access token lifetime in minutes.
|
||||
@@ -31,6 +32,7 @@ JWT_ACCESS_TOKEN_MINUTES=15
|
||||
# Refresh token lifetime in minutes (10080 = 7 days).
|
||||
JWT_REFRESH_TOKEN_MINUTES=10080
|
||||
# Key used to encrypt monitored target passwords at rest.
|
||||
# Never hardcode in source. Rotate with re-encryption plan.
|
||||
# Generate with:
|
||||
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||
ENCRYPTION_KEY=REPLACE_WITH_FERNET_KEY
|
||||
@@ -56,5 +58,5 @@ INIT_ADMIN_PASSWORD=ChangeMe123!
|
||||
# ------------------------------
|
||||
# Frontend
|
||||
# ------------------------------
|
||||
# Host port mapped to frontend container port 80.
|
||||
# Host port mapped to frontend container port 8080.
|
||||
FRONTEND_PORT=5173
|
||||
|
||||
35
.github/workflows/proxy-profile-validation.yml
vendored
Normal file
35
.github/workflows/proxy-profile-validation.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Proxy Profile Validation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "master", "development"]
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "ops/profiles/prod/**"
|
||||
- "ops/scripts/validate_proxy_profile.sh"
|
||||
- ".github/workflows/proxy-profile-validation.yml"
|
||||
- "README.md"
|
||||
- ".env.example"
|
||||
- "ops/.env.example"
|
||||
pull_request:
|
||||
paths:
|
||||
- "frontend/**"
|
||||
- "ops/profiles/prod/**"
|
||||
- "ops/scripts/validate_proxy_profile.sh"
|
||||
- ".github/workflows/proxy-profile-validation.yml"
|
||||
- "README.md"
|
||||
- ".env.example"
|
||||
- "ops/.env.example"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Validate proxy profile and mixed-content guardrails
|
||||
run: bash ops/scripts/validate_proxy_profile.sh
|
||||
32
README.md
32
README.md
@@ -20,8 +20,10 @@ It combines FastAPI, React, and PostgreSQL in a Docker Compose stack with RBAC,
|
||||
- [API Error Format](#api-error-format)
|
||||
- [`pg_stat_statements` Requirement](#pg_stat_statements-requirement)
|
||||
- [Reverse Proxy / SSL Guidance](#reverse-proxy--ssl-guidance)
|
||||
- [Production Proxy Profile](#production-proxy-profile)
|
||||
- [PostgreSQL Compatibility Smoke Test](#postgresql-compatibility-smoke-test)
|
||||
- [Dependency Exception Flow](#dependency-exception-flow)
|
||||
- [Secret Management (Production)](#secret-management-production)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Security Notes](#security-notes)
|
||||
|
||||
@@ -372,6 +374,21 @@ For production, serve frontend and API under the same public origin via reverse
|
||||
|
||||
This prevents mixed-content and CORS issues.
|
||||
|
||||
## Production Proxy Profile
|
||||
|
||||
A secure, repeatable production profile is included:
|
||||
|
||||
- `ops/profiles/prod/.env.production.example`
|
||||
- `ops/profiles/prod/nginx/nexapg.conf`
|
||||
- `docs/deployment/proxy-production-profile.md`
|
||||
|
||||
Highlights:
|
||||
|
||||
- explicit CORS recommendations per environment (`dev`, `staging`, `prod`)
|
||||
- required reverse-proxy header forwarding for backend context
|
||||
- API path forwarding (`/api/` -> backend)
|
||||
- mixed-content prevention guidance for HTTPS deployments
|
||||
|
||||
## PostgreSQL Compatibility Smoke Test
|
||||
|
||||
Run manually against one DSN:
|
||||
@@ -398,6 +415,19 @@ Python dependency vulnerabilities are enforced by CI via `pip-audit`.
|
||||
- Full process and required metadata are documented in:
|
||||
- `docs/security/dependency-exceptions.md`
|
||||
|
||||
## Secret Management (Production)
|
||||
|
||||
Secret handling guidance is documented in:
|
||||
|
||||
- `docs/security/secret-management.md`
|
||||
|
||||
It includes:
|
||||
|
||||
- secure handling for `JWT_SECRET_KEY`, `ENCRYPTION_KEY`, `DB_PASSWORD`, and SMTP credentials
|
||||
- clear **Do / Don't** rules
|
||||
- recommended secret provider patterns (Vault/cloud/orchestrator/CI injection)
|
||||
- practical rotation basics and operational checklist
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backend container keeps restarting during `make migrate`
|
||||
@@ -432,3 +462,5 @@ Set target `sslmode` to `disable` (or correct SSL config on target DB).
|
||||
- RBAC enforced on protected endpoints
|
||||
- Audit logs for critical actions
|
||||
- Collector error logging includes throttling to reduce repeated noise
|
||||
- Production secret handling and rotation guidance:
|
||||
- `docs/security/secret-management.md`
|
||||
|
||||
@@ -2,7 +2,7 @@ from functools import lru_cache
|
||||
from pydantic import field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
NEXAPG_VERSION = "0.2.2"
|
||||
NEXAPG_VERSION = "0.2.4"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
|
||||
78
docs/deployment/proxy-production-profile.md
Normal file
78
docs/deployment/proxy-production-profile.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Production Proxy Profile (HTTPS)
|
||||
|
||||
This profile defines a secure and repeatable NexaPG deployment behind a reverse proxy.
|
||||
|
||||
## Included Profile Files
|
||||
|
||||
- `ops/profiles/prod/.env.production.example`
|
||||
- `ops/profiles/prod/nginx/nexapg.conf`
|
||||
|
||||
## CORS Recommendations by Environment
|
||||
|
||||
| Environment | Recommended `CORS_ORIGINS` | Notes |
|
||||
|---|---|---|
|
||||
| `dev` | `*` or local explicit origins | `*` is acceptable only for local/dev usage. |
|
||||
| `staging` | Exact staging UI origins | Example: `https://staging-monitor.example.com` |
|
||||
| `prod` | Exact production UI origin(s) only | No wildcard; use comma-separated HTTPS origins if needed. |
|
||||
|
||||
Examples:
|
||||
|
||||
```env
|
||||
# dev only
|
||||
CORS_ORIGINS=*
|
||||
|
||||
# staging
|
||||
CORS_ORIGINS=https://staging-monitor.example.com
|
||||
|
||||
# prod
|
||||
CORS_ORIGINS=https://monitor.example.com
|
||||
```
|
||||
|
||||
## Reverse Proxy Requirements
|
||||
|
||||
For stable auth, CORS, and request context handling, forward these headers to backend:
|
||||
|
||||
- `Host`
|
||||
- `X-Real-IP`
|
||||
- `X-Forwarded-For`
|
||||
- `X-Forwarded-Proto`
|
||||
- `X-Forwarded-Host`
|
||||
- `X-Forwarded-Port`
|
||||
|
||||
Also forward API paths:
|
||||
|
||||
- `/api/` -> backend service (`:8000`)
|
||||
|
||||
## Mixed-Content Prevention
|
||||
|
||||
NexaPG frontend is designed to avoid mixed-content in HTTPS mode:
|
||||
|
||||
- Build/runtime default API base is relative (`/api/v1`)
|
||||
- `frontend/src/api.js` upgrades `http` API URL to `https` when page runs on HTTPS
|
||||
|
||||
Recommended production setting:
|
||||
|
||||
```env
|
||||
VITE_API_URL=/api/v1
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
1. Open app over HTTPS and verify:
|
||||
- login request is `https://.../api/v1/auth/login`
|
||||
- no browser mixed-content errors in console
|
||||
2. Verify CORS behavior:
|
||||
- allowed origin works
|
||||
- unknown origin is blocked
|
||||
3. Verify backend receives forwarded protocol:
|
||||
- proxied responses succeed with no redirect/proto issues
|
||||
|
||||
## CI Validation
|
||||
|
||||
`Proxy Profile Validation` workflow runs static guardrail checks:
|
||||
|
||||
- relative `VITE_API_URL` default
|
||||
- required API proxy path in frontend NGINX config
|
||||
- required forwarded headers
|
||||
- HTTPS mixed-content guard in frontend API resolver
|
||||
- production profile forbids wildcard CORS
|
||||
74
docs/security/secret-management.md
Normal file
74
docs/security/secret-management.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Secret Management (Production)
|
||||
|
||||
This guide defines secure handling for NexaPG secrets in production deployments.
|
||||
|
||||
## In Scope Secrets
|
||||
|
||||
- `JWT_SECRET_KEY`
|
||||
- `ENCRYPTION_KEY`
|
||||
- `DB_PASSWORD`
|
||||
- SMTP credentials (configured in Admin Settings, encrypted at rest)
|
||||
|
||||
## Do / Don't
|
||||
|
||||
## Do
|
||||
|
||||
- Use an external secret source (Vault, cloud secret manager, orchestrator secrets, or CI/CD secret injection).
|
||||
- Keep secrets out of Git history and out of image layers.
|
||||
- Use strong random values:
|
||||
- JWT secret: at least 32+ bytes random
|
||||
- Fernet key: generated via `Fernet.generate_key()`
|
||||
- Restrict access to runtime secrets (least privilege).
|
||||
- Rotate secrets on schedule and on incident.
|
||||
- Store production `.env` with strict permissions if file-based injection is used:
|
||||
- owner-only read/write (e.g., `chmod 600 .env`)
|
||||
- Audit who can read/update secrets in your deployment platform.
|
||||
|
||||
## Don't
|
||||
|
||||
- Do **not** hardcode secrets in source code.
|
||||
- Do **not** commit `.env` with real values.
|
||||
- Do **not** bake production secrets into Dockerfiles or image build args.
|
||||
- Do **not** share secrets in tickets, chat logs, or CI console output.
|
||||
- Do **not** reuse the same secrets between environments.
|
||||
|
||||
## Recommended Secret Providers
|
||||
|
||||
Pick one of these models:
|
||||
|
||||
1. Platform/Cloud secrets
|
||||
- AWS Secrets Manager
|
||||
- Azure Key Vault
|
||||
- Google Secret Manager
|
||||
2. HashiCorp Vault
|
||||
3. CI/CD secret injection
|
||||
- Inject as runtime env vars during deployment
|
||||
4. Docker/Kubernetes secrets
|
||||
- Prefer secret mounts or orchestrator-native secret stores
|
||||
|
||||
If you use plain `.env` files, treat them as sensitive artifacts and protect at OS and backup level.
|
||||
|
||||
## Rotation Basics
|
||||
|
||||
Minimum baseline:
|
||||
|
||||
1. `JWT_SECRET_KEY`
|
||||
- Rotate on schedule (e.g., quarterly) and immediately after compromise.
|
||||
- Expect existing sessions/tokens to become invalid after rotation.
|
||||
2. `ENCRYPTION_KEY`
|
||||
- Rotate with planned maintenance.
|
||||
- Re-encrypt stored encrypted values (target passwords, SMTP password) during key transition.
|
||||
3. `DB_PASSWORD`
|
||||
- Rotate service account credentials regularly.
|
||||
- Apply password changes in DB and deployment config atomically.
|
||||
4. SMTP credentials
|
||||
- Use dedicated sender account/app password.
|
||||
- Rotate regularly and after provider-side security alerts.
|
||||
|
||||
## Operational Checklist
|
||||
|
||||
- [ ] No production secret in repository files.
|
||||
- [ ] No production secret in container image metadata or build args.
|
||||
- [ ] Runtime secret source documented for your environment.
|
||||
- [ ] Secret rotation owner and schedule defined.
|
||||
- [ ] Incident runbook includes emergency rotation steps.
|
||||
@@ -10,7 +10,9 @@ RUN npm run build
|
||||
FROM nginx:1-alpine-slim
|
||||
RUN apk upgrade --no-cache \
|
||||
&& mkdir -p /var/cache/nginx /var/run /var/log/nginx /tmp/nginx \
|
||||
&& chown -R nginx:nginx /var/cache/nginx /var/run /var/log/nginx /tmp/nginx
|
||||
&& chown -R nginx:nginx /var/cache/nginx /var/run /var/log/nginx /tmp/nginx \
|
||||
&& sed -i 's#pid[[:space:]]\+/run/nginx.pid;#pid /tmp/nginx/nginx.pid;#' /etc/nginx/nginx.conf \
|
||||
&& sed -i 's#pid[[:space:]]\+/var/run/nginx.pid;#pid /tmp/nginx/nginx.pid;#' /etc/nginx/nginx.conf
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
USER 101
|
||||
|
||||
@@ -12,6 +12,7 @@ LOG_LEVEL=INFO
|
||||
# Core Database (internal metadata DB)
|
||||
# ------------------------------
|
||||
# Database that stores users, targets, metrics, query stats, and audit logs.
|
||||
# DEV default only. Use strong unique credentials in production.
|
||||
DB_NAME=nexapg
|
||||
DB_USER=nexapg
|
||||
DB_PASSWORD=nexapg
|
||||
@@ -23,7 +24,7 @@ DB_PORT=5433
|
||||
# ------------------------------
|
||||
# Host port mapped to backend container port 8000.
|
||||
BACKEND_PORT=8000
|
||||
# JWT signing secret. Change this in every non-local environment.
|
||||
# JWT signing secret. Never hardcode in source. Rotate regularly.
|
||||
JWT_SECRET_KEY=change_this_super_secret
|
||||
JWT_ALGORITHM=HS256
|
||||
# Access token lifetime in minutes.
|
||||
@@ -31,6 +32,7 @@ JWT_ACCESS_TOKEN_MINUTES=15
|
||||
# Refresh token lifetime in minutes (10080 = 7 days).
|
||||
JWT_REFRESH_TOKEN_MINUTES=10080
|
||||
# Key used to encrypt monitored target passwords at rest.
|
||||
# Never hardcode in source. Rotate with re-encryption plan.
|
||||
# Generate with:
|
||||
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||
ENCRYPTION_KEY=REPLACE_WITH_FERNET_KEY
|
||||
@@ -49,7 +51,7 @@ INIT_ADMIN_PASSWORD=ChangeMe123!
|
||||
# ------------------------------
|
||||
# Frontend
|
||||
# ------------------------------
|
||||
# Host port mapped to frontend container port 80.
|
||||
# Host port mapped to frontend container port 8080.
|
||||
FRONTEND_PORT=5173
|
||||
# Base API URL used at frontend build time.
|
||||
# For reverse proxy + SSL, keep this relative to avoid mixed-content issues.
|
||||
|
||||
48
ops/profiles/prod/.env.production.example
Normal file
48
ops/profiles/prod/.env.production.example
Normal file
@@ -0,0 +1,48 @@
|
||||
# NexaPG production profile (reverse proxy + HTTPS)
|
||||
# Copy to .env and adjust values for your environment.
|
||||
|
||||
# ------------------------------
|
||||
# Application
|
||||
# ------------------------------
|
||||
APP_NAME=NexaPG Monitor
|
||||
ENVIRONMENT=prod
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# ------------------------------
|
||||
# Core Database
|
||||
# ------------------------------
|
||||
DB_NAME=nexapg
|
||||
DB_USER=nexapg
|
||||
DB_PASSWORD=change_me
|
||||
DB_PORT=5433
|
||||
|
||||
# ------------------------------
|
||||
# Backend
|
||||
# ------------------------------
|
||||
BACKEND_PORT=8000
|
||||
JWT_SECRET_KEY=replace_with_long_random_secret
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_MINUTES=15
|
||||
JWT_REFRESH_TOKEN_MINUTES=10080
|
||||
ENCRYPTION_KEY=REPLACE_WITH_FERNET_KEY
|
||||
|
||||
# Production CORS:
|
||||
# - no wildcard
|
||||
# - set exact public UI origin(s)
|
||||
CORS_ORIGINS=https://monitor.example.com
|
||||
|
||||
POLL_INTERVAL_SECONDS=30
|
||||
ALERT_ACTIVE_CONNECTION_RATIO_MIN_TOTAL_CONNECTIONS=5
|
||||
ALERT_ROLLBACK_RATIO_WINDOW_MINUTES=15
|
||||
ALERT_ROLLBACK_RATIO_MIN_TOTAL_TRANSACTIONS=100
|
||||
ALERT_ROLLBACK_RATIO_MIN_ROLLBACKS=10
|
||||
|
||||
INIT_ADMIN_EMAIL=admin@example.com
|
||||
INIT_ADMIN_PASSWORD=ChangeMe123!
|
||||
|
||||
# ------------------------------
|
||||
# Frontend
|
||||
# ------------------------------
|
||||
# Keep frontend API base relative to avoid HTTPS mixed-content.
|
||||
FRONTEND_PORT=5173
|
||||
VITE_API_URL=/api/v1
|
||||
49
ops/profiles/prod/nginx/nexapg.conf
Normal file
49
ops/profiles/prod/nginx/nexapg.conf
Normal file
@@ -0,0 +1,49 @@
|
||||
# NGINX reverse proxy profile for NexaPG (HTTPS).
|
||||
# Replace monitor.example.com and certificate paths.
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name monitor.example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name monitor.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/monitor.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/monitor.example.com/privkey.pem;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# Baseline security headers
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
||||
# Frontend app
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5173;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
|
||||
# API forwarding to backend
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
}
|
||||
}
|
||||
38
ops/scripts/validate_proxy_profile.sh
Normal file
38
ops/scripts/validate_proxy_profile.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "[proxy-profile] validating reverse-proxy and mixed-content guardrails"
|
||||
|
||||
require_pattern() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local message="$3"
|
||||
if ! grep -Eq "$pattern" "$file"; then
|
||||
echo "[proxy-profile] FAIL: $message ($file)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Frontend should default to relative API base in container builds.
|
||||
require_pattern "frontend/Dockerfile" "ARG VITE_API_URL=/api/v1" \
|
||||
"VITE_API_URL default must be relative (/api/v1)"
|
||||
|
||||
# Frontend runtime proxy should forward /api with forward headers.
|
||||
require_pattern "frontend/nginx.conf" "location /api/" \
|
||||
"frontend nginx must proxy /api/"
|
||||
require_pattern "frontend/nginx.conf" "proxy_set_header X-Forwarded-Proto" \
|
||||
"frontend nginx must set X-Forwarded-Proto"
|
||||
require_pattern "frontend/nginx.conf" "proxy_set_header X-Forwarded-For" \
|
||||
"frontend nginx must set X-Forwarded-For"
|
||||
require_pattern "frontend/nginx.conf" "proxy_set_header Host" \
|
||||
"frontend nginx must forward Host"
|
||||
|
||||
# Mixed-content guard in frontend API client.
|
||||
require_pattern "frontend/src/api.js" "window\\.location\\.protocol === \"https:\".*parsed\\.protocol === \"http:\"" \
|
||||
"frontend api client must contain HTTPS mixed-content protection"
|
||||
|
||||
# Production profile must not use wildcard CORS.
|
||||
require_pattern "ops/profiles/prod/.env.production.example" "^CORS_ORIGINS=https://[^*]+$" \
|
||||
"production profile must use explicit HTTPS CORS origins"
|
||||
|
||||
echo "[proxy-profile] PASS"
|
||||
Reference in New Issue
Block a user