Por que eu chroot para sandboxing por segurança se meu aplicativo pode ser executado desde o início em um nível mais baixo?

14

Estou escrevendo um daemon de servidor HTTP em C (há razões para isso), gerenciando-o com o arquivo de unidade systemd.

Estou reescrevendo um aplicativo desenvolvido há 20 anos, por volta de 1995. E o sistema que eles usam é que eles fazem o chroot e depois o setuid, e o procedimento padrão.

Agora, no meu trabalho anterior, a política usual era que você nunca executasse nenhum processo como root. Você cria um usuário / grupo para ele e executa a partir daí. Obviamente, o sistema executou algumas coisas como root, mas poderíamos obter todo o processamento da lógica de negócios sem ser root.

Agora, para o daemon HTTP, posso executá-lo sem raiz se não fizer chroot dentro do aplicativo. Portanto, não é mais seguro que o aplicativo nunca seja executado como root?

Não é mais seguro executá-lo como usuário mydaemon desde o início? Em vez de iniciá-lo com root, chrooting e setuid para mydaemon-user?

mur
fonte
3
você precisa ser executado como root para usar a porta 80 ou 443. caso contrário, você pode fazer o que o tomcat e outros softwares de webapp / servidor da web fazem e executar em uma porta superior (por exemplo, 8080, 9090, etc.) e, em seguida, usar um apache / nginx para proxy da conexão com o software do servidor da web ou use o firewall do sistema para NAT / encaminhar o tráfego para o servidor da web a partir da porta 80. Se você não precisar da porta 80 ou 443, ou pode proxy ou encaminhar a conexão, então você não precisa executar como root, em um chroot ou de outra forma.
SnakeDoc
3
O @SnakeDoc no Linux não é mais verdadeiro. Graças a capabilities(7).
0xC0000022L
@SnakeDoc você pode usar authbind bem
Abdul Ahad

Respostas:

27

Parece que outros não entenderam o seu argumento, o que não foi uma razão para usar raízes alteradas, o que obviamente você já sabe, nem o que mais pode fazer para impor limites aos daemons, quando você também sabe claramente sobre correr sob o égide de contas de usuário sem privilégios; mas por que fazer essas coisas dentro do aplicativo . Na verdade, há um exemplo bastante claro do porquê.

Considere o design do httpdprograma daemon no pacote de arquivos públicos de Daniel J. Bernstein. A primeira coisa que ele faz é alterar a raiz para o diretório raiz que foi solicitado a usar com um argumento de comando e, em seguida, largar privilégios para o ID do usuário e o ID do grupo sem privilégios que são passados ​​em duas variáveis ​​de ambiente.

Os conjuntos de ferramentas de gerenciamento da Daemon têm ferramentas dedicadas para coisas como alterar o diretório raiz e passar para IDs de usuários e grupos sem privilégios. A runit de Gerrit Pape tem chpst. Meu conjunto de ferramentas nosh tem chroote setuidgid-fromenv. O s6 de Laurent Bercot tem s6-chroote s6-setuidgid. O Perp de Wayne Marshall tem runtoole runuid. E assim por diante. De fato, todos eles têm o próprio conjunto de ferramentas daemontools do M. Bernstein setuidgidcomo antecedente.

Alguém poderia pensar que poderia extrair a funcionalidade httpde usar essas ferramentas dedicadas. Então, como você imagina, nenhuma parte do programa do servidor é executada com privilégios de superusuário.

O problema é que uma conseqüência direta precisa fazer muito mais trabalho para configurar a raiz alterada, e isso expõe novos problemas.

Com Bernstein httpdcomo está, os únicos arquivos e diretórios que estão na árvore de diretórios raiz são os que serão publicados no mundo. Não há mais nada na árvore. Além disso, não há razão para existir um arquivo de imagem de programa executável nessa árvore.

Mas mover a mudança de diretório raiz em um programa da cadeia de carregamento (ou systemd), e de repente o arquivo de imagem programa httpd, qualquer biblioteca compartilhada que cargas e quaisquer arquivos especiais em /etc, /rune /devque o carregador de programa ou tempo de execução C acesso à biblioteca durante a inicialização do programa (que você pode achar bastante surpreendente se você truss/ straceum programa em C ou C ++), também deverá estar presente na raiz alterada. Caso contrário, httpdnão poderá ser acorrentado e não será carregado / executado.

Lembre-se de que este é um servidor de conteúdo HTTP (S). Pode servir potencialmente qualquer arquivo (legível pelo mundo) na raiz alterada. Isso agora inclui coisas como suas bibliotecas compartilhadas, seu carregador de programas e cópias de vários arquivos de configuração do carregador / CRTL para o seu sistema operacional. E se, por algum motivo (acidental), o servidor de conteúdo tiver acesso a coisas de gravação , um servidor comprometido poderá obter acesso de gravação à imagem do programa por httpdsi próprio ou até ao carregador de programas do seu sistema. (Lembre-se que você tem agora dois conjuntos paralelos de /usr, /lib, /etc, /run, e /devdiretórios para manter seguro.)

Nada disso é o caso em que httpdaltera a raiz e descarta os privilégios.

Portanto, você negociou com uma pequena quantidade de código privilegiado, que é bastante fácil de auditar e é executado logo no início do httpdprograma, rodando com privilégios de superusuário; por ter uma superfície de ataque bastante expandida de arquivos e diretórios na raiz alterada.

É por isso que não é tão simples quanto fazer tudo externamente ao programa de serviço.

Observe que, no entanto, isso é um mínimo de funcionalidade em httpdsi. Todo o código que faz coisas como procurar no banco de dados da conta do sistema operacional o ID do usuário e o ID do grupo para colocar nessas variáveis ​​de ambiente em primeiro lugar é externo ao httpdprograma, em comandos auditáveis ​​independentes simples como envuidgid. (E é claro que é uma ferramenta UCSPI, por isso não contém nenhum do código para escutar na porta TCP relevante (s) ou para aceitar conexões, sendo estes o domínio de comandos, como tcpserver, tcp-socket-listen, tcp-socket-accept, s6-tcpserver4-socketbinder, s6-tcpserver4d, e assim por diante.)

Leitura adicional

JdeBP
fonte
+1, culpado conforme cobrado. Achei o título e o último parágrafo ambíguos e, se você estiver certo, não entendi. Esta resposta fornece uma interpretação muito prática. Pessoalmente, eu explicaria explicitamente que ter que criar o ambiente chroot como esse é um esforço extra, que a maioria das pessoas gostaria de evitar. Mas os 2 pontos de segurança aqui já estão bem feitos.
sourcejedi
Outro ponto a ser lembrado é que, se o servidor eliminar privilégios antes de processar qualquer tráfego de rede, o código privilegiado não será exposto a nenhuma exploração remota.
kasperd
5

Eu acho que muitos detalhes da sua pergunta poderiam ser aplicados igualmente avahi-daemon, os quais eu olhei recentemente. (Eu poderia ter perdido outro detalhe que difere). A execução do avahi-daemon em um chroot tem muitas vantagens, caso o avahi-daemon seja comprometido. Esses incluem:

  1. ele não pode ler o diretório inicial de nenhum usuário e filtrar informações privadas.
  2. não pode explorar bugs em outros programas escrevendo para / tmp. Há pelo menos uma categoria inteira desses erros. Por exemplo, https://www.google.co.uk/search?q=tmp+race+security+bug
  3. ele não pode abrir nenhum arquivo de soquete unix que esteja fora do chroot, no qual outros daemons possam estar ouvindo e lendo mensagens.

O ponto 3 pode ser particularmente interessante quando você não está usando o dbus ou similar ... Acho que o avahi-daemon usa o dbus, por isso garante o acesso ao dbus do sistema, mesmo de dentro do chroot. Se você não precisar enviar mensagens no dbus do sistema, negar essa capacidade pode ser um recurso de segurança bastante interessante.

gerenciando-o com o arquivo de unidade systemd

Observe que, se o avahi-daemon fosse reescrito, ele poderia optar por confiar no systemd para segurança e usar, por exemplo ProtectHome. Propus uma alteração ao avahi-daemon para adicionar essas proteções como uma camada extra, juntamente com algumas proteções adicionais que não são garantidas pelo chroot. Você pode ver a lista completa de opções que propus aqui:

https://github.com/lathiat/avahi/pull/181/commits/67a7b10049c58d6afeebdc64ffd2023c5a93d49a

Parece que existem mais restrições que eu poderia ter usado se o avahi-daemon não usasse o próprio chroot, algumas das quais são mencionadas na mensagem de confirmação. Não tenho certeza de quanto isso se aplica.

Observe que as proteções que eu usei não limitariam o daemon de abrir arquivos de soquete unix (ponto 3 acima).

Outra abordagem seria usar o SELinux. No entanto, você meio que amarraria seu aplicativo a esse subconjunto de distribuições Linux. A razão pela qual pensei positivamente no SELinux aqui, é que o SELinux restringe o acesso que os processos têm ao dbus, de maneira refinada. Por exemplo, acho que você poderia esperar que systemdisso não estivesse na lista de nomes de ônibus para os quais você precisava enviar mensagens para :-).

"Gostaria de saber se o uso do sandbox systemd é mais seguro que chroot / setuid / umask / ..."

Resumo: por que não os dois? Vamos decodificar um pouco acima :-).

Se você pensa no ponto 3, usar chroot fornece mais confinamento. ProtectHome = e seus amigos nem tentam ser tão restritivos quanto chroot. (Por exemplo, nenhuma das opções nomeadas do systemd lista negra /run, onde tendemos a colocar arquivos de soquete unix).

O chroot mostra que restringir o acesso ao sistema de arquivos pode ser muito poderoso, mas nem tudo no Linux é um arquivo :-). Existem opções systemd que podem restringir outras coisas, que não são arquivos. Isso é útil se o programa estiver comprometido, você pode reduzir os recursos do kernel disponíveis, o que pode tentar explorar uma vulnerabilidade. Por exemplo, o avahi-daemon não precisa de soquetes bluetooth e acho que seu servidor da web também não :-). Portanto, não lhe dê acesso à família de endereços AF_BLUETOOTH. Basta colocar a lista de permissões AF_INET, AF_INET6 e talvez AF_UNIX, usando a RestrictAddressFamilies=opção

Leia os documentos para cada opção que você usa. Algumas opções são mais eficazes em combinação com outras e outras não estão disponíveis em todas as arquiteturas de CPU. (Não porque a CPU está ruim, mas porque a porta Linux para essa CPU não foi tão bem projetada. Eu acho).

(Existe um princípio geral aqui. É mais seguro se você pode escrever listas do que deseja permitir, e não do que deseja negar. Como definir um chroot, é exibida uma lista dos arquivos que você tem permissão para acessar, e isso é mais robusto. do que dizer que você deseja bloquear /home).

Em princípio, você pode aplicar todas as mesmas restrições antes de setuid (). É tudo apenas código que você pode copiar do systemd. No entanto, as opções de unidade do systemd devem ser significativamente mais fáceis de escrever e, como estão em um formato padrão, devem ser mais fáceis de ler e revisar.

Por isso, recomendo apenas ler a seção de sandbox man systemd.execna plataforma de destino. Mas se você quer o projeto mais seguro possível, eu não tenha medo de tentar chroot(e depois cair rootprivilégios) em seu programa de bem . Há uma troca aqui. O uso chrootimpõe algumas restrições ao seu design geral. Se você já possui um design que usa chroot e parece fazer o que você precisa, isso parece ótimo.

sourcejedi
fonte
+1 especialmente para as sugestões do sistema.
Mattdm 01/06
Eu aprendi um pouco com sua resposta, se a pilha exceder o fluxo permitisse uma resposta múltipla, eu também aceitaria sua resposta. Eu estava pensando, se estiver usando systemd sandboxing mais seguro que o chroot / setuid / umask / ...
mur
@ Mur feliz que você gostou :). Essa é uma resposta muito natural à minha resposta. Atualizei-o novamente para tentar responder à sua pergunta.
sourcejedi
1

Se você pode confiar no systemd, é realmente mais seguro (e mais simples!) Deixar o sandbox para o systemd. (Obviamente, o aplicativo também pode detectar se foi iniciado em área restrita pelo systemd ou não, e a própria área restrita se ainda for raiz.) O equivalente ao serviço que você descreve seria:

[Service]
ExecStart=/usr/local/bin/mydaemon
User=mydaemon-user
RootDirectory=...

Mas não precisamos parar por aí. O systemd também pode fazer muitos outros sandboxs para você - aqui estão alguns exemplos:

[Service]
# allocate separate /tmp and /var/tmp for the service
PrivateTmp=yes
# mount / (except for some subdirectories) read-only
ProtectSystem=strict
# empty /home, /root
ProtectHome=yes
# disable setuid and other privilege escalation mechanisms
NoNewPrivileges=yes
# separate network namespace with only loopback device
PrivateNetwork=yes
# only unix domain sockets (no inet, inet6, netlink, …)
RestrictAddressFamilies=AF_UNIX

Veja man 5 systemd.execmuito mais diretrizes e descrições mais detalhadas. Se você ativar o soquete daemon ( man 5 systemd.socket), poderá usar as opções relacionadas à rede: o único link do serviço para o mundo externo será o soquete de rede que recebeu do systemd, ele não poderá se conectar a mais nada. Se for um servidor simples que apenas escuta em algumas portas e não precisa se conectar a outros servidores, isso pode ser útil. (As opções relacionadas ao sistema de arquivos também podem tornar RootDirectoryobsoletas, na minha opinião, portanto, talvez você não precise se preocupar em configurar um novo diretório raiz com todos os binários e bibliotecas necessários.)

As versões mais recentes do systemd (desde a v232) também são suportadas DynamicUser=yes, onde o systemd alocará automaticamente o usuário do serviço apenas para o tempo de execução do serviço. Isto significa que você não tem que registrar um usuário permanente para o serviço, e funciona bem, desde que o serviço não escreve para quaisquer outros locais do sistema de arquivos do que o seu StateDirectory, LogsDirectorye CacheDirectory(que você também pode declarar no arquivo unidade - veja man 5 systemd.execnovamente - e qual systemd irá gerenciar, tendo o cuidado de atribuí-los corretamente ao usuário dinâmico).

Lucas Werkmeister
fonte