Conversation
A dangling symlink at a managed filename was classified as absent by os.Stat, so every presence check agreed the directory was empty and the freshly generated Ed25519 private key was written at the link target, outside KeysDir. Reproduced against all three managed files. The attack needs something to populate KeysDir before authcore first runs, which for this library is ordinary: a mounted container volume, a provisioning script, a restored backup, a Compose file. Every managed write now goes through createExclusive, which opens with O_CREATE|O_EXCL. POSIX requires that to fail with EEXIST when the path is a symlink, dangling ones included. It also closes the narrower race the old code left: a regular file appearing between the presence check and the write kept its own permissions, so a 0644 file planted at that moment received the private key. O_NOFOLLOW would say this more loudly but lives in syscall and is undefined on Windows, and this library ships there. Reads still follow symlinks, deliberately. docs/key-management.md tells the operator to mount a Kubernetes Secret at KeysDir, and Kubernetes projects a Secret volume as a tree of symlinks, so a loader that refused them would pass a security review and break every replica following the deployment guide. TestSymlinkedKeysStillLoad pins that case. Two things I had wrong until I sabotaged them: I wrote this with Lstat in the presence check, believing that was the fix. Reverting it to Stat left the test green, so Lstat was not load-bearing: O_EXCL is. Reverting O_EXCL turns all three subtests red. Measuring the two also inverted which is better. With Lstat the consistency check fires first and tells the operator to "delete all key files", which since auth/field shipped is the advice that destroys every encrypted column. With Stat the failure reaches createExclusive, which names the symlink and what to do about it. The test asserts the message now, both that it mentions the symlink and that it does not give the destructive advice. Closes #342 Signed-off-by: Jose <75870284+Jaro-c@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A dangling symlink at a managed filename was classified as absent by
os.Stat, so every presence check agreed the directory was empty and the freshly generated Ed25519 private key was written at the link target, outsideKeysDir. Reproduced against all three managed files.The attack needs something to populate
KeysDirbefore authcore first runs, which for this library is ordinary rather than exotic: a mounted container volume, a provisioning script, a restored backup, a Compose file.Every managed write now goes through
createExclusive, opening withO_CREATE|O_EXCL. POSIX requires that to fail withEEXISTwhen the path is a symlink, dangling ones included. It also closes the narrower race the old code left open: a regular file appearing between the presence check and the write kept its own permissions, so a 0644 file planted at that moment received the private key.O_NOFOLLOWwould state the property more loudly, but it lives insyscallrather thanosand is undefined on Windows, and this library ships to Windows consumers. The portable flag that already has the property is the one to rely on.Reads still follow symlinks, on purpose
docs/key-management.mdtells the operator to mount a Kubernetes Secret atKeysDir, and Kubernetes projects a Secret volume as a tree of symlinks. A loader that refused links would pass a security review and break every replica following the deployment guide.TestSymlinkedKeysStillLoadpins that case: an existing key set reached entirely through links loads, and loads the same key id.Read-following is the weaker exposure anyway. The read path already caps at 4 KiB and validates the pair, and anyone who can redirect a read can equally replace the file.
Two things I had wrong until I sabotaged them
I wrote this with
Lstatin the presence check, believing that was the fix, and the commit would have said so. Reverting it toStatleft the dangling-symlink test green, soLstatwas never load-bearing. RevertingO_EXCLturns all three subtests red.O_EXCLis the control.Measuring the two then inverted which is better. With
Lstatthe consistency check fires first and the operator is told to "delete all key files" — advice that, sinceauth/fieldshipped, destroys every encrypted column. WithStatthe failure reachescreateExclusive, which names the symlink and says what to do. SoinspectusesStat, and the test now asserts the message: that it mentions the symlink, and that it does not give the destructive advice.Closes #342