Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions SECURITY-COMPOSE-HARDENING.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,22 @@ Found during this work; **not fixed in this change** and not claimed to be:
4. **No verification that a real deployment has rotated its credentials.**
The `:?` guard proves a value was *supplied*, not that it is strong or
unique. A startup-time weak-credential check would close that.
5. **`lims` DEBUG follow-up (2026-08-13, separate branch):** a re-audit
found `docker-compose-release.yml`'s `lims` service had drifted to
`DJANGO_DEBUG: "true"` with `FIELD_ENCRYPTION_KEY` entirely absent, while
`docker-compose.release.yml` (the file actually shipped by
`electron-builder`) already carried the correct `"false"` +
`FIELD_ENCRYPTION_KEY` pairing from an earlier fix (951ad25) that missed
the dash file. `lab_data_manager/settings.py` reads `DJANGO_DEBUG`
directly from the environment with no override anywhere in the image, so
this was a live drift, not dead configuration — confirmed effective (not
merely present) before fixing. Not reachable through the packaged
Electron app's own startup path (`electron/main.js` never resolves to
the dash file), so this closes a parity/drift gap in the file documented
above as "kept in parity," not a live exposure in the shipped installer.
Fixed to match the dot file; pinned by
`tests/test_lims_debug_config.py` so the two files' `DJANGO_DEBUG` and
`FIELD_ENCRYPTION_KEY` wiring can't silently diverge again.

---

Expand Down
3 changes: 2 additions & 1 deletion docker-compose-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -401,13 +401,14 @@ services:
environment:
DJANGO_SETTINGS_MODULE: lab_data_manager.settings
DJANGO_ENV: prod
DJANGO_DEBUG: "true"
DJANGO_DEBUG: "false"
MYSQL_HOST: mysql
MYSQL_PORT: "3306"
MYSQL_DATABASE: limsdb
MYSQL_USER: root
MYSQL_PASSWORD: ${MYSQL_ROOT_PASSWORD:?MYSQL_ROOT_PASSWORD must be set}
DJANGO_SECRET_KEY: ${LIMSX_DJANGO_SECRET_KEY:?LIMSX_DJANGO_SECRET_KEY must be set}
FIELD_ENCRYPTION_KEY: ${LIMSX_FIELD_ENCRYPTION_KEY}
DJANGO_ALLOWED_HOSTS: "127.0.0.1,localhost,lims,0.0.0.0,*"
CORS_ALLOWED_ORIGINS: "https://webstudio.omnibioai.org,https://app.omnibioai.org,https://lims.omnibioai.org,https://omnibioai.org"
REDIS_URL: redis://redis:6379/0
Expand Down
104 changes: 104 additions & 0 deletions tests/test_lims_debug_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""HIPAA follow-up to the SECURITY-COMPOSE-HARDENING.md exposure work: static
regression test pinning `lims`'s DJANGO_DEBUG value across every release
compose configuration.

Purely static (no live services, no docker daemon) -- same approach as
test_compose_release_config.py and test_compose_network_exposure.py
alongside it.

Background: `omnibioai-lims/lab_data_manager/settings.py` reads
`DJANGO_DEBUG` directly via django-environ with no override anywhere in the
image (Dockerfile's persistent ENV layer deliberately omits it; only a
build-time `RUN DJANGO_DEBUG=true ... collectstatic` step sets it, which does
not affect the runtime container) -- whatever the compose environment sets
is exactly what Django runs with. `DEBUG=True` disables the
FIELD_ENCRYPTION_KEY requirement, flips CORS_ALLOW_ALL_ORIGINS on, and
enables Django's traceback/debug pages.

`docker-compose.release.yml` (the file electron-builder actually bundles
into packaged installers) already fixed this for `lims` in 951ad25 --
DJANGO_DEBUG: "false" plus FIELD_ENCRYPTION_KEY wired from
LIMSX_FIELD_ENCRYPTION_KEY. `docker-compose-release.yml` (dash), documented
in SECURITY-COMPOSE-HARDENING.md SS6 as "kept in parity" with the dot file
despite not being the shipped artifact, was missed by that commit and still
shipped DJANGO_DEBUG: "true" with FIELD_ENCRYPTION_KEY entirely absent. This
suite pins both files so that specific drift can't silently reappear --
mirroring test_compose_release_config.py's approach to the JWT-secret drift
between the same two files.
"""
from pathlib import Path

import pytest
import yaml

REPO_ROOT = Path(__file__).resolve().parent.parent

# Both are release/production configurations and must run lims identically
# with respect to DEBUG -- see test_compose_release_config.py and
# test_compose_network_exposure.py for the same pairing on other concerns.
RELEASE_COMPOSE_PATHS = [
REPO_ROOT / "docker-compose.release.yml",
REPO_ROOT / "docker-compose-release.yml",
]

DEV_COMPOSE = REPO_ROOT / "docker-compose.yml"


def _load(path):
with open(path) as f:
return yaml.safe_load(f)


@pytest.fixture(scope="module", params=RELEASE_COMPOSE_PATHS, ids=lambda p: p.name)
def release_compose(request):
return _load(request.param)


def test_lims_debug_is_false_in_release_configs(release_compose):
"""lab_data_manager/settings.py consumes DJANGO_DEBUG straight from the
environment with no override -- this compose value *is* the runtime
Django DEBUG value. A release/production configuration must never ship
DEBUG enabled: it discloses tracebacks, local variables, and settings,
and flips CORS_ALLOW_ALL_ORIGINS on."""
env = release_compose["services"]["lims"]["environment"]
assert env.get("DJANGO_DEBUG") == "false", (
f"lims's DJANGO_DEBUG must be \"false\" in a release configuration -- "
f"got {env.get('DJANGO_DEBUG')!r}. settings.py reads this value "
f"directly with no override, so this is the actual runtime Django "
f"DEBUG setting, not a cosmetic flag."
)


def test_lims_field_encryption_key_wired_in_release_configs(release_compose):
"""settings.py raises RuntimeError('FIELD_ENCRYPTION_KEY must be set in
non-debug environments') as soon as DEBUG=False and this is unset --
required for DJANGO_DEBUG: "false" above to actually boot rather than
crash-loop, and for encrypted-at-rest fields to use a real key rather
than none at all."""
env = release_compose["services"]["lims"]["environment"]
assert "FIELD_ENCRYPTION_KEY" in env, (
"lims must receive FIELD_ENCRYPTION_KEY in a release configuration "
"-- without it, DEBUG=False crash-loops the container at startup "
"(settings.py's fail-fast guard), and DEBUG=True would silently run "
"with no encryption key for encrypted-at-rest fields"
)
assert "LIMSX_FIELD_ENCRYPTION_KEY" in env["FIELD_ENCRYPTION_KEY"], (
f"lims's FIELD_ENCRYPTION_KEY must be sourced from "
f"LIMSX_FIELD_ENCRYPTION_KEY (the same convention "
f"DJANGO_SECRET_KEY/LIMSX_DJANGO_SECRET_KEY already use), got "
f"{env['FIELD_ENCRYPTION_KEY']!r}"
)


def test_dev_compose_lims_debug_still_false():
"""docker-compose.yml already got this fix in 951ad25 and is the
parity source of truth the release files are pinned against here --
guard it too so a future edit can't quietly loosen the dev file while
leaving the release files matching it, or vice versa."""
dev = _load(DEV_COMPOSE)
env = dev["services"]["lims"]["environment"]
assert env.get("DJANGO_DEBUG") == "false", (
f"docker-compose.yml's lims DJANGO_DEBUG is the documented source "
f"of truth the release files are pinned against -- got "
f"{env.get('DJANGO_DEBUG')!r}, expected \"false\""
)
Loading