Repository navigation
feat: add reverse proxy #246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import pytest | ||
| from flask_limiter.util import get_remote_address | ||
|
|
||
| FORWARDED = {"X-Forwarded-For": "203.0.113.7"} | ||
|
|
||
|
|
||
| def _probe_ip(app): | ||
| @app.route("/_probe_ip") | ||
| def probe_ip(): | ||
| return get_remote_address() | ||
|
|
||
| return app.test_client().get("/_probe_ip", headers=FORWARDED, environ_base={"REMOTE_ADDR": "10.0.0.5"}) | ||
|
|
||
|
|
||
| def test_forwarded_for_ignored_by_default(app): | ||
| assert _probe_ip(app).get_data(as_text=True) == "10.0.0.5" | ||
|
|
||
|
|
||
| def test_forwarded_for_honored_with_trusted_proxy(monkeypatch): | ||
| import SC4SNMP_UI_backend | ||
| from unittest import mock | ||
|
|
||
| monkeypatch.setenv("TRUSTED_PROXY_COUNT", "1") | ||
| with mock.patch("SC4SNMP_UI_backend.wait_for_mongodb_replicaset"): | ||
| app = SC4SNMP_UI_backend.create_app() | ||
| assert _probe_ip(app).get_data(as_text=True) == "203.0.113.7" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("value", ["abc", "-1", "1.5"]) | ||
| def test_invalid_trusted_proxy_count_raises(monkeypatch, value): | ||
| import SC4SNMP_UI_backend | ||
| from unittest import mock | ||
|
|
||
| monkeypatch.setenv("TRUSTED_PROXY_COUNT", value) | ||
| with mock.patch("SC4SNMP_UI_backend.wait_for_mongodb_replicaset"): | ||
| with pytest.raises(ValueError, match="TRUSTED_PROXY_COUNT"): | ||
| SC4SNMP_UI_backend.create_app() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| **/node_modules | ||
| **/dist | ||
| lerna-debug.log |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,21 @@ | ||
| FROM node:24-alpine AS build-step | ||
| # The bundle is architecture-independent, so build it once on the build platform. | ||
| FROM --platform=$BUILDPLATFORM node:24-alpine AS build-step | ||
| WORKDIR /frontend | ||
| ENV PATH=/frontend/node_modules/.bin:$PATH | ||
| COPY package.json yarn.lock lerna.json ./ | ||
| COPY ./packages ./packages | ||
| RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/* | ||
| RUN yarn install | ||
| RUN yarn install --frozen-lockfile | ||
| RUN yarn run build | ||
| RUN apk add curl | ||
| WORKDIR /frontend/packages/manager | ||
|
|
||
| FROM nginxinc/nginx-unprivileged:1.30.6-alpine | ||
| COPY --from=build-step /frontend/packages/manager/dist /usr/share/nginx/html | ||
| COPY nginx/nginx.conf /etc/nginx/nginx.conf | ||
| COPY nginx/default.conf.template nginx/security-headers.conf /etc/nginx/sc4snmp/ | ||
| COPY --chmod=755 nginx/entrypoint.sh /usr/local/bin/sc4snmp-entrypoint.sh | ||
|
|
||
| ENV DEMO_PORT=8080 \ | ||
| BACKEND_URL=http://ui-backend-entrypoint:5000 | ||
| EXPOSE 8080 | ||
| CMD ["yarn", "run", "start:demo"] | ||
| ENTRYPOINT ["/usr/local/bin/sc4snmp-entrypoint.sh"] | ||
| CMD [] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| server { | ||
| listen ${DEMO_PORT}; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this would only listen to IPv4 right? do we have any plan for IPv6 support for UI? if we can do the same in this PR would be good ? |
||
| server_name _; | ||
|
|
||
| root /usr/share/nginx/html; | ||
| index index.html; | ||
|
|
||
| # Bulk device import and config restore can exceed nginx's 1 MB default. | ||
| client_max_body_size 10m; | ||
|
|
||
| gzip on; | ||
| gzip_min_length 1024; | ||
| gzip_types text/css application/javascript image/svg+xml; | ||
|
|
||
| # The browser only talks to this origin; API calls are forwarded to the backend. | ||
| location /api/ { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ca we set |
||
| include /etc/nginx/sc4snmp/security-headers.conf; | ||
| gzip off; | ||
| proxy_http_version 1.1; | ||
| proxy_set_header Host $host; | ||
| # Overwritten (not appended) so clients cannot inject an address; $remote_addr | ||
| # is the real client IP when the request came from a trusted proxy (see realip). | ||
| proxy_set_header X-Forwarded-For $remote_addr; | ||
| proxy_pass ${BACKEND_URL}/; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| location = /index.html { | ||
| include /etc/nginx/sc4snmp/security-headers.conf; | ||
| add_header Cache-Control "no-cache" always; | ||
| } | ||
|
|
||
| location / { | ||
| include /etc/nginx/sc4snmp/security-headers.conf; | ||
| try_files $uri /index.html; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| #!/bin/sh | ||
| set -eu | ||
|
|
||
| DEMO_PORT="${DEMO_PORT:-8080}" | ||
| BACKEND_URL="${BACKEND_URL:-http://ui-backend-entrypoint:5000}" | ||
| TRUSTED_PROXY_CIDRS="$(printf '%s' "${TRUSTED_PROXY_CIDRS:-}" | tr -d ' ')" | ||
|
|
||
| fail() { | ||
| echo "sc4snmp-ui-frontend: $*" >&2 | ||
| exit 1 | ||
| } | ||
|
|
||
| # Values are rendered into the nginx config, so reject anything outside a strict whitelist. | ||
| case "$DEMO_PORT" in | ||
| ''|*[!0-9]*) fail "DEMO_PORT must be a port number, got '$DEMO_PORT'" ;; | ||
| esac | ||
| [ "$DEMO_PORT" -ge 1 ] && [ "$DEMO_PORT" -le 65535 ] || fail "DEMO_PORT out of range: $DEMO_PORT" | ||
|
|
||
| case "$BACKEND_URL" in | ||
| *[!A-Za-z0-9.:/-]*) fail "BACKEND_URL contains invalid characters" ;; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we plan for IPv6 then this regex would fail as it will reject the valid IPv6 |
||
| esac | ||
| printf '%s\n' "$BACKEND_URL" | grep -Eq '^https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?$' \ | ||
| || fail "BACKEND_URL must look like http://host[:port]" | ||
|
|
||
| CONF_DIR=/tmp/nginx/conf.d | ||
| mkdir -p "$CONF_DIR" | ||
| rm -f "$CONF_DIR"/*.conf | ||
|
|
||
| envsubst '${DEMO_PORT} ${BACKEND_URL}' \ | ||
| < /etc/nginx/sc4snmp/default.conf.template > "$CONF_DIR/default.conf" | ||
|
|
||
| if [ -n "$TRUSTED_PROXY_CIDRS" ]; then | ||
| case "$TRUSTED_PROXY_CIDRS" in | ||
| *[!0-9A-Fa-f:./,]*) fail "TRUSTED_PROXY_CIDRS contains invalid characters" ;; | ||
| esac | ||
| : > "$CONF_DIR/00-realip.conf" | ||
| OLD_IFS="$IFS" | ||
| IFS=',' | ||
| set -f | ||
| for cidr in $TRUSTED_PROXY_CIDRS; do | ||
| if printf '%s\n' "$cidr" | grep -Eq '^[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?$' \ | ||
| || printf '%s\n' "$cidr" | grep -Eq '^[0-9A-Fa-f:]*:[0-9A-Fa-f:]*(/[0-9]{1,3})?$'; then | ||
| echo "set_real_ip_from $cidr;" >> "$CONF_DIR/00-realip.conf" | ||
| else | ||
| fail "TRUSTED_PROXY_CIDRS entry is not an IP or CIDR: '$cidr'" | ||
| fi | ||
| done | ||
| set +f | ||
| IFS="$OLD_IFS" | ||
| { | ||
| echo "real_ip_header X-Forwarded-For;" | ||
| echo "real_ip_recursive on;" | ||
| } >> "$CONF_DIR/00-realip.conf" | ||
| fi | ||
|
|
||
| exec nginx -g 'daemon off;' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| worker_processes auto; | ||
| pid /tmp/nginx.pid; | ||
| error_log /dev/stderr warn; | ||
|
|
||
| events { | ||
| worker_connections 1024; | ||
| } | ||
|
|
||
| http { | ||
| include /etc/nginx/mime.types; | ||
| default_type application/octet-stream; | ||
|
|
||
| server_tokens off; | ||
| access_log /dev/stdout; | ||
| sendfile on; | ||
| keepalive_timeout 65; | ||
|
|
||
| # The root filesystem is read-only in the Helm chart; /tmp is the only writable path. | ||
| client_body_temp_path /tmp/client_temp; | ||
| proxy_temp_path /tmp/proxy_temp; | ||
| fastcgi_temp_path /tmp/fastcgi_temp; | ||
| uwsgi_temp_path /tmp/uwsgi_temp; | ||
| scgi_temp_path /tmp/scgi_temp; | ||
|
|
||
| include /tmp/nginx/conf.d/*.conf; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always; | ||
| add_header X-Frame-Options "DENY" always; | ||
| add_header X-Content-Type-Options "nosniff" always; | ||
| add_header Referrer-Policy "same-origin" always; |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { expect, describe, it } from "@jest/globals"; | ||
| import api from "../api"; | ||
|
|
||
| describe("api client", () => { | ||
| it("calls the backend through the same-origin /api prefix", () => { | ||
| expect(api.defaults.baseURL).toBe("/api"); | ||
| }); | ||
|
|
||
| it("sends credentials and the CSRF header", () => { | ||
| expect(api.defaults.withCredentials).toBe(true); | ||
| expect(api.defaults.headers["X-Requested-With"]).toBe("XMLHttpRequest"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProxyFixdefaultsx_prototo 1, so configuring onlyx_foralso trustsX-Forwarded-Proto. Is this intentional?Any random value such as
abccan become Flask's forwarded-scheme can we make this configurable(UI.backEnd.trustedProxyProtoCount), default it to0, and accept only validHTTP/HTTPSschemes from a verified proxy chain when enabled?