Cryo runs destructive operations against production data (snapshot expiry, orphan-file removal, manifest rewrite, partition rewrite via OPTIMIZE) and stores AWS credentials, OIDC client secrets, and database connection strings. If you have found a vulnerability, please follow the disclosure process below — do not file a public GitHub issue for security-sensitive findings.
Only the latest minor release line on main receives security fixes. Older revisions are not
patched. Pin your deployment to a tagged release when you need a stable target.
| Version line | Supported |
|---|---|
Latest main |
✅ |
Tagged releases (v0.x.y) |
✅ for the most recent tag only |
| Anything older | ❌ |
Open a GitHub Security Advisory against this repository. Advisories are visible
only to the maintainer team until a fix is published, and private vulnerability reporting
is enabled, so the report never becomes a public issue. Do not file a public GitHub issue
for a security-sensitive finding, and do not send findings to a guessed or personal
address. No fallback security mailbox is published. Route non-security support questions
according to docs/SUPPORT_POLICY.md.
When reporting, please include:
- The affected component (e.g.,
cryo/engines/trino/sql.py,cryo/security/crypto.py, a specific Helm template). - Reproduction steps — concrete commands, sample input, observed vs. expected behavior.
- Impact assessment — what an attacker could achieve, and under what conditions (auth required? network reachable? local?).
- Any known workarounds.
We aim to acknowledge new reports within 5 business days and to publish a fix or mitigation within 30 days for high-severity issues. Lower-severity reports may take longer; you will get a status update either way.
The following are not security vulnerabilities and should be filed as regular GitHub issues (after triage, not before):
- Vulnerabilities in dependencies that already have an upstream fix in a newer release — please open a PR bumping the pin instead.
- Theoretical concerns without a reproducible attack path — open an Issue for discussion.
- Reports about scan noise, false positives, or alert spam.
Two independent scanners check the git history for committed credentials:
| Scanner | Workflow | When | Scope | Fails on |
|---|---|---|---|---|
| gitleaks 8.30.1 | ci.yml, job Secret scan (gitleaks) |
Every PR, push to main that changes code, manual run |
PR: base..HEAD. Otherwise the full history of the checked-out ref |
Any match of a .gitleaks.toml rule that is not allowlisted |
| trufflehog 3.99.8 | secret-scan.yml, job Secret scan (trufflehog, changed commits) |
Every PR and push to main |
The commits the PR or push adds | A verified (live) credential, or an unknown one (verification errored) |
| trufflehog 3.99.8 | secret-scan.yml, job Secret scan (trufflehog, full history) |
Weekly, and manual workflow_dispatch |
Every commit reachable from HEAD |
Same as above |
gitleaks matches the shape of a secret; trufflehog asks the provider whether the credential
works. Unverified trufflehog matches (the provider said no) are not reported, because gitleaks
already covers those shapes with an allowlist in .gitleaks.toml. Add a gitleaks allowlist
entry only for a value you have confirmed is not a credential, and say why in the entry.
Each trufflehog job runs two passes. The first fails on a verified result from any detector.
The second fails on unknown results, except from the detectors that verify by connecting to
the host written in the string (HOST_DIALING_DETECTORS in the workflow: JDBC, URI, Postgres
and similar). Test and compose fixtures embed placeholder passwords in connection strings that point
at localhost or compose-only hosts the runner cannot reach, so those detectors can only report unknown for them.
The weekly full-history job also re-checks history after a rewrite and catches an old credential that a provider only starts verifying later. Run it by hand after any history rewrite: Actions → Secret scan (trufflehog) → Run workflow.
If a scanner flags a real credential, revoke and rotate it first. Removing it from the history does not make it safe again.
POST /api/v1/auth/token self-issues viewer, operator, or admin with no identity check
while the dev token is on. The route is on only when CRYO_ENVIRONMENT is dev or test and
no CRYO_OIDC_ISSUER_URL is set. CRYO_ALLOW_DEV_TOKEN_IN_PROD turns it back on in any
environment.
The chart defaults to CRYO_ENVIRONMENT: prod. A bare helm install refuses to render until
every enabled image is pinned by digest, and the API refuses to start without a real
CRYO_SECRET_KEY. Setting dev for a throwaway install re-opens the route unless an OIDC issuer
is set.
CRYO_SECRET_KEY defaults to dev-only-change-me. It signs HS256 API tokens (unless
CRYO_JWT_SECRET is set) and encrypts stored connection secrets. In staging and prod, every
Cryo process refuses to start when CRYO_SECRET_KEY or CRYO_JWT_SECRET is the default or
shorter than 32 characters.
Issued tokens expire in 24 hours. There is no published revocation list.
CRYO_SECRET_KEY encrypts stored connection secrets, so changing it alone makes them
unreadable. Roll it over in three steps:
- Deploy with the new value as
CRYO_SECRET_KEYand the old one asCRYO_SECRET_KEY_PREVIOUS. Every process now reads both. - Run
cryo rotate-secret-key(dry run: counts per table and column, no values), thencryo rotate-secret-key --apply. It writes nothing if any value is unreadable with either key. A second dry run reports 0. - Remove
CRYO_SECRET_KEY_PREVIOUSand redeploy.
HS256 API tokens signed with the old key stop validating at step 1 unless CRYO_JWT_SECRET is
set separately, so users sign in again.
Cryo uses Fernet (AES-128-CBC + HMAC-SHA256) from the cryptography library for
at-rest secret encryption (cryo/security/crypto.py). The platform key is loaded from the
CRYO_SECRET_KEY environment variable and is never logged or returned by any API. JWTs are
signed with HS256 (default) or RS256 (when OIDC is configured); see cryo/security/jwt_tokens.py.
If you find a flaw in our use of these primitives, treat it as a critical-severity report.