Please report security issues privately to the maintainers at security@gladia.io (placeholder — confirm address-of-record before publishing). Do not file public GitHub issues for vulnerabilities.
We aim to acknowledge reports within 3 business days and ship a fix or mitigation within 14 days for confirmed issues.
Affected versions: all commits before this fix landed (every release of OpenRunner up to and including the one immediately preceding the guardrail commit).
The bundled docker-compose.yml substituted an empty string when no
SECRET_KEY was provided, and .env.example shipped with the placeholder
change-me-in-production. The API server still booted and signed JWTs
with that value, which made authentication tokens trivially forgeable
against any account on a self-hosted instance that used the bundled
defaults unmodified.
Fix. A boot-time guardrail in app/core/config.py
(validate_production_security) now refuses to start the API server when
SECRET_KEY is empty or equals change-me-in-production and
DEBUG=false. Local development with DEBUG=true is unaffected. Operators
who shipped the bundled defaults must rotate SECRET_KEY to a strong
random value (python -c "import secrets; print(secrets.token_urlsafe(48))")
and invalidate any active sessions issued before the rotation.
To keep the README quickstart (git clone && docker compose up -d)
zero-friction without weakening the guard above (GLA-55), the api
container's entrypoint (src/api/scripts/entrypoint.sh) detects an empty
or placeholder SECRET_KEY while DEBUG=false and generates a strong
random key via secrets.token_urlsafe(48). The key is written to
/var/lib/openrunner/secret_key inside the container, which is bind-mounted
to ./.data/api-secrets on the host so it survives docker compose down/up cycles and keeps issued JWTs valid across restarts. The
container prints a loud stderr banner the first time it generates a key.
Production deployments must still set SECRET_KEY explicitly (e.g. via
your secret manager or .env) rather than relying on the auto-generated
file: an explicit key is rotatable on a controlled cadence, replicable
across multi-replica deployments, and audit-traceable. The auto-gen path
exists so a self-hoster's first docker compose up -d boots without manual
file edits — it is not an endorsement of running production off the
default volume.
In particular, multi-replica deployments must not rely on auto-gen: each
replica running the entrypoint without a shared SECRET_KEY would either
(a) race to write the file on a shared mount, or (b) generate independent
keys on independent volumes, and JWTs minted by one replica would be
unverifiable by the others. The single-node Docker Compose quickstart that
auto-gen targets has exactly one api replica, so the race is not reachable
there. For Helm / k8s, set SECRET_KEY from a real Secret object — see
the helm/openrunner/README.md deployment guide.
docker compose down -v deletes ./.data/api-secrets along with the
other data volumes, which forces a new key on the next boot and
invalidates every previously issued JWT. That is intentional: down -v
is a fresh-state operation.
Source: GLA-95 threat model wave 1, tracked as GLA-109. Three config-layer fixes that close the largest blast-radius default-credential holes the bundled self-host topology shipped with.
R-01 — strong-secret validators on SECRET_KEY, JWT_ACCESS_SECRET,
JWT_REFRESH_SECRET. Settings construction now refuses to load when any
of these is empty, set to a change-me* / generate-a-real-secret-key
placeholder, or shorter than 32 characters. The lifespan startup hook
re-validates and exits non-zero with a clear stderr message naming the
offending field. The container entrypoint auto-generates and persists
strong values to /var/lib/openrunner/ on first boot when the env still
carries the documented placeholders, so git clone && docker compose up -d keeps working for fresh-clone operators.
R-02 — full HMAC + constant-time compare on storage-proxy download
tokens. app/api/v1/media.py previously truncated the SHA-256 digest
to its first 32 hex chars (hexdigest()[:32]), halving the brute-force
search space. Tokens now use the full 64-char hex digest and verify with
hmac.compare_digest. The matching token generator in
app/services/media.py calls the same helper so signed and verified
tokens stay in lockstep.
R-07 — refresh-cookie Secure decoupled from DEBUG. The refresh
cookie's Secure flag is now driven by a dedicated COOKIE_SECURE
config knob (default true), not by DEBUG. Operators may turn it off
only for local-loopback dev — any inbound request whose Host header is
not localhost / 127.0.0.1 clamps Secure=true regardless. The cookie
also moves to SameSite=strict (it is consumed exclusively by
POST /api/v1/auth/refresh). A new ENVIRONMENT setting (default
development) refuses boot when combined with DEBUG=true and
ENVIRONMENT=production so debug mode cannot leak into a real deployment.
Operator action.
- Generate strong values for
SECRET_KEY,JWT_ACCESS_SECRET,JWT_REFRESH_SECRET(e.g.openssl rand -hex 32) and set them in.env. Existing sessions issued under prior weak keys are invalidated on rotation by design. - Set
ENVIRONMENT=productionfor production deployments. - Leave
COOKIE_SECURE=trueunless serving the SPA exclusively overhttp://localhost.
Tracked under GLA-1105
/ shipped as GLA-1111. Two-layer guardrail with one shared rule set
(.gitleaks.toml):
Layer 1 — local pre-commit hook. .pre-commit-config.yaml runs on
every git commit and combines:
gitleaks(staged-diff mode) — upstream default ruleset plus the OpenRunner-specific patterns below.detect-private-key— catchesBEGIN RSA/EC/OPENSSH/DSA/PGP PRIVATE KEYblocks.check-added-large-files— blocks blobs > 1 MB (typical secret-dump shape).forbid-new-submodules.localhook →python scripts/check_secret_leakage.py --staged— pure-stdlib mirror of the OpenRunner rules so contributors who do not have gitleaks installed still get coverage.
Install once after cloning:
pip install pre-commit && pre-commit install
# or
make precommit-installLayer 2 — CI backstop. .github/workflows/ci.yml defines a
secret-scan job that runs on every PR and on push to main. It uses
the same .gitleaks.toml so local and CI verdicts match. PRs scan the
diff against the base SHA via gitleaks/gitleaks-action@v2; pushes scan
the last 50 commits with the gitleaks binary directly. The job also
re-runs scripts/check_secret_leakage.py and the
tests/security/test_precommit_catches_leak.sh self-test that asserts a
synthetic leak is blocked.
OpenRunner-specific rules. Beyond the gitleaks defaults
(AWS, Stripe, Slack, GitHub, generic high-entropy keys) the
config flags:
SECRET_KEY=/JWT_ACCESS_SECRET=/JWT_REFRESH_SECRET=set to a real-shaped value (≥ 32 chars, not the documentedchange-me-*placeholders) outside.env.example,SECURITY.md,docs/, and in-repo test fixtures.MINIO_(SECRET|ACCESS)_KEY=minioadminoutside.env.example,docker-compose*.yml, andhelm/.RESEND_API_KEY=re_…(with the published Resend prefix) anywhere except documented placeholder strings.Authorization: Bearer <real-looking-token>outsidetests/,examples/,docs/, and the SPA-rendered curl examples.- Any path matching
^\.env$ | ^\.env\.local$ | ^\.env\.production$— real env files must never land in a commit.
Self-test.
make test-security
# or
bash tests/security/test_precommit_catches_leak.shThe harness creates a fake SECRET_KEY=$(openssl rand -hex 32) file,
stages it, runs pre-commit run --files <file>, asserts non-zero exit,
and cleans up the fixture regardless of outcome.
Escape hatch. git commit --no-verify skips local hooks and is
documented for emergency-debug use only. CI's secret-scan job is not
skippable — every PR is re-scanned regardless of --no-verify. The
diff itself records that a hook was bypassed, so this is an audit-trail
event, not a free pass.
Updating the rules. Any new rule, allowlist, or pattern change must
ship in both .gitleaks.toml (the source of truth used by gitleaks
and by CI) and scripts/check_secret_leakage.py (the stdlib mirror used
by the local pre-commit hook), plus a one-line note in this section
explaining why. False positives go in the path/value allowlist; never add
# nosec to live config or vendor a real secret.
Out of scope (see GLA-1105 plan doc). Push-side server hooks, history rewrite for any pre-existing leak, dependency-CVE / SBOM scanning, and the GitHub branch-protection toggle are tracked as separate follow-ups.