A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer answers technician calls, records both call legs as separate channels, and a Ruby backend transcribes them through Whisper and files an AI summary against the caller. Squashed to a single commit for sharing. No credentials are included; secrets live outside the repo in /etc/helpdesk/env on the server or a gitignored .claude/env.local locally. See .claude/env.local.example for the shape. Start at README.md, then docs/architecture.md.
65 lines
4.6 KiB
Text
65 lines
4.6 KiB
Text
# 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;
|