nordabiz/docs/architecture/flows/06-http-request-flow.md
Maciej Pienczyn fa4fb92390 docs: Add complete architecture documentation with C4 diagrams
- System Context diagram (C4 Level 1)
- Container diagram (C4 Level 2)
- Flask component diagram (C4 Level 3)
- Deployment architecture with NPM proxy
- Database schema (PostgreSQL)
- External integrations (Gemini AI, Brave Search, PageSpeed)
- Network topology (INPI infrastructure)
- Security architecture
- API endpoints reference
- Troubleshooting guide
- Data flow diagrams (auth, search, AI chat, SEO audit, news monitoring)

All diagrams use Mermaid.js and render automatically on GitHub.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 12:40:52 +01:00

37 KiB

HTTP Request Flow

Document Version: 1.0 Last Updated: 2026-01-10 Status: Production LIVE Flow Type: HTTP Request Handling & Response Cycle


Overview

This document describes the complete HTTP request flow for the Norda Biznes Hub application, from external user through reverse proxy to Flask application and back. It covers:

  • Complete request path (Internet → Fortigate → NPM → Flask → Response)
  • Critical port configurations and routing decisions
  • SSL/TLS termination and security boundaries
  • Request/response transformation at each layer
  • Common failure scenarios and troubleshooting

Key Infrastructure:

  • Public Entry: 85.237.177.83:443 (Fortigate NAT)
  • Reverse Proxy: NPM on 10.22.68.250:443 (SSL termination)
  • Backend Application: Flask/Gunicorn on 10.22.68.249:5000
  • Protocol Flow: HTTPS → NPM → HTTP → Flask → HTTP → NPM → HTTPS

⚠️ CRITICAL CONFIGURATION:

NPM MUST forward to port 5000, NOT port 80!
Port 80 on NORDABIZ-01 runs nginx that redirects to HTTPS
Forwarding to port 80 causes infinite redirect loop

Related Documentation:

  • Incident Report: docs/INCIDENT_REPORT_20260102.md (ERR_TOO_MANY_REDIRECTS)
  • Container Diagram: docs/architecture/02-container-diagram.md
  • Deployment Architecture: docs/architecture/03-deployment-architecture.md

1. Complete HTTP Request Flow

1.1 Successful Request Sequence Diagram

sequenceDiagram
    actor User
    participant Browser
    participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
    participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
    participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000
    participant DB as 💾 PostgreSQL<br/>localhost:5432

    Note over User,DB: SUCCESSFUL REQUEST FLOW

    User->>Browser: Navigate to https://nordabiznes.pl/
    Browser->>Fortigate: HTTPS GET / (Port 443)
    Note over Fortigate: NAT Translation<br/>85.237.177.83:443 → 10.22.68.250:443

    Fortigate->>NPM: HTTPS GET / (Port 443)
    Note over NPM: SSL/TLS Termination<br/>Let's Encrypt Certificate<br/>nordabiznes.pl (Cert ID: 27)

    Note over NPM: Request Processing<br/>• Validate certificate<br/>• Decrypt HTTPS<br/>• Extract headers<br/>• Check routing rules

    NPM->>Flask: HTTP GET / (Port 5000) ✓
    Note over NPM,Flask: ⚠️ CRITICAL: Port 5000<br/>NOT port 80!

    Note over Flask: Flask Request Handling<br/>• WSGI via Gunicorn<br/>• Route matching (app.py)<br/>• Session validation<br/>• CSRF check (if POST)

    Flask->>DB: SELECT * FROM companies WHERE status='active'
    DB->>Flask: Company data (80 rows)

    Note over Flask: Template Rendering<br/>• Jinja2 template: index.html<br/>• Inject company data<br/>• Apply filters & sorting

    Flask->>NPM: HTTP 200 OK<br/>Content-Type: text/html<br/>Set-Cookie: session=...<br/>HTML content

    Note over NPM: Response Processing<br/>• Encrypt response (HTTPS)<br/>• Add security headers<br/>• HSTS, CSP, X-Frame-Options

    NPM->>Fortigate: HTTPS 200 OK (Port 443)
    Fortigate->>Browser: HTTPS 200 OK
    Browser->>User: Display page

1.2 Failed Request (Wrong Port Configuration)

sequenceDiagram
    actor User
    participant Browser
    participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
    participant NginxSys as ⚠️ Nginx System<br/>10.22.68.249:80
    participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000

    Note over User,Flask: FAILED REQUEST FLOW (REDIRECT LOOP)

    User->>Browser: Navigate to https://nordabiznes.pl/
    Browser->>NPM: HTTPS GET / (Port 443)

    Note over NPM: SSL/TLS Termination<br/>Decrypt HTTPS

    NPM->>NginxSys: ❌ HTTP GET / (Port 80)<br/>WRONG PORT!

    Note over NginxSys: Nginx System Config<br/>Redirects ALL HTTP to HTTPS

    NginxSys->>NPM: HTTP 301 Moved Permanently<br/>Location: https://nordabiznes.pl/

    Note over NPM: Follow redirect

    NPM->>NginxSys: HTTP GET / (Port 80)
    NginxSys->>NPM: HTTP 301 Moved Permanently<br/>Location: https://nordabiznes.pl/

    Note over NPM: Redirect loop detected<br/>After 20 redirects...

    NPM->>Browser: ERR_TOO_MANY_REDIRECTS
    Browser->>User: ❌ Error: Too many redirects

    Note over User: Portal UNAVAILABLE<br/>30 minutes downtime<br/>See: INCIDENT_REPORT_20260102.md

2. Layer-by-Layer Request Processing

2.1 Layer 1: Fortigate Firewall (NAT Gateway)

Server: Fortigate Firewall Public IP: 85.237.177.83 Function: Network Address Translation (NAT) + Firewall

Processing Steps:

  1. Receive external request:

    Source: User IP (e.g., 93.104.x.x)
    Destination: 85.237.177.83:443
    Protocol: HTTPS
    
  2. NAT Translation:

    External: 85.237.177.83:443 → Internal: 10.22.68.250:443
    
  3. Firewall Rules:

    • Allow TCP port 443 (HTTPS)
    • Allow TCP port 80 (HTTP, redirects to HTTPS)
    • Block all other inbound ports
    • Stateful connection tracking
  4. Forward to NPM:

    Destination: 10.22.68.250:443 (NPM reverse proxy)
    Protocol: HTTPS (encrypted tunnel)
    

Configuration:

NAT Rule: DNAT 85.237.177.83:443 → 10.22.68.250:443
Firewall: ALLOW from any to 85.237.177.83:443 (state: NEW,ESTABLISHED)

2.2 Layer 2: NPM Reverse Proxy (SSL Termination)

Server: R11-REVPROXY-01 (VM 119) IP: 10.22.68.250 Port: 443 (HTTPS) Technology: Nginx Proxy Manager (Docker)

Processing Steps:

  1. Receive HTTPS request:

    Source: Fortigate (10.22.68.250:443)
    Method: GET
    Host: nordabiznes.pl
    Protocol: HTTPS/1.1 or HTTP/2
    
  2. SSL/TLS Termination:

    • Load SSL certificate (Let's Encrypt, Certificate ID: 27)
    • Validate certificate (nordabiznes.pl, www.nordabiznes.pl)
    • Decrypt HTTPS traffic
    • Establish secure connection with client
  3. Request Header Processing:

    GET / HTTP/1.1
    Host: nordabiznes.pl
    User-Agent: Mozilla/5.0 ...
    Accept: text/html,application/xhtml+xml
    Accept-Language: pl-PL,pl;q=0.9
    Accept-Encoding: gzip, deflate, br
    Connection: keep-alive
    
  4. NPM Proxy Configuration Lookup:

    -- NPM internal database query
    SELECT * FROM proxy_host WHERE id = 27;
    -- Result:
    domain_names: ["nordabiznes.pl", "www.nordabiznes.pl"]
    forward_scheme: "http"
    forward_host: "10.22.68.249"
    forward_port: 5000   CRITICAL!
    ssl_forced: true
    certificate_id: 27
    
  5. ⚠️ CRITICAL ROUTING DECISION:

    ✓ CORRECT: Forward to http://10.22.68.249:5000
    ❌ WRONG:   Forward to http://10.22.68.249:80 (causes redirect loop!)
    
  6. Forward to Backend (HTTP, unencrypted):

    GET / HTTP/1.1
    Host: nordabiznes.pl
    X-Real-IP: 93.104.x.x (original client IP)
    X-Forwarded-For: 93.104.x.x
    X-Forwarded-Proto: https
    X-Forwarded-Host: nordabiznes.pl
    Connection: close
    

NPM Configuration (Proxy Host ID: 27):

Parameter Value Notes
Domain Names nordabiznes.pl, www.nordabiznes.pl Primary + www alias
Forward Scheme http NPM→Backend uses HTTP (secure internal network)
Forward Host 10.22.68.249 NORDABIZ-01 backend server
Forward Port 5000 Flask/Gunicorn port (CRITICAL!)
SSL Certificate 27 (Let's Encrypt) Auto-renewal enabled
SSL Forced Yes Redirect HTTP→HTTPS
HTTP/2 Support Yes Modern protocol support
HSTS Enabled Yes max-age=31536000; includeSubDomains
Block Exploits Yes Nginx security module
Websocket Support Yes For future features

Verification Command:

# Check NPM configuration
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
  sqlite3 /data/database.sqlite \
  \"SELECT id, domain_names, forward_host, forward_port FROM proxy_host WHERE id = 27;\""

# Expected output:
# 27|["nordabiznes.pl","www.nordabiznes.pl"]|10.22.68.249|5000

2.3 Layer 3: Flask/Gunicorn Application (Request Processing)

Server: NORDABIZ-01 (VM 249) IP: 10.22.68.249 Port: 5000 Technology: Gunicorn 20.1.0 + Flask 3.0

Processing Steps:

  1. Gunicorn Receives HTTP Request:

    Binding: 0.0.0.0:5000 (all interfaces)
    Workers: 4 (Gunicorn worker processes)
    Worker Class: sync (synchronous workers)
    Timeout: 120 seconds
    
  2. Worker Selection:

    • Gunicorn master process receives connection
    • Distributes request to available worker (round-robin)
    • Worker loads WSGI app (Flask application)
  3. WSGI Interface:

    # Gunicorn calls Flask's WSGI application
    from app import app
    
    # WSGI environ dict contains:
    environ = {
        'REQUEST_METHOD': 'GET',
        'PATH_INFO': '/',
        'QUERY_STRING': '',
        'SERVER_NAME': 'nordabiznes.pl',
        'SERVER_PORT': '5000',
        'HTTP_HOST': 'nordabiznes.pl',
        'HTTP_X_REAL_IP': '93.104.x.x',
        'HTTP_X_FORWARDED_PROTO': 'https',
        'wsgi.url_scheme': 'http',
        # ... more headers
    }
    
  4. Flask Request Handling (app.py):

    a) Request Context Setup:

    # Flask creates request context
    from flask import request, session, g
    
    # Parse request
    request.method = 'GET'
    request.path = '/'
    request.url = 'https://nordabiznes.pl/'
    request.args = {}  # Query parameters
    request.headers = {...}  # HTTP headers
    

    b) Before Request Hooks:

    @app.before_request
    def load_logged_in_user():
        # Check session for user_id
        user_id = session.get('user_id')
        if user_id:
            g.user = db.session.query(User).get(user_id)
        else:
            g.user = None
    

    c) Route Matching:

    # Flask router matches route
    @app.route('/')
    def index():
        # Main catalog page
        pass
    

    d) View Function Execution:

    @app.route('/')
    def index():
        # Query companies from database
        companies = db.session.query(Company)\
            .filter_by(status='active')\
            .order_by(Company.name)\
            .all()  # 80 companies
    
        # Render template
        return render_template('index.html', companies=companies)
    
  5. Database Query (PostgreSQL):

    # SQLAlchemy ORM generates SQL
    SELECT * FROM companies
    WHERE status = 'active'
    ORDER BY name;
    
  6. Template Rendering (Jinja2):

    # templates/index.html
    from jinja2 import Environment, FileSystemLoader
    
    # Render template with context
    html = render_template('index.html',
                           companies=companies,
                           user=g.user,
                           config=app.config)
    
  7. Response Construction:

    # Flask creates HTTP response
    response = Response(
        response=html,  # Rendered HTML
        status=200,     # HTTP status code
        headers={
            'Content-Type': 'text/html; charset=utf-8',
            'Content-Length': len(html),
            'Set-Cookie': 'session=...; HttpOnly; Secure; SameSite=Lax'
        }
    )
    

Gunicorn Configuration:

# /etc/systemd/system/nordabiznes.service
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/nordabiznes
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
    --bind 0.0.0.0:5000 \
    --workers 4 \
    --timeout 120 \
    --access-logfile /var/log/nordabiznes/access.log \
    --error-logfile /var/log/nordabiznes/error.log \
    --log-level info \
    app:app

Verification Command:

# Test Flask directly (from server)
curl -I http://10.22.68.249:5000/health
# Expected: HTTP/1.1 200 OK

# Check Gunicorn workers
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn"
# Expected: 1 master + 4 worker processes

2.4 Layer 4: PostgreSQL Database (Data Retrieval)

Server: NORDABIZ-01 (same server as Flask) IP: 127.0.0.1 (localhost only) Port: 5432 Technology: PostgreSQL 14

Processing Steps:

  1. Receive SQL Query:

    -- SQLAlchemy ORM generates query
    SELECT companies.id, companies.name, companies.slug,
           companies.short_description, companies.category,
           companies.nip, companies.city, companies.website
    FROM companies
    WHERE companies.status = 'active'
    ORDER BY companies.name;
    
  2. Query Execution:

    • Parse SQL syntax
    • Create execution plan (Query planner)
    • Use indexes if available (idx_companies_status, idx_companies_name)
    • Fetch rows from disk/cache
  3. Return Results:

    # SQLAlchemy ORM returns Company objects
    [
        Company(id=1, name='ALMARES', slug='almares', ...),
        Company(id=2, name='AMA', slug='ama-spolka-z-o-o', ...),
        # ... 78 more companies
    ]
    

Connection Configuration:

# app.py database connection
DATABASE_URL = 'postgresql://nordabiz_app:PASSWORD@localhost:5432/nordabiz'

# SQLAlchemy engine config
engine = create_engine(DATABASE_URL,
    pool_size=10,          # Connection pool
    max_overflow=20,       # Additional connections
    pool_pre_ping=True,    # Validate connections
    pool_recycle=3600      # Recycle after 1 hour
)

Security:

  • PostgreSQL listens on 127.0.0.1 ONLY (no external access)
  • Application connects via localhost socket
  • User: nordabiz_app (limited privileges, no DROP/CREATE)
  • SSL: Not required (localhost connection)

Verification:

# Check PostgreSQL status
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql"

# Test connection (from server)
ssh maciejpi@10.22.68.249 "psql -U nordabiz_app -h 127.0.0.1 -d nordabiz -c 'SELECT COUNT(*) FROM companies;'"
# Expected: 80

3. Response Flow (Flask → User)

3.1 Response Path Sequence

sequenceDiagram
    participant Flask as 🌐 Flask/Gunicorn<br/>10.22.68.249:5000
    participant NPM as 🔒 NPM Reverse Proxy<br/>10.22.68.250:443
    participant Fortigate as 🛡️ Fortigate Firewall<br/>85.237.177.83
    participant Browser

    Note over Flask: Response Construction
    Flask->>Flask: Render Jinja2 template<br/>HTML content (50 KB)

    Flask->>NPM: HTTP/1.1 200 OK<br/>Content-Type: text/html; charset=utf-8<br/>Content-Length: 51234<br/>Set-Cookie: session=...<br/><br/>[HTML content]

    Note over NPM: Response Processing
    NPM->>NPM: Add security headers:<br/>• Strict-Transport-Security<br/>• X-Frame-Options: SAMEORIGIN<br/>• X-Content-Type-Options: nosniff<br/>• Referrer-Policy: strict-origin

    NPM->>NPM: Compress response (gzip)<br/>Size: 50 KB → 12 KB

    NPM->>NPM: Encrypt response (TLS 1.3)<br/>Certificate: Let's Encrypt

    NPM->>Fortigate: HTTPS 200 OK<br/>Encrypted response<br/>Size: 12 KB (gzip)

    Note over Fortigate: NAT reverse translation
    Fortigate->>Browser: HTTPS 200 OK<br/>Source: 85.237.177.83

    Note over Browser: Response Handling
    Browser->>Browser: Decrypt HTTPS<br/>Decompress gzip<br/>Parse HTML<br/>Render page

3.2 Response Headers (NPM → User)

Headers Added by NPM:

HTTP/2 200 OK
server: nginx/1.24.0 (Ubuntu)
date: Fri, 10 Jan 2026 10:30:00 GMT
content-type: text/html; charset=utf-8
content-length: 12345
strict-transport-security: max-age=31536000; includeSubDomains
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'

Headers from Flask (preserved):

set-cookie: session=eyJ...; HttpOnly; Path=/; SameSite=Lax; Secure
vary: Cookie
x-request-id: abc123def456

Response Body:

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Norda Biznes Hub - Katalog firm</title>
    <!-- ... CSS, meta tags ... -->
</head>
<body>
    <!-- Rendered company catalog -->
</body>
</html>

4. Port Mapping Reference

4.1 Complete Port Flow

┌─────────────────────────────────────────────────────────────────┐
│ EXTERNAL USER                                                    │
│ Browser: https://nordabiznes.pl/                                │
└────────────────────────────┬────────────────────────────────────┘
                             │ HTTPS (Port 443)
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ FORTIGATE FIREWALL                                              │
│ Public IP: 85.237.177.83:443                                    │
│ NAT: 85.237.177.83:443 → 10.22.68.250:443                      │
└────────────────────────────┬────────────────────────────────────┘
                             │ HTTPS (Port 443)
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ NPM REVERSE PROXY (R11-REVPROXY-01)                            │
│ IP: 10.22.68.250:443                                           │
│ Function: SSL Termination                                       │
│ Certificate: Let's Encrypt (nordabiznes.pl)                    │
│                                                                 │
│ ⚠️ CRITICAL ROUTING DECISION:                                  │
│ ✓ Forward to: http://10.22.68.249:5000  (CORRECT)             │
│ ❌ DO NOT use: http://10.22.68.249:80    (WRONG!)              │
└────────────────────────────┬────────────────────────────────────┘
                             │ HTTP (Port 5000) ✓
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ FLASK/GUNICORN (NORDABIZ-01)                                   │
│ IP: 10.22.68.249:5000                                          │
│ Binding: 0.0.0.0:5000                                          │
│ Workers: 4 (Gunicorn)                                          │
│ Function: Application logic, template rendering                │
└────────────────────────────┬────────────────────────────────────┘
                             │ SQL (localhost:5432)
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│ POSTGRESQL (NORDABIZ-01)                                       │
│ IP: 127.0.0.1:5432 (localhost only)                           │
│ Database: nordabiz                                             │
│ Function: Data storage                                         │
└─────────────────────────────────────────────────────────────────┘

4.2 Port Table (NORDABIZ-01)

Port Service Binding User Purpose NPM Should Use?
5000 Gunicorn/Flask 0.0.0.0 www-data Main Application ✓ YES
80 Nginx (system) 0.0.0.0 root HTTP→HTTPS redirect NO (causes loop!)
443 Nginx (system) 0.0.0.0 root HTTPS redirect NO (NPM handles SSL)
5432 PostgreSQL 127.0.0.1 postgres Database NO (localhost only)
22 SSH 0.0.0.0 - Administration NO (not HTTP)

⚠️ CRITICAL WARNING:

Port 80 and 443 on NORDABIZ-01 run a system nginx that:
1. Redirects ALL HTTP requests to HTTPS
2. Redirects ALL HTTPS requests to https://nordabiznes.pl

If NPM forwards to port 80:
  NPM (HTTPS) → Nginx (port 80) → 301 redirect to HTTPS
  → NPM receives redirect → forwards to port 80 again
  → INFINITE LOOP → ERR_TOO_MANY_REDIRECTS

SOLUTION: NPM must ALWAYS forward to port 5000!

5. Request Types and Routing

5.1 Static Assets

Request: GET https://nordabiznes.pl/static/css/style.css

Flow:

User → NPM → Flask → Static file handler → Return CSS

Flask Handling:

# Flask serves static files from /static directory
@app.route('/static/<path:filename>')
def static_files(filename):
    return send_from_directory('static', filename)

Optimization: NPM could cache static assets (future enhancement)


5.2 API Endpoints

Request: GET https://nordabiznes.pl/api/companies

Flow:

User → NPM → Flask → API route → Database → JSON response

Response:

{
  "companies": [
    {
      "id": 1,
      "name": "PIXLAB",
      "slug": "pixlab-sp-z-o-o",
      "category": "IT"
    }
  ],
  "total": 80
}

Headers:

Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: * (if CORS enabled)

5.3 Form Submissions (POST)

Request: POST https://nordabiznes.pl/login

Flow:

User → NPM → Flask → CSRF validation → Auth check → Database → Redirect

Additional Processing:

  • CSRF Token Validation: Flask-WTF checks token in form
  • Session Creation: Flask-Login creates session cookie
  • Database Update: Update last_login timestamp

Response:

HTTP/2 302 Found
Location: https://nordabiznes.pl/dashboard
Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax

5.4 Health Check Endpoint

Request: GET https://nordabiznes.pl/health

Purpose: Monitoring and verification

Flow:

Monitor → NPM → Flask → Simple response (no DB query)

Response:

HTTP/2 200 OK
Content-Type: application/json

{
  "status": "healthy",
  "timestamp": "2026-01-10T10:30:00Z"
}

Use Cases:

  • Verify NPM configuration after changes
  • External monitoring (Zabbix)
  • Load balancer health checks (future)

6. Security Layers

6.1 Security Boundaries

graph TB
    Internet["🌐 Internet<br/>(Untrusted)"]

    subgraph "Security Zone 1: DMZ"
        Fortigate["🛡️ Fortigate Firewall<br/>• NAT<br/>• DPI<br/>• IPS"]
        NPM["🔒 NPM Reverse Proxy<br/>• SSL/TLS Termination<br/>• HSTS<br/>• Rate Limiting"]
    end

    subgraph "Security Zone 2: Application Layer"
        Flask["🌐 Flask Application<br/>• CSRF Protection<br/>• Input Validation<br/>• XSS Prevention<br/>• Session Management"]
    end

    subgraph "Security Zone 3: Data Layer"
        DB["💾 PostgreSQL<br/>• No external access<br/>• User privileges<br/>• SQL injection protection"]
    end

    Internet --> Fortigate
    Fortigate --> NPM
    NPM --> Flask
    Flask --> DB

    classDef zoneStyle fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef appStyle fill:#3498db,stroke:#2980b9,color:#fff
    classDef dataStyle fill:#2ecc71,stroke:#27ae60,color:#fff

    class Fortigate,NPM zoneStyle
    class Flask appStyle
    class DB dataStyle

6.2 Security Features by Layer

Layer 1: Fortigate Firewall

  • Deep Packet Inspection (DPI)
  • Intrusion Prevention System (IPS)
  • Rate limiting (connection-level)
  • Geo-blocking (if configured)

Layer 2: NPM Reverse Proxy

  • SSL/TLS 1.3 encryption
  • HSTS (HTTP Strict Transport Security)
  • Certificate validation
  • Security headers injection
  • Request size limits

Layer 3: Flask Application

  • Flask-Login session management
  • CSRF token validation (Flask-WTF)
  • Input sanitization (XSS prevention)
  • SQL injection protection (SQLAlchemy ORM)
  • Rate limiting (Flask-Limiter)
  • Authentication & Authorization

Layer 4: PostgreSQL Database

  • Localhost-only binding (127.0.0.1)
  • User privilege separation (nordabiz_app)
  • Parameterized queries (SQLAlchemy)
  • Connection pooling with limits

7. Performance Metrics

7.1 Typical Request Latency

Layer Processing Time Notes
Fortigate NAT < 1 ms Hardware NAT, negligible latency
NPM SSL Termination 10-20 ms TLS handshake + decryption
NPM → Flask Network < 1 ms Internal 10 Gbps network
Flask Request Handling 50-150 ms Route matching, template rendering
Database Query 10-30 ms Indexed queries, connection pool
Template Rendering 20-50 ms Jinja2 template compilation
NPM SSL Encryption 5-10 ms Response encryption
Total (typical) 96-262 ms Median: ~180 ms

Factors Affecting Latency:

  • Database query complexity (JOIN operations, FTS)
  • Template size (company catalog vs. simple page)
  • Gunicorn worker availability (max 4 concurrent)
  • Network congestion (internal or external)

7.2 Throughput

Gunicorn Workers: 4 synchronous workers

Concurrent Requests: Max 4 simultaneous requests

Requests per Second (RPS):

  • Simple pages (health check): ~20-30 RPS
  • Database queries (catalog): ~5-10 RPS
  • Complex AI chat: ~2-3 RPS (limited by Gemini API)

Bottlenecks:

  1. Gunicorn worker count (4 workers)
  2. PostgreSQL connection pool (max 30)
  3. AI API rate limits (1,500 req/day Gemini)

8. Troubleshooting Guide

8.1 Common Issues and Diagnostics

Issue 1: ERR_TOO_MANY_REDIRECTS

Symptoms:

  • Browser error: "ERR_TOO_MANY_REDIRECTS"
  • Portal inaccessible from external network
  • Works from internal network (10.22.68.0/24)

Root Cause:

  • NPM forwarding to port 80 instead of 5000

Diagnosis:

# 1. Check NPM configuration
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
  sqlite3 /data/database.sqlite \
  \"SELECT forward_port FROM proxy_host WHERE id = 27;\""

# Expected: 5000
# If shows: 80 ← PROBLEM!

# 2. Test direct backend access
curl -I http://10.22.68.249:80/
# If returns: HTTP 301 → Problem confirmed

curl -I http://10.22.68.249:5000/
# Should return: HTTP 200 OK

Solution:

# Update NPM configuration via API or UI
# Set forward_port to 5000

# Verify fix
curl -I https://nordabiznes.pl/health
# Expected: HTTP 200 OK

Reference: docs/INCIDENT_REPORT_20260102.md


Issue 2: 502 Bad Gateway

Symptoms:

  • NPM returns "502 Bad Gateway"
  • Error in NPM logs: "connect() failed (111: Connection refused)"

Root Cause:

  • Flask/Gunicorn not running
  • Flask listening on wrong port
  • Firewall blocking port 5000

Diagnosis:

# 1. Check if Gunicorn is running
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"
# Expected: Active (running)

# 2. Check if port 5000 is listening
ssh maciejpi@10.22.68.249 "sudo netstat -tlnp | grep 5000"
# Expected: 0.0.0.0:5000 ... gunicorn

# 3. Test direct connection
curl -I http://10.22.68.249:5000/health
# Expected: HTTP 200 OK

Solution:

# Restart Gunicorn
ssh maciejpi@10.22.68.249 "sudo systemctl restart nordabiznes"

# Check logs
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -n 50"

Issue 3: 504 Gateway Timeout

Symptoms:

  • Request takes > 60 seconds
  • NPM returns "504 Gateway Timeout"

Root Cause:

  • Flask processing takes too long
  • Database query timeout
  • External API timeout (Gemini, PageSpeed)

Diagnosis:

# 1. Check Gunicorn worker status
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn"
# Look for workers in state 'R' (running) vs 'S' (sleeping)

# 2. Check application logs
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/error.log"

# 3. Check database connections
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
  \"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""

Solution:

  • Increase Gunicorn timeout (default: 120s)
  • Optimize slow database queries
  • Add timeout to external API calls

Issue 4: SSL Certificate Error

Symptoms:

  • Browser warning: "Your connection is not private"
  • Certificate expired or invalid

Root Cause:

  • Let's Encrypt certificate expired
  • Certificate renewal failed
  • Wrong certificate for domain

Diagnosis:

# Check certificate expiry
echo | openssl s_client -servername nordabiznes.pl \
  -connect 85.237.177.83:443 2>/dev/null | openssl x509 -noout -dates

# Expected: notAfter date in the future

Solution:

# Renew certificate via NPM UI or API
# NPM auto-renews 30 days before expiry

8.2 Diagnostic Commands Reference

Quick Health Check:

# External access test
curl -I https://nordabiznes.pl/health
# Expected: HTTP/2 200 OK

# Internal access test (from INPI network)
curl -I http://10.22.68.249:5000/health
# Expected: HTTP/1.1 200 OK

NPM Configuration Check:

# Show proxy host configuration
ssh maciejpi@10.22.68.250 "docker exec nginx-proxy-manager_app_1 \
  sqlite3 /data/database.sqlite \
  \"SELECT id, domain_names, forward_host, forward_port, ssl_forced \
   FROM proxy_host WHERE id = 27;\""

Application Status:

# Service status
ssh maciejpi@10.22.68.249 "sudo systemctl status nordabiznes"

# Worker processes
ssh maciejpi@10.22.68.249 "ps aux | grep gunicorn | grep -v grep"

# Recent logs
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -n 20 --no-pager"

Database Status:

# PostgreSQL status
ssh maciejpi@10.22.68.249 "sudo systemctl status postgresql"

# Connection count
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
  \"SELECT count(*) FROM pg_stat_activity WHERE datname='nordabiz';\""

# Database size
ssh maciejpi@10.22.68.249 "sudo -u postgres psql -c \
  \"SELECT pg_size_pretty(pg_database_size('nordabiz'));\""

Network Connectivity:

# Test NPM → Flask connectivity
ssh maciejpi@10.22.68.250 "curl -I http://10.22.68.249:5000/health"

# Test Flask → Database connectivity
ssh maciejpi@10.22.68.249 "psql -U nordabiz_app -h 127.0.0.1 \
  -d nordabiz -c 'SELECT 1;'"

9. Configuration Reference

9.1 NPM Proxy Host Configuration

File Location (Docker): /data/database.sqlite (inside NPM container)

Proxy Host ID: 27

Complete Configuration:

{
  "id": 27,
  "domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
  "forward_scheme": "http",
  "forward_host": "10.22.68.249",
  "forward_port": 5000,
  "access_list_id": 0,
  "certificate_id": 27,
  "ssl_forced": true,
  "caching_enabled": false,
  "block_exploits": true,
  "advanced_config": "",
  "allow_websocket_upgrade": true,
  "http2_support": true,
  "hsts_enabled": true,
  "hsts_subdomains": true
}

How to Update (NPM API):

import requests

NPM_URL = "http://10.22.68.250:81/api"
# Login to get token first, then:

data = {
    "domain_names": ["nordabiznes.pl", "www.nordabiznes.pl"],
    "forward_scheme": "http",
    "forward_host": "10.22.68.249",
    "forward_port": 5000,  # CRITICAL!
    "certificate_id": 27,
    "ssl_forced": True,
    "http2_support": True,
    "hsts_enabled": True
}

response = requests.put(
    f"{NPM_URL}/nginx/proxy-hosts/27",
    headers={"Authorization": f"Bearer {token}"},
    json=data
)

9.2 Gunicorn Configuration

Systemd Unit File: /etc/systemd/system/nordabiznes.service

[Unit]
Description=Norda Biznes Hub Flask Application
After=network.target postgresql.service

[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/var/www/nordabiznes
Environment="PATH=/var/www/nordabiznes/venv/bin"
ExecStart=/var/www/nordabiznes/venv/bin/gunicorn \
    --bind 0.0.0.0:5000 \
    --workers 4 \
    --worker-class sync \
    --timeout 120 \
    --keep-alive 5 \
    --access-logfile /var/log/nordabiznes/access.log \
    --error-logfile /var/log/nordabiznes/error.log \
    --log-level info \
    --capture-output \
    app:app

Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Parameters Explained:

  • --bind 0.0.0.0:5000 - Listen on all interfaces, port 5000
  • --workers 4 - 4 worker processes (matches CPU cores)
  • --worker-class sync - Synchronous workers (default)
  • --timeout 120 - 120 second request timeout
  • --keep-alive 5 - 5 second keep-alive for connections

9.3 Flask Application Configuration

Environment Variables (.env):

# Database
DATABASE_URL=postgresql://nordabiz_app:PASSWORD@localhost:5432/nordabiz

# Flask
SECRET_KEY=random_secret_key_here
FLASK_ENV=production

# Security
SESSION_COOKIE_SECURE=True
SESSION_COOKIE_HTTPONLY=True
SESSION_COOKIE_SAMESITE=Lax

# External APIs
GOOGLE_API_KEY=AIza...
BRAVE_API_KEY=BSA...
MS_GRAPH_CLIENT_ID=abc...
MS_GRAPH_CLIENT_SECRET=def...

Flask Config (app.py):

app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)

10. Monitoring and Logging

10.1 Log Locations

NPM Logs (Docker):

# Access logs
ssh maciejpi@10.22.68.250 "docker logs nginx-proxy-manager_app_1 --tail 100"

# Follow logs in real-time
ssh maciejpi@10.22.68.250 "docker logs -f nginx-proxy-manager_app_1"

Gunicorn Logs:

# Application logs (systemd journal)
ssh maciejpi@10.22.68.249 "sudo journalctl -u nordabiznes -f"

# Access logs (file-based)
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/access.log"

# Error logs
ssh maciejpi@10.22.68.249 "tail -f /var/log/nordabiznes/error.log"

PostgreSQL Logs:

# Query logs (if enabled)
ssh maciejpi@10.22.68.249 "sudo tail -f /var/log/postgresql/postgresql-14-main.log"

10.2 Key Metrics to Monitor

HTTP Metrics (NPM):

  • Requests per second (RPS)
  • HTTP status codes (200, 301, 404, 500, 502, 504)
  • Response time (p50, p95, p99)
  • SSL certificate expiry

Application Metrics (Gunicorn):

  • Worker utilization (all 4 workers busy?)
  • Request timeout rate
  • Error rate (5xx responses)
  • Memory usage per worker

Database Metrics (PostgreSQL):

  • Connection count (should be < 30)
  • Query execution time
  • Database size growth
  • Table bloat

System Metrics (NORDABIZ-01):

  • CPU usage (should be < 80%)
  • Memory usage (should be < 6 GB / 8 GB)
  • Disk I/O (should be low)
  • Network throughput

11. Future Enhancements

11.1 Planned Improvements

1. CDN Integration

  • Cloudflare or custom CDN for static assets
  • Reduces load on NPM and Flask
  • Improves global latency

2. Load Balancing

  • Multiple Flask backend instances
  • NPM upstream configuration
  • Session affinity handling

3. Caching Layer

  • Redis cache for frequent queries
  • Page cache for anonymous users
  • API response caching

4. Monitoring System

  • Zabbix integration for health checks
  • Alert on 5xx errors, high latency
  • Dashboard for key metrics

5. Rate Limiting

  • NPM-level rate limiting by IP
  • Protection against DDoS
  • API endpoint throttling

  • Incident Report: docs/INCIDENT_REPORT_20260102.md - NPM port configuration incident
  • System Context: docs/architecture/01-system-context.md - High-level system view
  • Container Diagram: docs/architecture/02-container-diagram.md - Container architecture
  • Deployment Architecture: docs/architecture/03-deployment-architecture.md - Infrastructure details
  • Flask Components: docs/architecture/04-flask-components.md - Application structure
  • Authentication Flow: docs/architecture/flows/01-authentication-flow.md - User authentication
  • Search Flow: docs/architecture/flows/02-search-flow.md - Company search process
  • CLAUDE.md: Main project documentation with NPM configuration reference

Glossary

  • NPM: Nginx Proxy Manager - Docker-based reverse proxy with web UI
  • NAT: Network Address Translation - Fortigate translates external IP to internal
  • SSL Termination: Decrypting HTTPS at NPM, forwarding HTTP to backend
  • HSTS: HTTP Strict Transport Security - Forces browsers to use HTTPS
  • WSGI: Web Server Gateway Interface - Python web application interface
  • Gunicorn: Python WSGI HTTP server (Green Unicorn)
  • Round Robin: Load balancing method distributing requests evenly
  • ERR_TOO_MANY_REDIRECTS: Browser error when redirect loop detected (> 20 redirects)

Document Status: Production-ready, verified against live system Last Incident: 2026-01-02 (ERR_TOO_MANY_REDIRECTS due to port 80 configuration) Next Review: After any NPM configuration changes


This document was created as part of the architecture documentation initiative to prevent configuration incidents and improve system understanding.