Repository navigation
feat(secrets): keep the host key outside the data directory with host.secret_key_path - #239
Open
rabbitson87 wants to merge 14 commits into
Open
rabbitson87 wants to merge 14 commits into
rabbitson87 wants to merge 14 commits into
Conversation
A new setting, host.secret_key_path, puts the host key outside the data directory. The host reads it and never creates it. The spec covers the setting and its checks, the start of the host, the key and secret commands, flats backup and its check of a copy, the docs and the compatibility with an older release. It is item 1 of #224.
A leftover secret.key or a key that resolves into the data directory is refused by the host, the key commands and flats backup, so a copy with an external key never holds it. The key checks run where CheckSecretKey runs today and exit with status 78. The reload lists the key as awaiting a restart. The text states what works for a systemd credential and a Docker secret, the order of a downgrade, the hand edit of an invalid file, the platform conditions of the tests, and the current-state section that the spec index asks for.
Operator keys read the key file at each use, so the spec says which reads use the key in memory. The backup leaves out and scans for the key, and refuses a missing key in the preflight. The list of earlier writes, the cross references, the Docker secret sentence and the criteria are corrected. flats key ls and rm do not call the location function.
The setting keeps the host key in another file than <data>/secret.key. It is empty by default, must be an absolute path, and must not lie inside the data directory. The check runs when the file is loaded, so config set refuses a data directory that contains the key path. schema_version stays 5.
A KeyFile value says where the host key lives, and every place that read or wrote <data>/secret.key now gets the path from it. With the setting, the host reads the file and never creates it: a missing or linked-to-nothing file, a file that is no key, a second key at <data>/secret.key and a key that resolves into the data directory are all refusals with status 78, and they run where CheckSecretKey runs. The key commands, flats backup and the console use the same location, restore-key gets --out, a SIGHUP reload lists the setting as awaiting a restart, and flats backup records the key as external, leaves the data directory's secret.key out and scans the copy for the key bytes.
…ks in a backup Tests for KeyFile (never created, every refusal, read-only places), the host start with the key outside the data directory, key set, operator keys that read the file at each use, backup-key, restore-key with and without --out, reset, flats backup with an external key (manifest, scan, key changes, refusals, the flag) and the reload. The test of a key that appears in the data directory during a backup showed that the second read of rule 22 repeated the checks of rule 3 and failed the backup. KeyFile.ReadAgain reads without them, and the backup uses it for that read.
… and recovery Adds the key lifecycle to configuration.md, a Docker secret section to container.md, and the settings row, design and CLI notes. Updates the host backup, secret key recovery and config reload specs, and marks the secret key path spec implemented.
Moves the inside-a-directory check to config.InsideDir for core and app, folds readKeyAgain into readKey, and tightens one container doc sentence.
Exits 78 when core.New refuses the key, reads the key in a backup without the checks of the preflight, reports a failure that a second run cannot clear ahead of one that it can, gives backup-key and reset the move-away message for an unusable configured key, and names the same-file case. Fixes the systemd, Docker and Compose docs and adds the tests that the completion criteria asked for.
…w round 2 Uses one key hint for the missing-key refusal and the reset output, so Windows gets no shell line in either. Fixes the chmod line of the Docker steps and one sentence of rule 21, and adds a test that reset refuses a second key in the data directory while the configured key is missing.
… console operator-key methods
…g accepts CodeQL flagged five go/path-injection alerts on the file calls of the configured key path. cleanDataDir already gives the form that its analysis accepts for host.data_dir, so the key path uses it too. The value is validated as an absolute path first, so the result is the same path.
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.
Refs #224 (item 1). Part of #237. Items 2 to 4 of #224 stay open.
What this adds
host.secret_key_pathputs the host key,secret.key, in another place, for example a Docker secret or a file that only the service user canread. The setting is empty by default, and empty keeps the key at
<data>/secret.keyas before. The design is the specsecret-key-path.md, which is marked implemented.
With the setting, the host only reads the key. It never creates, writes, renames or deletes the file and makes no temporary file beside it, so a
key on a read-only mount works. The start refuses, with status 78 and before any listener opens, when the file is missing (also when
flats.dbholds no sealed value), is not a regular file of 32 bytes, cannot be read, or when a second key exists at
<data>/secret.keyor the key resolvesinto the data directory. The message names the path and the setting.
How it works:
host.data_dir.config setandunsetchange it offline.config set host.data_dirrefuses adirectory that contains the key path.
schema_versionstays 5, and an older release refuses the new key as an unknown key.core.KeyFile, is the location of the key. The host,core.New, the operator-key seal and open,flats key set,flats secret backup-key,restore-keyandreset, andflats backupall get the path from it.flats key lsandrmtouch no key.restore-key --out FILEwrites the recovered key to a new file of mode 0600, for a key that only the service process can see. It changesnothing at the configured path.
flats backupleaves the key out of the copy, recordssecret_key: "external"(formatstays 1), prints the key path, reads the key twiceand fails when it changed, checks the sealed values of the copy against the key, and scans every file of the copy for the 32 key bytes, so a
hard link or a bind mount that escapes the path checks still fails the backup.
--verifyreports a note.--exclude-secret-keythen meansthat the command does not read the key.
docs/configuration.md(the settings row, a new section on the key lifecycle, moving the key out and back, the downgrade order,what a refusal means, systemd),
docs/container.md(Docker and Compose),docs/design.md,docs/surfaces.md, the spec index, and therules of
host-backup.md,secret-key-recovery.mdandconfig-reload.mdthat change.Where the change is
internal/config/: the keyhost.secret_key_path, its checks,InsideDir.internal/core/keyfile.goand the callers ininternal/core:KeyFile,CheckSecretKey,SealHostKey,OpenHostKey,reload.go.internal/app/:serve.go,keys.go,keycmd.go,secretkeycmd.go,backupcmd.go,netbird.go,setup.go.internal/backup/: the external key inbackup.go,copy.go(hashScan),verify.go,manifest.go.internal/config/secretkeypath_test.go,internal/core/keyfile_test.go,internal/app/secretkeypath_test.go,internal/app/secretkeypath_cmd_test.go,internal/backup/externalkey_test.go.go.modis unchanged.core.ReadSecretKeyandcore.RestoreSecretKeyare removed; nothing else used them.Known limits
The spec states most of them.
the key in a copy under any name; the start does not check.
LoadCredential=file, a Docker secret) is missing for the key commands andflats backup, which run as processes of their own.flats backupthen needs--exclude-secret-key, andrestore-keyneeds--out.flats installmakes) take the credentials path fromsystemd.exec(5)and were not run on a realsystemd. The Docker and Compose steps were checked by reading, not run in a container.
open at once and does not change flat secrets.
-raceran on Linux for the packages of this change on the commit that theimplementation review accepted, not on the last five commits (docs, wording, tests and the one line below). The CI run of the last commit passed all 25 checks.
Task report
This is the task report that the development protocol asks for. The reviewer's full reports are kept by the author on a local disk. That copy
is not accessible to other reviewers, so the lines below are exact excerpts of the verdicts and of each finding's headline. Nothing here
replaces a verdict.
origin/mainatb09f5fd.claude-sonnet-5-5.claude-fable-5-1; it did the review work in every run. The reviewer authored no part of the change.failure below, which is one run beyond the plan of 2 rounds per stage. Reviewer runs: 7. Every run after the first resumed the reviewer's session.
Verdicts and findings of each review round
Design, round 1:
DESIGN: REJECTpreparewrites before the check.<data>/secret.key".restartKeysis an explicit list.restore-key.config setcannot repair a file that the check refuses.--verifydoes not have.Design, round 2:
DESIGN: ACCEPTdocker execsees the secret.Implementation, round 1:
IMPLEMENTATION: REJECTflats installmakes a user unit.RerunError.core.Newreturns the key refusal with status 1.backup-keyandresetlack the move-away message for an unusable configured key.secret.keywith the setting on.host-backup.md.Implementation, round 2:
IMPLEMENTATION: ACCEPTRound 2 closed I1 to I12 and added:
chmodafterchownfails for a user that is not root.resetstill gives a shell line on Windows.Testing, round 1:
TESTING: ACCEPT-racerun and the delta do not match in the report.backup-keyhas no test for a second key or a resolved link.key lsandkey rmare not tested in a rule 3 state.Testing, round 2:
TESTING: ACCEPTRound 2 closed T2 to T6, left T1 open as a report defect, and added:
Impact assessment after the CI code scanning failure:
IMPACT: ACCEPTCI
The first CI run failed two checks.
CodeQLreported 5go/path-injectionalerts on the file calls of the configured key path. The value had been cleanedwith
filepath.Clean(p);host.data_diris cleaned withfilepath.Clean("/" + p)(cleanDataDir), the form that the analysis accepts, socleanKeyPathnowcalls
cleanDataDir. For a validated absolute path the result is the same string, and the impact assessment above confirmed it. The second run of CIon that commit passes all 25 checks and lists no open alert in this change.
test (other)also failed once, inTestDocsRealRuntimeEndToEnd(TempDir RemoveAll cleanup: directory not empty), and passed on the next run. The push run ofmainafter #238 failed the same job in another test of the package(
TestProxyEndToEnd/TLS_proxy_with_X-Forwarded-Proto,read: EOF). I did not find the cause of either, and neither touches the key.Dispositions: every finding was accepted and corrected. Optional suggestions were not all adopted. Not adopted: a read-only row for the setting in
the console's Settings view (out of scope in the design), merging
backup.isUnderwithconfig.InsideDir(it changes--outon Windows, so it isits own change), a sentence in
docs/agent/topics/env-secrets.md(true with the default path, pinned by tests), the order of the "nothing to reset"check of
reset, a check per case of the message in one start test, and a host-level reload test. The corrections that came after an acceptedround are covered by the next round, except the wording corrections R1 to R3 of the implementation round 2 and the report corrections of
the testing stage, which the testing round 2 read.
The mutation checks of the oracles found two weak tests, now fixed:
resetwithout its check of a second key, andrestore-key --outbelow thedata directory. They also show that two checks of a key that resolves into the data directory are covered on Linux only, because Windows skips the link tests
without a privilege.