diff --git a/Dockerfile b/Dockerfile index a27c553..ddb9a27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,26 @@ -FROM nginx:1.20.2 -COPY nginx.conf /etc/nginx/nginx.conf.org -COPY health-check.conf /etc/nginx/ -ENV EXPOSED_PORT 443 -ENV SERVER_NAME_DASHBOARD dashboard.opex.dev -ENV SERVER_NAME_ADMIN_PANEL admin.opex.dev -ENV SERVER_NAME_WEB_APP app.opex.dev -ENV SERVER_NAME_AUTH auth.opex.dev -ENV SERVER_NAME_HEALTH health.opex.dev -ENV SERVER_NAME_API api.opex.dev -ENV SERVER_NAME_KIBANA kibana.opex.dev -ENV SERVER_NAME_GRAFANA grafana.opex.dev -ENV SERVER_NAME_MOBILE_APP mobile.opex.dev -ENV SERVER_WALLET_STAT walletstat.opex.dev -ENV SERVER_NAME_KC kc.opex.dev -ENV SERVER_NAME_V2_AUTH v2auth.opex.dev -ENTRYPOINT sh -c 'envsubst \ -\$EXPOSED_PORT,\$SERVER_NAME_DASHBOARD,\$SERVER_NAME_ADMIN_PANEL,\$SERVER_NAME_WEB_APP,\$SERVER_NAME_AUTH,\$SERVER_NAME_HEALTH,\$SERVER_NAME_API,\$SERVER_NAME_MOBILE_APP,\$SERVER_WALLET_STAT,\$SERVER_NAME_GRAFANA,\$SERVER_NAME_KIBANA,\$SERVER_NAME_KC,\$SERVER_NAME_V2_AUTH \ -< /etc/nginx/nginx.conf.org \ -| tee /etc/nginx/nginx.conf \ -&& nginx -g "daemon off;"' -EXPOSE 443 +FROM really/nginx-modsecurity:latest +USER root +# Install envsubst safely from the same Alpine version +RUN apk add --no-cache gettext + +# nginx template +COPY nginx.conf.template /etc/nginx/nginx.conf.template + +# ModSecurity configuration +COPY ./modsecurity/ /etc/nginx/modsecurity/ + +# Lua scripts (if you need them) +COPY ./lua/ /etc/nginx/lua/ + +COPY ./health-check.conf /etc/nginx/health-check.conf + +# Startup script +COPY entrypoint.sh /entrypoint.sh + + +RUN sed -i 's/\r$//' /entrypoint.sh \ + && chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] + +EXPOSE 443 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c7686ac..e29f188 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,9 +17,14 @@ services: - SERVER_WALLET_STAT=$SERVER_WALLET_STAT - SERVER_NAME_KC=$SERVER_NAME_KC - SERVER_NAME_V2_AUTH=$SERVER_NAME_V2_AUTH + - SERVER_NAME_BETA_APP=$SERVER_NAME_BETA_APP + - SERVER_NAME_SWAGGER=$SERVER_NAME_SWAGGER + - SERVER_NAME_ADMIN_V2_PANEL=$SERVER_NAME_ADMIN_V2_PANEL secrets: - opex_dev_crt - private_pem + volumes: + - /var/www:/var/www:ro nginx-exporter: image: nginx/nginx-prometheus-exporter:1.1.0 ports: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..f732a03 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +set -e + +echo "Generating nginx.conf..." + +envsubst ' +$EXPOSED_PORT +$SERVER_NAME_DASHBOARD +$SERVER_NAME_ADMIN_PANEL +$SERVER_NAME_WEB_APP +$SERVER_NAME_AUTH +$SERVER_NAME_HEALTH +$SERVER_NAME_API +$SERVER_NAME_MOBILE_APP +$SERVER_WALLET_STAT +$SERVER_NAME_GRAFANA +$SERVER_NAME_KIBANA +$SERVER_NAME_KC +$SERVER_NAME_V2_AUTH +$SERVER_NAME_BETA_APP +$SERVER_NAME_SWAGGER +$SERVER_NAME_ADMIN_V2_PANEL +' \ +< /etc/nginx/nginx.conf.template \ +> /etc/nginx/nginx.conf + + +echo "Generated config size:" +wc -c /etc/nginx/nginx.conf + +echo "Checking events section:" +grep "events" /etc/nginx/nginx.conf || true + + +nginx -t + +exec nginx -g "daemon off;" \ No newline at end of file diff --git a/lua/upload_check.lua b/lua/upload_check.lua new file mode 100644 index 0000000..1622420 --- /dev/null +++ b/lua/upload_check.lua @@ -0,0 +1,71 @@ +local allowed_ext = {".png", ".jpg", ".jpeg"} +local blocked_ext = {".php", ".exe", ".sh", ".bat", ".js" , ".svg"} -- dangerous extensions + +local function ends_with(str, ending) + return str:lower():sub(-#ending) == ending:lower() +end + +local function contains_blocked(str) + str = str:lower() + for _, ext in ipairs(blocked_ext) do + if str:find(ext, 1, true) then + return true + end + end + return false +end + +local headers = ngx.req.get_headers() +local content_type = headers["content-type"] +local is_admin = false + +local admin_ips = { + ["188.136.193.194"] = true, + ["193.186.32.236"] = true, + ["188.75.66.34"] = true +} + +local client_ip = ngx.var.remote_addr + +local is_admin = admin_ips[client_ip] or false + +-- ngx.log(ngx.ERR, "Client IP: ", client_ip, " | Admin: ", tostring(is_admin)) +ngx.log( + ngx.ERR, + "URI=", ngx.var.request_uri, + " METHOD=", ngx.req.get_method(), + " CONTENT_TYPE=", tostring(content_type), + " ADMIN=", tostring(is_admin) +) +if content_type and content_type:find("multipart/form-data", 1, true) and not is_admin then + ngx.req.read_body() + local body = ngx.req.get_body_data() + if body then + for filename in body:gmatch('filename="(.-)"') do + -- 1. Check allowed extensions + local allowed = false + for _, ext in ipairs(allowed_ext) do + if ends_with(filename, ext) then + allowed = true + break + end + end + if not allowed then + ngx.status = 403 + ngx.say("Blocked upload: only PNG, JPG, JPEG allowed") + ngx.log(ngx.ERR,"Blocked upload: only PNG, JPG, JPEG allowed") + ngx.exit(403) + end + + -- 2. Check for multiple dangerous extensions + if contains_blocked(filename) then + ngx.status = 403 + ngx.say("Blocked upload: filename contains forbidden extension") + ngx.log(ngx.ERR,"Blocked upload: filename contains forbidden extension") + ngx.exit(403) + end + end + end +end +-- ngx.req.discard_body() + diff --git a/modsecurity/modsecurity.conf b/modsecurity/modsecurity.conf new file mode 100644 index 0000000..6573cbd --- /dev/null +++ b/modsecurity/modsecurity.conf @@ -0,0 +1,93 @@ +# Enable ModSecurity +SecRuleEngine On +SecRequestBodyAccess On +SecResponseBodyAccess Off + +# Logging +SecAuditEngine RelevantOnly +SecAuditLog /var/log/modsec_audit.log +SecAuditLogParts AIJDEFHZ +SecAuditLogType Serial +SecDebugLog /var/log/modsec_debug.log +SecDebugLogLevel 3 +# Temp directories +SecTmpDir /tmp/ +SecDataDir /tmp/ + +# Keep uploaded files temporarily for inspection +SecUploadDir /tmp/modsec-upload/ +SecUploadKeepFiles On + +# Default actions +SecDefaultAction "phase:1,log,auditlog,pass" +SecDefaultAction "phase:2,log,auditlog,pass" + +############################################################ +# 1. Block dangerous HTML/script payloads in parameters only +# (NOT raw multipart request body) +############################################################ +SecRule ARGS|ARGS_NAMES|REQUEST_COOKIES|REQUEST_HEADERS \ + "@rx (?i)(]*>)" \ + "id:1001,phase:2,t:none,deny,log,msg:'Blocked dangerous HTML/script tag in request'" + +############################################################ +# 2. Block dangerous MIME types +SecRule REQUEST_HEADERS:Content-Type "@rx (?i)(application/x-php|application/x-msdownload|application/x-sh|text/x-python|application/x-msdos-program)" \ + "id:1002,phase:1,t:none,deny,log,msg:'Blocked dangerous MIME type'" + +############################################################ +# 3. Block direct access to dangerous file URIs +SecRule REQUEST_URI "@rx (?i)\.(php[0-9]?|phtml|phar|asp|aspx|jsp|exe|sh|bat|pl|py|cgi)(\?|$)" \ + "id:1003,phase:1,t:none,deny,log,msg:'Blocked direct request to dangerous file type in URI'" + +############################################################ +# 4. Code injection patterns +SecRule REQUEST_BODY "(?i)(<\?php|system\(|exec\(|shell_exec\(|passthru\(|popen\(|cmd\.exe|/bin/sh|/bin/bash)" \ + "id:1004,phase:2,t:none,deny,log,msg:'Blocked possible code injection pattern'" + + diff --git a/nginx.conf b/nginx.conf.template similarity index 63% rename from nginx.conf rename to nginx.conf.template index b1ac5d1..b2e29e3 100644 --- a/nginx.conf +++ b/nginx.conf.template @@ -1,10 +1,17 @@ +user nginx; +pid /var/run/nginx.pid; worker_processes auto; +load_module /etc/nginx/modules/ngx_http_modsecurity_module.so; events { - worker_connections 1024; + worker_connections 1024; } + http { + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log debug; + include /etc/nginx/mime.types; sendfile on; @@ -38,10 +45,44 @@ http { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $server_name; - proxy_set_header X-Forwarded-Port $EXPOSED_PORT; + proxy_set_header X-Forwarded-Port ${EXPOSED_PORT}; resolver 127.0.0.11 valid=60s; + map $http_user_agent $is_mobile { + default 0; + ~*iphone 1; + ~*ipod 1; + ~*android 1; + ~*blackberry 1; + ~*windows\ phone 1; + mobile 1; + opera\ mini 1; + iemobile 1; + } + + # Set root path based on device type + map $is_mobile $web_app_root { + 0 /var/www/desktop; + 1 /var/www/mobile; + } + + real_ip_header X-Forwarded-For; + set_real_ip_from 0.0.0.0/0; + real_ip_recursive on; + + + # Lua + lua_package_path "/etc/nginx/lua/?.lua;;"; + + # ModSecurity global + modsecurity on; + modsecurity_rules_file /etc/nginx/modsecurity/modsecurity.conf; + + # Lua global for all requests + access_by_lua_file /etc/nginx/lua/upload_check.lua; + + server { listen 80; @@ -54,7 +95,7 @@ http { listen 443 ssl; listen [::]:443 ssl; - server_name $SERVER_WALLET_STAT; + server_name ${SERVER_WALLET_STAT}; location / { add_header Access-Control-Allow-Credentials true always; @@ -70,7 +111,7 @@ http { listen 443 ssl; listen [::]:443 ssl; - server_name $SERVER_NAME_KIBANA; + server_name ${SERVER_NAME_KIBANA}; location / { set $backend http://kibana:5601; @@ -82,7 +123,18 @@ http { listen 443 ssl; listen [::]:443 ssl; - server_name $SERVER_NAME_GRAFANA; + server_name ${SERVER_NAME_SWAGGER}; + + location / { + set $backend http://swagger-docs:8080; + proxy_pass $backend; + } + } + server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name ${SERVER_NAME_GRAFANA}; location / { set $backend http://grafana:3000; @@ -94,7 +146,7 @@ http { listen 443 ssl; listen [::]:443 ssl; - server_name $SERVER_NAME_ADMIN_PANEL; + server_name ${SERVER_NAME_ADMIN_PANEL}; location ~* \.(.*)$ { set $backend http://admin-panel; @@ -112,7 +164,32 @@ http { listen 443 ssl; listen [::]:443 ssl; - server_name $SERVER_NAME_WEB_APP; + server_name ${SERVER_NAME_ADMIN_V2_PANEL}; + + client_max_body_size 50M; + + location / { + proxy_pass http://admin-v2-panel-nginx; + + 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 https; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port 443; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + } + + server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name ${SERVER_NAME_WEB_APP}; location ~* \.(.*)$ { set $backend http://web-app; @@ -130,7 +207,7 @@ http { listen 443 ssl; listen [::]:443 ssl; - server_name $SERVER_NAME_MOBILE_APP; + server_name ${SERVER_NAME_MOBILE_APP}; location ~* \.(.*)$ { set $backend http://mobile-app; @@ -148,7 +225,7 @@ http { listen 443 ssl; listen [::]:443 ssl; - server_name $SERVER_NAME_AUTH; + server_name ${SERVER_NAME_AUTH}; location / { set $backend http://auth:8080; @@ -161,7 +238,65 @@ http { listen 443 ssl; listen [::]:443 ssl; - server_name $SERVER_NAME_HEALTH; + server_name ${SERVER_NAME_KC}; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + proxy_pass http://keycloak:8080; + } + } + + server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name ${SERVER_NAME_V2_AUTH}; + + if ($request_method = 'OPTIONS') { + return 204; + } + + location / { + 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 Authorization $http_authorization; + proxy_pass http://auth-gateway:8080; + } + } + + server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name v2api.opex.dev; + + if ($request_method = 'OPTIONS') { + return 204; + } + + location / { + 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 Authorization $http_authorization; + proxy_pass http://api:8080; + rewrite ^/(.*)$ /opex/$1 break; + } + } + + server { + listen 443 ssl; + listen [::]:443 ssl; + + server_name ${SERVER_NAME_HEALTH}; location /accountant { set $backend http://accountant:8080; @@ -222,6 +357,11 @@ http { proxy_pass $backend; rewrite ^/auth(.*)$ /actuator/health/$1 break; } + location /auth-gateway { + set $backend http://auth-gateway:8080; + proxy_pass $backend; + rewrite ^/auth-gateway(.*)$ /actuator/health/$1 break; + } location /wallet { set $backend http://wallet:8080; @@ -246,7 +386,7 @@ http { listen 443 ssl; listen [::]:443 ssl; - server_name $SERVER_NAME_API; + server_name ${SERVER_NAME_API}; if ($request_method = 'OPTIONS') { return 204; @@ -335,6 +475,12 @@ http { proxy_pass $backend; rewrite ^/api/(.*)$ /$1 break; } + + location /profile { + set $backend http://profile:8080; + proxy_pass $backend; + rewrite ^/profile/(.*)$ /$1 break; + } location /sapi { set $backend http://api:8080; @@ -342,6 +488,12 @@ http { rewrite ^/sapi/(.*)$ /$1 break; } + location /bc { + set $backend http://bc-gateway:8080; + proxy_pass $backend; + rewrite ^/bc/(.*)$ /$1 break; + } + location /captcha { add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Origin $http_origin always; @@ -353,7 +505,10 @@ http { rewrite ^/captcha/(.*)$ /$1 break; } - location /binance { + resolver 127.0.0.11 valid=30s; + resolver_timeout 15s; + + location /binance { proxy_set_header Host api.binance.com; proxy_set_header X-Real-IP ''; proxy_set_header X-Forwarded-For ''; @@ -362,6 +517,29 @@ http { proxy_pass $backend; rewrite ^/binance/(.*)$ /$1 break; } + } + + server { + listen 443 ssl http2; + server_name ${SERVER_NAME_BETA_APP}; + + set $web_app_root /var/www/desktop; + + if ($http_user_agent ~* (iphone|ipod|android|blackberry|windows\ phone|mobile|opera\ mini|iemobile)) { + set $web_app_root /var/www/mobile; + } + + location / { + root $web_app_root; + try_files $uri $uri/ /index.html; + } + + location = /index.html { + root $web_app_root; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + } + }