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

A falsa segurança de rodar serviços sem root ignorando as diretivas do systemd

PD

Pedro Dalben

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

A falsa segurança de rodar serviços sem root ignorando as diretivas do systemd

Muita gente sobe uma aplicação em VPS acreditando que cumpriu a cartilha de segurança ao criar um usuário dedicado, digamos deploy ou rails, sem privilégios de sudo. A lógica parece confortável: se o processo for comprometido por uma execução remota de código ou falha de upload, o invasor não é root, logo o estrago estaria contido.

Essa premissa cai por terra na primeira inspeção real do sistema de arquivos.

No Linux padrão, qualquer usuário sem privilégios consegue ler uma quantidade absurda de diretórios. Ele navega por /home, lê arquivos em /tmp, inspeciona processos em /proc e, frequentemente, consegue abrir arquivos de configuração esquecidos com permissão 644 contendo credenciais de banco ou tokens de API. Se você roda seus workers do Sidekiq ou a aplicação Puma sob um usuário comum sem restringir a superfície com o próprio init do sistema, você entregou a máquina inteira para leitura.

Existe uma corrente que defende empacotar absolutamente tudo em Docker para resolver isso com namespaces. Outra defende que permissões POSIX tradicionais bastam se você configurar chmod e chown manualmente em cada pasta. Eu discordo das duas abordagens em servidores dedicados ou instâncias virtuais enxutas.

O Docker adiciona uma camada de rede virtual, daemon rodando com privilégios elevados e complexidade de orquestração que muitas vezes mascara o problema básico de permissão em disco. Já gerenciar permissões POSIX no braço é frágil: basta uma atualização de pacote, uma automação de deploy desatenta ou um arquivo criado dinamicamente para abrir brechas no filesystem.

A ferramenta correta para estancar esse vazamento já está rodando no seu Linux e raramente é usada no seu potencial: o systemd.

O abismo entre permissão de usuário e isolamento de processo

Quando um processo sobe via systemd, o kernel cria o contexto daquele processo sob o PID 1. O systemd não serve apenas para reiniciar processos que caíram ou gerenciar dependências de boot; ele é uma interface direta para namespaces de mount, cgroups e restrições de chamadas de sistema.

Se o seu arquivo de unit se parece com isto:

[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/app/current
ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C config/puma.rb
Restart=always

[Install]
WantedBy=multi-user.target

Você tem um processo rodando sem root, mas com visão panorâmica do servidor. Se um atacante explorar uma falha na sua stack e conseguir uma shell interativa com esse usuário, ele poderá:

  1. Listar o conteúdo de /home/ de outros usuários e tentar ler chaves públicas, históricos de bash ou arquivos residuais.
  2. Ler e gravar livremente em /tmp e /var/tmp, usando essas pastas para baixar binários compilados, scanners de rede ou scripts de persistência.
  3. Bisbilhotar /etc atrás de senhas em texto puro de serviços auxiliares.

Mudar isso exige virar a chave mental: não basta definir quem roda o processo, é preciso definir o que o processo enxerga do sistema operacional.

Trancando a casa com ProtectHome e ProtectSystem

O primeiro passo para fechar a visibilidade desnecessária é dizer ao systemd que o processo não tem motivo para olhar para os diretórios residenciais de ninguém.

A diretiva ProtectHome=true monta /home, /root e /run/user como inacessíveis para o serviço. Eles simplesmente aparecem vazios ou com acesso negado via namespace de montagem privado, sem alterar as permissões reais no disco para os outros processos do sistema.

Se sua aplicação estiver hospedada dentro de /home/deploy/app, ativar ProtectHome=true vai quebrar o boot imediatamente. A alternativa aqui é mudar o deploy para caminhos padrão de servidor, como /var/www ou /srv, ou usar ProtectHome=read-only durante a transição. No longo prazo, manter código de produção dentro de /home de usuário é uma decisão de arquitetura ruim justamente por misturar ambiente interativo com serviço do sistema.

Em conjunto, aplicamos ProtectSystem=strict. Esta é a opção mais agressiva e necessária: ela monta todo o /usr, /boot, /etc e o restante do sistema de arquivos como somente leitura (read-only) para a unit.

ProtectSystem=strict
ProtectHome=true

Com apenas essas duas linhas, qualquer tentativa de alterar binários do sistema, sobrescrever arquivos em /etc ou mexer em bibliotecas compartilhadas falha imediatamente com erro de sistema de arquivos em modo somente leitura.

Abrindo exceções cirúrgicas com ReadWritePaths

Com ProtectSystem=strict, o processo não consegue gravar em lugar nenhum. Uma aplicação web precisa gravar logs, precisa de uma pasta de uploads temporários, precisa de sockets UNIX e de pastas de cache.

Em vez de relaxar a segurança global, você declara explicitamente quais caminhos têm permissão de escrita através de ReadWritePaths:

ReadWritePaths=/var/www/app/shared/log /var/www/app/shared/tmp /var/www/app/shared/storage

Qualquer escrita fora dessa lista recebe um EPERM do kernel. O processo tenta gravar um arquivo em /tmp? Bloqueado. Tenta alterar um script no diretório raiz do projeto fora de tmp ou storage? Bloqueado.

Para isolar ainda mais o uso de arquivos temporários, ativamos PrivateTmp=true. Essa diretiva cria um namespace de /tmp e /var/tmp exclusivo para aquela unit. Se o Puma gravar um arquivo em /tmp, nenhum outro usuário ou serviço na máquina consegue enxergá-lo, e o Puma também não enxerga o /tmp real do host.

Montando a unit completa

Juntando as diretivas de isolamento de sistema de arquivos com bloqueios de privilégios de kernel, a unit do serviço ganha outra densidade de segurança:

[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/www/app/current
ExecStart=/usr/local/bin/bundle exec puma -C config/puma.rb
Restart=always

## Isolamento de montagem e filesystem
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/www/app/shared/log /var/www/app/shared/tmp /var/www/app/shared/storage

## Restrições de privilégios e kernel
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictSUIDSGID=true

[Install]
WantedBy=multi-user.target

A linha NoNewPrivileges=true impede que o processo ou qualquer subprocesso gerado por ele ganhe novos privilégios via binários com bit setuid (como sudo ou pkexec). Mesmo se houver uma vulnerabilidade local de elevação de privilégio instalada no sistema, a chamada execve não altera as credenciais do processo.

RestrictSUIDSGID=true garante que qualquer tentativa de criar arquivos com permissões SUID ou SGID seja ignorada, impedindo a criação de backdoors simples em diretórios graváveis.

O custo operacional e onde o modelo costuma ranger

Seria desonesto afirmar que aplicar essas diretivas é um processo indolor. O isolamento rigoroso cobra seu preço na depuração diária.

O problema mais comum surge quando a aplicação precisa executar utilitários externos, como chamar o ImageMagick, ffmpeg ou ferramentas de backup que esperam gravar arquivos temporários em caminhos globais. Se a biblioteca de processamento tentar acessar /tmp sem que você tenha configurado caminhos explicitamente ou sem entender o PrivateTmp, o job falha com erros genéricos de permissão que não aparecem claramente no log da aplicação.

Outro ponto de atrito acontece durante o deploy. Se a sua ferramenta de deploy roda sob o mesmo usuário deploy via SSH, ela opera fora da unit do systemd e tem permissões normais do shell. Porém, se o processo em execução tentar recarregar arquivos ou reescrever assets durante a inicialização, a unit vai barrar qualquer operação fora dos ReadWritePaths.

Para auditar o impacto antes de aplicar em produção, o systemd oferece uma ferramenta excelente:

systemd-analyze security puma.service

Esse comando analisa cada flag de exposição da unit e gera uma pontuação de risco de 0 a 10, apontando exatamente quais proteções de exposição de rede, namespaces e filesystem continuam abertas.

Subir serviços em servidores próprios sem a camada de abstração pesada de containers não significa abrir mão de confinamento. O kernel Linux possui todas as primitivas necessárias para garantir que uma falha na camada de aplicação não comprometa a máquina inteira. Apenas certifique-se de que o isolamento seja imposto pelo init, e não pela frágil esperança de que um usuário comum não saberá onde procurar.

PD

Sobre Pedro Dalben

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