Files
darroyo 179b2d8fbd fix: deploy keys modal crash on null/undefined arrays
- sshmanager/deploy.go: initialize DeployResult with empty slices
  instead of nil to prevent null serialization in JSON
- Machines.tsx: use ?? [] fallback for messages and errors arrays
  in deploy keys modal to handle null/undefined gracefully
2026-07-09 20:38:41 -04:00

5.6 KiB

Revertir RunRemote a approach SSH-a-source sin envío base64

Estado

Estamos en plan mode (READ-ONLY). Los cambios de código Go están bloqueados. Este documento describe los cambios necesarios para proceder cuando salgamos de plan mode.

Contexto

Las claves SSH ya están pre-instaladas en cada máquina:

Máquina Clave instalada Pública autorizada en
Baby NAS (10.5.1.20) /var/lib/syncserver/ssh/keys/qnap.key (OpenSSH nativo, 387 bytes) qnap.key.pub ya en Qnap /mnt/HDA_ROOT/.config/ssh/authorized_keys
Qnap (10.5.0.144) n/a (server) baby-nas.key.pub ya en Baby NAS /root/.ssh/authorized_keys
Baby NAS known_hosts /var/lib/syncserver/ssh/known_hosts (3 host keys de Qnap en formato hashed)

Test crítico exitoso:

ssh -i /var/lib/syncserver/ssh/keys/baby-nas.key root@10.5.1.20 \
    "ssh -i /var/lib/syncserver/ssh/keys/qnap.key -o StrictHostKeyChecking=yes \
     admin@10.5.0.144 echo QNAP-FROM-BABY"
→ QNAP-FROM-BABY (exit 0)

Cambios pendientes

1. internal/syncengine/rsync_runner.go

Eliminar la función buildWrapperScript (líneas 86-101 del archivo actual).

Reemplazar la función RunRemote actual con la versión SSH-a-source:

func (r *RsyncRunner) RunRemote(ctx context.Context, pair *SyncPairConfig, src *MachineKeys, dst *MachineKeys, destKey string, onLine func(stream string, line string)) (*RsyncResult, error) {
	if src.Port == 0 {
		src.Port = 22
	}
	if src.PrivKey == "" {
		src.PrivKey = filepath.Join(r.sshDir, "id_ed25519")
	}
	if destKey == "" {
		destKey = filepath.Join(r.sshDir, "id_ed25519")
	}

	destUserHost := fmt.Sprintf("%s@%s", dst.SSHUser, dst.Host)
	args := r.buildArgs(pair)

	innerSSH := fmt.Sprintf("ssh -i %s -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=%s",
		destKey, filepath.Join(r.sshDir, "known_hosts"))
	rsyncFlags := strings.Join(args[:len(args)-2], " ")
	sourcePath := args[len(args)-2]
	destPath := args[len(args)-1]

	remoteCmd := fmt.Sprintf("rsync %s -e %q %s %s",
		rsyncFlags, innerSSH, sourcePath, destUserHost+":"+destPath)

	sshArgs := []string{
		"-i", src.PrivKey,
		"-o", "StrictHostKeyChecking=accept-new",
		"-o", "UserKnownHostsFile=" + filepath.Join(r.sshDir, "known_hosts"),
		"-p", fmt.Sprintf("%d", src.Port),
		fmt.Sprintf("%s@%s", src.SSHUser, src.Host),
	}
	sshArgs = append(sshArgs, remoteCmd)

	cmd := exec.CommandContext(ctx, "ssh", sshArgs...)
	return r.runCmd(ctx, cmd, onLine)
}

Modificar el struct MachineKeys para agregar SSHUser:

type MachineKeys struct {
	Host    string
	Port    int
	User    string
	SSHUser string
	PrivKey string
}

2. internal/syncengine/engine.go

Actualizar la llamada a RunRemote (líneas 263-283 actuales) para usar la nueva firma con destPrivKeyPath como argumento separado:

if isRemoteToRemote(srcMachine, dstMachine) {
	destPrivKeyPath, err := e.resolveSSHKey(dstMachine)
	if err != nil {
		slog.Warn("failed to resolve destination SSH key, using server key", "error", err)
		destPrivKeyPath = ""
	}
	result, err = runner.RunRemote(jobCtx, cfg,
		&syncengine.MachineKeys{
			Host:    srcMachine.Host,
			Port:    srcMachine.Port,
			User:    srcMachine.SSHUser,
			SSHUser: srcMachine.SSHUser,
			PrivKey: privKey,
		},
		&syncengine.MachineKeys{
			Host:    dstMachine.Host,
			Port:    dstMachine.Port,
			User:    dstMachine.SSHUser,
			SSHUser: dstMachine.SSHUser,
			PrivKey: destPrivKeyPath,
		},
		destPrivKeyPath,
		onLine)
}

3. Build y deploy

go build ./cmd/server
make clean && make package
./scripts/bump-version.sh  # bump a 1.0.28
git add -f cmd/server/main.go Makefile internal/syncengine/
git commit -m "fix: RunRemote uses SSH-to-source, dest key pre-installed on source"
git push

# Deploy manual:
scp dist/syncserver_1.0.28_amd64.deb root@10.5.1.30:/root/move-data-nas/dist/
ssh root@10.5.1.30 "cd /root/move-data-nas && dpkg -i dist/syncserver_1.0.28_amd64.deb"

4. Test

Trigger job 23 desde UI o via API. Verificar en DB:

ssh root@10.5.1.30 'sqlite3 /var/lib/syncserver/app.db \
  "SELECT id, status, error_code, substr(error_message,1,200) FROM jobs WHERE id=24;"'

Esperado: status=success, error_code=NULL, error_message=NULL.

Por qué este approach funciona

Antes:

  • LXC SSH a Baby NAS
  • Baby NAS ejecuta echo 'BASE64' | base64 -d > /tmp/syncserver-dest-key && rsync ...
  • El base64 round-trip generaba archivo corrupto o vacío
  • Baby NAS OpenSSH 8.9 no podía parsear el formato PKCS8

Ahora:

  • LXC SSH a Baby NAS (usa baby-nas.key)
  • Baby NAS ejecuta rsync -e ssh -i /var/lib/syncserver/ssh/keys/qnap.key ...
  • qnap.key YA está en Baby NAS en formato OpenSSH nativo (parseable por 8.9)
  • Host key de Qnap YA está en known_hosts de Baby NAS (no prompt)
  • Pública de qnap.key YA está en authorized_keys de Qnap (auth pasa)

Comportamiento esperado

job started job_id=24 pair=Peliculas
ssh -i baby-nas.key root@10.5.1.20 'rsync -aP -e "ssh -i qnap.key -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=..." /mnt/storage/multimedia/peliculas admin@10.5.0.144:/share/media/peliculas'
sending incremental file list
...
sent X bytes  received Y bytes  Z bytes/sec
total size is T  speedup is S
job completed job_id=24 pair=Peliculas

Si falla

Error Causa Fix
Load key invalid format clave corrupta en Baby NAS re-copiar qnap.key
Permission denied (publickey) pública no está en Qnap re-agregar a authorized_keys
Host key verification failed known_hosts desincronizado re-correr ssh-keyscan
Connection timed out problema L2/L3 real (no era el caso) investigar red