7c0cb5eabd5cb95428bc6e4c7bd28dc8b2af1d36
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1df9916c91 |
Add standalone backup script and admin update-check banner
ops/backup.sh snapshots Redis (BGSAVE, dynamic RDB path lookup) and ./config for standalone deployments, with retention. A background service polls GitHub releases every 24h and surfaces an admin-only banner in the UI when a newer version is published. |
||
|
|
c68fcc9ddb |
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). |
||
|
|
5139fbb79a |
Documentation cleanup for public release (#123)
Prepares the docs for the public release announcement: removes obsolete/dead material, fixes drift between the API reference and the actual routes, and standardizes on the default GitHub Pages URL. - Remove Vagrant entirely: delete Vagrantfile, docs/dev_setup.md, and stale vagrant references in .gitignore/.dockerignore; rewrite openresty/README.md to describe the actual (currently unused) directory and point to ops/nginx_conf/ for the real OpenResty config. - Delete docs/Update 4.11.md (personal scratch changelog) and drop both its and dev_setup.md's references from docs/README.md's Legacy Documentation section. - Remove checkmark emoji from docs/contributing.md's PR Requirements list. - Bring the auth model docs up to date with the code: document GET /api/auth/oidc/start + /callback, the /api/permission and /api/group RBAC routers, the /api/dns/dynamic/* sub-API, and /api/api-token (self -service PATs) in both nodejs/api.md and docs/api.md; add the missing "Clear Host Cache" section; drop the invite-token/SSH-key endpoints that no longer exist in nodejs/routes/user.js; note admin-only routes. Mention OIDC/LDAP/RBAC as core features in README.md. - Keep nodejs/api.md and docs/api.md fully in sync (same body, differing only in Jekyll front matter / relative links) instead of letting them drift. - Fix Node.js version references (20.x -> 22.x) in README.md and docs/installation.md to match ops/install.sh and the Dockerfile. - Note that the manual nginx-conf/systemd install steps in README.md and docs/installation.md won't auto-track repo changes the way install.sh's symlink approach does, and recommend install.sh. - Update the stale test/unit file lists in docs/contributing.md and nodejs/test/README.md to match the actual directory contents. - Add npm run test:integration to README.md's Running Tests section. - Add nodejs/conf/, nodejs/controller/, and nodejs/migrations/ to the project structure diagrams in README.md, docs/architecture.md, and docs/contributing.md. - Standardize "CloudFlare" -> "Cloudflare" everywhere to match the actual API value in nodejs/models/dns_provider.js. - Add the missing app_auth__adminGroups row to DEPLOYMENT.md's app_* table. - Delete docs/CNAME (custom domain) so GitHub Pages serves from the default https://theta42.github.io/proxy/, matching docs/README.md. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a9a48c3445 |
Add self-service API tokens (PATs) with UI + Bearer auth (#119)
Personal access tokens so scripts/CI can call the management API without an OIDC browser session. Each logged-in user mints their own token; it authenticates as the creator (groups snapshotted at mint, mirroring the proxy's browser AuthToken), and the existing authz layer (Permission.effectiveFor / roles.resolveEffective) applies unchanged. Local groups and owned-domain rights are recomputed live; only SSO/LDAP group membership is the mint-time snapshot. - models/api_token.js: new ApiToken model (prx_<id>_<secret> format; id is the lookup key, secret bcrypt-hashed + isPrivate, shown once). add()/rotate()/ authenticate(); optional expires_at; best-effort last_used_on; groups snapshot. No _ttl (persists). Deliberately NOT wrapped in ModelPs (so the last_used_on write on the auth path doesn't spam the socket). - routes/api_token.js: self-service CRUD (list/get/update/delete/rotate), owner-scoped (created_by === reqUsername(req), 403 otherwise). - middleware/auth.js + models/auth.js: accept `Authorization: Bearer prx_...` (precedence over the auth-token session header). Builds a synthetic req.token that satisfies the only three req.token reads (auth.js .user/.groupsArray, authz.js reqUsername .created_by) so the authz layer works unchanged. checkApiToken collapses every failure to one generic 401 (no leak). - views/api_tokens.ejs + routes/render.js (GET /api-tokens): self-service page (forceLogin, no admin gate) — create (token shown once), rotate, revoke. - views/top.ejs: "API Tokens" nav entry visible to all logged-in users. - public/js/app.js: app.apiToken client module. - DEPLOYMENT.md + docs/docker.md: API tokens section. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
8e78604a37 |
Persist Redis (AOF+vol), read config from mounted secrets.js, add backup/restore docs (#118)
Lossless upgrades + config story for the all-in-one proxy image. Redis persistence (Part A): - Replace in-memory `--save "" --appendonly no` with AOF + RDB persisted to /data. Host records, permissions, DNS creds, local users, AND the lua-resty-auto-ssl Let's Encrypt certs now all survive container recreation (persisting Redis persists the cert store — no LE re-issue / rate-limit on rebuild). - Add the `proxy-data` named volume -> /data in docker-compose.yml; fix the stale "in-memory, lost on recreation" comment. Config from ./config/proxy-secrets.js (Part B): - docker-entrypoint.sh: when /config/proxy-secrets.js is mounted, symlink it to /app/conf/secrets.js so @simpleworkjs/conf reads the oidc/ldap/auth config from the file. No app_* env should then be passed (app_* beats secrets.js). Falls back to app_* env when the file is absent (standalone still works). - docker-compose.yml: drop all app_oidc__* / app_ldap__* / app_auth__* env and add `./config:/config:ro`. Keep RESOLVER/REAL_IP_FROM/NODE_ENV/NODE_PORT (OpenResty-runtime / process env, not app_* config). No env_file. - New secrets.js.example (the proxy had none): oidc (enabled, endpoints, clientId/clientSecret, redirectUri, scopes, claims), ldap (url, bindDN, bindPassword, searchBase, userFilter, tlsOptions), auth (adminGroups, adminUsers, groupRoleMap), plus an orchestrator-only `stack` key. Backup/restore docs: - Full "Backups and restore" runbook in DEPLOYMENT.md (what lives where, manual backup, Redis restore with the AOF-vs-RDB note — AOF wins on startup so the AOF must be deleted before an RDB load; restoring Redis restores cert state at snapshot time; migrations note). Update the Setup + Auto-SSL sections. - docs/docker.md: update Quick start + How configuration works + Auto-SSL for the new ./config/ approach (app_* env now advanced/optional). Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
865fced691 |
docs: add a Logs (Docker) section
Document how to get logs when running the all-in-one image: docker compose logs for app + OpenResty (stdout/stderr), and the nginx access/error logs which go to /var/log/nginx on the proxy-logs volume (not docker logs). Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
94ad6143cc |
Dockerize the proxy (all-in-one image) + Docker docs
All-in-one Dockerfile bundling OpenResty + the Node mgmt app + Redis in one container, mirroring the bare-metal ops/install.sh layout: - Dockerfile (openresty/openresty:1.31.1.1-2-bookworm-fat base; dumb-init PID 1; luarocks install lua-resty-auto-ssl/luasocket/lua-resty-ipmatcher; node 22.x; npm ci --omit=dev; OpenResty confs + lua copied into place). - docker-entrypoint.sh: fallback cert, sed-parameterize RESOLVER/REAL_IP_FROM, start bundled redis + node app, exec openresty foreground. - docker-compose.yml (standalone), .dockerignore, DEPLOYMENT.md. - nodejs/routes/render.js: /health endpoint for healthchecks. - nodejs/models/user_ldap.js: tlsOptions forwarded to ldapts Client so the proxy can bind ldaps:// with a self-signed cert (app_ldap__tlsOptions__*). - nodejs/package.json: bump @simpleworkjs/conf to ^1.1.0 (app_* env overrides). - docs/docker.md + index.md: Docker deployment guide + fronting an SSO Manager. - ops/proxy.service: add WorkingDirectory=/var/www/proxy/nodejs (bare-metal cwd fix so relative conf/ paths resolve). Co-Authored-By: Claude <noreply@anthropic.com> |