Skip to content

Conversation

@adamoutler
Copy link
Collaborator

@adamoutler adamoutler commented Jan 28, 2026

This PR introduces security enhancements to the NetAlertX backend, focusing on protecting internal endpoints and modernizing request validation.

Refs
#1452
#1403
#1451
Likely others.

  1. Modern Security Headers (Fetch Metadata)
  • Implementation: Updated the Nginx configuration to support and prefer Sec-Fetch-Site. This provides robust protection against CSRF and unauthorized cross-origin requests by trusting browser-level security metadata.
  • Fallback: Retained a Referer-based fallback for legacy browsers to maintain compatibility while improving overall security for modern clients.
  • Efficiency: Consolidated the security logic using Nginx map directives for better performance and maintainability.
  1. Internal Endpoint Policy
  • Documentation: Updated docs/API.md to explicitly mark the /server endpoint as internal-only. This clarifies the API boundary and directs users towards the stable GraphQL and REST interfaces.
  1. Development & Test Infrastructure
  • Reliability: Refined .devcontainer setup and Docker compose test scenarios (test_docker_compose_scenarios.py) for better stability and cleaner exits.
  • Validation: Added a dedicated integration test suite (test_nginx_proxy_security.py) to verify the new security logic in a live container environment, covering modern browsers, legacy browsers, and unauthorized cross-site requests.
  1. Frontend Alignment
  • Compatibility: Updated front/js/api.js to ensure frontend requests align with the new security expectations and headers.

Port and path semantics (post-merge)

  • Port 20211 = Nginx (frontend entrypoint)

    • Used for the web UI.
    • Also provides an internal proxy path: http(s)://<host>:20211/server/* → proxied to localhost:20212 inside the container.
  • Port 20212 = NetAlertX service (API/backend)

    • Direct access to the API and the built-in API tester/docs:
      http://<host>:20212/docs
  • Important: /server is an internal convenience route via Nginx and is not a supported external integration endpoint.

    • External tools should not be documented as calling /server directly.
    • External tools either:
      • call the API on 20212 (only in trusted/internal networks; not recommended), or
      • call the API through an auth/SSL proxy (recommended).

Supported connection patterns

1) Default out-of-box Docker experience (web + optional direct API docs)

Use this to describe the default “it works after docker run” behavior, and mention :20212/docs for browser-based testing.

flowchart LR
  B[Browser]
  subgraph NAC[NetAlertX Container]
    N[Nginx listening on port 20211]
    A[Service on port 20212]
    N -->|Proxy /server to localhost:20212| A
  end
  B -->|port 20211| NAC
  B -->|port 20212| NAC
Loading

Doc notes:

  • Web UI via http://<host>:20211/
  • API docs/tester via http://<host>:20212/docs (browser convenience)
  • /server is for the UI ↔ backend flow through Nginx, not for external consumers.

2) Direct API consumer to port 20212 (NOT recommended)

This is the “possible, but discouraged” pattern.

flowchart LR
  B[Browser] -->|HTTPS| S[Any API Consumer app]
  subgraph NAC[NetAlertX Container]
    N[Nginx listening on port 20211]
    N -->|Proxy /server to localhost:20212| A[Service on port 20212]
  end
  S -->|Port 20212| NAC
Loading

Doc notes (include a warning callout):

  • Avoid exposing 20212 to untrusted networks.
  • Prefer putting auth/SSL in front (see patterns below).

3) Recommended: HTTPS/Auth reverse proxy in front of NetAlertX (proxy to 20211)

This is the “standard” deployment recommendation for web access.

flowchart LR
  B[Browser] -->|HTTPS| S[Any Auth/SSL proxy]
  subgraph NAC[NetAlertX Container]
    N[Nginx listening on port 20211]
    N -->|Proxy /server to localhost:20212| A[Service on port 20212]
  end
  S -->|port 20211| NAC
Loading

Doc notes:

  • Reverse proxy targets port 20211 (not 20212).
  • This gives TLS + auth in front of the web UI, while keeping backend concerns internal.

4) Recommended: Proxied API consumer (TLS + source limiting) to port 20212

Use this for “external tool integrates with NetAlertX API securely”.

flowchart LR
  B[Browser] -->|HTTPS| S[Any API Consumer app]
  C[HTTPS/source-limiting Proxy]
  subgraph NAC[NetAlertX Container]
    N[Nginx listening on port 20211]
    N -->|Proxy /server to localhost:20212| A[Service on port 20212]
  end
  S -->|Port 443| C
  C -->|Port 20212| NAC
Loading

Doc notes:

  • The proxy should enforce at least one of:
    • authentication (token/auth gateway),
    • IP allowlisting / source limiting,
    • TLS termination (HTTPS).
  • NetAlertX API remains on 20212, but it’s no longer “naked” to the network.

One-line documentation rule to add everywhere

Do not document /server as an API endpoint for external clients. /server is only the internal Nginx-to-service proxy route used within the container’s normal web flow.


Summary by CodeRabbit

  • New Features

    • Added a secure same-origin proxy endpoint that enforces strict origin/referer checks and proxies requests to the backend.
  • Documentation

    • Documented the internal proxy endpoint in the API docs.
  • Tests

    • Added extensive tests for proxy access control and origin handling; relaxed one fragile compose test assertion.
  • Chores

    • Included a JSON utility in images, set new default backend API/port values, and adjusted container setup/start scripts and test-side build metadata.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

Adds a protected internal reverse-proxy at /server with origin/referer-based access controls, makes the frontend default to that proxy, adds BACKEND_PORT resolution (including jq-based app-config override), installs jq in multiple images, updates devcontainer setup, and adds nginx proxy security tests.

Changes

Cohort / File(s) Summary
Docker & Devcontainer
\.devcontainer/Dockerfile, Dockerfile, Dockerfile.debian
Added jq to package install lists in builder/runner stages (Alpine and Debian).
Devcontainer Setup Script
\.devcontainer/scripts/setup.sh
Removed permissive chmod calls on docker.sock and broad recursive chmods; create /services symlink to SOURCE_DIR/install/production-filesystem/services.
App Config & Init
back/app.conf, server/initialise.py
Added BACKEND_API_URL and GRAPHQL_PORT entries; changed default BACKEND_API_URL from "" to "/server".
Frontend API
front/js/api.js
When BACKEND_API_URL is empty, apiBase now defaults to fixed proxy path "/server" instead of building host:port URL from window.location and GRAPHQL_PORT.
Nginx Template & Startup
install/production-filesystem/services/config/nginx/netalertx.conf.template, install/production-filesystem/services/start-nginx.sh
Added referer-based legacy trust map and strict same-origin is_trusted map; new /server/ location rewrites and proxies to 127.0.0.1:${BACKEND_PORT} and denies untrusted requests; start-nginx.sh resolves BACKEND_PORT with priority: app-config override (via jq), GRAPHQL_PORT env, default 20212, and injects it into the template.
Tests
test/api_endpoints/test_nginx_proxy_security.py, test/docker_tests/test_docker_compose_scenarios.py, test/docker_tests/run_docker_tests.sh
Added comprehensive pytest module validating modern/legacy origin checks and blocking scenarios; removed a brittle assertion in a docker-compose test; test runner now writes .VERSION and front/buildtimestamp.txt post-generation.
Docs
docs/API.md
Documented /server as an internal proxy endpoint (not for direct use).

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser/Client
    participant Nginx as Nginx Proxy
    participant Trust as Trust Logic
    participant Backend as Backend Server

    Client->>Nginx: GET /server/... (with Sec-Fetch-Site or Referer)
    Nginx->>Trust: Evaluate Sec-Fetch-Site
    alt Sec-Fetch-Site == "same-origin"
        Trust->>Nginx: TRUSTED
    else Sec-Fetch-Site missing
        Nginx->>Trust: Compare Referer host vs Host (sec_legacy)
        alt Referer matches Host
            Trust->>Nginx: TRUSTED
        else
            Trust->>Nginx: UNTRUSTED
        end
    else
        Trust->>Nginx: UNTRUSTED
    end

    alt TRUSTED
        Nginx->>Nginx: rewrite /server/... -> /...
        Nginx->>Backend: proxy to 127.0.0.1:BACKEND_PORT
        Backend->>Nginx: response (200/401/404/502)
        Nginx->>Client: forward response
    else UNTRUSTED
        Nginx->>Client: 403 Forbidden
    end
Loading

Possibly related PRs

Poem

🐰 I hop to guard the server gate,
With referer checks I separate,
/server whispers, "Only trusted through",
Nginx nods, the sly ones bid adieu,
A rabbit's watch — small, swift, and true.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Http sec fetch' is overly vague and does not clearly convey the scope or primary intent of the changeset. While it alludes to security-related headers (Sec-Fetch), it is abbreviated and generic, making it difficult for a teammate to understand the main changes without reading the full PR description. Consider revising the title to be more descriptive, such as 'Add Sec-Fetch-Site security header enforcement in Nginx proxy' or 'Implement Fetch Metadata security headers for CSRF protection', to better communicate the primary objective.
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@test/api_endpoints/test_nginx_proxy_security.py`:
- Around line 11-136: Tests currently call requests.get(...) without timeouts
(e.g., in test_nginx_proxy_security_modern_check,
test_nginx_proxy_security_block_cross_site,
test_nginx_proxy_security_block_no_headers, etc.), which can hang CI; replace
every direct requests.get call with a centralized http_get(url, headers=None)
helper that uses requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT) and
read REQUEST_TIMEOUT from the environment with a default of 5 seconds; update
all tests to call http_get(...) instead (preserving existing try/except behavior
and assertions) and ensure the helper is defined/imported at the top of the test
file so all test_* functions use it.
🧹 Nitpick comments (1)
install/production-filesystem/services/start-nginx.sh (1)

45-67: Guard jq parsing to avoid startup aborts on malformed APP_CONF_OVERRIDE.

With set -e, a bad JSON payload (or missing jq) will terminate the script. A small guard keeps nginx startup resilient.

♻️ Suggested hardening
-# Check override (highest priority)
-if [ -n "${APP_CONF_OVERRIDE:-}" ]; then
-    override_port=$(echo "${APP_CONF_OVERRIDE}" | jq -r '.GRAPHQL_PORT // empty')
-    if [ -n "${override_port}" ]; then
-        export BACKEND_PORT="${override_port}"
-    fi
-fi
+# Check override (highest priority)
+if [ -n "${APP_CONF_OVERRIDE:-}" ] && command -v jq >/dev/null 2>&1; then
+    override_port=$(echo "${APP_CONF_OVERRIDE}" | jq -r '.GRAPHQL_PORT // empty' 2>/dev/null || true)
+    if [ -n "${override_port}" ]; then
+        export BACKEND_PORT="${override_port}"
+    fi
+fi

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@test/api_endpoints/test_nginx_proxy_security.py`:
- Around line 131-141: The test_nginx_proxy_security_allow_port currently
asserts only a 200 response which is brittle; update the assertion to accept any
of the allowed status codes (200, 401, 404) to match other "allowed" tests.
Locate test_nginx_proxy_security_allow_port and change the assertion on
response.status_code (from http_get(url, headers=headers)) to check that
response.status_code is in (200, 401, 404) and keep the same ConnectionError
handling and message.
🧹 Nitpick comments (1)
test/api_endpoints/test_nginx_proxy_security.py (1)

5-13: Optional: centralize allowed status codes to avoid drift.

The allowed status list is repeated in multiple tests; consider a shared constant (e.g., ALLOWED_STATUS = {200, 401, 404, 502}) to keep expectations consistent.

@jokob-sk
Copy link
Collaborator

Thanks for the PR!

regarding

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/workspaces/NetAlertX/server/__main__.py", line 260, in <module>
    sys.exit(main())
             ^^^^^^
  File "/workspaces/NetAlertX/server/__main__.py", line 74, in main
    updateState("Initializing", None, None, None, 0)
  File "/app/server/app_state.py", line 190, in updateState
    return app_state_class(
           ^^^^^^^^^^^^^^^^
  File "/app/server/app_state.py", line 94, in __init__
    self.isNewVersion           = checkNewVersion()
                                  ^^^^^^^^^^^^^^^^^
  File "/app/server/helper.py", line 775, in checkNewVersion
    buildTimestamp, _version = getBuildTimeStampAndVersion()
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/server/helper.py", line 759, in getBuildTimeStampAndVersion
    with open(path, 'w') as f:
         ^^^^^^^^^^^^^^^
def getBuildTimeStampAndVersion():
    """
    Retrieves the build timestamp and version from files within the
    application directory. Initializes them if missing.

    Returns:
        tuple: (int buildTimestamp, str version)
    """
    files_defaults = [
        ('front/buildtimestamp.txt', '0'),
        ('.VERSION', 'unknown')
    ]

    results = []

    for filename, default in files_defaults:
        path = os.path.join(applicationPath, filename)
        if not os.path.exists(path):
            with open(path, 'w') as f:
                f.write(default)

        with open(path, 'r') as f:
            content = f.read().strip() or default
            # Convert buildtimestamp to int, leave version as string
            value = int(content) if filename.endswith('buildtimestamp.txt') else content
            results.append(value)

    return tuple(results)

The backend is expecting these 2 files to be present

        ('front/buildtimestamp.txt', '0'),
        ('.VERSION', 'unknown')

if there are missing (not added during build) it tries to create them and that's why write permission is needed here.

I know it's not great... so if we can ensure these files are available during build we can get rid of this check / fallback requiring write access.

This error seems to be related to this particular PR as running the tests on this PR here works fine: https://github.com/netalertx/NetAlertX/actions/runs/21407901199/job/61691616057?pr=1466

@jokob-sk jokob-sk merged commit 8ac5b14 into netalertx:main Jan 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants