# Multiplexing de conexões SSH: riscos dos sockets de controle

O multiplexing de conexões SSH pode deixar aberta uma rota autenticada para um host depois que o comando que a criou termina. Esse comportamento é intencional. Também é fácil interpretá-lo mal quando uma tarefa automatizada, uma execução de agente ou um job de implantação informa que terminou.

Já vi equipes investigarem uma janela de manutenção que supostamente havia sido encerrada e descobrirem que um master local do `ssh` ainda mantinha um transporte ativo, que um socket ainda aceitava novos clientes e que um comando posterior o reutilizava sem outra autenticação interativa. Nada de extraordinário havia acontecido. A configuração fazia exatamente o que o OpenSSH documenta. A equipe havia tratado o fim de um comando de shell como o fim do acesso.

A distinção importante é entre um **comando do cliente** concluído e um **transporte autenticado** encerrado. O multiplexing separa esses dois eventos. Se você executa trabalho autônomo por SSH, a limpeza e as evidências precisam levar essa separação em conta.

## Um socket de controle pode durar mais que o comando que o abriu

O multiplexing SSH usa uma conexão SSH de longa duração, chamada master, para transportar o trabalho de processos clientes SSH executados depois. Esses clientes posteriores costumam ser chamados de slaves na documentação do OpenSSH. Quando consegue entrar em contato com o master local pelo socket de controle, um slave não repete a configuração normal da conexão nem a autenticação do usuário.

Uma configuração típica parece inofensiva:

```sshconfig
Host build-box
    HostName 192.0.2.44
    User deploy
    ControlMaster auto
    ControlPath ~/.ssh/cm/%C
    ControlPersist 20m
```

O primeiro `ssh build-box` cria uma conexão de rede e um socket de domínio Unix em `~/.ssh/cm/`. Comandos posteriores, como `ssh build-box 'uname -a'`, `scp` e `sftp`, podem usar esse socket. Com `ControlPersist 20m`, o master continua disponível por vinte minutos depois que sua última sessão é encerrada.

Isso torna normal a seguinte sequência:

1. Uma tarefa executa `ssh build-box 'apply-change'` e termina com status zero.
2. O master continua conectado porque `ControlPersist` determina que ele permaneça disponível.
3. Dezenove minutos depois, outro processo local executa `ssh build-box 'read-status'`.
4. Esse processo abre um canal pelo transporte já autenticado.

O comando posterior pode ser autorizado pelo sistema operacional local e pelas permissões do socket, mas não faz o host remoto receber uma nova autenticação SSH. Um revisor que examine apenas o registro do login inicial pode concluir facilmente que a atividade terminou muito antes do que realmente terminou.

O OpenSSH descreve `ControlMaster` no manual de `ssh_config` como um recurso que permite várias sessões por uma única conexão de rede, e descreve `ControlPersist` como a opção que mantém o master aberto em segundo plano. Leia as duas descrições em conjunto. `ControlPersist` não é apenas uma configuração de desempenho. Ele altera o período durante o qual um processo local pode solicitar novos canais em uma conexão autenticada.

Para uma pessoa trabalhando em uma única máquina, essa troca pode fazer sentido. Para automação limitada ao escopo de uma tarefa, ela precisa de um responsável explícito e de uma ação explícita de encerramento.

## Um evento de autenticação não é um evento de comando

As equipes costumam misturar autenticação, conexão, canal e comando. No SSH, são coisas diferentes, e o multiplexing deixa essa diferença evidente.

O master inicial cria uma conexão TCP com o servidor, verifica o host, negocia a criptografia e autentica a conta. Depois de autenticado, ele pode abrir canais SSH. Um comando de shell, um shell interativo, uma transferência SFTP, uma solicitação de encaminhamento de porta local e uma solicitação de encaminhamento de porta remoto usam canais ou solicitações relacionadas a canais nesse transporte.

Quando uma nova invocação local de `ssh` encontra um socket de controle utilizável, ela envia uma solicitação por esse socket. O master decide se deve abrir o canal pedido. Ele não precisa da chave privada novamente. Não precisa de outra solicitação ao agente. O servidor não precisa receber outra tentativa de login.

Isso não significa que o OpenSSH armazene uma senha reutilizável no socket. Essa descrição é imprecisa e leva a análises ruins. O problema de segurança é um transporte já autenticado com uma interface local que pode pedir que ele atue. Um invasor que consiga usar o socket talvez nem precise da credencial original.

Essa distinção muda as perguntas feitas durante um incidente. «A chave continuou no disco?» é uma pergunta importante, mas não determina se o acesso continuou. Pergunte também:

- Um processo master continuou conectado ao destino remoto?
- Outro processo local conseguia alcançar o socket de controle?
- O master aceitou sessões adicionais, transferências de arquivos ou solicitações de encaminhamento?
- Quem podia executar processos como o proprietário do socket ou atravessar o diretório do socket?
- Quando o master realmente terminou?

Um executor de tarefas que informa o código de saída de um comando não responde sozinho a nenhuma dessas perguntas. Esse código descreve apenas o processo filho que ele aguardou.

## Sockets compartilhados transformam limites entre processos locais em limites de acesso

Um socket de controle é um socket de domínio Unix local. Sua localização no sistema de arquivos e suas permissões determinam quais processos locais podem tentar se comunicar com o master. Por isso, um `ControlPath` amplo pode unir trabalhos que não deveriam compartilhar nada, mesmo quando os aliases dos hosts remotos parecem organizados.

Considere uma conta de executor de CI com esta configuração:

```sshconfig
Host *
    ControlMaster auto
    ControlPath /tmp/ssh-%r@%h:%p
    ControlPersist 1h
```

O job A conecta-se como `deploy` a `app.internal`. Ele cria um master e um socket em `/tmp`. Depois, o job A termina. O job B, executado pela mesma conta local, conecta-se ao mesmo destino. Ele pode reutilizar o transporte autenticado do job A se conseguir descobrir e acessar esse socket.

Essa configuração é popular porque acelera comandos repetidos com quase nenhuma alteração na aplicação. Ela está errada para jobs independentes porque o limite de reutilização se baseia no host, na porta e no usuário, e não no job que possui a autorização. Uma conta compartilhada no executor transforma esse erro em acesso rotineiro entre tarefas.

O diretório importa tanto quanto o arquivo do socket. Em sistemas Unix, um processo precisa de permissão de busca no diretório para alcançar um caminho. Um socket armazenado em um diretório privado, pertencente ao usuário e com modo 0700, oferece um limite muito mais claro do que um diretório temporário compartilhado. As permissões do próprio socket continuam importantes, mas não devem ser tratadas como o único controle.

Use um diretório criado para a tarefa e pertencente à conta que a executa. Um wrapper de shell pode fazer isso sem depender de uma configuração SSH global:

```sh
set -eu
run_id="release-4821"
cm_dir="$HOME/.ssh/task-control/$run_id"
mkdir -p "$cm_dir"
chmod 700 "$cm_dir"

socket="$cm_dir/%C"
ssh -o ControlMaster=auto \
    -o ControlPersist=5m \
    -o ControlPath="$socket" \
    build-box 'id \u0026\u0026 hostname'
```

O token `%C` evita um nome literal muito longo e reduz colisões entre conjuntos diferentes de parâmetros de conexão. O manual de `ssh_config` do OpenSSH o define como um hash sobre os detalhes da conexão. Ele é útil, mas não inclui o ticket da implantação, a sessão do agente nem o ID da tarefa. Neste exemplo, o diretório pai fornece esse limite que faltava.

Não coloque um socket de controle literal em um checkout de repositório, em um workspace com permissão ampla de escrita ou em `/tmp` apenas porque esses locais são convenientes. É assim que um socket local se transforma em uma capacidade compartilhada por acidente.

## ControlPersist é uma política de retenção, não um plano de limpeza

`ControlPersist` determina que o SSH mantenha um master disponível depois que as sessões clientes forem encerradas. Ele aceita `yes`, que o mantém executando indefinidamente em segundo plano, ou um período como `10m`. As duas opções são escolhas de retenção.

Um timeout ajuda quando um cliente falha antes de executar a limpeza. Ele não prova que a tarefa terminou ao mesmo tempo que o acesso. Durante o timeout, um processo com acesso ao socket pode solicitar trabalho novo. Uma hora é especialmente difícil de justificar quando uma tarefa de implantação durou dois minutos.

Há situações em que um timeout curto é razoável. Um processo de automação controlado pode executar vários comandos em sequência e se beneficiar de uma conexão já aquecida. Nesse caso, torne o timeout menor que o intervalo ocioso esperado, isole o socket nessa execução e encerre o master pelo caminho de sucesso. Assim, o timeout funciona como fallback para falhas, não como mecanismo normal de encerramento.

A configuração `ControlPersist yes` precisa de um responsável com uma razão duradoura para manter o acesso. Uma estação de trabalho interativa de administrador pode ter essa razão. Uma tarefa descartável não tem.

Outro erro comum é presumir que uma falha de comando fecha o master. Um shell pode retornar cedo depois de um comando remoto falhar enquanto o master em segundo plano continua ativo. Uma tarefa cancelada pode fazer o mesmo. A limpeza precisa ser executada depois de sucesso, falha e interrupção, e deve registrar se o encerramento funcionou.

Se a automação usa um trap, mantenha a limpeza restrita e verifique o alvo. Não remova cegamente primeiro o caminho do socket. Remover o caminho pode dificultar a investigação enquanto o processo master e a conexão continuam existindo.

```sh
cleanup() {
    ssh -S "$socket" -O exit build-box e/dev/null 2e261 || true
    rmdir "$cm_dir" 2e/dev/null || true
}
trap cleanup EXIT HUP INT TERM
```

Esse padrão pede que o master termine antes de tentar remover o diretório. Se `ssh -O exit` falhar, preserve o diretório e investigue, em vez de apagar as evidências. Em código de produção, registre no histórico da tarefa a falha, o ID do processo e o caminho do socket.

## `exit` e `stop` têm significados operacionais diferentes

O OpenSSH disponibiliza comandos de controle por meio de `ssh -O`. Os operadores costumam usar o comando errado porque os dois nomes parecem indicar limpeza.

`ssh -O check host` pergunta se há um master executando e informa seu ID de processo quando recebe uma resposta. `ssh -O exit host` pede que o master termine. Para uma tarefa concluída, `exit` normalmente é o comando correto, porque encerra o transporte reutilizável.

`ssh -O stop host` informa ao master que pare de aceitar novas sessões multiplexadas. As sessões existentes continuam. Isso pode ser útil quando um operador precisa drenar o trabalho ativo, mas não atende à afirmação de que todo o acesso SSH terminou quando a tarefa foi concluída. Um master ativo com canais ativos ainda possui uma conexão de rede ativa.

Use explicitamente o caminho do socket quando o diagnóstico não puder depender da configuração atual do usuário:

```sh
ssh -S "$socket" -O check build-box
# Master running (pid=41782)

ssh -S "$socket" -O exit build-box
# Exit request sent.

ssh -S "$socket" -O check build-box
# Control socket connect(...): No such file or directory
```

As palavras e o texto exato dos erros variam conforme a plataforma e a versão do OpenSSH. Por isso, capture a saída padrão e a saída de erro, em vez de interpretar uma única frase como um contrato. O formato das evidências importa: um check bem-sucedido identifica um master ativo, um exit bem-sucedido envia a solicitação de encerramento e um check posterior malsucedido apoia a afirmação de que nenhum socket de controle respondeu.

Há um caso incômodo que merece atenção. O socket pode permanecer depois de uma falha do master, e um PID pode desaparecer entre duas verificações. Um caminho antigo não prova que o acesso ainda existe. Por outro lado, uma verificação sem resposta não prova que a conexão remota terminou se você removeu o caminho antes de examiná-lo. Verifique a tabela de processos e os sockets Unix abertos antes de apagar qualquer coisa.

No macOS, `lsof` costuma ser a ferramenta local mais rápida para inspeção:

```sh
lsof -nP -U | grep '/.ssh/task-control/'
ps -p 41782 -o pid=,ppid=,lstart=,etime=,command=
```

Capture essa saída no registro da tarefa. O primeiro comando associa um socket Unix a um processo. O segundo fornece processo pai, horário de início, tempo decorrido e detalhes da invocação. Use `grep` como auxílio interativo, não como controle de auditoria. Um coletor real deve consultar e armazenar diretamente os registros relevantes.

## O encaminhamento de portas torna um master ocioso mais importante

Um master que parece ocioso ainda pode transportar um estado de encaminhamento ou aceitar uma solicitação posterior de encaminhamento. Por isso, contar apenas comandos de shell produz uma visão incompleta.

O encaminhamento local expõe um listener local que envia tráfego pela conexão SSH. O encaminhamento remoto pede que o servidor escute e envie as conexões de volta pelo cliente. O encaminhamento dinâmico cria um proxy SOCKS. Cada um deles pode durar mais que o comando que o estabeleceu, dependendo de como a sessão e o master foram invocados.

Os manuais do OpenSSH documentam comandos de controle como `forward` e `cancel` para solicitações de encaminhamento quando o multiplexing está ativo. Isso é útil na operação. Também significa que um processo que alcance o socket pode solicitar caminhos de rede além de um comando de shell comum, sujeito à política do servidor e ao estado do master.

Para tarefas automatizadas, trate o encaminhamento como uma exceção explícita. Registre o endereço de bind, a porta local ou remota, o host e a porta de destino e o resultado da limpeza. Não permita que um wrapper SSH genérico herde silenciosamente diretivas `LocalForward`, `RemoteForward` ou `DynamicForward` da configuração ampla `Host *` de um usuário.

Inspecione as configurações resolvidas antes de confiar em um alias:

```sh
ssh -G build-box | grep -E '^(controlmaster|controlpath|controlpersist|localforward|remoteforward|dynamicforward) '
```

`ssh -G` imprime a configuração efetiva depois que o OpenSSH aplica a correspondência de hosts e os valores padrão. Essa verificação detecta uma falha surpreendentemente comum: uma tarefa usa um alias simples de host, mas um arquivo de configuração incluído ativa multiplexing ou encaminhamento longe da configuração da própria tarefa. O comando pode mostrar mais campos em versões diferentes. Armazene a saída completa de `ssh -G`, e não apenas as linhas esperadas.

Um sistema remoto pode registrar uma única conexão de origem enquanto várias conexões de aplicações encaminhadas passam por ela. A telemetria de rede, os logs SSH do servidor e os logs da tarefa respondem a partes diferentes dessa história. Nenhum deles substitui os outros.

## Os logs do servidor, sozinhos, não reconstroem as decisões locais

O servidor SSH remoto vê o transporte e tudo que sua configuração de logs registrar sobre canais e comandos. Ele não consegue informar de maneira confiável por que um processo local recebeu permissão para usar um socket de controle, qual tarefa era proprietária daquele diretório ou se outro processo local o reutilizou depois que a tarefa original terminou.

Isso não é uma crítica aos logs do servidor. Eles operam em outro ponto do sistema. `sshd` pode registrar eventos de autenticação e conexão. Um wrapper de comando forçado ou um subsistema de auditoria pode registrar comandos remotos. Esses registros continuam úteis. Por padrão, eles não identificam todos os clientes locais de multiplexing, porque o servidor pode enxergar todos os canais como parte de um único transporte já autenticado.

Mantenha evidências dos dois lados desse limite. Um registro útil da tarefa inclui:

- a configuração completa do cliente, resolvida por `ssh -G`, sem incluir segredos;
- o hostname remoto, o endereço, a conta, o resultado da verificação da impressão digital do host e o PID do master inicial;
- o caminho do socket de controle, o diretório pai privado e os horários observados de criação e encerramento;
- cada comando remoto, transferência ou operação de encaminhamento solicitada, com seu status de saída;
- o resultado de `check` antes da limpeza, o resultado de `exit` e a inspeção posterior do processo e do socket.

Adicione um identificador de tarefa ao nome do diretório e ao registro, não a um caminho de socket global compartilhado por todas as tarefas. O identificador conecta os eventos locais, mas não deve ser tratado sozinho como um limite de segurança.

Fazer hash ou assinar o registro depois da coleta ajuda a detectar alterações posteriores, mas não corrige eventos que não foram registrados. Colete os eventos do ciclo de vida enquanto acontecem. Um registro montado a partir do histórico do shell depois de um incidente é uma evidência fraca, especialmente quando entram em cena masters em segundo plano e novas tentativas.

Para trabalhos conduzidos por agentes, diferencie a intenção do agente da ação SSH executada. «Implantar a versão X» é uma intenção. `ssh build-box 'sudo systemctl restart api'` é uma ação. O registro de reutilização do socket mostra se essa ação obteve um transporte novo ou usou um transporte existente. São fatos de auditoria diferentes.

## Sockets limitados à tarefa dão um responsável à limpeza

O padrão mais seguro para trabalhos automatizados curtos é simples: use um diretório exclusivo de socket de controle por tarefa, permita reutilização apenas dentro dessa tarefa e envie uma solicitação explícita de `exit` antes que a tarefa informe que terminou.

Um wrapper prático precisa de um ciclo de vida claro. Ele deve criar o diretório com permissões restritivas, gravar um registro antes da primeira conexão, executar os comandos com o mesmo `ControlPath` explícito, desligar o master, verificar o resultado e só então remover o diretório vazio. Uma tarefa que não consegue verificar a limpeza deve informar falha de limpeza, mesmo que o comando remoto tenha sido bem-sucedido.

Este exemplo usa `mktemp` para evitar a necessidade de adivinhar um nome de diretório exclusivo:

```sh
set -eu
base="$HOME/.ssh/task-control"
mkdir -p "$base"
chmod 700 "$base"
cm_dir=$(mktemp -d "$base/run.XXXXXX")
chmod 700 "$cm_dir"
socket="$cm_dir/%C"

finish() {
    status=$?
    ssh -S "$socket" -O exit build-box ee"$cm_dir/cleanup.log" 2e261 || \
        printf '%s\n' 'master exit request failed' ee"$cm_dir/cleanup.log"
    ssh -S "$socket" -O check build-box ee"$cm_dir/cleanup.log" 2e261 || true
    exit "$status"
}
trap finish EXIT HUP INT TERM

ssh -o ControlMaster=auto \
    -o ControlPersist=2m \
    -o ControlPath="$socket" \
    build-box 'deployctl apply release-4821'
```

A configuração curta de `ControlPersist` cobre o caso em que um processo morre antes de executar seu trap. Ela não deve ser interpretada como permissão para outra tarefa reutilizar o master. O diretório aleatório impede essa reutilização porque a segunda tarefa não conhece nem herda o caminho da primeira.

Há uma correção a fazer antes de adotar este exemplo exatamente assim. Não registre argumentos de comandos sensíveis, valores de ambiente ou dados privados copiados em `cleanup.log`. Registros de auditoria precisam de detalhes suficientes para estabelecer quem fez o quê, mas não podem se transformar em um novo repositório de segredos. Redija os argumentos no limite do wrapper, enquanto ainda conhece seu significado.

O Sallyport encaminha as ações SSH pelo seu helper stateless `sp-ssh`, mantendo as chaves SSH no cofre criptografado em vez de expô-las ao agente. Isso elimina uma falha comum no tratamento de credenciais, mas as equipes ainda precisam definir o limite da ação e manter registros que mostrem quando a execução terminou.

## Configurações globais de conveniência anulam os limites das tarefas

Uma seção global `Host *` costuma ativar o multiplexing para todo shell humano, script, repositório e processo filho de automação executado por uma conta. Isso é amplo demais quando a mesma conta executa tarefas com aprovações ou responsáveis diferentes.

A configuração pode vir de arquivos `Include`, de ferramentas de gerenciamento de configuração, dos dotfiles pessoais de um desenvolvedor ou de uma imagem de build. A linha de comando também pode substituí-la. Nunca deduza o comportamento ativo a partir de um único arquivo de configuração visível. Use `ssh -G` com o alias exato do host e o contexto de usuário que a tarefa usará.

Se o ambiente de automação não puder garantir um caminho de controle privado, desative o multiplexing para essa ação:

```sh
ssh -o ControlMaster=no \
    -o ControlPath=none \
    build-box 'maintenancectl status'
```

Isso exige uma nova conexão e uma nova autenticação em cada invocação. É um custo adequado para ações de alto impacto, acesso emergencial raro ou trabalho que atravesse limites de tarefa e confiança. A autenticação repetida fornece um evento de autorização mais claro e torna o raciocínio sobre o ciclo de vida muito menos ambíguo.

Não confunda `ControlMaster=auto` com uma garantia de que um processo só reutilizará sua própria conexão. `auto` significa que o cliente tentará encontrar um master no caminho configurado e criará um se não encontrar. O caminho configurado determina qual conexão ele pode encontrar.

Algumas equipes afirmam que um socket compartilhado não é problema porque todos os jobs executam sob uma única conta de serviço. Esse argumento só vale se todo job com essa identidade Unix tiver a mesma autoridade para alcançar todos os destinos por meio da conta e se a equipe aceitar que um job possa herdar o transporte ativo de outro. A maioria dos ambientes maduros não quer isso de fato.

## Um encerramento limpo da tarefa precisa provar que o transporte foi fechado

Não declare uma tarefa SSH concluída quando o último comando remoto retornar. Declare-a concluída quando a tarefa tiver fechado o master e registrado a prova, ou informado que não conseguiu fazer isso.

O registro final deve dizer ao investigador se um comando posterior tinha algum caminho para reutilizar a conexão. Ele precisa conter a identidade da tarefa, a configuração SSH resolvida, o caminho do socket, os detalhes do processo master, os registros de atividade, os resultados explícitos dos comandos de controle e a observação posterior à limpeza. Também precisa de evidências remotas para alterações que continuaram de forma independente do SSH, como uma reinicialização de serviço ou um processo destacado.

Estabeleça um procedimento que os operadores consigam executar sob pressão: isole o socket, inspecione-o antes da limpeza, solicite `exit`, verifique que nenhum master responde e preserve o resultado. Esse procedimento é mais útil do que um timeout padrão longo porque transforma uma suposição sobre o acesso em um fato verificável.

Quando o trabalho for sensível a ponto de um transporte autenticado deixado para trás ser inaceitável, não o otimize com um master compartilhado. Abra uma conexão nova, execute a ação, feche-a e mantenha o registro. Os segundos extras custam menos do que explicar por que um caminho de acesso sobreviveu depois que a tarefa deveria ter terminado.
