O hook funciona quando você o executa à mão como root e retorna Operation not permitted no momento em que o serviço o inicia. O arquivo não mudou entre essas execuções; o ambiente mudou. O systemd entrega ao processo um mundo menor: menos capacidades, um /dev diferente, caminhos do sistema de arquivos só de leitura e um teto de tarefas. Leia a falha como uma fronteira que recusou uma operação específica, não como um script que quebrou.

Comece pela operação que foi recusada

Uma linha de log dizendo que a unit falhou não lhe diz quase nada. A linha útil nomeia a chamada que foi recusada: um nó de dispositivo que não pôde ser aberto, um diretório no qual não se pôde escrever, um namespace que não pôde ser criado. Encontre essa chamada antes de tocar em uma única diretiva, porque cada uma corresponde a uma restrição diferente. Depois leia a unit que realmente rodou, com os drop-ins incluídos, em vez do arquivo que o pacote entregou.

A consulta abaixo imprime o caminho do fragmento, os diretórios de drop-in e as propriedades que importam para uma recusa de dispositivo ou de sistema de arquivos. Leia todas antes de decidir qual fronteira é relevante:

systemctl show "[email protected]" -p FragmentPath -p DropInPaths -p PrivateDevices -p DevicePolicy -p DeviceAllow -p ProtectHome -p ProtectSystem -p TasksMax
journalctl -u "[email protected]" -b --no-pager | grep -iE 'operation not permitted|permission denied|cannot open|failed'

Nada nesse bloco escreve. Se a chamada recusada falta no journal, aumente a verbosidade do cliente para uma execução em vez de adivinhar diretivas. Um processo em execução prova que a unit iniciou, não que o hook rodou com os privilégios de que precisava.

Um namespace precisa de direitos de montagem que um dispositivo TUN nunca usa

Duas operações que parecem iguais dentro de um mesmo hook têm perfis de permissão diferentes. Abrir /dev/net/tun e criar a interface exige administração de rede mais acesso a esse nó. Criar um namespace de rede nomeado também exige trabalho de montagem, porque a ferramenta de namespaces monta uma /sys visão nova e amarra um diretório de estado no lugar. a unit de cliente original deixa deliberadamente a administração de montagem fora do seu conjunto de limites, então o passo do namespace é recusado enquanto o túnel em si teria funcionado.

Por isso conceder o nó de dispositivo não salva o script. Uma permissão de dispositivo responde a uma pergunta, se este processo pode abrir este nó, e a pergunta do namespace é outra. Case a chamada que falha com a sua capacidade antes de ampliar qualquer coisa.

Um teste no seu shell herda todas as capacidades que um root sem restrições tem, e a unit herda o que o seu conjunto de limites manteve. As duas execuções são experimentos diferentes mesmo quando o binário é idêntico.

Dispositivos, caminhos protegidos e tetos de tarefas

Uma vez conhecida a operação recusada, três grupos de diretivas explicam a maioria das falhas. O acesso a dispositivos é regido por PrivateDevices, que substitui /dev por um conjunto mínimo de pseudo-dispositivos, e por DevicePolicy com DeviceAllow, que decidem o que o cgroup permite. /dev/net/tun não faz parte desse conjunto mínimo, então uma unit com PrivateDevices=yes não consegue abri-lo, diga o que disser a política de dispositivos.

A proteção do sistema de arquivos responde a uma segunda pergunta. ProtectHome=yes torna /home, /root e /run/user inacessíveis, e ProtectSystem=full torna /usr, /boot e /etc só de leitura. Por isso um hook que escreve ao lado do próprio arquivo de configuração funciona no seu shell e falha sob a unit, com a mesma forma de permissão que uma capacidade ausente. Mova a entrada gravável para um caminho que a política permita, ou acrescente uma entrada ReadWritePaths= estreita.

Os limites de recursos são o terceiro grupo, e o menos óbvio. TasksMax= limita quantas tarefas a unit pode criar, e um teto de descritores de arquivo se comporta da mesma forma para sockets e handles. Uma implantação que cresceu além do teto falha com erro de recurso em vez de erro de permissão, e é por isso que a mensagem exata ainda importa.

Tire o passo do namespace do hook

A preparação que exige um privilégio que o cliente não deve ter pertence à sua própria unit. Um oneshot que roda antes do cliente, guarda o seu resultado e está ordenado antes da conexão dá ao passo do namespace o que ele precisa sem entregar esses direitos ao processo do túnel. O cliente então referencia essa unit em vez de chamar o script a partir do seu hook up.

O modelo abaixo é uma forma inicial, não algo que se aplique sem ler. Ele mantém a preparação onde vive o privilégio e deixa a unit do túnel intacta:

# /etc/systemd/system/[email protected]
[Unit]
Description=Namespace preparation for %i
Before=openvpn-client@%i.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/netns-up.sh %i
CapabilityBoundingSet=CAP_NET_ADMIN CAP_SYS_ADMIN
[Install]
WantedBy=multi-user.target

Acrescente a ordem com um drop-in em vez de editar a unit empacotada, para que uma atualização não possa descartar a sua mudança em silêncio. O diretório é vinculado ao nome da instância, então um segundo perfil precisa do próprio arquivo:

sudo mkdir -p /etc/systemd/system/[email protected]
sudo tee /etc/systemd/system/[email protected]/10-namespace.conf >/dev/null <<'CONF'
[Unit]
[email protected]
[email protected]
CONF
sudo systemctl daemon-reload

Repita a mesma invocação depois da mudança

Um reparo só se confirma quando a mesma invocação produz um resultado diferente. Reinicie as duas units, execute a operação que falhou e compare o resultado com a linha de log que você guardou antes da mudança. Uma unit que inicia enquanto o hook nunca mais roda não é um reparo.

A sequência abaixo mantém a comparação honesta, porque verifica a chamada recusada e não a cor da unit. Guarde a saída dela ao lado da linha de log que você capturou antes da mudança:

sudo systemctl restart "[email protected]" "[email protected]"
systemctl status "[email protected]" --no-pager
journalctl -u "[email protected]" -b --no-pager | grep -c 'Operation not permitted'
ip -brief addr show tun0

Conte as recusas em vez de ler a última linha, porque um hook pode falhar, tentar de novo e ter sucesso, e só a contagem mostra se a fronteira se moveu. Deixe um comentário no drop-in nomeando a diretiva que você afrouxou e o motivo, já que a próxima atualização compara a sua configuração com o padrão do pacote. Uma unit que só inicia depois de todas as proteções serem desligadas não está consertada.

Artigos relacionados