# O encaminhamento de ambiente SSH pode vazar contexto local?

O encaminhamento de ambiente SSH pode vazar muito mais contexto local do que uma tarefa remota precisa. O canal é criptografado, mas a criptografia protege apenas o percurso. Ela não transforma `AWS_PROFILE`, `GIT_AUTHOR_EMAIL`, um nome de cliente ou um sinalizador interno em entradas apropriadas para um processo em outra máquina.

Trato cada variável encaminhada como um argumento do comando remoto. Portanto, ela precisa de nome, motivo, responsável e teste. Um curinga copiado de uma configuração SSH conveniente para notebook não atende a esse critério, pois seu significado muda sempre que alguém adiciona uma variável local correspondente.

O objetivo seguro não é um ambiente remoto vazio. `sshd`, a conta de login, o shell e os invólucros criam sua própria base. O objetivo é mais preciso: o cliente contribui exatamente com as variáveis do fluxo descritas em um contrato, o servidor não aceita um conjunto mais amplo e um teste comprova tanto os valores obrigatórios quanto as iscas rejeitadas.

## O encaminhamento só acontece quando os dois lados do SSH concordam

O encaminhamento de ambiente do OpenSSH tem duas barreiras. No cliente, `SendEnv` seleciona nomes do ambiente do processo local, enquanto `SetEnv` fornece pares literais `NOME=VALOR`. No servidor, `AcceptEnv` decide quais nomes enviados pelo cliente entram no ambiente da sessão. Em geral, um nome só atravessa quando o cliente o oferece e o servidor o aceita.

O RFC 4254 descreve o mecanismo por baixo dessas opções. Antes de iniciar o shell ou comando, o cliente pode enviar uma solicitação de canal SSH do tipo `env`, contendo um nome e um valor. O RFC também alerta que definir o ambiente de um processo privilegiado sem controle pode ser perigoso e recomenda uma lista de nomes permitidos ou a redução prévia de privilégios. Esse aviso faz parte da especificação do protocolo.

Os manuais do OpenSSH acrescentam uma exceção que costuma confundir auditorias: quando o cliente solicita um pseudoterminal, `TERM` sempre é enviada e aceita porque o protocolo precisa dela. Execute automação com `ssh -T` quando não houver necessidade de terminal. Isso remove desse caminho o comportamento interativo, surpresas de inicialização e `TERM`.

Quatro observações decorrem desse acordo.

- Um `SendEnv` amplo fica inativo diante de um servidor que não aceita nada, mas ganha efeito assim que um administrador amplia `AcceptEnv`.
- Um `AcceptEnv` amplo fica inativo para um cliente cuidadoso, mas qualquer usuário autorizado pode conectar-se com outra configuração e fornecer um valor correspondente.
- Nenhum lado consegue impor sozinho todo o contrato quando pessoas diferentes controlam as configurações.
- Um comando bem-sucedido não revela quais solicitações de ambiente o servidor recusou.

O último ponto importa. O OpenSSH pode ignorar uma variável oferecida e continuar a sessão. Se o shell remoto já define o mesmo nome, um teste que verifica apenas o valor final pode atribuir ao encaminhamento um valor que nunca foi entregue.

## Curingas transformam estado local futuro em entrada remota

`SendEnv WORKFLOW_*` não significa “as variáveis que revisei hoje”. Significa todos os nomes correspondentes presentes no ambiente de cada futuro processo SSH. Semanas depois, alguém pode adicionar `WORKFLOW_DEBUG_DUMP`, `WORKFLOW_CUSTOMER` ou `WORKFLOW_TOKEN_FILE` ao perfil, e a regra antiga adquire novo comportamento em silêncio.

O encaminhamento de localidade mostra como isso se espalha. Muitas estações enviam `LANG` e `LC_*` para uma sessão interativa exibir texto corretamente. Isso pode ser razoável para um login humano, mas não pertence automaticamente a uma conta não interativa de compilação ou implantação. A localidade altera ordenação, classes de caracteres, datas e mensagens. Uma tarefa que analisa saída pode falhar sem que nenhum segredo atravesse.

Os padrões de maior risco são os que parecem namespaces convenientes:

- `AWS_*` pode incluir perfis, regiões, valores relacionados a credenciais e caminhos de configuração.
- `GIT_*` pode levar identidade, opções de rastreamento, diretórios alternativos de objetos ou comportamento de askpass.
- `CI_*` costuma misturar rótulos inofensivos, contexto do provedor e caminhos temporários.
- `LC_*` parece cosmético até um script depender de ordenação ou mensagens estáveis.
- `APP_*` cresce com a aplicação e raramente tem um único significado de segurança.

Não classifique um namespace inteiro como seguro. Classifique nomes individuais. `DEPLOY_REGION` pode ser necessária, enquanto `AWS_PROFILE` é contexto da estação. `BUILD_REF` pode ser necessária, enquanto `GIT_CONFIG_COUNT` muda a configuração do Git em execução. O prefixo comum organiza nomes, mas não cria uma fronteira de confiança.

Valores também revelam contexto sem serem credenciais. Um caminho expõe usuário e estrutura do repositório. Um perfil identifica conta ou ambiente. Um sinalizador de rastreamento pode fazer uma ferramenta remota gravar dados de comando em um log compartilhado. Um agente pode reutilizar esse contexto depois. Vazamento inclui influência e divulgação não planejadas, não apenas strings secretas.

## Uma exceção aparentemente segura pode crescer meses depois

A deriva de configuração costuma criar esse vazamento sem uma única mudança obviamente imprudente. Primeiro, alguém adiciona `SendEnv APP_*` porque um teste precisa de `APP_COLOR=0`. O servidor rejeita, então nada acontece. Mais tarde, um administrador adiciona `AcceptEnv APP_*` para outra equipe que precisa de duas opções no mesmo host compartilhado. Cada revisão vê apenas metade do acordo.

A conexão seguinte junta as duas regras adormecidas. O shell local agora contém `APP_CUSTOMER=acme-lab`, `APP_TRACE=1` e `APP_CONFIG=/Users/lee/work/private/config`. O cliente oferece tudo, o daemon aceita tudo e um invólucro executa `env` após uma falha, gravando a saída em um log legível pelo grupo. Nenhum token foi copiado, mas identidade local, cliente, caminho e rastreamento cruzaram a fronteira sem aprovação.

Remover o curinga do servidor corrige sessões futuras, mas não apaga o log antigo nem mostra quais comandos observaram os valores. Trate a descoberta como um incidente pequeno:

1. Interrompa a aceitação ampla e valide a configuração do daemon.
2. Identifique contas, clientes e período em que os dois padrões coexistiram.
3. Pesquise os nomes expostos nos logs autorizados, nunca seus valores sensíveis.
4. Determine se cada valor mudou o comportamento ou divulgou contexto a outro usuário.
5. Substitua o curinga por nomes exatos e acrescente iscas para as classes rejeitadas.

Por isso, “o servidor rejeita agora” não defende uma regra ampla do cliente. Configuração adormecida não tem dono no momento em que desperta. Remova ofertas desnecessárias do cliente mesmo com servidor rígido e permissões desnecessárias do servidor mesmo com clientes cuidadosos.

A deriva inversa também acontece. Um servidor aceita `LC_*` há anos para usuários interativos; depois, uma atualização da imagem instala uma configuração que a envia. A implantação herda `LC_COLLATE` escolhido por um desenvolvedor e produz outra ordem de arquivos. Talvez não haja divulgação, mas o mesmo acordo causou uma falha de integridade. Revise confidencialidade e comportamento juntos.

## Inspecione a configuração que o SSH realmente usará

Leia a configuração avaliada antes de editar um arquivo. `ssh -G` aplica blocos `Host` e `Match`, inclusões e arquivos de usuário e sistema, e então imprime o resultado para o destino. O arquivo que parece autoritativo pode perder para um valor anterior ou receber entradas de uma inclusão.

Execute a partir da mesma conta e do mesmo contexto do fluxo:

```sh
ssh -G deploy-prod |
  awk '$1 == "sendenv" || $1 == "setenv" { print }'
```

Um resultado inseguro representativo tem esta forma:

```text
sendenv LANG
sendenv LC_*
sendenv AWS_*
setenv WORKFLOW_KIND=deploy
```

`ssh -G` não imprime os valores atuais selecionados por `SendEnv`; ele mostra as regras efetivas. Isso evita despejar credenciais durante a revisão. Ainda é preciso comparar nomes e padrões com o ambiente do processo que realmente inicia o SSH.

Capture apenas os nomes:

```sh
env | sed 's/=.*//' | LC_ALL=C sort > local-env.names
grep -E '^(LANG|LC_|AWS_|WORKFLOW_)' local-env.names
```

A expressão regular acima é uma ajuda de inspeção, não uma política reutilizável. Adapte-a a todos os padrões de `ssh -G`. Se um supervisor, IDE, agente ou tarefa agendada inicia o SSH, examine o ambiente desse processo, não seu shell interativo. A herança segue a árvore de processos.

Revise o servidor separadamente. Em um servidor OpenSSH, `sshd -T` valida e imprime as opções efetivas. Forneça critérios de conexão quando blocos `Match` afetarem a conta:

```sh
sudo sshd -T \
  -C user=deploybot,host=deploy.example,addr=192.0.2.44 |
  grep '^acceptenv'
```

Use usuário, host e endereço reais. Mantenha o endereço de documentação apenas em exemplos. Execute também `sudo sshd -t` antes de recarregar o daemon; endurecer o ambiente não justifica interromper acesso remoto.

## Um arquivo de cliente dedicado deixa a lista legível

Para automação, a configuração mais confiável é um arquivo SSH pequeno passado com `-F`. O manual afirma que um arquivo explícito substitui o arquivo normal do usuário e faz o arquivo global do sistema ser ignorado. Assim, uma atualização da estação ou da frota não adiciona curingas de localidade ao fluxo.

```sshconfig
Host deploy-prod
    HostName deploy.example
    User deploybot
    IdentityFile ~/.ssh/deploy_ed25519
    IdentitiesOnly yes
    RequestTTY no
    SendEnv DEPLOY_REGION
    SendEnv BUILD_REF
    SetEnv WORKFLOW_KIND=deploy
```

Invoque-o explicitamente:

```sh
ssh -F ./deploy-ssh.conf deploy-prod /usr/local/bin/release
```

`SendEnv` lê o valor do ambiente do processo `ssh` local. Use-o para um valor que muda legitimamente a cada execução. `SetEnv` do cliente coloca um valor literal na configuração; use-o para um rótulo constante e não secreto, não para uma credencial ou um valor que alguém esquecerá de atualizar.

O OpenSSH também permite que um padrão `SendEnv` prefixado por `-` remova nomes escolhidos anteriormente:

```sshconfig
Host deploy-prod
    SendEnv -*
    SendEnv DEPLOY_REGION BUILD_REF
```

A limpeza afeta as seleções acumuladas até aquele ponto. Uma inclusão posterior pode adicionar os nomes outra vez. Por isso, não uso subtração como limite final. Um arquivo dedicado com `-F` tem menos peças e pode ser entendido em uma tela.

Não coloque credenciais nesse contrato. Um token encaminhado vira estado comum do processo remoto e pode chegar a filhos, rastreamento, relatórios de falha, inspeção de `/proc` quando permitida ou uma execução acidental de `env`. SSH protege o transporte, não a vida do valor depois da entrega.

## O servidor deve aceitar nomes exatos para uma conta limitada

O servidor é o último ponto capaz de rejeitar uma variável antes do início da sessão. O OpenSSH não aceita nenhuma por padrão, além do caso de `TERM` com pseudoterminal. Preserve esse padrão global e acrescente nomes exatos apenas para a conta do fluxo.

```sshdconfig
Match User deploybot
    AcceptEnv DEPLOY_REGION
    AcceptEnv BUILD_REF
    AcceptEnv WORKFLOW_KIND
```

Não use `AcceptEnv APP_*`, `AcceptEnv AWS_*` ou um curinga isolado. O manual de `sshd_config` avisa que algumas variáveis podem contornar ambientes restritos. Um curinga ainda concede nomes futuros sem nova mudança no servidor.

Um `AcceptEnv` global amplo enfraquece todas as contas. Um bloco `Match User` estreito não apaga nomes já aceitos globalmente; as entradas podem se acumular. Se usuários interativos precisam de localidade e a conta de implantação não, limite a exceção ao grupo certo e verifique cada conexão com `sshd -T -C`.

`SetEnv` do servidor é outro controle. Ele define constantes em sessões filhas e substitui valores padrão ou fornecidos por `AcceptEnv` e `PermitUserEnvironment`. Se o servidor é dono da constante, defina-a nele em vez de aceitá-la do cliente.

`PermitUserEnvironment` controla `~/.ssh/environment` e opções `environment=` em authorized_keys, e o OpenSSH o desativa por padrão. Uma auditoria limitada a `AcceptEnv` pode ignorar essa origem. Mantenha-o desligado sem uma necessidade específica e inclua seus padrões no mesmo contrato caso seja ativado.

Uma lista exata de nomes controla nomes, não valores. Um `DEPLOY_REGION` permitido pode conter espaços, metacaracteres, nova linha ou região proibida. O protocolo leva o valor como dados, mas `eval`, expansão sem aspas, configuração gerada ou string de comando pode transformá-lo em sintaxe.

Valide cada valor no ponto de entrada remoto:

```sh
case ${DEPLOY_REGION-} in
    us-east-1|us-west-2) ;;
    *)
        printf 'invalid DEPLOY_REGION\n' >&2
        exit 64
        ;;
esac

case ${BUILD_REF-} in
    [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*)
        ;;
    *)
        printf 'invalid BUILD_REF\n' >&2
        exit 64
        ;;
esac
```

Use validação correspondente ao contrato real. O padrão curto apenas ilustra a forma; se uma publicação exige um identificador completo, imponha o comprimento exato e confirme que o repositório o contém. Não aceite um validador que apenas rejeita a string vazia.

Depois de editar, examine o resultado efetivo:

```sh
sudo sshd -t
sudo sshd -T \
  -C user=deploybot,host=deploy.example,addr=192.0.2.44 |
  grep -E '^(acceptenv|permituserenvironment|setenv)'
```

Guarde essa saída com a revisão. Ela registra o que o daemon interpretou para a conexão testada, algo mais útil do que uma captura de um fragmento em um diretório cheio de inclusões.

## Comprove valores obrigatórios e iscas rejeitadas no remoto

Um teste útil verifica casos positivos e negativos dentro do processo remoto. Defina marcadores inconfundíveis para cada valor obrigatório, iscas que coincidiriam com os antigos curingas, use uma sessão não interativa com o arquivo dedicado e faça o shell falhar se o contrato estiver errado.

Esta sequência espera apenas `DEPLOY_REGION`, `BUILD_REF` e o valor fixo `WORKFLOW_KIND` do cliente:

```sh
export DEPLOY_REGION='env-audit-region'
export BUILD_REF='env-audit-ref'
export AWS_PROFILE='must-not-cross'
export LC_AUDIT_CANARY='must-not-cross'
export APP_PRIVATE_PATH='must-not-cross'

ssh -F ./deploy-ssh.conf -T deploy-prod 'sh -s' <<'REMOTE'
set -eu

test "$(printenv DEPLOY_REGION)" = 'env-audit-region'
test "$(printenv BUILD_REF)" = 'env-audit-ref'
test "$(printenv WORKFLOW_KIND)" = 'deploy'

for name in AWS_PROFILE LC_AUDIT_CANARY APP_PRIVATE_PATH; do
    if printenv "$name" >/dev/null 2>&1; then
        printf 'unexpected forwarded variable: %s\n' "$name" >&2
        exit 1
    fi
done

printf '%s\n' 'SSH environment contract passed'
REMOTE
```

A saída esperada é uma linha:

```text
SSH environment contract passed
```

Faça um segundo teste negativo removendo um valor local obrigatório. `SendEnv` não envia um nome ausente, então a verificação remota deve falhar:

```sh
unset BUILD_REF
if ssh -F ./deploy-ssh.conf -T deploy-prod 'test -n "$BUILD_REF"'; then
    printf '%s\n' 'test failed: BUILD_REF appeared unexpectedly' >&2
    exit 1
fi
```

Se o fluxo deve parar sem `BUILD_REF`, faça o iniciador local falhar com `${BUILD_REF:?BUILD_REF is required}`. A verificação remota continua necessária porque valida o transporte e detecta uma recusa do servidor.

As iscas devem cobrir cada curinga ou classe duvidosa encontrada e ter valores obviamente falsos. Nunca use uma credencial real: uma falha pode gravá-la em logs enquanto comprova o problema.

Uma execução bem-sucedida não prova que nenhuma variável imaginável possa atravessar. Combine-a com duas verificações estáticas: `ssh -G` deve conter somente nomes exatos em `SendEnv`, e `sshd -T -C` apenas os `AcceptEnv` previstos. Se aparecer `*` ou `?`, nenhum teste consegue enumerar nomes futuros.

Execute o teste a partir de cada iniciador distinto. Shell de desenvolvedor, executor de CI, tarefa do editor e agente autônomo podem chamar o mesmo binário com ambientes e caminhos diferentes. Grave o caminho `-F` no próprio iniciador, não em um alias interativo.

Também há diferença entre observar o ambiente final e provar a contribuição do SSH. Se `/etc/profile` já define `DEPLOY_REGION=us-east-1`, a verificação pode passar mesmo com a recusa do servidor. Use marcadores únicos que não existam na base remota e execute o caso de entrada ausente.

O teste não afirma que o ambiente remoto contém só três variáveis. Uma sessão normal também recebe `HOME`, `USER`, `SHELL`, `PATH` e metadados SSH, e outras camadas podem adicionar mais. A verificação prova que a parte contribuída pelo cliente corresponde ao contrato. Audite a base do servidor separadamente.

## A construção do comando pode contornar SendEnv por completo

Limpar `SendEnv` não impede o shell local de expandir variáveis dentro da string do comando remoto. Esse é outro caminho de dados:

```sh
ssh deploy-prod "release '$TENANT' '$TOKEN'"
```

O shell substitui os dois valores antes de `ssh` iniciar. Eles viajam na solicitação criptografada de comando, não em uma solicitação `env`, então `AcceptEnv` não pode recusá-los. Também podem aparecer em processos, histórico, logs de CI ou mensagens de erro.

Este prefixo altera o ambiente do processo SSH local, mas só atravessa se a configuração selecionar o nome:

```sh
DEPLOY_REGION=west ssh -F ./deploy-ssh.conf deploy-prod /usr/local/bin/release
```

Equipes confundem esses casos. Estar no ambiente durante a execução não prova encaminhamento, enquanto desativar `SendEnv` não prova que um invólucro deixou de interpolar o valor em argumentos ou entrada padrão. Siga o canal real.

Código de inicialização remoto cria outras origens. Shell, `/etc/environment`, PAM, comando forçado, `sudo`, gerenciador de serviço ou contêiner pode remover, substituir ou adicionar valores depois que o SSH os aceita. Quando um marcador faltar, use `ssh -vvv`, logs permitidos e um comando mínimo antes do invólucro. Não amplie `AcceptEnv` imediatamente.

`sudo` merece uma verificação própria. A política pode limpar ou preservar o ambiente do filho privilegiado, mas o valor já chegou à sessão sem privilégios, onde scripts e invólucros podem vê-lo. Configure sudoers para a necessidade do comando e mantenha a lista SSH estreita antes dessa camada.

O host remoto tampouco é um tubo passivo. Quem controla a conta geralmente consegue imprimir seu ambiente, e o administrador controla a máquina. Se o host nunca deve saber um valor, não o envie por SSH em forma alguma.

Evite despejos completos de `env` em logs comuns. Durante uma auditoria autorizada, capture nomes primeiro e revele valores apenas para marcadores falsos.

## SSH executado por agentes precisa de uma fronteira de processo menor

Um agente autônomo costuma herdar o ambiente do terminal, editor ou orquestrador que o iniciou. Esse ambiente foi montado para uma pessoa trabalhando em várias contas e repositórios, não para uma única ação. Encaminhar seus namespaces fornece contexto extra ao agente e entradas não revisadas às ferramentas remotas.

Inicie o fluxo com ambiente local e arquivo SSH explícitos. Um invólucro mínimo pode exigir os dois valores variáveis, descartar o estado herdado e restaurar apenas o que o cliente precisa:

```sh
#!/bin/sh
set -eu
: "${DEPLOY_REGION:?DEPLOY_REGION is required}"
: "${BUILD_REF:?BUILD_REF is required}"

exec env -i \
  HOME="$HOME" \
  PATH='/usr/bin:/bin:/usr/sbin:/sbin' \
  DEPLOY_REGION="$DEPLOY_REGION" \
  BUILD_REF="$BUILD_REF" \
  ssh -F ./deploy-ssh.conf deploy-prod /usr/local/bin/release
```

Ajuste `PATH` e entradas ao sistema e mantenha a lista explícita. `HOME` aparece porque o OpenSSH pode precisar de known_hosts e do arquivo de identidade. Se um executor gerenciado fornecer esses caminhos por outras opções, remova também `HOME`.

Para SSH operado por agentes em um Mac, Sallyport pode manter a chave SSH em seu cofre criptografado e executar por `sp-ssh`, sem entregar a credencial ao agente. Isso muda a custódia, mas não substitui um contrato exato de ambiente ao redor do comando autorizado.

Não encaminhe credenciais locais de nuvem como atalho. Dê à carga remota sua própria identidade limitada ou encaminhe a ação por um gateway que guarda a credencial. Copiar a credencial ambiente do operador une duas fronteiras de confiança e dificulta a revogação.

## Mantenha o contrato executável quando a configuração mudar

As regras derivam porque pacotes adicionam padrões, administradores consolidam fragmentos e fluxos ganham entradas. Uma regra em prosa não detecta essa mudança. Revise juntos o arquivo dedicado, a conta do servidor e a verificação com iscas.

Execute a verificação após mudanças em:

- pacotes do cliente SSH ou do sistema operacional;
- inclusões de configuração de usuário e sistema;
- `sshd_config`, PAM, arquivos de inicialização ou comandos forçados;
- iniciador do agente, CI, gerenciador de serviço ou imagem;
- lista de entradas obrigatórias do fluxo.

Trate uma variável nova como mudança de interface. Documente por que o comando precisa dela, quem possui o valor, se contém contexto sensível e qual teste prova sua ausência em outros lugares. Sem respostas, passe uma entrada validada ou redesenhe a ação em vez de ampliar um curinga.

Deixe a falha evidente. Uma implantação que recorre silenciosamente a um valor remoto após a quebra do encaminhamento é mais difícil de diagnosticar e mais fácil de usar de forma errada. A entrada obrigatória deve falhar no iniciador local e no ponto remoto.

Por fim, revise a conta pelo lado remoto. Uma lista perfeita não protege uma conta capaz de executar shells arbitrários, ler ambientes de outros usuários ou reescrever arquivos de inicialização. Torne esse canal exato, comprove seu comportamento e limite a autoridade do comando.
