ContextLab / ContextLab/clustrix
Security hardening: rotate local-only HF tokens, enable secret scanning, fix credential file permissions
- Dominant language
- Python
- Stars
- 10
- Forks
- 4
- Avg merge
- 6h 27m
- Merged PRs (30d)
- 9
Description
Part of #108 · **Phase 0** · label: security
Bundles the concrete, low-effort security fixes surfaced by the 2026-08-17 audit. The deeper architectural security work (pickle trust model, host-key verification) is tracked separately — see the Phase 3 security issue.
## 1. Rotate two real HuggingFace tokens (local-only exposure)
Two genuine HF tokens (`hf_Fbf...`, `hf_hSV...`) exist in local git objects:
| Event | Commit | Date |
|-|-|-|
| Introduced in `notes/huggingface_validation_fix_2025-06-29.md` | `d30acd2` | 2025-06-29 10:02 |
| Removed | `9c3cdfc` | 2025-06-29 10:08 |
**They never reached GitHub** — verified three ways: `git merge-base --is-ancestor d30acd2 origin/master` -> false; `gh api repos/ContextLab/clustrix/commits/d30acd2` -> `422 No commit found for SHA`; the rewritten master at `f0278e8` shows `hf_XXXX...` redaction. The only ref retaining them is `refs/original/refs/heads/master`, a local `git filter-branch` backup.
Still rotate — they sat in a working tree for ~8 months.
- [ ] Revoke both at https://huggingface.co/settings/tokens
- [ ] Purge the local backup ref:
`git update-ref -d refs/original/refs/heads/master && git reflog expire --expire=now --all && git gc --prune=now`
## 2. Enable GitHub secret scanning + push protection
Currently **disabled** on this public repo: `gh api repos/ContextLab/clustrix/secret-scanning/alerts` -> `404 Secret scanning is disabled`. This is exactly the control that would have blocked the 2025-06-29 commit at push time.
- [ ] Settings -> Code security -> enable secret scanning **and** push protection
## 3. Stop generating scanner bait
Issue #107 was a false positive triggered by placeholders in the `.env` template:
`credential_manager.py:355` (`AKIAIOSFODNN7EXAMPLE`, AWS's own doc placeholder), `:356`, `:393` (`hf_abcdefghij...`).
- [ ] Replace with non-matching forms (`AKIA_YOUR_KEY_HERE`, `hf_YOUR_TOKEN_HERE`) so scanners stop firing on this file
## 4. `.gitignore` does not ignore a bare `.env`
Only `.env.local` (line 61) and `.env.validation` (line 62) are covered. `git check-ignore .env` returns nothing. Mitigated in practice because the manager defaults to `~/.clustrix/.env`, but a repo-root `.env` is currently committable.
- [ ] Add `.env` and `.env.*` (with `!.env.example` if wanted)
## 5. Credentials written world-readable
`clustrix/config.py:216-225` (`save_to_file`) and `:308-323` (`save_config`) do `asdict(self)` -> `yaml.dump`/`json.dump` with **no `chmod`**. `ClusterConfig.password` (`config.py:16`) is a plain field, so an SSH password lands at default umask (0644).
- [ ] Exclude `password` (and other secret fields) from serialization, **or** write to a 0600 temp file and `os.replace` into position
- [ ] Test asserting file mode is 0600 and that no secret appears in the serialized output
## 6. GCP service-account JSON leaked to /tmp
`clustrix/cli_credentials.py:502-506` writes the key to `NamedTemporaryFile(delete=False)`, exports the path as `GOOGLE_APPLICATION_CREDENTIALS`, and **never unlinks it**.
- [ ] `try/finally` or `atexit` cleanup; create the file 0600
## 7. Write-then-chmod TOCTOU
`cli_credentials.py:634-639` and `credential_manager.py:338-339` write secrets at default umask *before* calling `chmod(0o600)`. `secure_credentials.py:144-147` wraps `cred_dir.chmod(0o700)` in `except Exception: pass`, so a failed permission change is silent.
- [ ] Create with restrictive mode via `os.open(..., 0o600)`; never chmod after the fact
- [ ] Remove the silent swallow
## Explicitly out of scope / verified clean
No credential is ever logged or printed (`auth_fallbacks.py:137,157`, `auth_manager.py:70,76` log variable *names* only). No `sshpass`, no `op`, no `--password` on argv, no `echo |` — nothing reaches a command line or `ps`. No `shell=True` in `clustrix/`. No `verify=False` anywhere. `clustrix.yml`, which contains a real host and NetID, is correctly gitignored and untracked.
Contributor guide
Research direction
Start by reading save_to_file and save_config in clustrix/config.py, the temporary-file handling in clustrix/cli_credentials.py, and credential writes in credential_manager.py and secure_credentials.py. Check the existing configuration and credential tests, then verify the repository settings and .gitignore changes. Done means secrets are revoked or excluded, files are created restrictively and cleaned up, permission failures are visible, and the requested mode and serialization tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, github, python
- Domain
- devops, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100