load_module modules/ngx_http_js_module.so; worker_processes auto; # Expose the instance-access JWT secret to njs (process.env) so the edge # gateway can verify desktop access tokens locally. Values come from the # container environment; they are not logged. env INSTANCE_ACCESS_TOKEN_SECRET; env JWT_SECRET; events { worker_connections 8192; } http { include /etc/nginx/mime.types; default_type application/octet-stream; js_import desktop from /etc/nginx/njs/desktop_auth.js; # Cluster DNS resolver, templated by start.sh from /etc/resolv.conf so the # desktop location can resolve per-instance Service FQDNs at request time. resolver __DNS_RESOLVER__ valid=10s ipv6=off; sendfile on; tcp_nopush on; tcp_nodelay on; gzip on; gzip_comp_level 6; gzip_min_length 1024; gzip_proxied any; gzip_vary on; gzip_types application/javascript application/json application/xml image/svg+xml text/css text/javascript text/plain text/xml; keepalive_timeout 75; keepalive_requests 10000; server_tokens off; map $http_upgrade $connection_upgrade { default upgrade; '' close; } map $desktop_target $desktop_proxy_mode { "deny" "deny"; "http://127.0.0.1:9001" "fallback"; default "direct"; } log_format desktop_proxy '$remote_addr - $status "$request_method $desktop_clean_uri $server_protocol" ' 'mode=$desktop_proxy_mode upstream="$upstream_addr" ' 'rt=$request_time urt=$upstream_response_time bytes=$body_bytes_sent'; upstream clawreef_backend { server 127.0.0.1:9001; keepalive 128; } server { listen 8443 ssl; http2 on; server_name _; ssl_certificate /etc/nginx/tls/tls.crt; ssl_certificate_key /etc/nginx/tls/tls.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; root /usr/share/nginx/html; index index.html; # Allow runtime workspace archives through the API. # Default nginx client_max_body_size (1 MiB) would reject normal # workspace uploads at the edge with 413 Content Too Large before # the request reaches the backend. start.sh rewrites this value from # CLAWMANAGER_WORKSPACE_ARCHIVE_MAX_MIB when set. client_max_body_size 500m; location = /healthz { access_log off; add_header Content-Type text/plain; return 200 'ok'; } location ^~ /assets/ { try_files $uri =404; access_log off; expires 1y; add_header Cache-Control "public, max-age=31536000, immutable"; } location ~* ^/(?:lobster_logo|lobster_transparent|openclaw|hermes)\.png$ { try_files $uri =404; access_log off; expires 30d; add_header Cache-Control "public, max-age=2592000"; } location ~* ^/vendor-icons/.+\.(?:png|jpg|jpeg|gif|webp|ico|svg)$ { try_files $uri =404; access_log off; expires 30d; add_header Cache-Control "public, max-age=2592000"; } location ~ ^/s/ { proxy_pass http://clawreef_backend; 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 Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_connect_timeout 15s; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; proxy_request_buffering off; } location ~ ^/api/v1/instances/(?[0-9]+)/proxy(?/.*)?$ { # njs verifies the instance-access JWT locally and chooses the # proxy target: the instance Service (direct), the in-process # control-plane proxy (gray fallback), or "deny". js_set $desktop_target desktop.resolveTarget; # Forward the original URI to the upstream but with the JWT "token" # query param stripped, so the access token never reaches webtop. js_set $desktop_clean_uri desktop.cleanUri; access_log /var/log/nginx/access.log desktop_proxy; add_header X-ClawManager-Desktop-Proxy $desktop_proxy_mode always; error_page 463 = @desktop_denied; if ($desktop_target = "deny") { return 463; } proxy_http_version 1.1; proxy_set_header Host $http_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-Prefix /api/v1/instances/$inst_id/proxy; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; # Browsers may include the original iframe URL (with ?token=...) # as Referer on subresource requests. Do not forward it to webtop. proxy_set_header Referer ""; proxy_ssl_verify off; proxy_ssl_server_name on; proxy_connect_timeout 15s; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; proxy_request_buffering off; proxy_pass $desktop_target$desktop_clean_uri; } location /api/ { proxy_pass http://clawreef_backend; 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 Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_connect_timeout 15s; proxy_read_timeout 3600s; } location / { try_files $uri $uri/ /index.html; } location @desktop_denied { add_header Content-Type text/plain always; return 401 "Access token expired or invalid"; } } }