A variável $! do shell só conhece o pid do processo iniciado pelo shell. Como você suspeitava, a chamada ssh usando -f bifurca seu próprio processo para que ele possa ir para o plano de fundo, portanto, a árvore do processo geral se parece com [1]:
shell
|
+--ssh<1> (pid is $!)
|
+--ssh<2> (pid is different)
ssh<1> sai muito pouco depois da invocação; portanto, é improvável que o valor em $! seja útil. É ssh<2> que está realizando a comunicação remota e fazendo seu tunelamento para você, e a única maneira de obter seu PID de forma confiável é examinando a tabela de processos, como você está fazendo com pgrep [2]. O método pgrep provavelmente será o correto aqui.
Por que funciona no script, mas não de forma interativa, essa é provavelmente uma condição de corrida. Como você coloca o primeiro ssh em segundo plano, o shell e o ssh estão sendo executados simultaneamente, e ssh faz algumas autenticações criptográficas com CPU moderada e algumas viagens de ida e volta na rede. É provável que o pgrep que você executa no script esteja sendo executado antes que ssh<1> se forme para ir para o segundo plano. Para contornar isso, execute pgrep mais tarde, seja por uma chamada sleep , ou apenas ligando apenas quando você realmente precisar do PID mais tarde.
[1]: Tecnicamente, pode ser mais complicado do que isso, se ssh estiver usando um plano duplo clássico como plano de fundo. Nesse caso, haveria outro processo efêmero de ssh entre os dois.
[2]: A menos que você seja systemd e esteja usando cgroups ou algo para acompanhar todos os seus filhos. Que você não é.