banger/internal/daemon/image_seed.go
Thales Maciel 2e6e64bc04
guest sshd: drop DEBUG3 + StrictModes no; normalise /root perms
Previously /etc/ssh/sshd_config.d/99-banger.conf landed with:

  LogLevel DEBUG3
  PermitRootLogin yes
  PubkeyAuthentication yes
  AuthorizedKeysFile /root/.ssh/authorized_keys
  StrictModes no

DEBUG3 was debug leftover that floods journald in normal use.
StrictModes no was a workaround for /root perm drift on the work
disk — the real fix is to make those perms correct at provisioning
time.

New drop-in:

  PermitRootLogin prohibit-password
  PubkeyAuthentication yes
  PasswordAuthentication no
  KbdInteractiveAuthentication no
  AuthorizedKeysFile /root/.ssh/authorized_keys

prohibit-password blocks password root login even if PasswordAuth
gets flipped on elsewhere; KbdInteractiveAuth no closes the last
interactive fallback; StrictModes is now on (sshd's default).

normaliseHomeDirPerms chown/chmods /root to 0755 root:root at every
work-disk mount (ensureAuthorizedKeyOnWorkDisk,
seedAuthorizedKeyOnExt4Image); the .ssh dir also explicitly
chown'd root:root. Verified end-to-end against a real VM:
`sshd -T` reports strictmodes yes and all five directives match.

Regression test (sshd_config_test.go) pins the allow-list and the
deny-list (DEBUG3, StrictModes no, bare `PermitRootLogin yes`) so
the next accidental reintroduction fails fast.

README's Security section updated to reflect the new posture.
2026-04-19 13:40:40 -03:00

96 lines
2.7 KiB
Go

package daemon
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"banger/internal/guest"
"banger/internal/model"
"banger/internal/system"
)
func (d *Daemon) seedAuthorizedKeyOnExt4Image(ctx context.Context, imagePath string) (string, error) {
if strings.TrimSpace(d.config.SSHKeyPath) == "" {
return "", nil
}
fingerprint, err := guest.AuthorizedPublicKeyFingerprint(d.config.SSHKeyPath)
if err != nil {
return "", fmt.Errorf("derive authorized ssh key fingerprint: %w", err)
}
publicKey, err := guest.AuthorizedPublicKey(d.config.SSHKeyPath)
if err != nil {
return "", fmt.Errorf("derive authorized ssh key: %w", err)
}
mountDir, cleanup, err := system.MountTempDir(ctx, d.runner, imagePath, false)
if err != nil {
return "", err
}
defer cleanup()
if err := d.flattenNestedWorkHome(ctx, 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, d.runner, mountDir); err != nil {
return "", err
}
sshDir := filepath.Join(mountDir, ".ssh")
if _, err := d.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil {
return "", err
}
if _, err := d.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil {
return "", err
}
if _, err := d.runner.RunSudo(ctx, "chown", "0:0", sshDir); err != nil {
return "", err
}
authorizedKeysPath := filepath.Join(sshDir, "authorized_keys")
existing, err := d.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 := d.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil {
return "", err
}
return fingerprint, nil
}
func (d *Daemon) refreshManagedWorkSeedFingerprint(ctx context.Context, image model.Image, fingerprint string) error {
if !image.Managed || strings.TrimSpace(image.WorkSeedPath) == "" || strings.TrimSpace(fingerprint) == "" {
return nil
}
seededFingerprint, err := d.seedAuthorizedKeyOnExt4Image(ctx, image.WorkSeedPath)
if err != nil {
return err
}
if seededFingerprint == "" || seededFingerprint == image.SeededSSHPublicKeyFingerprint {
return nil
}
image.SeededSSHPublicKeyFingerprint = seededFingerprint
image.UpdatedAt = model.Now()
return d.store.UpsertImage(ctx, image)
}