Fix Docker sticky footer, commit hash, and configurable local admin password (#133)

* Fix TLS handshake failure for any host without a cached target

Reported: fallback SSL doesn't work in the Docker build. Reproduced —
it's worse than the fallback specifically: TLS was broken for nearly
every connection, including ones with no SNI at all:

  $ curl -vk https://127.0.0.1/
  * TLSv1.3 (IN), TLS alert, internal error (592)
  * OpenSSL/3.0.13: error:0A000438:SSL routines::tlsv1 alert internal error

Root cause: targetinfo.lua's M.get() is shared by two call sites in
two incompatible nginx phases —

  - proxy.conf's access_by_lua_block (a normal HTTP request phase,
    where ngx.exit() is valid)
  - nginx.conf's request_domain callback, which runs during the TLS
    handshake itself (ssl_certificate_by_lua*), where ngx.exit() is
    NOT a supported API

M.get() called ngx.exit() on every lookup failure (no domain/SNI, a
Redis error, or an unregistered host). When invoked from the SSL
phase, that aborted the handshake with a bare "internal error" alert
and produced no log output anywhere — silent and total, not limited
to the unregistered-domain case, since even a connection with no SNI
hits the same code path immediately.

Fix: M.get() no longer calls ngx.exit() itself — it returns
(nil, httpStatus) on failure. proxy.conf now checks the return value
and calls ngx.exit() itself (the phase where that's actually
supported). nginx.conf's request_domain guards the now-possibly-nil
result before indexing it, and leaves ngx.ctx.toAllow unset on
failure so allow_domain() correctly denies issuance and auto-ssl
falls through to the static fallback cert in autossl.conf.

Verified end to end against a running Docker build (deployed the
changed files into a live container and reloaded, rather than relying
on a full rebuild each iteration):
- No SNI at all: TLS now completes; HTTP layer correctly returns 406
  (previously: broken handshake, no response at all)
- Unregistered SNI: same — TLS completes, 406, and openssl s_client
  confirms the cert served is genuinely the fallback
  (CN=sni-support-required-for-valid-ssl)
- A real registered Host: TLS completes and proxies through to the
  backend correctly (confirms the success path is unaffected)
- npm test: 192/192 pass

* Fix footer not sticking to the bottom on short pages

body had no sticky-footer layout at all (sso-manager-node already had
this; proxy never did), so on any page with little content (e.g.
/login) the footer sat right after the content instead of at the
bottom of the viewport, leaving a large gap below it.

Added the same flex-based pattern already used in sso-manager-node:
body is a column flex container, #spa-shell grows to fill the
remaining space, pushing the footer (the next sibling) to the bottom.

Verified visually (screenshot) and via computed layout
(footer.getBoundingClientRect().bottom === window.innerHeight) before
and after.

* Fix commit hash not showing in Docker builds

build_info.js computed buildHash via `git rev-parse --short HEAD` at
runtime, but the final image intentionally has no git binary and no
.git directory (kept lean, per .dockerignore) — so this always failed
silently and the footer's version line showed "unknown" for every
Docker deployment. Working correctly only for bare-metal/dev, where
git + .git are actually present.

Added a throwaway gitinfo build stage that reuses the main base image
(no extra pull) with git installed just for this stage, reads .git
from the build context (now no longer excluded — see .dockerignore),
and bakes the resolved short hash into a small file that IS copied
into the final image. build_info.js reads that file first, falling
back to the old git-rev-parse behavior (still needed for bare-metal).

Verified against a real build: `docker exec proxy cat
/app/.build_commit` matches `git rev-parse --short HEAD` on the host,
and the footer now shows the real hash instead of "unknown".

* Allow the local anti-lockout admin's initial password to be configured

The local "proxyadmin2" bootstrap account was always created with
username == password == "proxyadmin2" — a hardcoded, publicly-known
default with no way to set it to something else before first boot.
Fine for a quick local test, not for anything exposed publicly, and
orchestrators like theta-env's setup.sh (which already generates a
random password for the SSO admin) had no way to do the same here.

Added conf.auth.localAdminPass (proxy-secrets.js / app_auth__localAdminPass):
if set, it's used as the initial password instead of the hardcoded
default. Only read on first creation — once the account exists this
is never consulted again, so it's safe to leave set. Falls back to
the previous behavior (password == username) when unset, so this is
fully backward compatible.

Verified: with app_auth__localAdminPass set, login with the new
password succeeds and the old default ("proxyadmin2") is correctly
rejected. Confirmed in a real Docker build too (secrets.js
auth.localAdminPass), and npm test 192/192 pass.

* Support GIT_COMMIT build-arg override for submodule builds

The gitinfo stage from the previous commit works for a standalone
clone (.git is a real directory) but not when this repo is built as a
git submodule (e.g. from theta-env): a submodule's .git is a pointer
FILE, not a directory — the real object database lives in the
superproject's .git/modules/, outside this repo's own directory and
therefore outside Docker's build context entirely. `git rev-parse`
can never resolve it from in here no matter what, so builds via
theta-env still baked in "unknown" despite the earlier fix.

Add an optional GIT_COMMIT build-arg that, when set, wins over the
in-context git resolution. theta-env's setup.sh now computes it on the
host (where the submodule DOES resolve correctly) and passes it via
docker-compose.yml's build.args.

Verified via theta-env's actual setup.sh end to end: rebuilding with
this change, `docker exec proxy cat /app/.build_commit` now matches
`git -C proxy rev-parse --short HEAD` on the host (previously:
"unknown", confirmed via the "[Warning] One or more build-args
[GIT_COMMIT] were not consumed" message before this fix synced into
the docker-compose.yml side).
This commit is contained in:
2026-07-14 22:32:24 -04:00
committed by GitHub
parent 22f382b968
commit c68fcc9ddb
7 changed files with 81 additions and 8 deletions
+4 -1
View File
@@ -19,7 +19,10 @@ docs/
.github/
# Git + editor + secrets.
.git/
# NOTE: .git/ is intentionally NOT excluded — the gitinfo build stage in the
# Dockerfile reads it to bake the commit hash into the image (see
# nodejs/utils/build_info.js), then it's discarded before the final stage.
# It never ends up in the final image.
.gitignore
*.md
!README.md