Estou escrevendo um daemon de servidor HTTP em C (existem razões para isso), gerenciando-o com o arquivo de unidade systemd.
Estou reescrevendo um aplicativo projetado há 20 anos, por volta de 1995. E o sistema que eles usam é chroot e depois setuid, e o procedimento padrão.
Agora, em 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í. É claro que o sistema executou algumas coisas como root, mas poderíamos realizar todo o processamento da lógica de negócios sem ser root.
Agora, para o daemon HTTP, posso executá-lo sem root se não fizer chroot dentro do aplicativo. Portanto, não é mais seguro para o aplicativo nunca ser executado como root?
Não é mais seguro executá-lo como mydaemon-user desde o início? Em vez de iniciá-lo com root, chrooting e setuid para mydaemon-user?
Parece que outros não entenderam o seu ponto, o que não foi motivo para usar raízes alteradas, o que é claro que você já sabe, nem o que mais você pode fazer para impor limites aos daemons, quando você também sabe claramente sobre correr sob a égide de contas de usuário sem privilégios; mas por que fazer essas coisas dentro do aplicativo . Na verdade, há um exemplo bastante preciso do porquê.
Considere o projeto do
httpd
programa daemon no pacote publicfile de Daniel J. Bernstein. A primeira coisa que ele faz é alterar root para o diretório raiz que foi instruído a usar com um argumento de comando e, em seguida, descartar privilégios para o ID do usuário não privilegiado e o ID do grupo que são passados em duas variáveis de ambiente.Os conjuntos de ferramentas de gerenciamento de daemon têm ferramentas dedicadas para coisas como alterar o diretório raiz e acessar IDs de usuários e grupos sem privilégios. O runit de Gerrit Pape tem
chpst
. Meu conjunto de ferramentas nosh temchroot
esetuidgid-fromenv
. O s6 de Laurent Bercot tems6-chroot
es6-setuidgid
. Perp de Wayne Marshall temruntool
erunuid
. E assim por diante. De fato, todos eles têm o próprio conjunto de ferramentas daemontools de M. Bernsteinsetuidgid
como antecedente.Alguém poderia pensar que é possível extrair a funcionalidade
httpd
e 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, como consequência direta, é necessário muito mais trabalho para configurar a raiz alterada, e isso expõe novos problemas.
Com o Bernstein
httpd
como está, os únicos arquivos e diretórios que estão na árvore do diretório raiz são aqueles que serão publicados para o mundo. Não há mais nada na árvore. Além disso, não há razão para que qualquer arquivo de imagem de programa executável exista nessa árvore.Mas mova o diretório raiz para um programa de carregamento em cadeia (ou systemd) e, de repente, o arquivo de imagem do programa para
httpd
, quaisquer bibliotecas compartilhadas que ele carregue e quaisquer arquivos especiais em/etc
,/run
, e/dev
que o carregador de programa ou a biblioteca de tempo de execução C acesse durante a inicialização do programa (o que você pode achar bastante surpreendente se vocêtruss
/strace
um programa C ou C++), também deve estar presente na raiz alterada. Caso contráriohttpd
, não pode ser acorrentado e não será carregado/executado.Lembre-se de que este é um servidor de conteúdo HTTP(S). Ele pode servir potencialmente qualquer arquivo (legível por todo o mundo) na raiz alterada. Isso agora inclui coisas como suas bibliotecas compartilhadas, seu carregador de programa e cópias de vários arquivos de configuração de carregador/CRTL para seu sistema operacional. E se por algum meio (acidental) o servidor de conteúdo tiver acesso para gravar coisas, um servidor comprometido pode possivelmente obter acesso de gravação à imagem do programa para
httpd
si mesmo ou até mesmo ao carregador de programas do seu sistema. (Lembre-se de que agora você tem dois conjuntos paralelos de diretórios/usr
,/lib
,/etc
,/run
e/dev
para mantê-los seguros.)Nada disso é o caso em que
httpd
altera o root e descarta os próprios privilégios.Então você negociou com uma pequena quantidade de código privilegiado, que é bastante fácil de auditar e que roda logo no início do
httpd
programa, rodando com privilégios de superusuário; por ter uma superfície de ataque bastante expandida de arquivos e diretórios dentro da raiz alterada.É por isso que não é tão simples como fazer tudo externamente ao programa de serviço.
Observe que isso é, no entanto, um mínimo de funcionalidade em
httpd
si. Todo o código que faz coisas como procurar no banco de dados de contas do sistema operacional o ID do usuário e o ID do grupo para colocar nessas variáveis de ambiente é externo aohttpd
programa, em comandos auditáveis autônomos simples, comoenvuidgid
. (E, claro, é uma ferramenta UCSPI, portanto, não contém nenhum código para escutar na(s) porta(s) TCP relevante(s) ou para aceitar conexões, sendo essas o domínio de comandos comotcpserver
,tcp-socket-listen
,tcp-socket-accept
,s6-tcpserver4-socketbinder
,s6-tcpserver4d
e assim por diante.)Leitura adicional
httpd
. publicfile . cr.yp.to.httpd
. Todos os softwares de Daniel J. Bernstein em um . Programas. Jonathan de Boyne Pollard. 2016.gopherd
. Todos os softwares de Daniel J. Bernstein em um . Programas. Jonathan de Boyne Pollard. 2017.Acho que muitos detalhes da sua pergunta podem se aplicar igualmente a
avahi-daemon
, que examinei recentemente. (Posso 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:O ponto 3 pode ser particularmente bom quando você não está usando dbus ou similar... Acho que o avahi-daemon usa dbus, então ele garante o acesso ao dbus do sistema mesmo de dentro do chroot. Se você não precisa da capacidade de enviar mensagens no dbus do sistema, negar essa capacidade pode ser um bom recurso de segurança.
Observe que, se o avahi-daemon for reescrito, ele poderá optar por confiar no systemd para segurança e usar, por exemplo,
ProtectHome
. Propus uma alteração no 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 há 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 usei não teriam limitado o daemon de abrir arquivos de soquete unix (ponto 3 acima).
Outra abordagem seria usar o SELinux. No entanto, você estaria vinculando seu aplicativo a esse subconjunto de distribuições do Linux. A razão pela qual pensei no SELinux positivamente aqui é que o SELinux restringe o acesso que os processos têm no dbus, de maneira refinada. Por exemplo, acho que você pode esperar que
systemd
isso não esteja na lista de nomes de ônibus para os quais você precisa enviar mensagens :-)."Eu queria saber se usar sandbox systemd é mais seguro do que chroot/setuid/umask/..."
Resumo: por que não os dois? Vamos decodificar um pouco o que foi dito acima :-).
Se você pensar no ponto 3, usar o chroot fornece mais confinamento. ProtectHome= e seus amigos nem tentam ser tão restritivos quanto o chroot. (Por exemplo, nenhuma das listas negras de opções do systemd nomeadas
/run
, onde tendemos a colocar arquivos de soquete unix).chroot mostra que restringir o acesso ao sistema de arquivos pode ser muito poderoso, mas nem tudo no Linux é um arquivo :-). Existem opções do 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 para ele, o que pode tentar explorar uma vulnerabilidade. Por exemplo, o avahi-daemon não precisa de soquetes bluetooth e acho que seu servidor web também não :-). Portanto, não dê acesso à família de endereços AF_BLUETOOTH. Apenas coloque AF_INET, AF_INET6 e talvez AF_UNIX na lista de permissões, usando a
RestrictAddressFamilies=
opção.Por favor, leia os documentos para cada opção que você usa. Algumas opções são mais eficazes em combinação com outras e algumas não estão disponíveis em todas as arquiteturas de CPU. (Não porque a CPU seja ruim, mas porque a porta do Linux para essa CPU não foi tão bem projetada. Eu acho).
(Existe um princípio geral aqui. É mais seguro se você puder escrever listas do que deseja permitir, não do que deseja negar. Como definir um chroot, fornece uma lista de arquivos que você pode acessar, e isso é mais robusto do que dizer que deseja bloquear
/home
).Em princípio, você mesmo 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.
Portanto, recomendo apenas a leitura da seção de sandbox
man systemd.exec
em sua plataforma de destino. Mas se você deseja o design mais seguro possível, não teria medo de tentarchroot
(e depois descartarroot
privilégios) em seu programa também . Há uma compensação aqui. O usochroot
impõe algumas restrições em seu design geral. Se você já tem um design que usa chroot e parece fazer o que você precisa, isso parece ótimo.Se você pode confiar no systemd, então é realmente mais seguro (e mais simples!) deixar o sandbox para o systemd. (Claro, o aplicativo também pode detectar se foi iniciado em sandbox pelo systemd ou não, e o próprio sandbox se ainda for root.) O equivalente ao serviço que você descreve seria:
Mas não precisamos parar por aí. O systemd também pode fazer muitos outros tipos de sandbox para você – aqui estão alguns exemplos:
Consulte
man 5 systemd.exec
para muito mais diretivas e descrições mais detalhadas. Se você tornar seu daemon soquete ativável (man 5 systemd.socket
), você pode até 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 escuta apenas 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 torná-loRootDirectory
obsoleto, na minha opinião, então talvez você não precise mais 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 v232) também suportam
DynamicUser=yes
, onde o systemd alocará automaticamente o usuário do serviço para você apenas durante o tempo de execução do serviço. Isso significa que você não precisa registrar um usuário permanente para o serviço e funciona bem, desde que o serviço não grave em nenhum local do sistema de arquivos diferente deStateDirectory
,LogsDirectory
eCacheDirectory
(que você também pode declarar no arquivo de unidade – vejaman 5 systemd.exec
, novamente – e qual systemd irá gerenciar, tomando cuidado para atribuí-los corretamente ao usuário dinâmico).