diff --git a/internal/daemon/image_seed.go b/internal/daemon/image_seed.go index e4e7785..6e06ede 100644 --- a/internal/daemon/image_seed.go +++ b/internal/daemon/image_seed.go @@ -3,13 +3,10 @@ package daemon import ( "context" "fmt" - "os" - "path/filepath" "strings" "banger/internal/guest" "banger/internal/model" - "banger/internal/system" ) func (s *ImageService) seedAuthorizedKeyOnExt4Image(ctx context.Context, imagePath string) (string, error) { @@ -24,56 +21,7 @@ func (s *ImageService) seedAuthorizedKeyOnExt4Image(ctx context.Context, imagePa if err != nil { return "", fmt.Errorf("derive authorized ssh key: %w", err) } - mountDir, cleanup, err := system.MountTempDir(ctx, s.runner, imagePath, false) - if err != nil { - return "", err - } - defer cleanup() - - if err := flattenNestedWorkHome(ctx, s.runner, mountDir); err != nil { - return "", err - } - - // Same rationale as in ensureAuthorizedKeyOnWorkDisk — the seed's - // filesystem root becomes /root inside the guest, and sshd's - // StrictModes check walks its ownership and mode. - if err := normaliseHomeDirPerms(ctx, s.runner, mountDir); err != nil { - return "", err - } - - sshDir := filepath.Join(mountDir, ".ssh") - if _, err := s.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil { - return "", err - } - if _, err := s.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil { - return "", err - } - if _, err := s.runner.RunSudo(ctx, "chown", "0:0", sshDir); err != nil { - return "", err - } - - authorizedKeysPath := filepath.Join(sshDir, "authorized_keys") - existing, err := s.runner.RunSudo(ctx, "cat", authorizedKeysPath) - if err != nil { - existing = nil - } - merged := mergeAuthorizedKey(existing, publicKey) - tmpFile, err := os.CreateTemp("", "banger-image-authorized-keys-*") - if err != nil { - return "", err - } - tmpPath := tmpFile.Name() - if _, err := tmpFile.Write(merged); err != nil { - _ = tmpFile.Close() - _ = os.Remove(tmpPath) - return "", err - } - if err := tmpFile.Close(); err != nil { - _ = os.Remove(tmpPath) - return "", err - } - defer os.Remove(tmpPath) - if _, err := s.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil { + if err := provisionAuthorizedKey(ctx, s.runner, imagePath, publicKey); err != nil { return "", err } return fingerprint, nil diff --git a/internal/daemon/vm_authsync.go b/internal/daemon/vm_authsync.go index af24a3e..ad56359 100644 --- a/internal/daemon/vm_authsync.go +++ b/internal/daemon/vm_authsync.go @@ -37,61 +37,12 @@ func (s *WorkspaceService) ensureAuthorizedKeyOnWorkDisk(ctx context.Context, vm return fmt.Errorf("derive authorized ssh key: %w", err) } vmCreateStage(ctx, "prepare_work_disk", "provisioning SSH access on work disk") - workMount, cleanupWork, err := system.MountTempDir(ctx, s.runner, vm.Runtime.WorkDiskPath, false) - if err != nil { - return err - } - defer cleanupWork() - if err := flattenNestedWorkHome(ctx, s.runner, workMount); err != nil { + workDisk := vm.Runtime.WorkDiskPath + if err := provisionAuthorizedKey(ctx, s.runner, workDisk, publicKey); err != nil { return err } - // Normalise the work-disk filesystem root: inside the guest this - // mounts at /root, which sshd inspects when StrictModes is on (the - // default after the hardening drop-in). Any drift — owner != root, - // group/other-writable — would make sshd silently reject the key. - if err := normaliseHomeDirPerms(ctx, s.runner, workMount); err != nil { - return err - } - - sshDir := filepath.Join(workMount, ".ssh") - if _, err := s.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil { - return err - } - if _, err := s.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil { - return err - } - if _, err := s.runner.RunSudo(ctx, "chown", "0:0", sshDir); err != nil { - return err - } - - authorizedKeysPath := filepath.Join(sshDir, "authorized_keys") - existing, err := s.runner.RunSudo(ctx, "cat", authorizedKeysPath) - if err != nil { - existing = nil - } - merged := mergeAuthorizedKey(existing, publicKey) - - tmpFile, err := os.CreateTemp("", "banger-authorized-keys-*") - if err != nil { - return err - } - tmpPath := tmpFile.Name() - if _, err := tmpFile.Write(merged); err != nil { - _ = tmpFile.Close() - _ = os.Remove(tmpPath) - return err - } - if err := tmpFile.Close(); err != nil { - _ = os.Remove(tmpPath) - return err - } - defer os.Remove(tmpPath) - - if _, err := s.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil { - return err - } if prep.ClonedFromSeed && image.Managed { vmCreateStage(ctx, "prepare_work_disk", "refreshing managed work seed") if err := s.imageWorkSeed(ctx, image, fingerprint); err != nil { @@ -101,6 +52,37 @@ func (s *WorkspaceService) ensureAuthorizedKeyOnWorkDisk(ctx context.Context, vm return nil } +// provisionAuthorizedKey writes the managed SSH key into +// /.ssh/authorized_keys on an ext4 image via the sudoless toolkit. +// Shared between work-disk and image-seed paths — both need the same +// sequence: normalise fs-root perms, create /.ssh, merge against any +// existing authorized_keys, rewrite with root:root:0600. +// +// The fs root doubles as /root inside the guest, which sshd walks +// under StrictModes; forcing 0755 root:root here keeps a drifted +// seed image from silently rejecting the key at login time. +func provisionAuthorizedKey(ctx context.Context, runner system.CommandRunner, imagePath string, publicKey []byte) error { + if err := system.EnsureExt4RootPerms(ctx, runner, imagePath, 0o755, 0, 0); err != nil { + return err + } + if err := system.MkdirExt4(ctx, runner, imagePath, "/.ssh", 0o700, 0, 0); err != nil { + return err + } + var existing []byte + exists, err := system.Ext4PathExists(ctx, runner, imagePath, "/.ssh/authorized_keys") + if err != nil { + return err + } + if exists { + existing, err = system.ReadExt4File(ctx, runner, imagePath, "/.ssh/authorized_keys") + if err != nil { + return err + } + } + merged := mergeAuthorizedKey(existing, publicKey) + return system.WriteExt4FileOwned(ctx, runner, imagePath, "/.ssh/authorized_keys", 0o600, 0, 0, merged) +} + // normaliseHomeDirPerms forces the home-directory mount point to // 0755 root:root. sshd's StrictModes (the default, re-enabled after // banger stopped shipping "StrictModes no") rejects authorized_keys diff --git a/internal/daemon/vm_test.go b/internal/daemon/vm_test.go index ade0818..c29297a 100644 --- a/internal/daemon/vm_test.go +++ b/internal/daemon/vm_test.go @@ -847,65 +847,6 @@ func TestFlattenNestedWorkHomeCopiesEntriesIndividually(t *testing.T) { runner.assertExhausted() } -func TestEnsureAuthorizedKeyOnWorkDiskRepairsNestedRootLayout(t *testing.T) { - t.Parallel() - - workDiskDir := t.TempDir() - nestedHome := filepath.Join(workDiskDir, "root") - if err := os.MkdirAll(filepath.Join(nestedHome, ".ssh"), 0o700); err != nil { - t.Fatalf("MkdirAll(.ssh): %v", err) - } - if err := os.WriteFile(filepath.Join(nestedHome, ".bashrc"), []byte("export TEST_PROMPT=1\n"), 0o644); err != nil { - t.Fatalf("WriteFile(.bashrc): %v", err) - } - existingKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILEgacykey existing@test\n" - if err := os.WriteFile(filepath.Join(nestedHome, ".ssh", "authorized_keys"), []byte(existingKey), 0o600); err != nil { - t.Fatalf("WriteFile(authorized_keys): %v", err) - } - - privateKey, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - t.Fatalf("GenerateKey: %v", err) - } - privateKeyPEM := pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - }) - sshKeyPath := filepath.Join(t.TempDir(), "id_rsa") - if err := os.WriteFile(sshKeyPath, privateKeyPEM, 0o600); err != nil { - t.Fatalf("WriteFile(private key): %v", err) - } - - d := &Daemon{ - runner: &filesystemRunner{t: t}, - config: model.DaemonConfig{SSHKeyPath: sshKeyPath}, - } - wireServices(d) - vm := testVM("seed-repair", "image-seed-repair", "172.16.0.61") - vm.Runtime.WorkDiskPath = workDiskDir - - if err := d.ws.ensureAuthorizedKeyOnWorkDisk(context.Background(), &vm, model.Image{}, workDiskPreparation{}); err != nil { - t.Fatalf("ensureAuthorizedKeyOnWorkDisk: %v", err) - } - if _, err := os.Stat(filepath.Join(workDiskDir, "root")); !os.IsNotExist(err) { - t.Fatalf("nested root still exists: %v", err) - } - if _, err := os.Stat(filepath.Join(workDiskDir, ".bashrc")); err != nil { - t.Fatalf(".bashrc missing at top level: %v", err) - } - data, err := os.ReadFile(filepath.Join(workDiskDir, ".ssh", "authorized_keys")) - if err != nil { - t.Fatalf("ReadFile(authorized_keys): %v", err) - } - content := string(data) - if !strings.Contains(content, strings.TrimSpace(existingKey)) { - t.Fatalf("authorized_keys missing pre-existing key: %q", content) - } - if !strings.Contains(content, "ssh-rsa ") { - t.Fatalf("authorized_keys missing managed key: %q", content) - } -} - func TestEnsureGitIdentityOnWorkDiskCopiesHostGlobalIdentity(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed")