# Reference copy of the Helpdesk parts of the operator console vhost on moje.al.army # (/opt/nginx/conf/sites-available/moje.al.army.conf). mTLS (ssl_verify_client on) is enforced by that # server block; this forwards to the loopback-only WEBrick backend and injects the operator identity + # the shared secret it checks. # # The X-Proxy-Secret value must equal HELPDESK_PROXY_SECRET from /etc/helpdesk/env - inject the real # value when installing; never commit it. The live conf is chmod 640 root:root because it holds the secret. # # THREE PARTS. Part 1 goes at the top of the file, OUTSIDE server{} (http context). Parts 2 and 3 both go # inside `server { listen 443 ssl; ... }` - part 2 at server level, part 3 as the location block. # ---------------------------------------------------------------- part 1: http context # Audit log: who reached the console, not just from where. $ssl_client_s_dn is the verified client-cert # subject, so this is the only record at the edge of which operator did what - the backend does not log # it. Operator names are staff identities, not secrets. No Authorization header is captured (the console # plane does not use one, and the device plane's bearer token must never be logged). # The serial is what actually identifies a certificate - operators can share a subject DN, so the DN # alone cannot tell you WHICH certificate was used. Both are needed for the audit trail to mean anything. log_format helpdesk_console '$remote_addr - [$time_local] "$request" $status $body_bytes_sent ' 'op="$ssl_client_s_dn" serial=$ssl_client_serial "$http_user_agent"'; # Backstop against a runaway console tab, NOT an attack control: limit_req runs after the TLS and client # cert handshake, so only an already-authenticated operator can reach it. WEBrick is thread-per-request # and a stuck polling loop could exhaust it. One console tab sits at roughly 1.5 r/s (pop 1.2s, lists 4s, # device 3s, status 30s), and every operator in the office shares one public IP through NAT, so this is # set well above realistic use: several operators with several tabs each still fit, while a loop spinning # thousands of times a second is cut off. limit_req_zone $binary_remote_addr zone=helpdesk_console:10m rate=20r/s; # ------------------------------------------- part 2: server context, next to the ssl_ lines (NOT the location) # These sit at SERVER level on purpose. A client with no valid certificate is rejected with a 400 during # request processing, before nginx picks a location at all, so headers declared inside the location would # be missing from exactly the response a stray browser sees. At server level they are inherited by the # location AND cover that 400. Inheritance is all-or-nothing: if the location ever declares an add_header # of its own, it silently loses every one of these. # No Content-Security-Policy: console.html is one large inline script, so any workable policy needs # 'unsafe-inline' and buys little. Extracting that script is the prerequisite for a real CSP. add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; # the console is never framed add_header Referrer-Policy "no-referrer" always; # ---------------------------------------------------------------- part 3: inside server { listen 443 ssl; } location / { limit_req zone=helpdesk_console burst=40 nodelay; proxy_pass http://127.0.0.1:4000; 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 $scheme; proxy_set_header X-Operator-Cert $ssl_client_s_dn; # verified client-cert subject proxy_set_header X-Proxy-Secret "REPLACE_WITH_HELPDESK_PROXY_SECRET"; # Expiry of the certificate THIS operator just presented. There is one per operator, they live in # people's browsers and the server holds no copy, so this is the only way to warn someone that their # own certificate is running out. The console shows it in the same badge as the server-side ones. proxy_set_header X-Operator-Cert-Days $ssl_client_v_remain; # whole days left proxy_set_header X-Operator-Cert-Expires $ssl_client_v_end; # "Dec 15 12:00:00 2027 GMT" proxy_read_timeout 65s; client_max_body_size 25m; } # And point the vhost's access log at the format above: # access_log /opt/nginx/logs/moje.al.army.access.log helpdesk_console;