daemon: rewrite authsync + image seeding on ext4 toolkit
ensureAuthorizedKeyOnWorkDisk and seedAuthorizedKeyOnExt4Image both
drove mount + sudo mkdir/chmod/chown/cat/install to patch
/.ssh/authorized_keys into a work disk or work-seed. Both now delegate
to a shared provisionAuthorizedKey helper that uses the ext4 toolkit
introduced in 7704396 — EnsureExt4RootPerms + MkdirExt4 +
Ext4PathExists/ReadExt4File + WriteExt4FileOwned. No mount, no sudo,
no host-path staging.
Drops ~10 sudo call sites from the VM create and image pull flows
and deletes the TestEnsureAuthorizedKeyOnWorkDiskRepairsNestedRootLayout
premise (flattenNestedWorkHome will disappear entirely in the next
commit — the no-seed path no longer copies /root, and the work-seed
path produces flat seeds).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0e28504892
commit
f0685366ec
3 changed files with 34 additions and 163 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue