Skip to content

fix: prevent world-readable race window when storing local secrets#7307

Open
aknownuser wants to merge 2 commits into
masterfrom
fix-secrets-file-race-condition
Open

fix: prevent world-readable race window when storing local secrets#7307
aknownuser wants to merge 2 commits into
masterfrom
fix-secrets-file-race-condition

Conversation

@aknownuser

Copy link
Copy Markdown

Summary

The local secrets provider wrote the secrets JSON file with the process's default umask permissions and only tightened them to 0600 afterwards. Under the common umask 022, the store file was briefly world-readable (0644)
while it already contained secret values, allowing another local user on ashared filesystem (HPC/CI) to race-read secrets created by nextflow secrets set. This closes that window.

Changes and Reasoning

  • LocalSecretsProvider.storeSecrets() — the store file is now created atomically with owner-only permissions before any secret content is written, using a POSIX file-attribute at creation time. Because rw------- has no group/other bits, the umask cannot widen it, so the file is never observable as world-readable while holding secrets. Permissions are also re-asserted in case the file already existed with broader access. This mirrors the already-correct create→chmod→write ordering used by the sibling makeTempSecretsFile() method.
  • Directory hardening (defense in depth) — the secrets directory is now restricted to rwx------ both in makeStoreFile() (constructor path) and in storeSecrets(). Directory traversal only leaks secret names, not values,
    but tightening it removes the traversal precondition the advisory relies on.

Testing

  • Added should create the store file with owner-only permissions — asserts the store file is rw------- and the secrets directory is rwx------.
  • Added should never expose secret content through world-readable permissions during write — a watcher thread busy-polls the file while storeSecrets() runs 100 times; the test fails if the file is ever observed with group/other read bits. Reproduces the advisory's PoC race and confirms it can no longer win.
  • Existing LocalSecretsProviderTest suite still passes (./gradlew :nextflow:test --tests "nextflow.secret.LocalSecretsProviderTest").

@netlify

netlify Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploy Preview for nextflow-docs canceled.

Name Link
🔨 Latest commit 0fb1e92
🔍 Latest deploy log https://app.netlify.com/projects/nextflow-docs/deploys/6a4f4fe2060e1d0007d4cc63

@aknownuser
aknownuser marked this pull request as ready for review July 8, 2026 09:47

@jorgee jorgee 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.

Changes look fine to me

@pditommaso pditommaso left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The core file-level fix (create → chmod → write, atomically owner-only before content) is correct and worth shipping. But the directory-hardening additions can throw where the old code succeeded, and can bite exactly the shared-filesystem scenario this PR targets. Requesting changes on that part.

Risk 1 — parent.setPermissions('rwx------') on a dir the user doesn't own (real regression)

storeSecrets() now runs parent.setPermissions(ONLY_OWNER_DIR_PERMS) unconditionally, on every write. chmod(2) requires you to own the directory (or be root) — being able to write into it is not enough.

  • Old contract: storing secrets only needed write+execute on the parent dir.
  • New contract: you must also own the parent dir, or the chmod fails with FileSystemException: Operation not permitted (EPERM), which propagates out of close()nextflow secrets set aborts.

This regresses precisely the HPC/CI shared filesystem case the PR motivates: NXF_SECRETS_FILE pointing into a group-writable directory the user can write to but doesn't own. Previously worked, now throws.

The same call in makeStoreFile() is lower risk — it's guarded to a directory we just mkdirs'd, so we own it.

Risk 2 — non-POSIX filesystem (pre-existing, slightly widened)

setPermissions / asFileAttribute(fromString(...)) throw UnsupportedOperationException on filesystems without POSIX support (CIFS/SMB, some FUSE/network mounts). The class already assumed POSIX (loadSecrets() reads getPermissions()), so this isn't new — but note makeStoreFile() now chmods in the constructor on first run, moving the failure from "when loading/storing" to "at provider construction."

Side effect even when it doesn't throw

parent.setPermissions('rwx------') on every store silently reclamps the directory to 700. If a user intentionally set the secrets dir to 750, or pointed NXF_SECRETS_FILE at a shared ~/.nextflow, every write strips group/other bits — destructive and surprising for a step labelled "defense in depth."

Suggested change

Directory hardening is explicitly defense-in-depth; it should never abort the operation the file-level fix already secures. Two minimal fixes:

  1. Only chmod the dir when we create it (mirror makeStoreFile's guard), not on every storeSecrets.
  2. Make the dir chmod non-fatal — the security guarantee lives on the file perms, not the directory.
// storeSecrets: only harden the dir we create, and never let it abort the write
if( !parent.exists() ) {
    if( !parent.mkdirs() )
        throw new IOException("Unable to create directory: $parent -- Check file system permissions")
    try { parent.setPermissions(ONLY_OWNER_DIR_PERMS) }
    catch( Exception e ) { log.warn "Cannot restrict permissions on secrets dir '$parent' - $e.message" }
}

Keep the file-level createFile + setPermissions fatal — that one is the security guarantee.

aknownuser and others added 2 commits July 9, 2026 09:37
The local secrets provider wrote the secrets JSON with default umask
permissions and only afterwards tightened them to 0600. Under the common
umask 022 the store file was briefly world-readable (0644) while it
already held secret values, letting another local user on a shared
filesystem race-read them.

Create the store file atomically with owner-only permissions before
writing any secret content (0600 has no group/other bits, so umask
cannot widen it), and restrict the secrets directory to the owner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Oussama El Azizi <oussama.elazizi@seqera.io>
Signed-off-by: Oussama El Azizi <oussama.elazizi@seqera.io>
@aknownuser
aknownuser force-pushed the fix-secrets-file-race-condition branch from f5783ee to 0fb1e92 Compare July 9, 2026 07:38
@aknownuser
aknownuser requested a review from pditommaso July 9, 2026 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants