Close the review's gaps round the agent account (hq ADR 0266)
mesh/merge-gate pass: builds claude-code, claude-licence-manager, restic, sudo → ace, g14, novox, shanks; no bus step; every machine composes with the chan…
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
mesh/merge-gate pass: builds claude-code, claude-licence-manager, restic, sudo → ace, g14, novox, shanks; no bus step; every machine composes with the chan…
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
The managed settings apply to every account's claude, so a key-helper script under the agent's home would run as the operator, and through sudo as root: the command is now carried whole in the root-owned managed settings and reads the key only as the agent. The helper reads only a regular file, bounded, without blocking, and a request it does not answer fails in time instead of holding every later one; a directory entry it answers that is not a name is refused. The guard's record is matched on its binary in the query, a half-named agent account is not taken as the operator's, the kept API key stays until the agent's home holds one, restic refuses to restore beside a directory others may write, and the agent-home check cleans a path before judging it.
This commit is contained in:
@@ -495,8 +495,8 @@ func (b *Backups) Restore(ctx context.Context, module, snapshot, path string) (*
|
||||
}
|
||||
|
||||
// besideIsUnsafe says why root must not restore at target, beside the live data: a directory on the way to it
|
||||
// is a symbolic link, or belongs to an account other than root and the one this process runs as — one that
|
||||
// could swap a link in while root writes. Empty when every directory on the way is root's or the runtime's.
|
||||
// is a symbolic link, belongs to an account other than root and the one this process runs as, or may be written
|
||||
// by its group or everyone — any of which could swap a link in while root writes. Empty when every directory on the way is root's or the runtime's.
|
||||
func (b *Backups) besideIsUnsafe(ctx context.Context, target string) string {
|
||||
me := strconv.Itoa(os.Getuid())
|
||||
dir := filepath.Dir(filepath.Clean(target))
|
||||
@@ -508,17 +508,27 @@ func (b *Backups) besideIsUnsafe(ctx context.Context, target string) string {
|
||||
}
|
||||
}
|
||||
for i := len(chain) - 1; i >= 0; i-- {
|
||||
out, err := b.Run(ctx, "stat", "-c", "%u %F", chain[i])
|
||||
out, err := b.Run(ctx, "stat", "-c", "%u %a %F", chain[i])
|
||||
if err != nil {
|
||||
return chain[i] + " could not be read"
|
||||
}
|
||||
owner, kind, _ := strings.Cut(strings.TrimSpace(out), " ")
|
||||
owner, rest, _ := strings.Cut(strings.TrimSpace(out), " ")
|
||||
perm, kind, _ := strings.Cut(rest, " ")
|
||||
if kind == "symbolic link" {
|
||||
return chain[i] + " is a symbolic link"
|
||||
}
|
||||
if owner != "0" && owner != me {
|
||||
return chain[i] + " belongs to another account"
|
||||
}
|
||||
// Another account may write into a directory open to its group or to everyone, sticky or not, and so
|
||||
// put a link where root is about to write.
|
||||
mode, err := strconv.ParseUint(perm, 8, 32)
|
||||
if err != nil {
|
||||
return chain[i] + "'s mode could not be read"
|
||||
}
|
||||
if mode&0o022 != 0 {
|
||||
return chain[i] + " may be written by other accounts"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -190,8 +190,13 @@ func TestWithTheRealResticAMistakeIsUndoneBesideTheLiveData(t *testing.T) {
|
||||
settings := filepath.Join(root, "settings.xml")
|
||||
must(t, os.WriteFile(settings, []byte("<Config>kept</Config>\n"), 0o600))
|
||||
where := placed(t, "# mail\npath "+store+"\npath "+settings+"\n# pg\nrun echo 'every row' > "+dumps+"/all.dump\npath "+dumps+"\n")
|
||||
// As whoever runs the test, against its own repository: no sudo.
|
||||
// As whoever runs the test, against its own repository: no sudo. The test's directory lies in a temporary
|
||||
// directory everyone may write, which a real restore is right to refuse beside; the directories above the
|
||||
// test's own are said to be root's here, so the restore beside is what is tested.
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if name == "stat" && len(args) > 0 && strings.HasPrefix(root, strings.TrimSuffix(args[len(args)-1], "/")+"/") {
|
||||
return "0 755 directory\n", nil
|
||||
}
|
||||
out, err := exec.CommandContext(ctx, name, args...).Output()
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
return string(out), errors.New(string(ee.Stderr))
|
||||
@@ -294,9 +299,12 @@ func TestARestoreBesideAnotherAccountsDirectoryGoesToTheState(t *testing.T) {
|
||||
ran = append(ran, name+" "+strings.Join(args, " "))
|
||||
if name == "stat" {
|
||||
if args[len(args)-1] == "/home/agent" {
|
||||
return "1001 directory\n", nil
|
||||
return "1001 700 directory\n", nil
|
||||
}
|
||||
return "0 directory\n", nil
|
||||
if args[len(args)-1] == "/srv/shared" {
|
||||
return "0 1777 directory\n", nil
|
||||
}
|
||||
return "0 755 directory\n", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -307,11 +315,14 @@ func TestARestoreBesideAnotherAccountsDirectoryGoesToTheState(t *testing.T) {
|
||||
if why := b.besideIsUnsafe(context.Background(), "/srv/data.restored-x"); why != "" {
|
||||
t.Fatalf("root's directories are safe beside: %q", why)
|
||||
}
|
||||
if why := b.besideIsUnsafe(context.Background(), "/srv/shared/data.restored-x"); !strings.Contains(why, "other accounts") {
|
||||
t.Fatalf("a directory everyone may write is unsafe beside, sticky or not: %q", why)
|
||||
}
|
||||
run2 := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
if name == "stat" && args[len(args)-1] == "/home" {
|
||||
return "0 symbolic link\n", nil
|
||||
return "0 777 symbolic link\n", nil
|
||||
}
|
||||
return "0 directory\n", nil
|
||||
return "0 755 directory\n", nil
|
||||
}
|
||||
b.Run = run2
|
||||
if why := b.besideIsUnsafe(context.Background(), "/home/agent/x"); !strings.Contains(why, "symbolic link") {
|
||||
|
||||
Reference in New Issue
Block a user