From e0c6f474f12dd17272faefbdd93d5a03e2d44657 Mon Sep 17 00:00:00 2001 From: TheRealSeber Date: Sat, 7 Mar 2026 20:14:48 +0100 Subject: [PATCH 1/5] feat: expose file-storage signed URL downloads through nginx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add nginx location /buckets/ that proxies to file-storage with GET-only enforcement (limit_except GET deny all) — write operations remain inaccessible from outside the docker network - Add SIGNING_SECRET and ROOT_DIRECTORY env vars to file-storage service - Mount file-storage-media volume so uploaded files survive restarts - Add FILE_STORAGE_PUBLIC_URL and SIGNED_URL_SECRET_KEY to backend so signed URLs are browser-reachable and signatures match file-storage Co-Authored-By: Claude Sonnet 4.6 --- docker-compose/docker-compose.yaml | 6 ++++++ nginx/conf/nginx.conf | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/docker-compose/docker-compose.yaml b/docker-compose/docker-compose.yaml index 2dea475..f45320b 100644 --- a/docker-compose/docker-compose.yaml +++ b/docker-compose/docker-compose.yaml @@ -17,6 +17,10 @@ services: image: seber/maxit-file-storage:latest environment: - APP_PORT=8888 + - SIGNING_SECRET= + - ROOT_DIRECTORY=/data + volumes: + - file-storage-media:/data backend: image: maxit/backend:latest depends_on: @@ -37,6 +41,8 @@ services: - DB_NAME=maxit - FILE_STORAGE_HOST=file-storage - FILE_STORAGE_PORT=8888 + - FILE_STORAGE_PUBLIC_URL=https://mini-maxit.pl + - SIGNED_URL_SECRET_KEY= db: image: postgres:17 environment: diff --git a/nginx/conf/nginx.conf b/nginx/conf/nginx.conf index fd13923..890ea3f 100644 --- a/nginx/conf/nginx.conf +++ b/nginx/conf/nginx.conf @@ -72,6 +72,23 @@ http { add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # File storage - signed URL downloads only, no write access from outside + location /buckets/ { + limit_except GET { + deny all; + } + + proxy_pass http://file-storage:8888; + 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_connect_timeout 30s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + # Frontend serving location / { limit_req zone=frontend burst=50 nodelay; From 4dbee0cf5b713fffd2f22f59888d4d1e65c58e6b Mon Sep 17 00:00:00 2001 From: TheRealSeber Date: Sat, 7 Mar 2026 23:50:51 +0100 Subject: [PATCH 2/5] chore: another one --- docker-compose/docker-compose.yaml | 2 ++ nginx/conf/nginx.conf | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docker-compose/docker-compose.yaml b/docker-compose/docker-compose.yaml index f45320b..c9eb1d0 100644 --- a/docker-compose/docker-compose.yaml +++ b/docker-compose/docker-compose.yaml @@ -18,6 +18,7 @@ services: environment: - APP_PORT=8888 - SIGNING_SECRET= + - INTERNAL_API_KEY= - ROOT_DIRECTORY=/data volumes: - file-storage-media:/data @@ -74,6 +75,7 @@ services: - JOBS_DATA_VOLUME=maxit_jobs-data # watch out for prefix "maxit_" the same as compose name - RESPONSE_QUEUE_NAME=worker_response_queue - WORKER_QUEUE_NAME=worker_queue + - STORAGE_INTERNAL_KEY= frontend: image: maxit/frontend:latest depends_on: diff --git a/nginx/conf/nginx.conf b/nginx/conf/nginx.conf index 890ea3f..c742cb2 100644 --- a/nginx/conf/nginx.conf +++ b/nginx/conf/nginx.conf @@ -83,6 +83,8 @@ http { 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; + # Strip the internal API key so external clients cannot forge it + proxy_set_header X-Internal-Key ""; proxy_connect_timeout 30s; proxy_send_timeout 60s; From 71f61883c730bc21100488d9e51a09a77782dcc9 Mon Sep 17 00:00:00 2001 From: HermanPlay Date: Mon, 9 Mar 2026 17:57:10 +0100 Subject: [PATCH 3/5] refactor: remove SIGNED_URL_SECRET_KEY from backend env; update file-storage ports --- docker-compose/docker-compose.yaml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docker-compose/docker-compose.yaml b/docker-compose/docker-compose.yaml index c9eb1d0..f656c32 100644 --- a/docker-compose/docker-compose.yaml +++ b/docker-compose/docker-compose.yaml @@ -15,10 +15,13 @@ services: image: rabbitmq:3.13-management file-storage: image: seber/maxit-file-storage:latest + ports: + - "8888:8080" # public server (for signed URL access) + - "8889:8081" # internal server (for backend/worker) environment: - - APP_PORT=8888 + - APP_PORT=8080 + - INTERNAL_PORT=8081 - SIGNING_SECRET= - - INTERNAL_API_KEY= - ROOT_DIRECTORY=/data volumes: - file-storage-media:/data @@ -41,9 +44,8 @@ services: - DB_PASSWORD=password - DB_NAME=maxit - FILE_STORAGE_HOST=file-storage - - FILE_STORAGE_PORT=8888 - - FILE_STORAGE_PUBLIC_URL=https://mini-maxit.pl - - SIGNED_URL_SECRET_KEY= + - FILE_STORAGE_PORT=8889 + - FILE_STORAGE_PUBLIC_URL=https://mini-maxit.pl/files db: image: postgres:17 environment: @@ -75,7 +77,8 @@ services: - JOBS_DATA_VOLUME=maxit_jobs-data # watch out for prefix "maxit_" the same as compose name - RESPONSE_QUEUE_NAME=worker_response_queue - WORKER_QUEUE_NAME=worker_queue - - STORAGE_INTERNAL_KEY= + - STORAGE_HOST=file-storage + - STORAGE_PORT=8889 frontend: image: maxit/frontend:latest depends_on: From d88f0b53da4073ff088df94f83e79c45a095ff90 Mon Sep 17 00:00:00 2001 From: HermanPlay Date: Tue, 10 Mar 2026 13:14:36 +0100 Subject: [PATCH 4/5] chore: fix file-storage ports and remove stale SIGNING_SECRET env var --- docker-compose/docker-compose.yaml | 7 +++---- nginx/conf/nginx.conf | 9 +++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docker-compose/docker-compose.yaml b/docker-compose/docker-compose.yaml index f656c32..38ce565 100644 --- a/docker-compose/docker-compose.yaml +++ b/docker-compose/docker-compose.yaml @@ -17,11 +17,10 @@ services: image: seber/maxit-file-storage:latest ports: - "8888:8080" # public server (for signed URL access) - - "8889:8081" # internal server (for backend/worker) + - "8081:8081" # internal server (for backend/worker) environment: - APP_PORT=8080 - INTERNAL_PORT=8081 - - SIGNING_SECRET= - ROOT_DIRECTORY=/data volumes: - file-storage-media:/data @@ -44,7 +43,7 @@ services: - DB_PASSWORD=password - DB_NAME=maxit - FILE_STORAGE_HOST=file-storage - - FILE_STORAGE_PORT=8889 + - FILE_STORAGE_PORT=8081 - FILE_STORAGE_PUBLIC_URL=https://mini-maxit.pl/files db: image: postgres:17 @@ -78,7 +77,7 @@ services: - RESPONSE_QUEUE_NAME=worker_response_queue - WORKER_QUEUE_NAME=worker_queue - STORAGE_HOST=file-storage - - STORAGE_PORT=8889 + - STORAGE_PORT=8081 frontend: image: maxit/frontend:latest depends_on: diff --git a/nginx/conf/nginx.conf b/nginx/conf/nginx.conf index c742cb2..95c3d13 100644 --- a/nginx/conf/nginx.conf +++ b/nginx/conf/nginx.conf @@ -73,18 +73,19 @@ http { add_header Referrer-Policy "strict-origin-when-cross-origin" always; # File storage - signed URL downloads only, no write access from outside - location /buckets/ { + # Frontend URLs are like: /files/buckets/maxit/task/1/file.pdf?expires=...&signature=... + # We strip the /files prefix so file-storage receives /buckets/... (matching the signed path) + location /files/ { limit_except GET { deny all; } - proxy_pass http://file-storage:8888; + rewrite ^/files(/.*)$ $1 break; + proxy_pass http://file-storage:8080; 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; - # Strip the internal API key so external clients cannot forge it - proxy_set_header X-Internal-Key ""; proxy_connect_timeout 30s; proxy_send_timeout 60s; From 2a7f658c881a4534d3ee9671b50a8ad517d91cb1 Mon Sep 17 00:00:00 2001 From: HermanPlay Date: Tue, 10 Mar 2026 19:27:34 +0100 Subject: [PATCH 5/5] fix --- docker-compose/docker-compose.yaml | 17 ++-- nginx/conf/nginx.conf | 20 ++++- nginx/conf/nginx.local.conf | 120 +++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 nginx/conf/nginx.local.conf diff --git a/docker-compose/docker-compose.yaml b/docker-compose/docker-compose.yaml index 38ce565..d543d7d 100644 --- a/docker-compose/docker-compose.yaml +++ b/docker-compose/docker-compose.yaml @@ -10,17 +10,19 @@ services: - ../nginx/ssl:/etc/nginx/ssl:ro depends_on: - frontend + - backend + - file-storage restart: unless-stopped rabbitmq: image: rabbitmq:3.13-management file-storage: image: seber/maxit-file-storage:latest ports: - - "8888:8080" # public server (for signed URL access) - - "8081:8081" # internal server (for backend/worker) + - "8888:8888" # public server (for signed URL access) + - "8080:8080" # internal server (for backend/worker) environment: - - APP_PORT=8080 - - INTERNAL_PORT=8081 + - APP_PORT=8888 + - INTERNAL_PORT=8080 - ROOT_DIRECTORY=/data volumes: - file-storage-media:/data @@ -43,7 +45,7 @@ services: - DB_PASSWORD=password - DB_NAME=maxit - FILE_STORAGE_HOST=file-storage - - FILE_STORAGE_PORT=8081 + - FILE_STORAGE_PORT=8080 - FILE_STORAGE_PUBLIC_URL=https://mini-maxit.pl/files db: image: postgres:17 @@ -77,14 +79,15 @@ services: - RESPONSE_QUEUE_NAME=worker_response_queue - WORKER_QUEUE_NAME=worker_queue - STORAGE_HOST=file-storage - - STORAGE_PORT=8081 + - STORAGE_PORT=8080 frontend: image: maxit/frontend:latest depends_on: - backend environment: - - BACKEND_URL=http://backend:8000/api/v1 + - PUBLIC_BACKEND_API_URL=https://mini-maxit.pl/api/v1 - ORIGIN=https://mini-maxit.pl + - BODY_SIZE_LIMIT=20M expose: - "3000" volumes: diff --git a/nginx/conf/nginx.conf b/nginx/conf/nginx.conf index 95c3d13..816ecf5 100644 --- a/nginx/conf/nginx.conf +++ b/nginx/conf/nginx.conf @@ -33,8 +33,9 @@ http { application/atom+xml image/svg+xml; - # Rate limiting for frontend + # Rate limiting limit_req_zone $binary_remote_addr zone=frontend:10m rate=30r/s; + limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; proxy_buffer_size 32k; proxy_buffers 8 32k; @@ -72,6 +73,21 @@ http { add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # Backend API + location /api/ { + limit_req zone=api burst=50 nodelay; + + proxy_pass http://backend:8000; + 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_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + # File storage - signed URL downloads only, no write access from outside # Frontend URLs are like: /files/buckets/maxit/task/1/file.pdf?expires=...&signature=... # We strip the /files prefix so file-storage receives /buckets/... (matching the signed path) @@ -81,7 +97,7 @@ http { } rewrite ^/files(/.*)$ $1 break; - proxy_pass http://file-storage:8080; + proxy_pass http://file-storage:8888; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/nginx/conf/nginx.local.conf b/nginx/conf/nginx.local.conf new file mode 100644 index 0000000..ef89998 --- /dev/null +++ b/nginx/conf/nginx.local.conf @@ -0,0 +1,120 @@ +events { + worker_connections 1024; +} + +http { + client_max_body_size 25m; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=frontend:10m rate=30r/s; + limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s; + + proxy_buffer_size 32k; + proxy_buffers 8 32k; + proxy_busy_buffers_size 64k; + large_client_header_buffers 4 64k; + + server { + listen 80; + server_name _; + + # Backend API + location /api/ { + limit_req zone=api burst=50 nodelay; + + proxy_pass http://backend:8000; + 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_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # File storage - signed URL downloads only, no write access from outside + # Frontend URLs are like: /files/buckets/maxit/task/1/file.pdf?expires=...&signature=... + # We strip the /files prefix so file-storage receives /buckets/... (matching the signed path) + location /files/ { + limit_except GET { + deny all; + } + + rewrite ^/files(/.*)$ $1 break; + proxy_pass http://file-storage:8888; + 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_connect_timeout 30s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Frontend serving + location / { + limit_req zone=frontend burst=100 nodelay; + + proxy_pass http://frontend:3000; + 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; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Nginx health check endpoint + location /health { + access_log off; + return 200 "nginx healthy\n"; + add_header Content-Type text/plain; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + } +}