O encaminhamento de ambiente SSH pode vazar contexto local?
O encaminhamento de ambiente SSH pode expor contexto local a comandos remotos. Audite SendEnv e AcceptEnv e teste uma lista exata.

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
SendEnvamplo fica inativo diante de um servidor que não aceita nada, mas ganha efeito assim que um administrador ampliaAcceptEnv. - Um
AcceptEnvamplo 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:
- Interrompa a aceitação ampla e valide a configuração do daemon.
- Identifique contas, clientes e período em que os dois padrões coexistiram.
- Pesquise os nomes expostos nos logs autorizados, nunca seus valores sensíveis.
- Determine se cada valor mudou o comportamento ou divulgou contexto a outro usuário.
- 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:
ssh -G deploy-prod |
awk '$1 == "sendenv" || $1 == "setenv" { print }'
Um resultado inseguro representativo tem esta forma:
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:
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:
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.
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:
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:
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.
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:
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:
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:
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:
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:
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:
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:
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:
#!/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.
FAQ
O SSH envia todas as variáveis locais por padrão?
Não. O OpenSSH envia nomes escolhidos por SendEnv e valores literais de SetEnv, e o servidor normalmente precisa aceitá-los com AcceptEnv. Um pseudoterminal trata TERM de forma especial.
Qual é a diferença entre SendEnv e SetEnv?
SendEnv lê o valor atual do processo SSH local. SetEnv do cliente define um par literal, adequado a um rótulo fixo e não secreto, não a uma entrada por execução.
AcceptEnv pode receber uma variável que o cliente não enviou?
Não. Ele permite uma solicitação, mas não cria o valor. O mesmo nome pode vir do servidor, PAM, shell ou invólucro, então identifique a origem antes de alterar o SSH.
É seguro encaminhar LANG e LC_*?
Pode ser aceitável para contas interativas, mas é amplo demais para a maioria das automações. A localidade muda ordenação, análise e mensagens.
Posso remover regras SendEnv herdadas?
Um padrão prefixado por - apaga seleções anteriores. Regras posteriores podem adicioná-las novamente, por isso um arquivo dedicado com ssh -F é mais fácil de auditar.
Como vejo as regras SendEnv de um host?
Execute ssh -G host na conta do fluxo e filtre sendenv e setenv. A saída mostra as regras avaliadas, mas não os valores atuais.
Por que uma variável encaminhada não aparece no remoto?
Ela pode estar ausente localmente, não selecionada, recusada ou removida por outra camada. Teste essas barreiras nessa ordem com um marcador falso.
ssh -T interrompe todo encaminhamento de ambiente?
Não. -T desativa o pseudoterminal e o caminho especial de TERM. Outros nomes selecionados e aceitos ainda podem atravessar.
Variáveis de ambiente são seguras para credenciais SSH?
São um transporte ruim. Depois de aceita, a credencial vira estado do processo remoto e pode chegar a filhos, logs e diagnósticos. Use identidade remota ou gateway.
Com que frequência devo testar o ambiente SSH?
Depois de mudanças em pacotes, configurações, daemon, iniciadores, imagens ou entradas. Mantenha a verificação com iscas na revisão normal.