diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..29ff65d --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,21 @@ +services: + openresty: + image: openresty/openresty:1.21.4.1-0-jammy + container_name: openresty + ports: + - 8081:8081 + volumes: + - ./nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf + expose: + - "8081" + node: + build: + context: ./node + container_name: node + volumes: + - ./node/web.js:/usr/src/app/web.js + environment: + NODE_ENV: production + PORT: 3000 + ports: + - 3000:3000 diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..df273fe --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,142 @@ +worker_processes 4; + +error_log stderr debug; + +worker_rlimit_nofile 40000; + +events { + worker_connections 16383; + multi_accept on; + use epoll; +} + +http { + + ## + # Basic stuff + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 1200; + types_hash_max_size 2048; + map_hash_bucket_size 128; + server_tokens off; + + # see https://stackoverflow.com/a/37656784 + resolver 127.0.0.11 ipv6=off; + + default_type application/octet-stream; + types { + text/plain log; + text/plain asc; + } + + ## + # Logging + ## + + log_format main '$remote_addr - [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" $request_time ' + '$upstream_response_time $pipe'; + + access_log /dev/stdout; + + ## + # Gzip + ## + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_types text/plain text/css application/x-javascript application/json text/xml application/xml application/xml+rss text/javascript application/rss+xml application/javascript; + + + ## + # Timeout variables (currently disabled) + ## + + client_body_timeout 10m; + client_max_body_size 1024m; + + large_client_header_buffers 4 16k; + + # Add map for websockets + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + # Add secure header values if not set upstream, else add headers with the + # implicit default value of empty string, which is ignored by the `add_header` + # directive. + map $upstream_http_strict_transport_security $sts { + '' 'max-age=31536000'; + } + map $upstream_http_x_frame_options $frame_options { + default '$upstream_http_x_frame_options'; + '' 'DENY'; + 'ALLOWALL' ''; + } + map $upstream_http_x_content_type_options $content_type_options { + '' 'nosniff'; + } + map $upstream_http_x_xss_protection $xss_protection { + '' '1; mode=block'; + } + map $upstream_http_content_type $default_content_type { + '' 'text/plain; charset=utf-8'; + } + map $upstream_status $mapped_content_type { + default '$default_content_type'; + '204' ''; + '304' ''; + } + + include /etc/nginx/conf.d/*.conf; + + server { + listen 8081; + + set $backend "http://node:3000"; + + # proxy all traffic + location / { + + root /usr/local/openresty/nginx/html; + index index.html index.htm; + + ## + # Security + ## + + add_header Strict-Transport-Security $sts always; + add_header X-Content-Type-Options $content_type_options always; + add_header X-XSS-Protection $xss_protection always; + add_header Content-Type $mapped_content_type always; + + header_filter_by_lua_block { + if ngx.var.upstream_http_content_type == nil and ngx.var.mapped_content_type == "" then + ngx.log(ngx.INFO, "no Content-Type header") + end + } + + proxy_buffering off; + proxy_buffer_size 16k; + proxy_buffers 4 16k; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $host; + proxy_set_header Proxy ""; + proxy_redirect off; + proxy_pass_request_headers on; + proxy_connect_timeout 10; + proxy_read_timeout 600; + proxy_pass $backend; + } + } +} + diff --git a/docker/node/Dockerfile b/docker/node/Dockerfile new file mode 100644 index 0000000..6d68d90 --- /dev/null +++ b/docker/node/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 + +ARG NODE_VERSION=20.10.0 + +FROM node:${NODE_VERSION}-alpine + +# Use production node environment by default. +ENV NODE_ENV production + +WORKDIR /usr/src/app + +# Run the application as a non-root user. +USER node + +# Copy the rest of the source files into the image. +COPY . . + +# Expose the port that the application listens on. +EXPOSE 3000 + +# Run the application. +CMD node web.js diff --git a/docker/node/web.js b/docker/node/web.js new file mode 100644 index 0000000..6ce34f9 --- /dev/null +++ b/docker/node/web.js @@ -0,0 +1,25 @@ +const http = require('http'); +const os = require('os'); +const port = process.env.PORT || 5000; + +http.createServer( (req, res) => { + var url = req.url; + + if (url === "/foo") { + res.writeHead(204); + res.end(); + } else if (url === "/bar") { + res.writeHead(304); + res.end(); + } else if (url === "/html") { + res.writeHead(200, undefined, { + 'Content-Type': 'text/html' + }); + res.end('

foobar

'); + } else { + res.writeHead(200); + res.end(`Hello World from NodeJS on port ${port} from container ${os.hostname()}`); + } +}).listen(port, () => { + console.log("Listening on " + port); +}); diff --git a/jobs/secureproxy/templates/config/nginx.conf.erb b/jobs/secureproxy/templates/config/nginx.conf.erb index 416114e..26cab10 100644 --- a/jobs/secureproxy/templates/config/nginx.conf.erb +++ b/jobs/secureproxy/templates/config/nginx.conf.erb @@ -104,6 +104,11 @@ http { map $upstream_http_content_type $default_content_type { '' 'text/plain; charset=utf-8'; } + map $upstream_status $mapped_content_type { + default '$default_content_type'; + '204' ''; + '304' ''; + } <% if p('secureproxy.csp.enable') %> # right now, we're doing this with report-only. later, we'll drop report-only and move to enforce with report @@ -179,7 +184,7 @@ http { add_header Strict-Transport-Security $sts always; add_header X-Content-Type-Options $content_type_options always; add_header X-XSS-Protection $xss_protection always; - add_header Content-Type $default_content_type always; + add_header Content-Type $mapped_content_type always; # Clear X-Frame-Options before setting so that ALLOWALL is cleared if set more_clear_headers X-Frame-Options; @@ -190,6 +195,12 @@ http { more_set_headers "<%= csp_header %>: $content_security_policy"; <% end %> + header_filter_by_lua_block { + if ngx.var.upstream_http_content_type == nil and ngx.var.mapped_content_type == "" then + ngx.log(ngx.INFO, "no Content-Type header") + end + } + ## # Implement per-domain IP Whitelist ## @@ -279,7 +290,13 @@ server { add_header Strict-Transport-Security $sts always; add_header X-Content-Type-Options $content_type_options always; add_header X-XSS-Protection $xss_protection always; - add_header Content-Type $default_content_type always; + add_header Content-Type $mapped_content_type always; + + header_filter_by_lua_block { + if ngx.var.upstream_http_content_type == nil and ngx.var.mapped_content_type == "" then + ngx.log(ngx.INFO, "no Content-Type header") + end + } # Clear X-Frame-Options before setting so that ALLOWALL is cleared if set more_clear_headers X-Frame-Options;