Skip to content

feat(security): no-issue: add security.requireHttps setting to reject non-HTTPS clients#10154

Open
passcod wants to merge 5 commits into
mainfrom
feat/security-require-https
Open

feat(security): no-issue: add security.requireHttps setting to reject non-HTTPS clients#10154
passcod wants to merge 5 commits into
mainfrom
feat/security-require-https

Conversation

@passcod

@passcod passcod commented Jun 26, 2026

Copy link
Copy Markdown
Member

Changes

🤖 Server-side mitigation companion to #9942 (cleartext token-storage fallback for HTTP-only deployments). For deployments where HTTPS is available, this lets operators forbid non-HTTPS client access.

Setting: security.requireHttps (boolean), defined at the global, central, and per-facility scopes with no default — unset everywhere means HTTPS is not required.

Cascade / override semantics: the effective value resolves through the normal settings cascade, so a central or facility value overrides the global value in either direction.

  • Simple case: set it once at global to require HTTPS on every server.
  • Complex case: leave global unset and decide per central/facility, or set global on and opt a specific site out.

Enforcement: when in effect, a request that did not arrive over HTTPS is rejected with 403 before reaching any route. Detection is via X-Forwarded-Proto (honoured through the existing loopback trust proxy config); the index/health route stays reachable over HTTP. A facility server hosting multiple facilities enforces if the requirement is in effect for any hosted facility.

Anti-lockout guard: the admin settings endpoint refuses to enable security.requireHttps unless the enabling request is itself HTTPS — proving HTTPS reaches the server before it becomes mandatory. (Uses a truthy check so a non-boolean truthy value cannot slip past the guard yet still trigger enforcement.)

Spec: adds specs/administration/settings/require-https.md describing the subsystem.

Operational note (in the setting description): enforcement relies on proxy.trusted being configured (default loopback) and the TLS-terminating proxy forwarding X-Forwarded-Proto.

Tests: unit tests for the middleware decision logic (both req.settings shapes, enforce-if-any, error propagation); a facility-server integration test that enables the setting and confirms HTTP is rejected / HTTPS passes; and central admin guard tests (enable blocked over HTTP, allowed + persisted over HTTPS). Unit + settings-schema tests pass locally; the server integration tests were not run in my local worktree (symlinked deps / unbuilt shared dist) and will run in CI.

Auto-Deploy

  • Deploy
Options
  • Artillery load test
  • Seed from closest snapshot
  • Generate fake data
  • More data (20Gi)
  • No facility servers (central-only)
  • No sync (facility tasks scaled to zero)
  • AMD64 architecture (default is arm64)
  • Skip mobile build
  • Always build mobile
  • Stay up for 8 hours
  • Stay up for 24 hours
  • Stay up (no TTL)
  • Build images only (don't deploy)
  • Pause this deploy

Tests

  • Run E2E tests
  • Run DAST scan

Review Hero

  • Run Review Hero
  • Auto-fix review suggestions Wait for Review Hero to finish, resolve any comments you disagree with or want to fix manually, then check this to auto-fix the rest.
  • Auto-fix CI failures Check this to auto-fix lint errors, test failures, and other CI issues.
  • Auto-merge upstream Check this to merge the base branch into this PR, with AI conflict resolution if needed.
  • Save suppressions Check this to capture 👎 reactions on Review Hero comments as suppression rules in .github/review-hero/suppressions.yml. Also runs automatically at the end of any auto-fix run.

Remember to...

  • ...write or update tests
  • ...add UI screenshots and testing notes to the Linear issue
  • ...add any manual upgrade steps to the Linear issue
  • ...update the config reference, settings reference, or any relevant runbook(s)
  • ...call out additions or changes to config files for the deployment team to take note of

… non-HTTPS clients

Adds a boolean `security.requireHttps` setting at both the central and per-facility
scopes (default false). When enabled, middleware on the central and facility servers
rejects any request that did not arrive over HTTPS — detected via `X-Forwarded-Proto`,
which is honoured through the existing loopback trust-proxy config — with a 403. The
index/health route stays accessible over HTTP.

To avoid locking everyone out, the admin settings endpoint refuses to enable
`security.requireHttps` unless the request enabling it is itself over HTTPS, proving
HTTPS reaches the server before it becomes mandatory.

This is the server-side mitigation companion to the cleartext token-storage fallback
used by HTTP-only deployments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@review-hero

This comment was marked as resolved.

@review-hero

This comment was marked as resolved.

@passcod passcod marked this pull request as ready for review June 26, 2026 06:58

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1c18005. Configure here.

Comment thread packages/shared/src/utils/requireHttps.js
Comment thread packages/facility-server/app/createApiApp.js
passcod and others added 2 commits June 26, 2026 19:11
…al/scoped setting

Adds `security.requireHttps` at the global scope and drops the hard `false` default from
the central and facility scopes, so the setting is unset by default and resolves through the
normal settings cascade. The global value acts as the default for every server (one toggle to
require HTTPS everywhere), while a central or facility value overrides it in either direction
for the complex per-site case. Unset everywhere means HTTPS is not required.

Also adds a spec describing the subsystem under specs/administration/settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dd enforcement tests

The enable-guard used a strict `=== true` check, which let a truthy non-boolean (e.g. `1`)
slip past while the middleware would still enforce on it. Switch to a truthy check so the
guard matches enforcement.

Add a facility-server integration test that enables the setting and confirms plain-HTTP
requests are rejected while HTTPS requests pass, and strengthen the central admin test to
read back the persisted value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread packages/shared/src/utils/requireHttps.js Outdated
Comment thread packages/shared/src/utils/requireHttps.js Outdated
Comment thread packages/central-server/app/createApi.js
@review-hero

review-hero Bot commented Jun 26, 2026

Copy link
Copy Markdown

🦸 Review Hero Summary
12 agents reviewed this PR | 1 critical | 2 suggestions | 0 nitpicks | Filtering: consensus 3 voters, 9 below threshold

Below consensus threshold (9 unique issues not confirmed by majority)
Location Agent Severity Comment
packages/central-server/__tests__/admin/settings.test.js:233 Bugs & Correctness suggestion The test 'allows enabling security.requireHttps over an HTTPS connection' relies on X-Forwarded-Proto: https being honoured — which only works if config.proxy.trusted includes the loopback addr...
packages/central-server/__tests__/admin/settings.test.js:245 Bugs & Correctness suggestion Test 2 persists security.requireHttps: true at facility scope for facility.id, and test 3 implicitly relies on this state (it resets to false). If test 2 is skipped or fails mid-way, test 3's...
packages/central-server/app/admin/adminRoutes.js:221 BES Requirements nitpick Four-line comment block. The coding rules allow a comment when the WHY is non-obvious, but limit it to one short line. The truthy-check rationale ('so a 1 cannot slip past') is also moot because ...
packages/central-server/app/admin/adminRoutes.js:221 Bugs & Correctness suggestion The enable guard getAtPath(settings, 'security.requireHttps') && !req.secure only inspects the top-level security.requireHttps key in the incoming payload. A settings payload that also contains...
packages/central-server/app/admin/adminRoutes.js:225 Security suggestion The enabling guard validates that the request to the central server is over HTTPS, but the setting can be applied at facility scope (as the test explicitly exercises). An admin connecting to cent...
packages/shared/src/utils/requireHttps.js:5 BES Requirements nitpick The project coding rules say 'Don't write multi-paragraph docstrings or multi-line comment blocks — one short line max.' Both JSDoc blocks here span 7–8 lines and partly explain WHAT the functions ...
packages/shared/src/utils/requireHttps.js:17 BES Requirements suggestion The duck-type check typeof settings.get === 'function' is the only thing distinguishing central-server settings (a single ReadSettings) from facility-server settings (a plain reduce-object). If `...
packages/shared/src/utils/requireHttps.js:20 Bugs & Correctness suggestion The facility-map branch calls reader.get(REQUIRE_HTTPS_SETTING) on each value without checking that reader is non-null or that reader.get is a function. The top-level if (!settings) guards ...
packages/shared/src/utils/requireHttps.js:36 Security suggestion The middleware is documented to depend on req.secure reflecting X-Forwarded-Proto via the trust proxy setting. However, if proxy.trusted is misconfigured to trust a broad range (e.g. true...
Local fix prompt (copy to your coding agent)

Fix these issues identified on the pull request. One commit per issue fixed.


packages/shared/src/utils/requireHttps.js:20: The coding rules require capping concurrency for parallel DB work: 'Don't do things in parallel without a good reason, and when you do, cap concurrency (e.g. async-pool). We have a finite pool of DB connections.' This Promise.all fans out one DB read per hosted facility on every inbound request when the setting is active. In practice most servers host 1–2 facilities, but the pattern is unconditional and will amplify under load. Use async-pool with a small cap (e.g. 3) to stay within project convention.


packages/shared/src/utils/requireHttps.js:20: In the facility-server map path, Promise.all rejects entirely if any single reader throws (e.g. a transient DB error for one facility). The outer catch then forwards a 500 to the client rather than failing open. For a health-check or routine request hitting a facility whose reader is momentarily flaky, this means the entire request fails rather than the middleware gracefully allowing it through. Consider using Promise.allSettled and treating rejected reads as 'not required', consistent with the if (!settings) { return false } fail-open policy elsewhere in the same function: const results = await Promise.allSettled(Object.values(settings).map(r => r.get(REQUIRE_HTTPS_SETTING))); return results.some(r => r.status === 'fulfilled' && Boolean(r.value));


packages/central-server/app/createApi.js:134: The requireHttps middleware is mounted on the central server but there is no integration test that verifies it actually enforces rejection on the central server. The central server tests in __tests__/admin/settings.test.js deliberately avoid this path — they use SETTINGS_SCOPES.FACILITY throughout specifically so the setting cannot trigger enforcement for the central test server. That means there is zero integration coverage for: (1) enabling security.requireHttps at central (or global) scope and verifying that a subsequent HTTP request to the central API returns 403, and (2) that an HTTPS request (via X-Forwarded-Proto) is then allowed. The facility server has exactly this coverage in the new describe('security.requireHttps enforcement') block in packages/facility-server/__tests__/apiv1/Settings.test.js. Add a parallel enforcement describe-block to the central server test suite (using SETTINGS_SCOPES.CENTRAL or SETTINGS_SCOPES.GLOBAL, resetting the setting in afterEach) to close this gap.

passcod and others added 2 commits June 26, 2026 19:24
…andshakes

WebSocket upgrades attach to the HTTP server directly and never pass through the Express
middleware stack, so the requireHttps middleware did not cover them — a cleartext client
could still open a WebSocket while ordinary HTTP requests were rejected.

Add a Socket.IO allowRequest guard on both servers that refuses a handshake which did not
arrive over HTTPS when security.requireHttps is in effect. Protocol detection reuses the
server's compiled Express trust-proxy function, so X-Forwarded-Proto is honoured exactly as
it is for HTTP requests (and spoofed values from untrusted peers are ignored). Fails closed
if the setting cannot be read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cover central enforcement

Cap the per-facility setting reads in the requireHttps check with a small async-pool concurrency
limit instead of an unbounded Promise.all, per the project's finite-DB-pool convention. The
all-or-nothing semantics are kept deliberately, so a settings-read failure still fails closed
rather than letting a request through over plain HTTP.

Add a central-server enforcement test block (global scope) confirming the middleware returns 403
for non-HTTPS requests and allows HTTPS requests, mirroring the facility-server coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant