Skip to content

w736: the vault reads systemd's credential file for its key (mode 440 is the ACL mask) - #238

Merged
bryding merged 3 commits into
mainfrom
w736-vault-key-credential
Oct 9, 2026
Merged

bryding merged 3 commits into
mainfrom
w736-vault-key-credential

Conversation

@bryding

@bryding bryding commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Request: w736

TL;DR: the vault key was refused because systemd's credential file reads mode 440. That 440 is an ACL mask, not a grant to a group. Fixed in readKey: the key that comes from $CREDENTIALS_DIRECTORY is accepted under a stricter, specific rule; every other key file keeps the strict one. Needs a portal deploy.

Cause (evidence)

  • system_status: "Token vault: 0 entries; key unreadable (the vault key /run/credentials/fff-portal.service/fff-vault-key is readable by other users (mode 440); chmod 600 it)". The check is server/vault.ts readKey: st.mode & 0o077 refuses.
  • Sourced: systemd's src/core/exec-credential.c write_credential: fchmod(fd, 0400), then for a service user that is not the manager's: fd_add_uid_acl_permission(fd, uid, ACL_READ) (falling back to fchown only where the file system has no ACLs). fff-portal.service runs User=fff.
  • Measured (Ubuntu 24.04, systemd 255, WSL, as root): a 0400 root file with setfacl -m u:nobody:r gives stat mode 440, owner 0:0, and getfacl: user::r-- user:nobody:r-- group::--- mask::r-- other::---. So 440 is the ACL mask; the group entry is empty. The same machine with systemd-run -p User=nobody -p LoadCredential=... took the fchown path (WSL tmpfs): mode 400, owner nobody, which the old check accepts: that is why this only shows on the VM.
  • A hand chmod 600 cannot hold (systemd recreates the file at each start) and LoadCredential= has no mode setting.

The choice, and why

Two options were on the table: (A) readKey understands systemd's credential file; (B) the unit or install path gives the credential the mode the code needs. B does not exist: nothing in LoadCredential/SetCredential/LoadCredentialEncrypted sets a mode, and a 0600 file owned by root would be unreadable for fff (the ACL is what lets it read). A ExecStartPre chmod cannot touch the credentials folder, which is read-only for the service. So A.

The rule (credentialProblem, applies only when keySource says the file is $CREDENTIALS_DIRECTORY/fff-vault-key): directly in that folder after realpath (no link out), a regular file, no other bits, no group/other write bit, owned by root or the portal's own uid, in a folder with no other bits owned by the same. Group read bits are not read as a grant there (the mask). Everything else (config vault.keyFile, /etc/fff/vault.key, the same 0440 file anywhere outside the credentials folder) keeps "no group or other bit at all". readKey(file, { credentialsDir, owners }); the signature's platform argument moved into the options (only caller: Vault.key).

Residual: the check cannot see the ACL entries (no ACL API in Node), so it does not prove group:: is empty; it rests on the credentials folder being private to the unit, which systemd arranges (docs/vault.md says this too: same-user shell commands can still read it, the accepted D2 boundary).

Tests

  • server/vault.test.ts (new, POSIX): the credential at 0440 opens through keySource + Vault (status loaded, a Claude token comes back); the same mode via vault.keyFile or as a bare path is refused; 0444/0404/0460, a stranger as owner, an open folder, a symlink out, a file not directly in the folder, are refused; a wrong key is still "not 32 bytes" and its content is not in the message. Run on Linux (WSL, node 23.7): 23 pass, 0 skipped; on Windows it skips.
  • deploy/vm/test/ci-vm-e2e.sh: vault_key_probe runs the deployed release's keySource/readKey as fff against the live credential of the running fff-portal.service, prints the file's mode/owner/ACL (MEASURE), and must print KEY-OK 32 bytes, credential=yes. It runs after the install and again after the nightly cold restart (the credential is recreated at that boot).
  • Not run: a red run of the e2e against the old readKey (it would take a CI cycle); the unit test is the red/green proof (the old readKey throws on the 0440 case).

After the merge

Needs a portal deploy (fffctl update), together with #235 (w698) and #234 (w694). Then system_status should say the vault key is loaded.

🤖 Generated with Claude Code

…t + ACL shows as mode 440)

system_status said "key unreadable (... readable by other users (mode 440); chmod 600 it)". For a service that is not
root, systemd writes the credential 0400 root-owned with a user ACL entry for the service account; stat shows the ACL
mask in the group bits. readKey refused any group bit. LoadCredential has no mode, and a hand chmod does not survive a
restart, so readKey now knows the credential: from $CREDENTIALS_DIRECTORY, directly in that folder, regular file, no
other bits, no group/other write bit, root or portal-owned, in a folder closed to others. Other key files stay strict.

The VM end to end runs the deployed readKey as fff on the live credential after the install and the cold restart.

Request: w736

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@bryding
bryding merged commit 65e4313 into main Oct 9, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant