Execute uma ação pós-conexão VPN como o usuário, não como o daemon
Uma variável de ambiente que diz USER=alice não muda em qual conta um hook é executado. Separe o evento, o gatilho e a ação, e depois prove o UID.
Os hooks de conexão em um daemon de VPN são executados como o daemon, e normalmente isso significa que são executados como root. No momento em que um desses hooks grava no diretório pessoal de alguém ou reinicia um assistente de desktop, você deu a um evento de rede a capacidade de agir como qualquer usuário local. O atalho habitual, ler um nome de usuário de uma variável de ambiente e repassá-lo, não corrige isso. Ele apenas reetiqueta a ação sem mudar qual conta a executa. O que funciona é uma entrega em três partes, em que o passo privilegiado diz o que aconteceu e uma unidade separada realiza a ação sob a identidade correta.
Um nome de usuário no ambiente não é um comutador de identidade
O Unix decide o que um processo pode tocar pelo seu ID de usuário efetivo, não pelas cadeias do seu ambiente. Definir USER=alice ou exportar LOGNAME muda o que um software descuidado imprime na sua saída; não muda nada nas verificações de propriedade de arquivos. Um hook executado como root com essa variável definida continua abrindo os arquivos da Alice como root.
Os nomes também não são o mesmo nome. O login de acesso remoto, a conta Unix local e o nome comum do certificado são três identificadores que costumam coincidir e às vezes não. Um usuário que se autentica como a.smith pode não ter conta local alguma, ou pode ser mapeado para asmith por meio de um serviço de diretório. Qualquer projeto que presuma que uma única cadeia atende aos três papéis vai quebrar no primeiro prestador de serviço.
Portanto a primeira pergunta não é «qual usuário está conectado», mas «qual conta local tem permissão para executar esta ação». Responda isso a partir do seu próprio banco de dados de contas, não do que o cliente enviou:
id -u alice
getent passwd alice | cut -d: -f1,3,6
stat -c '%U:%G %a %n' /home/alice/.local/state/vpn-status.json
O terceiro comando é o que as pessoas pulam. Se o arquivo de destino pertence ao root porque o hook anterior o criou, uma ação de usuário configurada corretamente falhará com um erro de permissão que parece um problema de systemd e não é.
O daemon fica onde está
Mudar o User= do próprio serviço de VPN para Alice é a alavanca errada. Altera como o daemon vincula interfaces, lê chaves, grava seu arquivo de estado e gerencia rotas, e transfere todo esse risco para uma unidade que funcionava. O daemon precisa dos seus privilégios; a ação seguinte não.
Veja o que a unidade realmente faz antes de tocá-la. Dois comandos dizem a identidade de execução e as linhas de comando exatas em vigor, incluindo tudo o que uma substituição tenha acrescentado:
systemctl show -p User -p Group -p ExecStart openvpn-server@server
systemctl cat openvpn-server@server
Numa instalação de pacote padrão o daemon é root e o script --up herda isso. Confirme em vez de presumir, porque uma distribuição endurecida pode tê-lo movido para uma conta dedicada, e um drop-in escrito à mão pode executar o hook através de um invólucro com regras diferentes.
Três partes nomeadas, um gatilho estreito
O arranjo que sobrevive à manutenção tem um evento, um gatilho e uma ação. O daemon relata que uma sessão nomeada subiu. Um gatilho aceita esse pequeno fato e pede uma ação fixa. A ação é executada sob a conta local que possui os arquivos que ela toca. Nada na cadeia aceita um caminho de comando arbitrário.
A ação em si é uma unidade de serviço comum, e a identidade é uma única diretiva dentro dela:
[Service]
Type=oneshot
User=alice
Group=alice
ExecStart=/usr/local/bin/refresh-vpn-status --slot %i
Note que User=alice se aplica à unidade inteira, e o systemd documenta uma exceção que vale a pena conhecer: um comando com o prefixo + é executado com todos os privilégios independentemente disso. Esse prefixo é uma ferramenta legítima para um único passo privilegiado e uma forma cómoda de desfazer a separação que acabou de construir, por isso trate qualquer + nessa unidade como algo a justificar na revisão.
Mantenha a entrada do gatilho mínima e não secreta. O identificador de sessão, a conta mapeada e um carimbo de data e hora costumam ser tudo o que é preciso. Nunca faça passar um fragmento de shell, um caminho de executável ou um bloco de ambiente fornecido pelo cliente pela fronteira, porque o lado que recebe não tem como distinguir um valor legítimo de um injetado.
Verifique a identidade que realmente foi executada
A revisão da configuração não é prova. Depois de uma conexão de teste aprovada, confirme o UID que tocou o arquivo e o UID que o daemon ainda usa:
stat -c '%U %G %y %n' /home/alice/.local/state/vpn-status.json
ps -o user=,pid=,cmd= -p "$(pgrep -o -f 'openvpn --daemon')"
systemd-run --uid=alice --pipe /usr/local/bin/refresh-vpn-status --dry-run
A primeira linha deve mostrar Alice e um carimbo de data e hora dentro da sua janela de teste. A segunda deve continuar a mostrar root ou a conta de serviço do pacote, o que prova que o daemon não se moveu. A terceira é a forma mais barata de testar as permissões da ação sem esperar por uma reconexão real.
Se o proprietário do arquivo é Alice mas o grupo ou o modo mudou entre execuções, procure um UMask= ou uma diferença de permissões de diretório em vez de um erro de lógica. Se o carimbo de data e hora não se moveu nada, o gatilho nunca disparou, e o passo seguinte útil é journalctl -u para a unidade de ação em vez de outra reconexão do cliente.
O que um evento de conexão ainda não prova
Um túnel estabelecido é um único fato. Não significa que a sessão de desktop esteja desbloqueada, que o bus de sessão esteja acessível ou que o recurso privado que a ação deseja já esteja disponível. Uma unidade oneshot sob o UID da Alice tem a identidade dela e nada da sessão dela, por isso tudo o que exige um bus gráfico tem de passar pelo mecanismo de sessão de usuário suportado e definir o que acontece quando ela termina a sessão.
As reconexões também se repetem. Um portátil que dorme num comboio pode produzir uma dúzia de eventos de conexão numa hora, e um hook que acrescenta uma linha de cada vez transforma um arquivo de estado num registo que ninguém consegue ler. Torne a ação idempotente, ou deixe-a substituir um único registo com chave por sessão.
Por fim, limite o raio de dano na própria unidade: uma visão do sistema de arquivos só de leitura fora do diretório de estado, sem privilégios novos e com um tempo limite explícito. A separação entre o daemon e a ação de usuário vale por si, mas vale muito mais quando a ação não pode fazer nada além da única coisa que você aprovou.