Compare commits
4 Commits
3932aa56f7
...
0.2.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 597579376f | |||
| f25792b8d8 | |||
| 6093c5dea8 | |||
| 84bc7b0384 |
@@ -12,6 +12,7 @@ LOG_LEVEL=INFO
|
|||||||
# Core Database (internal metadata DB)
|
# Core Database (internal metadata DB)
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Database that stores users, targets, metrics, query stats, and audit logs.
|
# Database that stores users, targets, metrics, query stats, and audit logs.
|
||||||
|
# DEV default only. Use strong unique credentials in production.
|
||||||
DB_NAME=nexapg
|
DB_NAME=nexapg
|
||||||
DB_USER=nexapg
|
DB_USER=nexapg
|
||||||
DB_PASSWORD=nexapg
|
DB_PASSWORD=nexapg
|
||||||
@@ -23,7 +24,7 @@ DB_PORT=5433
|
|||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Host port mapped to backend container port 8000.
|
# Host port mapped to backend container port 8000.
|
||||||
BACKEND_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_SECRET_KEY=change_this_super_secret
|
||||||
JWT_ALGORITHM=HS256
|
JWT_ALGORITHM=HS256
|
||||||
# Access token lifetime in minutes.
|
# Access token lifetime in minutes.
|
||||||
@@ -31,6 +32,7 @@ JWT_ACCESS_TOKEN_MINUTES=15
|
|||||||
# Refresh token lifetime in minutes (10080 = 7 days).
|
# Refresh token lifetime in minutes (10080 = 7 days).
|
||||||
JWT_REFRESH_TOKEN_MINUTES=10080
|
JWT_REFRESH_TOKEN_MINUTES=10080
|
||||||
# Key used to encrypt monitored target passwords at rest.
|
# Key used to encrypt monitored target passwords at rest.
|
||||||
|
# Never hardcode in source. Rotate with re-encryption plan.
|
||||||
# Generate with:
|
# Generate with:
|
||||||
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||||
ENCRYPTION_KEY=REPLACE_WITH_FERNET_KEY
|
ENCRYPTION_KEY=REPLACE_WITH_FERNET_KEY
|
||||||
@@ -56,5 +58,5 @@ INIT_ADMIN_PASSWORD=ChangeMe123!
|
|||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Frontend
|
# Frontend
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Host port mapped to frontend container port 80.
|
# Host port mapped to frontend container port 8080.
|
||||||
FRONTEND_PORT=5173
|
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)
|
- [API Error Format](#api-error-format)
|
||||||
- [`pg_stat_statements` Requirement](#pg_stat_statements-requirement)
|
- [`pg_stat_statements` Requirement](#pg_stat_statements-requirement)
|
||||||
- [Reverse Proxy / SSL Guidance](#reverse-proxy--ssl-guidance)
|
- [Reverse Proxy / SSL Guidance](#reverse-proxy--ssl-guidance)
|
||||||
|
- [Production Proxy Profile](#production-proxy-profile)
|
||||||
- [PostgreSQL Compatibility Smoke Test](#postgresql-compatibility-smoke-test)
|
- [PostgreSQL Compatibility Smoke Test](#postgresql-compatibility-smoke-test)
|
||||||
- [Dependency Exception Flow](#dependency-exception-flow)
|
- [Dependency Exception Flow](#dependency-exception-flow)
|
||||||
|
- [Secret Management (Production)](#secret-management-production)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Troubleshooting](#troubleshooting)
|
||||||
- [Security Notes](#security-notes)
|
- [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.
|
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
|
## PostgreSQL Compatibility Smoke Test
|
||||||
|
|
||||||
Run manually against one DSN:
|
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:
|
- Full process and required metadata are documented in:
|
||||||
- `docs/security/dependency-exceptions.md`
|
- `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
|
## Troubleshooting
|
||||||
|
|
||||||
### Backend container keeps restarting during `make migrate`
|
### 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
|
- RBAC enforced on protected endpoints
|
||||||
- Audit logs for critical actions
|
- Audit logs for critical actions
|
||||||
- Collector error logging includes throttling to reduce repeated noise
|
- 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 import field_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
NEXAPG_VERSION = "0.2.2"
|
NEXAPG_VERSION = "0.2.4"
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
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
|
FROM nginx:1-alpine-slim
|
||||||
RUN apk upgrade --no-cache \
|
RUN apk upgrade --no-cache \
|
||||||
&& mkdir -p /var/cache/nginx /var/run /var/log/nginx /tmp/nginx \
|
&& 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 nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
USER 101
|
USER 101
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ LOG_LEVEL=INFO
|
|||||||
# Core Database (internal metadata DB)
|
# Core Database (internal metadata DB)
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Database that stores users, targets, metrics, query stats, and audit logs.
|
# Database that stores users, targets, metrics, query stats, and audit logs.
|
||||||
|
# DEV default only. Use strong unique credentials in production.
|
||||||
DB_NAME=nexapg
|
DB_NAME=nexapg
|
||||||
DB_USER=nexapg
|
DB_USER=nexapg
|
||||||
DB_PASSWORD=nexapg
|
DB_PASSWORD=nexapg
|
||||||
@@ -23,7 +24,7 @@ DB_PORT=5433
|
|||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Host port mapped to backend container port 8000.
|
# Host port mapped to backend container port 8000.
|
||||||
BACKEND_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_SECRET_KEY=change_this_super_secret
|
||||||
JWT_ALGORITHM=HS256
|
JWT_ALGORITHM=HS256
|
||||||
# Access token lifetime in minutes.
|
# Access token lifetime in minutes.
|
||||||
@@ -31,6 +32,7 @@ JWT_ACCESS_TOKEN_MINUTES=15
|
|||||||
# Refresh token lifetime in minutes (10080 = 7 days).
|
# Refresh token lifetime in minutes (10080 = 7 days).
|
||||||
JWT_REFRESH_TOKEN_MINUTES=10080
|
JWT_REFRESH_TOKEN_MINUTES=10080
|
||||||
# Key used to encrypt monitored target passwords at rest.
|
# Key used to encrypt monitored target passwords at rest.
|
||||||
|
# Never hardcode in source. Rotate with re-encryption plan.
|
||||||
# Generate with:
|
# Generate with:
|
||||||
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||||
ENCRYPTION_KEY=REPLACE_WITH_FERNET_KEY
|
ENCRYPTION_KEY=REPLACE_WITH_FERNET_KEY
|
||||||
@@ -49,7 +51,7 @@ INIT_ADMIN_PASSWORD=ChangeMe123!
|
|||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Frontend
|
# Frontend
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Host port mapped to frontend container port 80.
|
# Host port mapped to frontend container port 8080.
|
||||||
FRONTEND_PORT=5173
|
FRONTEND_PORT=5173
|
||||||
# Base API URL used at frontend build time.
|
# Base API URL used at frontend build time.
|
||||||
# For reverse proxy + SSL, keep this relative to avoid mixed-content issues.
|
# 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