Identidade do executável do agente após uma troca de link simbólico
Teste a identidade do executável de um agente contra trocas de links simbólicos no macOS, vincule aprovações ao código em execução e mantenha aliases de caminhos fora das decisões de confiança.

Um nome de arquivo é um localizador, não uma identidade. A diferença parece preciosismo até que um agente receba aprovação enquanto executa como ~/bin/agent, alguém troque esse caminho por outro programa e o gateway decida que o próximo solicitante é confiável porque o rótulo ainda parece familiar.
Os links simbólicos facilitam reproduzir o erro, mas não são sua causa. Qualquer indireção mutável pode criá-lo: um wrapper de shell, uma entrada no PATH, um binário copiado, um checkout de desenvolvimento ou um inicializador que resolve um caminho a cada solicitação. Um gateway que aprova um nome de caminho aprovou um objeto que muitas vezes pode ser substituído por um usuário sem privilégios.
No macOS, a unidade útil é o processo em execução e sua identidade de código. Os Apple Code Signing Services podem descrever e validar o código associado a um processo. Um caminho pode aparecer no registro para diagnóstico, mas não pode decidir a autorização.
Um link simbólico é um serviço de nomes, não o programa
Um link simbólico armazena uma string que o kernel resolve quando um programa a abre ou executa. Substituir essa string muda o que uma chamada futura de exec encontrará. Isso não reescreve a imagem executável de um processo que já foi iniciado.
Esse detalhe dá uma falsa sensação de segurança. As pessoas testam uma troca de link simbólico, veem o processo original continuar funcionando e concluem que o ataque é inofensivo. O teste só mostrou o comportamento normal de um processo. Ele não testou a decisão importante: se o gateway reconhece um processo posterior iniciado pelo alias como o processo que havia aprovado antes.
Considere um agente de programação iniciado por este caminho:
~/bin/build-agent -> ~/work/approved-agent
O gateway recebe a primeira solicitação, vê ~/bin/build-agent e pede permissão ao operador. Depois, um invasor altera o link:
~/bin/build-agent -> ~/Downloads/replacement-agent
Se o processo aprovado continuar fazendo solicitações, o gateway pode corretamente manter essa sessão até o processo terminar. Esse não é o erro. O erro aparece quando outro processo é iniciado pelo mesmo nome e o gateway diz: «Eu já aprovei ~/bin/build-agent.»
O segundo processo é um novo sujeito de segurança. Ele precisa de uma nova inspeção e, a menos que corresponda a uma regra de atualização definida intencionalmente, de uma nova aprovação.
O mesmo erro aparece sem links simbólicos. Um gateway que executa codesign contra o caminho fornecido pelo cliente pode inspecionar um arquivo enquanto aceita uma solicitação de outro processo. O cliente pode explorar uma condição de corrida alterando o caminho depois da inspeção ou simplesmente apontar o gateway para um alias estável que se refere a alvos mutáveis. Um caminho com boa aparência no cartão de aprovação não corrige esse projeto.
A Apple trata os requisitos de código como restrições de identidade, não como nomes de arquivo. A documentação diz que um designated requirement contém os critérios usados para decidir se um código é o mesmo código visto antes, normalmente derivado da autoridade de assinatura e do identificador incorporado quando o desenvolvedor não declarou um. Essa é a direção certa, com uma ressalva importante: um requisito de código descreve a identidade do código, não uma sessão de processo ativa.
O processo em execução é o objeto que recebeu a aprovação
O gateway deve tomar sua decisão de autorização com base em uma referência a um processo ativo obtida do sistema operacional. Depois, deve manter essa decisão ligada à execução do processo, não a um caminho declarado pelo cliente, a um nome de executável ou a uma variável de ambiente reutilizável.
No macOS, uma boa inspeção tem duas tarefas:
- Obter as informações de assinatura do processo que realmente fez a chamada.
- Validar esse processo antes de usar as informações de identidade como evidência de autorização.
São tarefas separadas. SecCodeCopyDesignatedRequirement pode devolver um designated requirement para código assinado, mas a Apple observa explicitamente que a chamada não valida a assinatura. Código modificado depois da assinatura ou assinado de forma incorreta ainda pode produzir informações incompletas ou enganosas. O gateway também precisa verificar a validade.
O registro do processo deve conter evidências suficientes para explicar a decisão mais tarde:
- identificador do processo ou, de preferência, uma credencial de processo emitida pelo sistema operacional que resista à reutilização de PIDs
- caminho do executável observado no momento da inspeção, mantido apenas como contexto
- identificador de assinatura e identificador da equipe, quando disponíveis
- autoridade de assinatura e cadeia de certificados, quando aplicável
- designated requirement
- cdhash ou o conjunto de valores de cdhash compatíveis
- resultado da validação da assinatura e horário da inspeção
Não confunda essa lista com uma linguagem de políticas. O gateway precisa de uma resposta objetiva: qual processo em execução está fazendo o pedido e um humano aprovou essa execução? Alguns fatos de identidade permitem responder. Um conjunto de regras de caminho, não.
A parte difícil é lidar com atualizações. Um designated requirement costuma permanecer estável entre versões legítimas. Por isso o macOS o usa para manter a continuidade quando um usuário autoriza um app a acessar um serviço protegido. A Apple Technical Note TN3127 traz o exemplo conhecido de uma atualização de app que volta a usar o microfone: o macOS compara a nova versão com o designated requirement registrado.
Esse comportamento faz sentido para uma permissão duradoura do sistema operacional. Ele é amplo demais se você reutilizar silenciosamente uma aprovação humana antiga para uma nova execução autônoma de agente. O gateway pode mostrar a autoridade de assinatura para ajudar o operador a reconhecer o programa e ainda pedir aprovação novamente quando um novo processo for iniciado. O signatário estabelece a origem. A fronteira do processo estabelece a duração do consentimento.
Verificações estáticas de caminho deixam uma corrida que não pode ser ignorada
Uma verificação estática responde a uma pergunta estática: «Que assinatura este arquivo tem neste caminho agora?» Sozinha, ela não responde: «Qual código emitiu esta solicitação?»
Essa diferença importa mesmo quando a verificação está correta do ponto de vista técnico. Suponha que um gateway receba uma solicitação cujo caminho de executável seja /Users/dev/bin/agent. Ele executa:
codesign --verify --strict --verbose=2 /Users/dev/bin/agent
codesign -d -r- /Users/dev/bin/agent
Os dois comandos podem informar uma assinatura válida e um designated requirement. O gateway então armazena o caminho como identidade aprovada. Entre a verificação e uma solicitação posterior, um invasor pode redirecionar agent para outro arquivo. Na próxima solicitação, o gateway pode executar os comandos novamente e obter informações sobre o substituto. Nenhum dos comandos prova uma ligação com o processo que fez a solicitação.
Há uma segunda corrida que as equipes não percebem. Um helper pode inspecionar um caminho antes de iniciar um agente e depois receber uma conexão de um processo filho. A verificação descreve o arquivo pai no momento da inicialização. A conexão descreve um processo no momento da solicitação. Se o helper não vincular esses dois eventos usando uma credencial do sistema operacional, criou uma suposição e a chamou de origem.
As APIs de assinatura de código da Apple expõem categorias separadas de informação por esse motivo. kSecCSSigningInformation solicita dados de certificados e CMS, kSecCSRequirementInformation solicita requisitos e kSecCSDynamicInformation solicita informações dinâmicas de validade do código em execução. A superfície da API não transforma essas partes em um projeto completo de gateway, mas deixa a distinção clara: a inspeção estática da assinatura e o estado do código em execução são entradas diferentes.
O nome do arquivo ainda pertence à tela de aprovação. Os operadores precisam saber que uma solicitação veio de um checkout em ~/work/demo, e não de uma ferramenta instalada em /Applications. Trate isso como contexto de interface. O registro de autorização deve continuar ligado ao solicitante identificado pelo sistema operacional.
Reproduza a troca sem tocar em um agente real
Você pode demonstrar o problema do caminho com dois binários pequenos em um diretório temporário. Esse teste não exige um token de produção, uma chave SSH ou um bundle de aplicativo modificado.
Crie um espaço de trabalho e compile dois programas que exibam marcadores diferentes. O código permanece intencionalmente simples, porque o comportamento em teste é a substituição, não a lógica do programa.
work="$(mktemp -d /tmp/agent-identity.XXXXXX)"
cd "$work"
cat > approved.c <<'EOF'
#include <cstdio.h>
#include <unistd.h>
int main(void) {
printf("approved process pid=%d\n", getpid());
fflush(stdout);
sleep(60);
return 0;
}
EOF
cat > replacement.c <<'EOF'
#include <cstdio.h>
#include <unistd.h>
int main(void) {
printf("replacement process pid=%d\n", getpid());
fflush(stdout);
sleep(60);
return 0;
}
EOF
clang approved.c -o approved-agent
clang replacement.c -o replacement-agent
codesign --force --sign - --identifier com.example.approved approved-agent
codesign --force --sign - --identifier com.example.replacement replacement-agent
ln -s "$work/approved-agent" agent
Uma assinatura ad hoc basta para um teste local de funcionamento, mas não tem cadeia de certificados. A Apple documenta que código assinado ad hoc não tem certificados e possui dados CMS vazios. Não use uma assinatura ad hoc para simular um signatário de distribuição nem para decidir o que seu gateway de produção aceita.
Registre para onde o alias aponta, inspecione sua assinatura e inicie o primeiro processo:
printf 'alias before: %s\n' "$(readlink agent)"
codesign -d -r- agent 2>&1 | sed -n '1,8p'
./agent &
first_pid=$!
printf 'first pid: %s\n' "$first_pid"
ps -p "$first_pid" -o pid=,comm=,args=
O formato da saída deve ser parecido com este:
alias before: /tmp/agent-identity.xxxxxx/approved-agent
Executable=/tmp/agent-identity.xxxxxx/agent
designated => identifier "com.example.approved"
approved process pid=48291
first pid: 48291
48291 ... ./agent
Agora substitua o link enquanto o primeiro processo dorme:
rm agent
ln -s "$work/replacement-agent" agent
printf 'alias after: %s\n' "$(readlink agent)"
codesign -d -r- agent 2>&1 | sed -n '1,8p'
ps -p "$first_pid" -o pid=,comm=,args=
Você verá com.example.replacement ao inspecionar agent, enquanto first_pid continuará ativo. Inicie o alias uma segunda vez:
./agent &
second_pid=$!
printf 'second pid: %s\n' "$second_pid"
wait "$first_pid" "$second_pid"
O segundo processo exibirá replacement process. Os dois PIDs executaram imagens de programa diferentes, embora ambos tenham sido chamados como ./agent.
Esse é o resultado que deve mudar seu plano de testes do gateway. Um teste que apenas pergunta se o PID antigo continuou executando quase não provou nada. O teste útil pergunta se o gateway trata o segundo PID como um novo solicitante e mostra sua identidade observada antes de permitir o uso de uma ação aprovada.
Um teste de aprovação útil tem três execuções distintas
Não se contente com um caso feliz de aprovação e um comando de link simbólico. Você precisa de três execuções, porque cada uma prova uma propriedade diferente.
Primeiro, inicie o alvo aprovado pelo alias e faça uma solicitação de ação. O gateway deve criar um registro de sessão e mostrar uma aprovação que identifique o solicitante real em termos que o operador possa avaliar. Registre o identificador da sessão, o PID, o caminho observado e os fatos de assinatura do código.
Segundo, altere o alias, mas mantenha o primeiro processo ativo. Faça com que o primeiro processo envie outra solicitação. Se seu modelo de sessão aprova uma execução até que ela termine, o gateway pode permitir essa solicitação. A imagem do executável não mudou. Não classifique incorretamente esse resultado esperado como uma vulnerabilidade.
Terceiro, inicie um novo processo pelo alias substituído e faça a mesma solicitação. O gateway não deve herdar a aprovação do primeiro processo apenas porque o nome do executável, a linha de comando, o diretório de trabalho ou a ação solicitada coincide. Ele deve criar uma sessão separada e exigir o fluxo normal de autorização.
Use uma tabela de resultados durante o teste:
| Execução | Alvo do alias no início | Resultado esperado |
|---|---|---|
| Primeiro processo | approved-agent | Nova aprovação, depois permitido nessa execução |
| Primeiro processo após a troca | replacement-agent no disco, mas a imagem antiga continua na memória | O comportamento da sessão existente continua até o encerramento |
| Segundo processo | replacement-agent | Nova aprovação ou negação, nunca a aprovação herdada |
Há mais um caso que vale testar. Troque o alias de volta para o binário original e inicie um terceiro processo. O gateway ainda deve criar uma nova sessão. A mesma identidade de código não significa o mesmo processo. Se seu produto oferecer deliberadamente uma relação de confiança persistente para um editor assinado, transforme isso em uma decisão separada e explícita do operador. Não a esconda em um recurso de aprovação por processo.
A autorização por sessão do Sallyport vem ativada por padrão, e seu cartão de aprovação começa pela autoridade de assinatura do processo, em vez de um nome de arquivo mutável. Isso oferece ao operador um fato mais sólido para avaliar quando um novo processo de agente pede para agir.
Signatário, designated requirement e cdhash respondem a perguntas diferentes
As equipes costumam resumir três conceitos distintos na frase «assinado pelo mesmo app». Esse atalho produz fadiga de aprovação ou permissões que duram tempo demais.
Uma autoridade de assinatura responde quem assinou o código. Para software distribuído, ela pode incluir uma cadeia de certificados e um identificador de equipe. Isso ajuda o operador a distinguir um fornecedor conhecido de um executável aleatório. Ela não identifica uma versão exata.
Um designated requirement responde se o macOS deve considerar um código como a mesma identidade entre atualizações. A Apple diz que todo código assinado tem um designated requirement, explícito ou sintetizado. O padrão normalmente incorpora a autoridade de assinatura e o identificador incorporado. Isso o torna adequado para continuidade, mas pode aceitar versões posteriores que você não inspecionou pessoalmente.
Um cdhash identifica um CodeDirectory assinado específico. Para fins de autorização, ele se aproxima da impressão digital de uma versão. É uma excelente evidência de auditoria porque permite distinguir duas versões que compartilham signatário e identificador. Em geral, é uma regra permanente ruim para ferramentas de desenvolvimento, porque atualizações comuns o alteram.
Use as evidências de acordo com a decisão:
- Use a credencial do processo em execução para vincular uma sessão a um solicitante.
- Mostre a autoridade de assinatura e o identificador na interface de aprovação para que o operador reconheça a origem.
- Registre o designated requirement e o cdhash para que uma investigação posterior distinga a continuidade do editor da versão exata.
- Peça aprovação novamente para um novo processo de agente, mesmo quando o signatário e o designated requirement coincidirem.
O último ponto é deliberadamente mais rigoroso que as permissões do macOS. Um gateway de agente pode emitir solicitações HTTP ou comandos SSH usando credenciais que nunca entram no processo do agente. Isso é uma fronteira de ação, não um pedido único de acesso ao microfone. Reutilizar um clique antigo para um processo futuro e não relacionado enfraquece o controle humano que justificou o gateway.
Não transforme um observador de arquivos em autorização
Uma resposta tentadora é observar o caminho do agente e revogar a permissão quando o arquivo mudar. Isso parece simples: salvar o inode original, assinar eventos do sistema de arquivos e invalidar a aprovação depois de uma gravação ou renomeação.
É a base errada.
Eventos do sistema de arquivos são telemetria útil, não prova da identidade do solicitante. Os caminhos podem ter vários nomes. Um binário pode ser copiado. Um processo pode ser iniciado a partir de um arquivo desvinculado. A entrega de eventos pode atrasar ou agrupar notificações. Um invasor não precisa vencer uma corrida dramática se seu projeto já autoriza o caminho em vez do processo.
Comparar inodes também não corrige o modelo. Isso pode ajudar a detectar uma substituição em um diretório, mas não diz nada útil sobre um processo iniciado por outro hard link ou por um alvo copiado. Também cria um comportamento frágil em fluxos normais de desenvolvimento, nos quais as ferramentas recompilam, renomeiam e substituem arquivos constantemente.
Trate as observações de arquivos como motivo para adicionar contexto útil à auditoria. Se o caminho atual apontar para um alvo diferente daquele observado no momento da aprovação, registre esse fato. Se o gateway identificar um novo processo solicitante, inspecione-o e aplique o fluxo normal de autorização. A fronteira do processo cuida da decisão de segurança sem confiar no observador como se ele fosse um monitor de referência.
Essa regra também evita um erro mais sutil: autorizar um wrapper porque sua assinatura parece familiar e ignorar o que ele inicia. Um wrapper assinado pode executar um filho sem assinatura, carregar um script local ou selecionar um alvo com base no ambiente. O gateway precisa identificar o processo que realmente se conecta e solicita a ação privilegiada. Se o wrapper é o solicitante, ele é o objeto aprovado. Se o filho é o solicitante, inspecione o filho.
Vincule a aprovação a uma credencial do sistema operacional e falhe de forma segura diante de ambiguidades
Uma implementação prática precisa de um identificador fornecido pelo sistema operacional para o solicitante. No macOS, isso normalmente significa obter um objeto de código convidado para o processo chamador por meio dos Code Signing Services, usando atributos do processo fornecidos pelo contexto de conexão confiável, não valores copiados da entrada do agente.
A sequência segura é:
- Aceite uma conexão e obtenha a identidade do solicitante pelo mecanismo de conexão.
- Resolva essa identidade para um objeto de código em execução.
- Faça a verificação da validade do código antes de ler os dados de identidade para fins de autorização.
- Leia a autoridade de assinatura, o identificador, o designated requirement e o cdhash desse objeto de código em execução.
- Crie um registro de sessão vinculado à credencial do solicitante e exija aprovação se não houver uma sessão aprovada.
- Verifique novamente, antes de cada ação, se a credencial ainda se refere ao mesmo processo ativo. Destrua a sessão quando o processo terminar ou o operador a revogar.
Os detalhes da primeira etapa dependem do transporte. Um socket Unix local, uma conexão XPC e um pipe de processo filho expõem credenciais diferentes. O fallback perigoso é o mesmo em todos os casos: aceitar um PID, caminho, identificador de bundle ou resumo de assinatura enviado pelo agente no corpo da solicitação. O agente controla esse conteúdo. Ele não prova nada.
A reutilização de PID merece atenção especial. Um PID isolado só é único enquanto o processo existe. Depois do encerramento, o sistema operacional pode atribuir o mesmo número a outro processo. Armazene uma credencial mais completa se a API oferecer uma. Se o transporte não puder fornecer uma vinculação durável ao solicitante, limite a sessão ao tempo de vida da conexão e autorize novamente após a reconexão. Falhar de forma segura é menos conveniente que aceitar um solicitante ambíguo, mas é justamente na ambiguidade que vivem os erros de troca de caminhos.
Mantenha os pedidos de aprovação honestos. Se o gateway encontrar uma assinatura ad hoc, diga isso. Se não existir uma assinatura válida, diga isso. Se o programa tiver um nome de exibição familiar, mas uma nova autoridade de assinatura, não esconda a autoridade atrás de um controle expansível. A primeira linha deve informar quem assinou o processo e qual ação ele quer executar.
O histórico de auditoria deve preservar a decisão, não apenas a solicitação
Um registro que diga POST /deploy foi bem-sucedido não consegue explicar um incidente de troca de link simbólico. Ele informa o que aconteceu, mas não por que o gateway permitiu que aquele solicitante fizesse isso.
Para cada sessão aprovada, armazene uma fotografia das evidências de identidade observadas no momento da aprovação. Para cada ação, armazene a referência da sessão e o resultado. O registro da ação não precisa repetir os dados do certificado, mas deve levar ao registro exato de aprovação que os continha.
Um registro compacto pode ter esta aparência:
{
"session": "6F2A...",
"caller": {
"process": "OS-issued caller credential",
"path_observed": "/private/tmp/demo/agent",
"signing_identifier": "com.example.approved",
"designated_requirement": "identifier com.example.approved",
"cdhash": "<observed digest>",
"validity": "valid"
},
"approval": "granted",
"action": "HTTP POST /deploy",
"result": "success"
}
O caminho continua sendo útil. Ele pode indicar que um processo foi iniciado em um diretório temporário ou por um alias de shell. Mas não pode bastar para associar uma solicitação posterior a essa sessão.
O Sallyport mantém um diário de Sessions para as execuções dos agentes e um diário de Activity para chamadas individuais, projetados a partir de um único registro de auditoria criptografado e encadeado por hashes. Seu comando offline sp audit verify verifica a cadeia sem exigir a chave do cofre. Esse projeto facilita revisar um teste de troca de caminho, porque o evento de aprovação e a ação posterior são fatos separados, e não uma única mensagem vaga de sucesso.
Execute o teste de link simbólico antes de escrever uma regra de aprovação que você não consiga explicar depois. Se um substituto recém-iniciado puder usar a aprovação do primeiro processo, pare de tratar o nome do executável como um simples campo de conveniência. Ele entrou na decisão de confiança.
FAQ
Uma assinatura de código torna seguro confiar em um caminho de executável?
Um executável assinado ainda pode ser acessado por um link simbólico, um hard link, um caminho copiado ou um script de inicialização. O nome apenas informa onde um processo foi encontrado. Ele não prova qual código está sendo executado nem quem o assinou.
Substituir um link simbólico pode alterar um processo que já está em execução?
Não. Um processo em execução continua usando a imagem do programa carregada por exec, enquanto a substituição de um link simbólico afeta a resolução de caminhos posteriores. A falha de segurança começa quando um gateway consulta novamente o caminho mutável e trata essa consulta como evidência sobre o processo já aprovado.
Como um app do macOS deve identificar um processo de agente?
Use o processo em execução como objeto da inspeção, não o caminho fornecido pelo cliente. No macOS, colete as informações de assinatura do processo por meio dos Code Signing Services e valide-as antes de conceder uma sessão.
O que é um designated requirement no macOS?
Um designated requirement identifica uma família de código que o macOS considera pertencente ao mesmo editor e à mesma identidade de aplicativo entre atualizações legítimas. Ele não identifica exclusivamente uma versão exata, portanto deve ser usado para continuidade, não como o único registro do que foi executado.
Devo aprovar um agente por cdhash?
Um cdhash identifica um diretório de código assinado específico e é uma evidência útil de uma versão concreta. Ele é mais restritivo que um signatário ou um designated requirement, mas fixá-lo indefinidamente fará as atualizações normais falharem até que alguém aprove intencionalmente a nova versão.
A aprovação de um agente deve sobreviver a uma reinicialização?
Não. A aprovação deve pertencer a uma execução do processo, não a um nome de arquivo nem a uma identidade permanente de desenvolvedor. Um novo processo deve criar um novo evento de aprovação, mesmo que venha do mesmo aplicativo assinado.
Por que a aprovação baseada em caminhos é perigosa para agentes de IA?
O padrão perigoso é aprovar um helper uma vez e tratar como aprovados todos os processos futuros iniciados por esse caminho. Isso é atraente para invasores porque eles podem substituir um link simbólico, um wrapper ou um binário depois da decisão inicial sem alterar o nome de comando conhecido.
O que um registro de auditoria de ações do agente deve guardar?
Um registro de auditoria deve incluir o identificador do processo ou o audit token, o caminho do executável observado como contexto, a autoridade de assinatura, o identificador, o designated requirement, o cdhash, o resultado da validação e o horário de início da sessão. O caminho ajuda no diagnóstico, mas não pode ser a âncora de confiança.
O que deve acontecer quando o executável substituto não tem assinatura ou é assinado de forma diferente?
Um substituto sem assinatura deve falhar se sua regra de aprovação exigir uma assinatura válida. Um substituto assinado por outra autoridade deve exigir uma nova aprovação. Uma atualização legítima assinada com a identidade que você decidiu permitir deve seguir a política de atualização escolhida.
É seguro testar uma troca de link simbólico no meu Mac de trabalho?
Faça a troca em um diretório temporário com binários compilados por você e aponte o alias para esses binários. Não substitua arquivos dentro de um bundle de aplicativo que você não criou, não desative as proteções da plataforma e não teste com uma credencial de produção.