Visão Geral de Segurança
Esta página descreve o modelo de segurança do EzyShield do ponto de vista do usuário. Para a análise de ameaças detalhada, veja o SECURITY-REVIEW interno (disponível no repositório ezy-shield).
Arquitetura
logs (SSH, Nginx)
↓
[ Collector ] — tail de arquivo, leitura do journald
↓
[ Parser ] — evento estruturado (IP, método, status)
↓
[ Rule Engine ] — scoring offline (roda sempre, sem rede)
↓
[ IA (opcional) ] — Anthropic/OpenAI/Ollama (só para eventos ambíguos)
↓
[ Decision Engine ] — decide ban/allow/defer
↓
[ Enforcer (com separação de privilégios) ] — aplica bans (nftables, Cloudflare)Princípio central: o daemon principal nunca detém privilégios elevados. Mutações de firewall só acontecem através de um binário separado, o ezyshield-enforcer, que detém CAP_NET_ADMIN.
Garantia de anti-lockout
O EzyShield tem uma regra dura: sua sessão SSH ativa e os CIDRs de admin nunca podem ser banidos, mesmo que casem com um padrão de ataque.
Antes de qualquer ban ser gravado no firewall, duas checagens independentes rodam em sequência:
- Checagem de allowlist: o IP alvo é comparado com a allowlist estática — sua
allowlistconfigurada, osadmin_cidrsdo policy.yaml e o peer SSH capturado quando o daemon iniciou. - Re-checagem de SSH ao vivo: o IP alvo é comparado com os peers SSH ativos agora — pela tabela de conexões do kernel (
/proc/net/tcpe/proc/net/tcp6, pontas remotas de conexões estabelecidas na(s) porta(s) do sshd — funciona sob systemd, sem depender de ambiente; portas lidas dosshd_config, fallback 22) e, em contextos interativos, pela variável de ambienteSSH_CLIENTatual.
Se qualquer uma das checagens casar, o ban é rejeitado.
Bans manuais (ezyshield ban) passam exatamente pelos mesmos guards — incluindo o rate limit de bans — e tentativas recusadas ficam registradas no audit log.
Isso é garantido em código, não por uma rule. Nenhum threshold mal configurado consegue te trancar fora.
A checagem roda duas vezes, em camadas independentes: o motor de decisão filtra primeiro, e um gate único na frente de todos os backends de enforcement re-checa cada ban e cada reconcile antes de chegar ao nftables ou a qualquer plataforma de edge. Mesmo um backend sem lógica própria de allowlist nunca recebe um endereço protegido — inclusive via um sync que o reintroduziria.
A re-checagem de SSH ao vivo protege uma conexão, não um endereço para sempre. Um bruteforcer que reconecta mais rápido do que a tabela de peers é relida mantém uma conexão estabelecida visível em toda avaliação, então cada tentativa dentro da janela do threshold é (corretamente) recusada. Para fechar a brecha em que uma rajada dessas nunca seria banida, um IP cujo ban foi recusado apenas por causa de uma conexão SSH ativa é reavaliado logo depois que essa conexão fecha — a partir do histórico de eventos ainda dentro da janela, pelos mesmos guards (allowlist, uma nova sondagem de peer SSH ao vivo, rate limit de bans). Se a conexão ainda estiver estabelecida no momento da reavaliação — uma sessão legítima do operador — a recusa simplesmente se repete e nada é banido. Um ban só pode resultar de uma checagem completa que não encontrou nenhuma conexão SSH estabelecida naquele instante; a sessão do operador continua imbanível enquanto estiver aberta.
Supremacia da allowlist
A allowlist é checada PRIMEIRO, antes de qualquer decisão do rule engine. Um IP na allowlist não pode ser banido por nenhuma rule, decisão de IA ou tentativa de ban manual.
allowlist:
- 10.0.0.0/8 # rede interna
- 198.51.100.7 # um host específico
admin_cidrs:
- 192.0.2.0/24 # faixas de admin, re-checadas antes de cada banLimite de taxa
Uma rule quebrada ou um feed envenenado não consegue banir a internet inteira. A trava max_bans_per_minute (default 30) rejeita bans excedentes com um erro explícito — nunca em silêncio, nunca descartando o limite. Bans de escalação que re-bloqueiam um IP cujo ban anterior terminou dentro da escalation_exempt_window (default 24h) ficam isentos dessa trava, para que um reincidente que já foi banido não volte a entrar enquanto o rate limit está saturado; bans de primeira ofensa sempre contam para a trava.
Resiliência da detecção
Detecção só serve se continuar rodando. Cada fonte de log (um tail de arquivo, um leitor de journalctl, um stream de container Docker) é vigiada por seu próprio supervisor. Se um collector sofre uma falha transitória — um arquivo de log momentaneamente ausente na inicialização, um logrotate cuja reabertura demora, o journald reiniciando e matando o journalctl — o supervisor reinicia esse collector automaticamente com backoff exponencial limitado, de modo que um contratempo operacional rotineiro não desative silenciosamente a detecção naquela fonte até o próximo restart do daemon.
O supervisor distingue uma falha real de um desligamento limpo: em SIGTERM/SIGINT (cancelamento de contexto) os collectors param e não são reiniciados, então o shutdown nunca entra em hot-loop. Um collector que falha repetidamente dispara uma notificação crítica nomeando a fonte, para que uma entrada permanentemente quebrada apareça para você em vez de tentar de novo em silêncio. As demais fontes de log continuam rodando o tempo todo — um collector com falha nunca derruba o pipeline.
Tratamento de segredos
Nenhum segredo aparece em:
- Arquivos de config (use a sintaxe
env:VAR_NAME) - Saída de log
- Mensagens de erro
- Prompts de IA
- Audit trail
Tokens de API são resolvidos uma única vez no startup e nunca impressos. Se um segredo for referenciado em um erro, o erro é reescrito para omiti-lo.
Segurança da IA
Quando a IA está habilitada para eventos ambíguos (scores dentro da ambiguous_band configurável):
- Validação de schema: a saída da IA é parseada em um tipo estruturado; respostas malformadas causam uma decisão de fallback.
- Clamp pela policy: a IA só pode sugerir dentro dos thresholds e durações de ban que você configurou. Ela não pode escalar além deles.
- Vínculo ao alvo: um veredito só pode nomear um IP que estava no lote analisado. Se o modelo alucinar (ou for comprometido) e nomear qualquer outro endereço, o veredito é descartado com um warning — a IA nunca escolhe o alvo do ban, apenas pontua IPs que os logs de fato observaram.
- Audit trail: todo veredito de IA (fonte, score, motivo) é persistido junto com o strike, então você pode auditar e sobrescrever se necessário.
- Sem prompt injection: linhas de log são passadas como dados, nunca interpoladas em instruções. O prompt é fixo e controlado.
Separação de privilégios
- Daemon principal (
ezyshield): roda como usuário sem privilégios, lê logs, toma decisões, comunica-se via unix socket - Enforcer (
ezyshield-enforcer): detém apenasCAP_NET_ADMIN, aceita um conjunto fixo e tipado de verbos (ping,add,del,list,flushe os verbos de allowlist), muta o nftables de forma segura e idempotente
O enforcer não é uma biblioteca. É um processo separado. O daemon principal não pode modificar o firewall diretamente.
A única concessão que muda isso: o grupo docker
Coletores de log docker (e o observador de docker exec) leem através do socket do Docker Engine, e o acesso a esse socket é concedido pela participação no grupo docker. Esse grupo não é uma permissão de leitura — ele é a API do Engine. Qualquer processo que o alcance pode iniciar um container com o filesystem do host montado, o que é root no host.
Portanto, em um host Docker onde o usuário de serviço está em docker, o primeiro item acima deixa de valer: o daemon é equivalente a root, e o hardening do systemd na unidade dele (NoNewPrivileges, ProtectSystem=strict, seccomp) não o contém, porque a fuga é pedir ao daemon do Docker — um processo root sem confinamento — que faça o trabalho.
O grupo é, portanto, a última de três formas de ler logs de container, e o EzyShield nunca o concede por conta própria. Em ordem de privilégio:
Um arquivo de log no host. O container grava o access log em um caminho bind-mountado do host e um coletor
kind: fileo lê. O EzyShield não recebe nenhum acesso ao Docker. Considere isso primeiro.Um proxy somente-leitura do socket. Um proxy com filtro na frente do socket do Engine, publicado em
127.0.0.1, responde logs e eventos de container e recusa criação de container, exec e mounts. Aponte odocker.hostpara ele:yamldocker: host: tcp://127.0.0.1:2375O daemon fala a API do Engine sobre TCP em loopback e fica fora do grupo
docker. O EzyShield não abre nenhum listener para isso — o proxy é um container da sua própria stack, e é a única coisa que toca o socket. Veja Docker + nginx + WordPress para o trecho de compose.O grupo
docker. Equivalente a root, como acima. Só quando nenhuma das outras duas serve.
Como a escolha é feita:
- o
ezyshield initoferece as três apenas quando você configura ao menos um coletor docker, pré-seleciona a de menor privilégio que se aplica e nomeia a consequência do grupo antes de pedi-lo; - instalações scriptadas escolhem explicitamente:
--docker-host tcp://127.0.0.1:2375(chave de respostacollectors.docker_host) ou--docker-group(chavecollectors.docker_group: true). Passar os dois é erro — o proxy substitui o grupo, não o acompanha. O--yesaceita padrões seguros, e nenhuma das duas concessões é padrão; - não escolher nada ainda escreve os coletores — eles apenas não conseguem ler os logs dos containers enquanto o acesso não existir;
- o
ezyshield doctoravisa sempre que o usuário de serviço está emdocker, e diz se algo na sua configuração justifica isso. Quando odocker.hosté um endpoint TCP, o doctor também o sonda: ele precisa responderGET /_pinge precisa recusarPOST /containers/create. Um endpoint que aceita criação de container é acesso equivalente a root ao host pela rede, e essa verificação FALHA.
Para verificar e revogar o grupo:
getent group docker # o ezyshield aparece na lista?
sudo gpasswd -d ezyshield docker # revoga
sudo systemctl restart ezyshield # tira o grupo do daemon em execuçãoRevogar desabilita os coletores docker, a menos que um dos dois primeiros caminhos esteja no lugar.
O docker.host aceita unix:///caminho (o padrão é unix:///var/run/docker.sock) e tcp://host:porta. Um host TCP precisa ser um literal de IP de loopback; qualquer outro valor é recusado, a não ser que você também defina docker.allow_remote: true, que aceita um endpoint do Engine em texto claro, sem autenticação e alcançável de fora do host. TLS para um engine remoto não é suportado.
Sem listeners de rede
O EzyShield não abre nenhum listener de rede para controle (o dashboard opcional faz bind apenas em um endereço loopback — 127.0.0.1 ou ::1 — e recusa qualquer outra coisa). O controle é via:
- CLI:
ezyshield ban,ezyshield list, etc. (somente local) - Unix socket:
/run/ezyshield/ezyshield.sock(permissões de filesystem)
Camadas de acesso do socket de controle
O acesso ao socket é imposto pelo kernel via grupo unix; o daemon adiciona uma allowlist de verbos por cima (issue #212). Quem pode fazer o quê:
| Camada | Socket | Grupo | Verbos |
|---|---|---|---|
| Operador | ezyshield.sock | ezyshield | tudo: verbos de leitura mais ban, unban, allow, unallow, arm, disarm, disable_all, prune, feeds_refresh |
| Visualizador | ezyshield-ro.sock | ezyshield-view | somente leitura: status, list, list_allow, events, report, subscribe (watch), metrics, feeds_status |
Os dois sockets têm modo 0660 e — como todo unix socket — NÃO são listeners de rede. A allowlist do visualizador é fechada e deny-by-default: um verbo adicionado no futuro é recusado no socket read-only até ser classificado deliberadamente como leitura. Pertencer ao ezyshield-view nunca muta estado — sem unban, sem disarm, sem edição de allowlist.
O postinstall dos pacotes cria os dois grupos; adicione um usuário de monitoramento com usermod -aG ezyshield-view <user>. A CLI recua automaticamente para o socket read-only quando o socket de operador nega permissão, então usuários da camada de visualização rodam ezyshield status, list, watch e report sem flags extras.
Toda requisição de verbo mutante — inclusive as recusadas — é registrada no audit journal append-only como uma entrada socket_cmd com uid/gid do peer requisitante (SO_PEERCRED), então "quem desarmou o daemon?" sempre tem resposta.
Fluxo de dados
Toda conexão de saída que o EzyShield pode fazer — providers de AI, Cloudflare, atualizações de GeoIP, notifiers, ezyshield update — é opt-in e está documentada, com gatilho, payload e forma de desligar, na Referência de Fluxo de Dados. Aquela página também traz a configuração exata para rodar totalmente local, com zero tráfego de saída. Não há telemetria.
Audit trail
Toda ação é registrada em SQLite:
- Quando: timestamp
- O quê: IP, rule, score, decisão (ban/allow/defer)
- Por quê: nome da rule, resposta da IA (se a IA foi consultada)
Exportação para compliance:
ezyshield report --json > report.json # histórico por IP com evidênciasSincronização com o Cloudflare
Ao usar Cloudflare Lists:
- Sync idempotente: o EzyShield reconcilia sua visão com o Cloudflare no startup do daemon, a cada 5 minutos e sempre que bans expiram (adiciona entradas faltantes, remove as obsoletas)
- Fonte da verdade: a tabela
bans_activeno SQLite é a fonte da verdade. Se o EzyShield cair e reiniciar, ele restaura os bloqueios do Cloudflare a partir do DB. - Rules alheias preservadas: o EzyShield só toca na sua própria lista de IPs (
ezyshield_blocked) e nas rules de WAF que ele mesmo criou (marcadas pela descrição). Rules do Cloudflare criadas à mão ficam intactas.
Dry-run por padrão
armed: false é o default no policy.yaml. Enforcement é opt-in. Você precisa definir explicitamente armed: true para começar a bloquear.
Antes de armar, rode em dry-run por 24+ horas e revise as decisões.
Dependências
O EzyShield é distribuído como dois binários Go estáticos (ezyshield + o ezyshield-enforcer com separação de privilégios) com dependências mínimas de runtime:
- nftables do kernel Linux (para enforcement local)
- API do Cloudflare (opcional, TLS verificado)
- API do provedor de IA (opcional, TLS verificado)
Sem Python, sem Ruby, sem runtime Java. Sem inspeção de pacotes de terceiros. Sem módulos de kernel.
Modelo de ameaças
No escopo (protegemos contra):
- Tentativas de login SSH por força bruta
- Scanners de login de WordPress/Drupal
- Port scanners e enumeração de serviços
- Bots e scrapers HTTP
Fora do escopo:
- Exploits de kernel
- Chaves SSH comprometidas
- Bugs de lógica na camada de aplicação
- Ameaças internas (insider threats)
- Comprometimento do provedor de IA (assumimos que a API da Anthropic/OpenAI é confiável)
Compliance
O EzyShield mantém:
- Audit trail completo (consultável via SQL)
- Nenhum PII nos logs (apenas endereços IP)
- Limite de taxa para prevenir negação de serviço
- Allowlist para tráfego liberado
- Modo dry-run para testar antes do enforcement
Os dados desse audit trail podem sustentar requisitos de registro de requisições de SOC 2, ISO 27001 e GDPR — a compliance em si depende dos controles organizacionais que você constrói ao redor dele, não do EzyShield sozinho.
Reportando problemas de segurança
Encontrou uma vulnerabilidade? Abra um security advisory privado no GitHub (aba Security → Report a vulnerability) — veja o SECURITY.md. Não abra uma issue pública.