Home Projetos & Releases BigBangcraft Blog Sobre & Carreira
DevOps & Cloud • Hub 7 min de leitura 20 visualizações
Raw .md

A ingenuidade de confiar apenas no usuário sem sudo para conter um processo web

PD

Pedro Dalben

CTO no Integrar Plus • Publicado em 22 de October de 2026

A ingenuidade de confiar apenas no usuário sem sudo para conter um processo web

Existe uma divisão clássica entre quem opera servidores próprios. De um lado, estão os defensores da conteinerização irrestrita, que insistem que qualquer serviço rodando direto no sistema operacional é uma falha de segurança e que namespaces só devem ser consumidos via Docker ou Podman. Do outro lado, ficam os minimalistas que criam um usuário local sem privilégios de sudo, colocam o Puma ou o sidekiq para rodar sob essa conta e acreditam que o problema de contenção foi encerrado aí.

Eu discordo das duas posturas. A exigência de rodar containers para qualquer processo em uma VPS adiciona camadas de rede virtual, sistemas de arquivos em camadas e daemons extras para resolver algo que o kernel já faz nativamente. Por outro lado, supor que um usuário comum do Linux sem acesso ao sudo oferece isolamento aceitável é uma ilusão perigosa.

Se a sua aplicação web sofrer uma execução remota de código ou se uma dependência for comprometida, a conta do processo não precisa de root para causar estrago. Um usuário comum consegue ler boa parte da hierarquia do Linux. Ele lê chaves públicas em diretórios home vizinhos se a máscara de criação falhar, vasculha configurações globais em /etc, inspeciona argumentos de outros processos em /proc e usa diretórios públicos como /tmp ou /var/tmp para descarregar binários compilados e manter persistência.

O isolamento real em um servidor Linux sem a sobrecarga de containers passa pelo gerenciador de inicialização. O systemd gerencia cgroups e namespaces de montagem no próprio PID 1. Quando você configura uma unit com diretivas de restrição de sistema de arquivos, o kernel monta uma visão customizada da árvore de diretórios exclusiva para aquele processo antes de invocar o executável.

O buraco aberto nas permissões tradicionais

A maioria dos deploys tradicionais cria um usuário de serviço, como app, e coloca o código em /var/www ou dentro de /home/app. Quem adota esse modelo quase sempre confia que a ausência de permissão de escrita fora do diretório da aplicação protege a máquina.

Isso ignora a superfície de leitura. Uma aplicação Rails carrega dezenas de gems nativas. Se uma dessas bibliotecas for vulnerável a leitura arbitrária de arquivos, o processo tem passe livre para inspecionar arquivos legíveis por qualquer usuário. Em muitas distribuições, diretórios de usuários sob /home são criados historicamente com permissão de leitura para terceiros, a menos que alguém tenha tido o cuidado explícito de ajustar o umask ou aplicar chmod 700.

Além disso, áreas compartilhadas de escrita são um convite para execução indireta de código. O /tmp padrão tem permissão de escrita irrestrita com sticky bit. Se o processo pode escrever lá, ele pode criar arquivos executáveis, scripts e sockets Unix que interagem com outros serviços da máquina sem que você perceba.

Fechando o cerco com ProtectHome e ProtectSystem

O primeiro passo para conter o processo é retirar a visão dos dados de usuários humanos da máquina. O systemd resolve isso com uma única diretiva chamada ProtectHome.

Ao declarar ProtectHome=true, os diretórios /home, /root e /run/user ficam totalmente inacessíveis para o processo da aplicação, mapeados como diretórios vazios e sem permissão. Se você quiser um nível mais agressivo, ProtectHome=strict transforma esses caminhos em montagens somente leitura. Em servidores onde a aplicação roda sob um usuário dedicado e não precisa de nenhum arquivo pessoal de administradores, essa diretiva elimina o risco de um processo comprometido extrair chaves SSH ou históricos de shell de quem mantém o servidor.

Em seguida vem o sistema operacional como um todo. A diretiva ProtectSystem cuida das pastas que compõem a base do Linux:

ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

O valor strict monta praticamente toda a hierarquia do sistema de arquivos, incluindo /usr, /boot, /etc e até /var, em modo somente leitura para a unit. Isso quebra a premissa de qualquer script malicioso que tente alterar bibliotecas compartilhadas, injetar arquivos de configuração ou tocar em binários do sistema.

A diretiva PrivateTmp=true complementa essa barreira criando um namespace de arquivos temporários isolado. O /tmp e o /var/tmp que a sua aplicação enxerga são exclusivos dela, invisíveis para o resto do sistema operacional e descartados automaticamente quando a unit para.

Liberando apenas o necessário com ReadWritePaths

Quando você aplica ProtectSystem=strict, a aplicação para de funcionar imediatamente se tentar escrever logs, arquivos temporários de compilação de assets, uploads locais ou sockets de conexão. Essa quebra é esperada e desejável. Ela obriga você a declarar formalmente onde o processo tem autorização explícita para gravar dados.

É aqui que entra o ReadWritePaths. Em vez de deixar o sistema inteiro aberto e tentar bloquear caminhos específicos, você bloqueia tudo na base e abre exceções pontuais:

[Unit]
Description=Aplicacao Web Rails Puma
After=network.target

[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/srv/app/current

## Isolamento de filesystem
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true

## Pastas permitidas para gravacao
ReadWritePaths=/srv/app/current/log /srv/app/shared/storage /srv/app/current/tmp

## Protecoes complementares de processo
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictRealtime=true

ExecStart=/srv/app/current/bin/puma -C /srv/app/current/config/puma.rb
Restart=always

[Install]
WantedBy=multi-user.target

Nessa configuração, mesmo que a aplicação sofra um ataque que permita gravação arbitrária de arquivos, ela não conseguirá tocar em nada fora dos três caminhos declarados em ReadWritePaths. O /etc é somente leitura, o /usr é somente leitura e os diretórios de outros usuários sequer existem na árvore visível pelo processo.

A diretiva NoNewPrivileges=true impede que o binário ou qualquer processo filho ganhe privilégios adicionais por meio de binários setuid ou capacidades especiais do kernel. Se alguém tentar executar um binário que normalmente elevaria privilégios na máquina, o kernel bloqueia a transição.

O custo operacional e onde o modelo exige atenção

Existe uma fricção real ao adotar esse nível de contenção. Atualizações de aplicação, criação de novas pastas de cache ou mudanças na arquitetura de logs exigem atualizar o arquivo de unit do systemd e recarregar a configuração com systemctl daemon-reload.

Outro ponto que exige cuidado é a inicialização de ferramentas que precisam ler configurações locais. Se a sua aplicação invoca utilitários do sistema que dependem de arquivos dentro de /etc que não foram liberados ou se uma gem tenta verificar variáveis de hardware em /sys, você precisará inspecionar as falhas pelo journalctl e adicionar diretivas como ReadOnlyPaths ou relaxar proteções pontuais.

Eu costumo testar essas restrições primeiro em um ambiente de homologação idêntico à produção, monitorando os erros de permissão do kernel no log do sistema antes de travar o serviço principal. A complexidade de manter essa dúzia de linhas no arquivo de unit é muito menor do que a manutenção de uma pilha inteira de orquestração de containers, entregando uma contenção que opera diretamente nas primitivas de segurança do kernel Linux.

PD

Sobre Pedro Dalben

CTO e arquiteto full stack especializado em Ruby on Rails, Vue.js, sistemas multi-tenant e infraestrutura distribuída.