Todos os sistemas operacionais 6 regiões offshore Checkout sem KYC
Hands-on Guia de campo

Logs de servidor em um VPS: os dados que você decidiu não guardar

Você mudou a máquina para o exterior, pagou em Monero e colocou tudo atrás de um túnel. Depois o nginx escreveu uma linha. Toda requisição que o seu servidor responde deixa um registro datado de quem perguntou, de onde, para quê — e, em uma imagem padrão, esse registro sobrevive quinze dias em /var/log, um mês no seu banco de dados do fail2ban, e o tempo que os seus backups guardarem, o que costuma ser mais longo que os dois. Este guia mira a máquina em vez da rede: decidir o que o seu próprio servidor anota, truncar o endereço antes que a linha seja montada em vez de limpá-la depois, e manter só o suficiente para bloquear um atacante e depurar um 500 sem acumular um histórico de visitantes que você preferiria não estar guardando.

Atualizado em 2026-09-15 · 15 min de leitura · Operações de frota
Nesta página
  1. A requisição que você atendeu agora é um registro que você guarda
  2. Seis logs, e os dois que identificam pessoas
  3. Trunque no momento da escrita, não na rotação
  4. O que o truncamento quebra, e os três trabalhos que um log realmente faz
  5. journald, auth.log, e o rastro que leva de volta até você
  6. A sua política de retenção é o que os seus backups disserem que é
  7. As cópias que você não controla
  8. Minimização por design, não exclusão ao ser notificado
  9. Passo a passo
SP·01

A requisição que você atendeu agora é um registro que você guarda

Tudo o mais nesta série aponta para fora. Esconder a origem, criptografar o disco, escolher a jurisdição, impedir que outra pessoa guarde um log seu. Este aqui aponta para a própria máquina, porque um servidor que responde requisições anota quem as fez — por padrão, em vários lugares ao mesmo tempo, com uma retenção que ninguém escolheu.

Veja o que uma imagem padrão está guardando quinze dias depois do deploy. /var/log/nginx/access.log tem uma linha por requisição: endereço, timestamp, caminho, referrer, user agent, rotacionado diariamente e mantido por quatorze dias. /var/log/auth.log tem cada sessão SSH, incluindo o endereço de origem de cada uma das suas. O journal tem os mesmos eventos de novo, mais o que quer que os seus serviços tenham impresso no stderr. Se o Docker estiver rodando, cada contêiner tem um arquivo de log JSON que, de fábrica, não tem limite de tamanho nenhum. Se o fail2ban estiver rodando, ele tem um log de cada endereço que já baniu e um banco de dados SQLite dizendo a mesma coisa, e os endereços em ambos estão completos.

Nada disso é malicioso, e a maior parte é genuinamente útil — durante mais ou menos uma hora depois que algo dá errado. O problema é o formato: fidelidade total, guardada por muito tempo, por acidente e não por decisão. A pergunta que vale a pena fazer sobre cada um desses arquivos não é "isso é sensível?", mas sim "o que eu realmente faria com a linha que escrevi três semanas atrás?" Para a esmagadora maioria das linhas na esmagadora maioria dos servidores, a resposta é nada, e um registro que você nunca vai ler é puro risco — diante de um comprometimento, de um backup que sobrevive à máquina, de quem mais cedo ou mais tarde vier pedir por ele.

Minimização de dados é o nome formal para a correção, e é a ideia menos controversa em proteção de dados: colete o que o trabalho exige, guarde enquanto o trabalho exigir, depois pare. O que vem a seguir aplica isso a uma máquina que você realmente administra, partindo do princípio de que você já passou pela primeira hora após a implantação e que há algo que vale a pena proteger na máquina.

SP·02

Seis logs, e os dois que identificam pessoas

Antes de mudar qualquer coisa, conheça o inventário. Um VPS Debian ou Ubuntu pequeno rodando um serviço web normalmente escreve seis streams, e eles se sobrepõem mais do que as pessoas esperam — o mesmo evento frequentemente cai em três arquivos com três retenções diferentes.

O access log do nginx é o que identifica os seus visitantes. Uma linha por requisição, com o endereço do cliente, o caminho exato, o referrer e uma string de user-agent detalhada o bastante para ser, sozinha, um fingerprint fraco. O error log do nginx é o que as pessoas esquecem: ele registra client: 203.0.113.9 em todo timeout de upstream, todo 403, toda requisição malformada — e, diferente do access log, o formato dele é fixo e não pode ser personalizado com template.

O auth log/var/log/auth.log na família Debian, /var/log/secure na família RHEL — identifica você. Toda linha de publickey aceita carrega o seu endereço de origem e o fingerprint da chave que abriu a sessão. Quem ler um mês dele descobre de quais redes você administra, em que horários, e quantas chaves distintas você possui. Em uma máquina de posse anônima, esse arquivo costuma ser mais revelador do que qualquer coisa que os seus visitantes tenham gerado.

O journal guarda uma cópia da maior parte do que já foi dito, mais o stdout e o stderr de cada unidade, e em uma instalação padrão ele pode crescer até 10% do sistema de arquivos, com teto de 4 GB, antes de começar a descartar as entradas mais antigas. Em qualquer disco de 40 GB ou mais, esse teto é os 4 GB completos, o que, nos volumes que um servidor pequeno produz, equivale a muitos meses.

Os logs de aplicação são o coringa. Um framework em modo debug registra URLs completas, incluindo query strings, e query strings costumeiramente carregam tokens de sessão, links de redefinição de senha e termos de busca. O PHP-FPM pode ser configurado para escrever o próprio access log, que duplica a linha de requisição que o nginx já escreveu, a partir de um arquivo que as suas configurações de nginx nunca tocam.

Os logs de contêiner são o silencioso da lista. O driver json-file, padrão do Docker, não tem rotação nenhuma a menos que você configure, então /var/lib/docker/containers/*/*-json.log guarda tudo o que o contêiner já disse desde que foi criado. É uma forma comum de descobrir que uma política de retenção de "14 dias" na verdade está guardando onze meses, e é o mesmo tipo de surpresa que as regras de firewall que o Docker escreve por trás do ufw.

Dos seis, dois carregam identificadores de seres humanos que vale a pena minimizar de propósito: o access log do nginx (os seus visitantes) e o auth log (você). Os demais, na maior parte, só precisam de um teto de tamanho e um relógio mais curto.

SP·03

Trunque no momento da escrita, não na rotação

O instinto é continuar registrando normalmente e limpar depois — um cron noturno que reescreve o arquivo de ontem com os endereços removidos. Não construa isso. Um job de limpeza significa que os endereços brutos realmente existiram no disco por até um dia inteiro, e durante esse dia eles foram capturados pelo seu backup, possivelmente copiados pelo snapshot em nível de bloco do seu provedor, e deixados em qualquer coisa que o sistema de arquivos tenha feito com os blocos antigos. Pior, o job é uma peça móvel: ele falha silenciosamente na semana em que o disco enche, e nada te avisa que o arquivo de ontem ainda está completo.

A única redução em que você realmente pode confiar é a que acontece antes de a linha ser escrita. No nginx isso é um bloco map avaliado no momento do log, que reescreve o endereço em uma variável nova, e um log_format que usa essa variável nova em vez de $remote_addr. O endereço completo nunca chega a ser serializado. Não há nada para limpar depois, nada para agendar, e nada para dar errado no dia em que você não está olhando.

# /etc/nginx/conf.d/00-privacy-log.conf  --  http context, loaded before the sites
map $remote_addr $ip_trunc {
    # IPv4: keep the /24, zero the host part
    ~(?<v4>\d+\.\d+\.\d+)\.     "${v4}.0";
    # IPv6: keep the first two groups, drop the rest
    ~(?<v6>[^:]+:[^:]+):          "${v6}::";
    # anything the two patterns cannot parse -- including compressed forms
    # like ::1 -- falls through here, i.e. fails closed rather than open
    default                       "0.0.0.0";
}

log_format privacy '$ip_trunc - - [$time_local] "$request" $status '
                   '$body_bytes_sent "$http_referer" "$http_user_agent" '
                   'rid=$request_id rt=$request_time';

Três detalhes decidem se isso realmente funciona. Primeiro, o map precisa viver no contexto http — dentro de um bloco server o nginx se recusa a iniciar. Colocá-lo em conf.d/ com um nome que ordene cedo é a forma mais simples de garantir isso.

Segundo, se você está atrás da Cloudflare ou de qualquer outra edge, verifique o que $remote_addr contém. O módulo realip o substitui pelo endereço real do cliente logo no início do processamento da requisição, e mantém o endereço de conexão em $realip_remote_addr. Essa ordem está a seu favor: o map roda no momento do log, então ele trunca o visitante real, e não a edge. Mas isso também significa que, se você não configurou o realip, você está truncando o endereço da Cloudflare e não aprendendo nada, enquanto o endereço verdadeiro fica sentado em um header.

Terceiro, audite o resto da format string. Truncar $remote_addr não resolve absolutamente nada se a linha ainda terminar com "$http_x_forwarded_for" ou carregar $http_cf_connecting_ip — e boa parte dos formatos padrão e de painel de controle inclui um desses. O endereço completo está nos headers da requisição; ele só fica de fora do arquivo se você deixar todo header que o carrega fora do format.

Note o que o substituiu: $request_id, uma string hexadecimal aleatória de 32 caracteres que o nginx gera por requisição. Esse é o eixo da próxima seção.

SP·04

O que o truncamento quebra, e os três trabalhos que um log realmente faz

Sempre tem alguém que objeta que logs anonimizados são inúteis, e essa pessoa tem razão pela metade — porque "os logs" são três trabalhos sem relação nenhuma vestindo um único nome de arquivo, e só um deles precisa do endereço.

Trabalho um: bloquear quem estiver martelando você agora. Isso precisa do endereço completo, e precisa dele em segundos. Não precisa dele amanhã. O fail2ban é a ferramenta de sempre, e ele genuinamente não consegue funcionar com um arquivo truncado — banir 203.0.113.0 bane um host inocente e deixa o atacante conectado. Mas esse trabalho fica satisfeito com um arquivo que existe por um dia, ou sem usar arquivo nenhum: o próprio limit_req e limit_conn do nginx guardam o estado em memória compartilhada, agem em microssegundos em vez de no intervalo de polling do fail2ban, e não escrevem nada em disco.

Trabalho dois: descobrir por que aquela requisição retornou um 500. Isso precisa de correlação, não de identidade. Um request ID que atravessa do nginx até a aplicação amarra a linha do access log, o erro de upstream e o stack trace da aplicação a um único evento — que é o que você realmente estava tentando fazer quando recorria ao endereço. Na prática o ID é melhor: ele sobrevive a um cliente em rede móvel cujo endereço muda no meio da sessão, e não perde validade quando quatro visitantes compartilham um endereço CGNAT.

Trabalho três: entender o tráfego ao longo do tempo. Volume, mistura de status codes, quais caminhos estão quentes, se o crawler está fora de controle. Um endereço truncado é suficiente aqui, e o /24 que sobrevive ao truncamento já basta para ver que uma única rede é responsável por 40% das suas requisições.

Então o desenho não é "registrar menos", mas dividir por relógio: um stream de fidelidade total que vive por um dia e alimenta as ferramentas de bloqueio, e um stream truncado que vive pelo tempo que você quiser manter estatísticas. Os dois são escritos pelo nginx no mesmo instante, então não há etapa de processamento no meio, nem uma janela em que a coisa errada fique em disco por mais tempo do que deveria.

# inside the server block, or in a snippet included by it
access_log  /var/log/nginx/access.log       privacy;   # truncated, keep for weeks
access_log  /var/log/nginx/security.log     secip;     # full address, keep for a day

# static assets are pure noise in a privacy log -- drop them entirely
location ~* \.(?:css|js|svg|png|jpe?g|webp|woff2?|ico)$ {
    access_log off;
    expires 30d;
}

O formato secip é uma linha só, ao lado do outro — $remote_addr, o timestamp, a requisição e o status, nada mais. É o único arquivo na máquina onde um endereço de visitante completo tem permissão para ficar, o que torna a sua retenção uma única decisão em um único lugar, em vez de uma propriedade sobre a qual você precisa raciocinar espalhada por seis arquivos.

SP·05

journald, auth.log, e o rastro que leva de volta até você

A privacidade do visitante é a parte sobre a qual todo mundo escreve. O rastro do administrador é a parte que importa em uma máquina cujo objetivo inteiro é que o seu nome não esteja vinculado a ela, e ele está quase inteiramente em dois lugares.

/var/log/auth.log registra uma linha Accepted publickey para cada sessão que você abre, com o seu endereço de origem e o fingerprint da sua chave. Ao longo de um mês, isso é uma agenda dos seus hábitos de trabalho e uma lista das redes que você usa. Se você sempre se conecta pelo mesmo túnel, isso é um único endereço repetido e relativamente sem graça. Se você se conecta de onde quer que esteja, isso é um histórico de viagens.

O journal guarda os mesmos eventos, mais tudo o que as suas units imprimiram, e os padrões dele são generosos: SystemMaxUse= é 10% do sistema de arquivos, e MaxRetentionSec= não está definido, o que significa que não há limite de tempo nenhum — só um limite de tamanho. Em um servidor tranquilo, essa combinação retém meses.

Existe uma troca real aqui, e ela merece ser dita sem rodeios, não apenas mencionada de passagem. Os logs que descrevem você são os mesmos logs que dizem como alguém entrou. Defina Storage=volatile e o journal passa a viver só na RAM, desaparecendo a cada reboot — genuinamente privado, e genuinamente inútil na manhã em que você encontrar um processo que não iniciou, porque o primeiro reboot do seu atacante apagou a evidência. Para a maioria das pessoas, o meio-termo sensato é armazenamento persistente com um teto rígido e um relógio curto: longo o bastante para investigar um incidente que você percebe em até uma semana, curto o bastante para que o arquivo não seja um diário.

Duas notas de implementação que pegam as pessoas de surpresa. Imagens Debian e Ubuntu diferem quanto a ter o rsyslog instalado ou não; se /var/log/auth.log existe na sua máquina, então é o rsyslog que está escrevendo nele, e os limites do journald não governam esse arquivo — isso é trabalho do logrotate. E ForwardToSyslog= é o que alimenta o rsyslog a partir do journal, então desativá-lo em uma máquina que tem os dois evita que você fique guardando duas cópias de tudo sob duas políticas de retenção diferentes.

SP·06

A sua política de retenção é o que os seus backups disserem que é

Esta é a que desfaz todo o trabalho cuidadoso feito até aqui, e ela é invisível a menos que você vá procurar.

Digamos que o logrotate mantenha quatorze dias de logs do nginx, e você esteja satisfeito com isso. Agora acrescente o backup fora do site que você fez certo em configurar: uma execução diária do Borg ou do restic, com retenção de sete diários, quatro semanais e seis mensais. Cada um desses arquivos contém o /var/log como ele estava no dia em que rodou. O arquivo mensal mais antigo tem seis meses e guarda os quatorze dias de logs que eram atuais naquele momento. A sua retenção efetiva de logs não é quatorze dias. São seis meses, em um repositório criptografado que você não consegue dar grep sem restaurar, em uma segunda máquina, em um país diferente.

Existem exatamente duas soluções honestas. Excluir os diretórios de log do backup — eles são a única coisa em um servidor que você quase sempre consegue reconstruir ou passar sem, e uma restauração que omite o /var/log não é uma restauração pior. Ou incluí-los de propósito e aceitar que a sua retenção real é a do repositório, e nesse caso diga isso em qualquer política que você publicar, porque a alternativa é uma política declarada que a sua própria infraestrutura contradiz.

# exclude logs from the backup, and prove it took
borg create --stats                       \
    --exclude '/var/log'                  \
    --exclude '/var/lib/docker/containers' \
    ::'{hostname}-{now:%Y-%m-%d}' /

# the check that matters: can you still find an address in the newest archive?
borg list ::"$(borg list --short --last 1)" | grep -c '^var/log/' || echo "clean"

Já que você está nesse estado de espírito, mais dois vizinhos do mesmo problema. Os snapshots do seu provedor não são seus. Um snapshot em nível de hypervisor captura o disco como ele estava, incluindo logs que você já rotacionou para fora desde então, e ele vive no armazenamento do provedor, sob a retenção do provedor — o que é mais um motivo para os endereços nunca terem sido escritos por completo, e não um motivo para fazer algo esperto depois.

E não recorra ao shred em um VPS. Sobrescrever um arquivo em um disco virtual thin-provisioned, sobre um sistema de arquivos copy-on-write, em cima de um SSD que remapeia blocos para wear-levelling, não sobrescreve de forma confiável as células físicas que o guardavam. Exclusão segura em armazenamento alugado e virtualizado é teatro. A redução que funciona é a que você fez no momento da escrita; tudo depois disso é um melhor esforço que você não consegue verificar. Criptografar o volume muda essa equação — mas muda antes de os dados serem escritos, o que é a mesma lição de novo.

SP·07

As cópias que você não controla

Minimização na sua própria máquina é uma camada entre várias, e ter clareza sobre as outras é o que impede que isso vire uma falsa sensação de completude.

A rede da sua hospedagem vê o registro de fluxo. Origem, destino, portas, bytes, timing — de cada conexão que entra e sai da máquina, quer você registre alguma coisa, quer não. Nenhuma configuração no servidor muda isso. É boa parte do motivo pelo qual a jurisdição em que a máquina está é uma variável real, e não uma de marketing; o que é exigido por lei da rede difere enormemente de país para país.

A sua CDN ou edge registra os próprios logs, na borda. Se a Cloudflare termina o TLS para você, ela tem a linha da requisição e o endereço do cliente antes de o seu servidor sequer entrar em cena, na própria agenda de retenção dela, sujeita ao próprio processo legal dela. Truncar o seu log de origem não alcança isso retroativamente. Rodar uma edge que você mesmo possui é a versão disso que você realmente consegue configurar — e as regras de log deste guia se aplicam primeiro à máquina de edge, já que é lá que os endereços não truncados chegam.

Rastreadores de erro e analytics tiram isso da máquina por você. O Sentry e a maioria dos concorrentes dele anexam o IP do cliente a cada evento por padrão; a configuração normalmente se chama algo como send_default_pii, e vale a pena checar em vez de presumir. Qualquer analytics hospedado é, por construção, uma cópia de terceiro do access log que você acabou de passar uma tarde truncando.

E-mail é o mais vazador de todos. Se qualquer coisa na máquina manda e-mail, os headers carregam o host e o endereço de quem enviou, e cada relay no caminho guarda uma cópia do envelope com timestamps. Um formulário de contato que te manda e-mail é um log que você não administra.

Nada disso torna o trabalho local inútil — a cópia local é a que é apreendida junto com a máquina, exfiltrada em uma invasão, ou entregue por você mesmo. Ela é simplesmente a única camada que você controla por completo, e tratá-la como o quadro inteiro é que é o erro.

SP·08

Minimização por design, não exclusão ao ser notificado

Vale a pena ser preciso aqui, porque as duas coisas são confundidas, e a diferença entre elas é a diferença inteira entre prática de engenharia padrão e algo que você não deveria fazer.

Decidir de antemão o que o seu serviço coleta e por quanto tempo guarda isso é prática comum, documentada e incentivada. Sob o GDPR, são dois dos princípios centrais — minimização de dados e limitação de armazenamento — e uma retenção de log mais curta é um controle que os auditores pedem, não um que eles questionam. Não existe obrigação geral, para um operador de site ou um cliente de hospedagem na UE, de reter logs de tráfego; a diretiva de retenção geral que antes sugeria o contrário foi derrubada pelo Tribunal de Justiça da União Europeia em 2014, e as leis nacionais que sobrevivem a isso majoritariamente vinculam provedores de telecomunicações, não pessoas que rodam um servidor web. Os Estados Unidos também não têm nenhum mandato geral de retenção para operadores de site.

Destruir registros específicos depois de você ter sido notificado sobre eles é um ato completamente diferente. Um pedido de preservação de dados, uma retenção determinada por litígio, uma ordem judicial ou uma investigação policial muda o que você pode fazer com os dados que existem naquele momento, e "minha política de retenção apagou isso" não é uma defesa se você acelerou a exclusão por causa da notificação. Nada neste guia é sobre isso. Uma política de retenção é algo que você define em uma terça-feira qualquer e depois deixa quieto; se ela só é encurtada quando alguma coisa acontece, ela nunca foi uma política.

A mesma distinção atravessa o resto deste site: existe uma linha real entre engenharia de privacidade e a postura "bulletproof" que se vende como imunidade. Desenhar um serviço que nunca acumula um histórico de visitantes fica confortavelmente do lado certo dessa linha, na mesma categoria de criptografar os seus discos e não pedir aos usuários um endereço de e-mail que você não precisa.

Duas consequências práticas. Se você publica uma política de privacidade, faça os números de retenção dela baterem com o que realmente está no disco — quatorze dias declarados e seis meses reais em um repositório de backup é o tipo de lacuna que transforma um operador de boa-fé em um de má-fé no papel. E anote a decisão para você mesmo, em um comentário no topo do arquivo do logrotate se não houver outro lugar, porque a pessoa que vai ter que justificar esses números daqui a dezoito meses é você, e ela não vai lembrar por que o número era sete. Nada do que foi dito acima é aconselhamento jurídico; se você opera em algum lugar com deveres de retenção específicos de setor, verifique-os contra a sua própria situação antes de encurtar qualquer coisa.

SP·09

Passo a passo

  1. 01

    Descubra o que a máquina já está guardando

    Não configure nada antes de medir. O objetivo desta etapa é achar o arquivo que você esqueceu, que na maioria das máquinas é um log de contêiner ou um log de aplicação que ninguém olha desde o deploy.

    # biggest log files anywhere on the box, largest last
    sudo du -ah /var/log /var/lib/docker/containers 2>/dev/null \
      | sort -h | tail -20
    
    # how much disk the journal holds, and how far back it goes
    journalctl --disk-usage
    journalctl --output=short-iso | head -1
    
    # how old is the oldest nginx line still on disk?
    zcat -f /var/log/nginx/access.log* 2>/dev/null | head -1

    Depois olhe uma linha de cada arquivo e pergunte o que ela identifica. O comando abaixo conta quantos endereços completos e distintos estão atualmente recuperáveis a partir dos seus logs web — normalmente é esse número que justifica o resto deste guia.

    zcat -f /var/log/nginx/access.log* 2>/dev/null \
      | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u | wc -l

    Anote o que você encontrar. Você vai rodar esses mesmos comandos no final para provar que a mudança fez efeito.

  2. 02

    Trunque o endereço do cliente antes de o nginx escrever a linha

    Crie o map e os dois formatos em um arquivo carregado dentro do contexto http. No Debian e no Ubuntu, /etc/nginx/conf.d/ é incluído a partir do nginx.conf antes das configurações dos sites, que é exatamente onde isso pertence.

    sudo tee /etc/nginx/conf.d/00-privacy-log.conf > /dev/null <<'EOF'
    # The client address is reduced here, at log time, and never written in full
    # to the long-retention file. $realip_remote_addr still holds the connecting
    # address if you need it while debugging a proxy problem.
    map $remote_addr $ip_trunc {
        ~(?<v4>\d+\.\d+\.\d+)\.     "${v4}.0";
        ~(?<v6>[^:]+:[^:]+):          "${v6}::";
        # anything the two patterns cannot parse -- including compressed forms
        # like ::1 -- falls through here, i.e. fails closed rather than open
        default                       "0.0.0.0";
    }
    
    # Long retention: no full address, no forwarded-for header, request id instead.
    log_format privacy '$ip_trunc - - [$time_local] "$request" $status '
                       '$body_bytes_sent "$http_referer" "$http_user_agent" '
                       'rid=$request_id rt=$request_time';
    
    # Short retention: the one file allowed to hold a complete address.
    log_format secip   '$remote_addr [$time_local] "$request" $status';
    EOF
    
    sudo nginx -t && sudo systemctl reload nginx

    Agora aponte o site para eles. No seu bloco server, substitua a linha access_log existente pelo par, e desligue o log para os assets estáticos já que está nisso.

    access_log  /var/log/nginx/access.log    privacy;
    access_log  /var/log/nginx/security.log  secip;

    Recarregue, carregue uma página, e leia o resultado. O primeiro campo deve terminar em .0, e a linha deve carregar um valor rid=.

    sudo nginx -t && sudo systemctl reload nginx
    curl -s -o /dev/null https://your-domain.example/
    sudo tail -1 /var/log/nginx/access.log

    Se o endereço ainda estiver completo, o bloco server está sobrescrevendo o formato em algum lugar mais abaixo — grep -rn access_log /etc/nginx/ encontra a linha que vence.

  3. 03

    Mantenha endereços completos só onde algo age sobre eles

    Se o fail2ban estiver rodando, ele está atualmente lendo o arquivo que você acabou de truncar. Aponte-o para o log de segurança em vez disso, e dê a esse log uma vida de um dia para que os endereços completos expirem sozinhos.

    sudo tee /etc/fail2ban/jail.d/nginx-privacy.local > /dev/null <<'EOF'
    [nginx-http-auth]
    enabled  = true
    logpath  = /var/log/nginx/error.log
    
    [nginx-botsearch]
    enabled  = true
    logpath  = /var/log/nginx/security.log
    maxretry = 6
    findtime = 10m
    bantime  = 1h
    EOF
    
    sudo fail2ban-client reload

    Depois cuide da própria memória do fail2ban, que a maioria das pessoas nunca toca: ele mantém o próprio log de banimentos sob o padrão rotate 4 weekly, e um banco de dados SQLite de cada banimento que já emitiu. dbpurgeage é o que expira linhas desse banco de dados — defina algo próximo do seu banimento mais longo, em vez de deixar no padrão.

    sudo tee /etc/fail2ban/fail2ban.d/retention.local > /dev/null <<'EOF'
    [Definition]
    dbpurgeage = 2d
    loglevel   = NOTICE
    EOF
    
    sudo systemctl restart fail2ban

    Melhor ainda, para floods simples, faça o trabalho no nginx, onde nada é registrado. limit_req guarda os contadores em memória compartilhada, responde em microssegundos em vez de em um intervalo de polling, e não deixa registro nenhum de quem foi limitado — veja o runbook da primeira hora de DDoS para dimensionar as zonas sob carga real.

    # http context: keyed on the truncated address, so nothing complete is
    # held in memory either. 10m of shared state is plenty for a small site.
    limit_req_zone $ip_trunc zone=perip:10m rate=20r/s;
    
    # in the location you want protected
    limit_req zone=perip burst=40 nodelay;

    Definir a chave da zona por $ip_trunc em vez de $binary_remote_addr é uma troca deliberada: o limite agora se aplica a um /24 inteiro de uma vez, então um escritório movimentado atrás de um único bloco compartilha um orçamento. Para um site pequeno isso costuma ser bom, e ocasionalmente uma melhoria; se não for, defina a chave da zona pelo endereço completo — o estado de um rate limiter vive em memória compartilhada e nunca é escrito em disco, então isso não faz parte do que você está minimizando aqui.

  4. 04

    Limite o journal e pare a segunda cópia

    O journald aceita um drop-in, que sobrevive a atualizações de pacote de um jeito que editar o journald.conf não sobrevive. Os valores abaixo mantêm cerca de uma semana — o suficiente para investigar algo que você percebe na segunda-feira e que começou na sexta — dentro de um teto rígido de 200 MB.

    sudo mkdir -p /etc/systemd/journald.conf.d
    sudo tee /etc/systemd/journald.conf.d/retention.conf > /dev/null <<'EOF'
    [Journal]
    Storage=persistent
    SystemMaxUse=200M
    SystemMaxFileSize=20M
    MaxRetentionSec=7day
    MaxFileSec=1day
    ForwardToSyslog=no
    EOF
    
    sudo systemctl restart systemd-journald
    journalctl --disk-usage

    O restart aplica os tetos de tamanho imediatamente; o relógio de retenção é aplicado conforme novos arquivos são rotacionados, então um journal já maior do que deveria encolhe na próxima rotação em vez de instantaneamente. journalctl --vacuum-time=7d força isso agora, se você quiser o disco de volta hoje.

    ForwardToSyslog=no importa em qualquer imagem que venha com o rsyslog: sem isso, toda entrada do journal também é anexada a /var/log/syslog, sob a agenda do logrotate em vez da do journald, e você fica com duas cópias com duas datas de expiração diferentes. Verifique em qual situação você está antes de presumir:

    systemctl is-active rsyslog 2>/dev/null || echo "rsyslog not running"
    ls -la /var/log/auth.log /var/log/syslog 2>/dev/null

    Se esses arquivos existirem, o rsyslog é dono deles, e o passo seis é onde a retenção deles é definida. Se não existirem, o journal é a única cópia, e você acabou de limitá-la.

  5. 05

    Impeça a aplicação de registrar de novo o que você removeu

    Nada do que foi feito até aqui toca a sua aplicação, e uma aplicação no modo errado vai escrever alegremente o endereço completo, a URL completa e o token de sessão em um arquivo próprio dela. Três coisas para checar.

    O PHP-FPM traz uma diretiva access.log na configuração do pool, comentada por padrão mas ativada por muitos painéis de controle. Se estiver ativa, é uma segunda cópia de cada linha de requisição, a partir de um arquivo que o seu trabalho no nginx nunca tocou.

    grep -rn '^access.log' /etc/php/*/fpm/pool.d/ || echo "fpm access log off"

    O nível de log do seu framework decide se query strings e corpos de requisição vão parar no disco. O modo debug, na maioria dos frameworks, registra a URL completa, e um link de redefinição de senha é uma URL completa. Defina o nível de produção e confirme que é realmente esse o nível carregado, e não o do arquivo que você acha que está sendo lido.

    O driver padrão do Docker nunca rotaciona. Corrija isso no nível do daemon, para que todo contêiner futuro herde o teto. Note que isso se aplica a contêineres criados depois do restart — os existentes mantêm o arquivo atual, sem limite, até serem recriados.

    sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
    {
      "log-driver": "json-file",
      "log-opts": { "max-size": "10m", "max-file": "3" }
    }
    EOF
    
    sudo systemctl restart docker
    docker inspect --format '{{.HostConfig.LogConfig}}' $(docker ps -q) 2>/dev/null

    Se um contêiner já está segurando um arquivo grande, recriá-lo com docker compose up -d --force-recreate é o que realmente trunca o histórico — só reiniciar mantém o mesmo arquivo de log.

  6. 06

    Defina a retenção de propósito, em um único lugar por arquivo

    O logrotate é onde vive o relógio de tudo que o rsyslog e o nginx escrevem. Edite o bloco já existente em vez de adicionar um segundo: dois blocos nomeando o mesmo caminho fazem o logrotate falhar com um erro de entrada duplicada e parar de rotacionar esse arquivo por completo, que é a forma mais comum de uma mudança de retenção silenciosamente virar uma retenção infinita.

    # check for duplicates BEFORE editing, then again after
    sudo logrotate --debug /etc/logrotate.conf 2>&1 | grep -i 'duplicate\|error'

    No caso do nginx, os dois arquivos querem relógios diferentes: o truncado pode viver por semanas, o que guarda endereços completos não deveria sobreviver ao dia. Adicione um bloco separado para o log de segurança — um caminho diferente, então sem duplicata — e encurte o que já vem pronto.

    sudo tee /etc/logrotate.d/nginx-security > /dev/null <<'EOF'
    # The only file on this box that holds complete client addresses.
    # One day, uncompressed so fail2ban can read it. Do not lengthen without
    # a reason you would be happy to write down here.
    /var/log/nginx/security.log {
        daily
        rotate 1
        maxage 1
        missingok
        notifempty
        nocompress
        create 0640 www-data adm
        sharedscripts
        postrotate
            [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
        endscript
    }
    EOF
    
    sudo sed -i 's/^\trotate 14$/\trotate 7/' /etc/logrotate.d/nginx
    sudo logrotate --debug /etc/logrotate.d/nginx-security

    Faça a mesma aritmética para /etc/logrotate.d/rsyslog se o rsyslog estiver instalado — os padrões dele mantêm quatro semanas de auth.log, o que são quatro semanas das suas próprias sessões SSH. E rode a passagem de debug mais uma vez: ela imprime exatamente quais arquivos ela rotacionaria e apagaria, o que é a única forma de confirmar que os números que você acabou de digitar são os números em vigor.

  7. 07

    Prove isso, inclusive através do backup

    Rode de novo as medições do passo um. A contagem de endereços completos e distintos no log de retenção longa agora deve parar de crescer, e depois de um ciclo de rotação ela deve ser zero.

    # should print 0 once the pre-change files have rotated out
    zcat -f /var/log/nginx/access.log* 2>/dev/null \
      | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' \
      | grep -v '\.0$' | sort -u | wc -l
    
    # and the journal should now be bounded
    journalctl --disk-usage

    Depois, a verificação que quase ninguém faz: olhe dentro do backup. Um repositório que ainda carrega /var/log está guardando a fidelidade e a retenção que você acabou de passar uma tarde removendo, e vai continuar guardando enquanto o seu arquivo mais antigo sobreviver.

    borg list | head -3          # borg lists oldest first
    borg list ::"$(borg list --short | head -1)" 2>/dev/null \
      | grep -c '^var/log/' || echo "no logs in oldest archive"

    Se a contagem for diferente de zero, ou você adiciona a exclusão de antes e deixa os arquivos antigos expirarem, ou você os poda deliberadamente. Até que uma dessas coisas aconteça, a sua retenção real é a do repositório — que é a frase mais útil deste guia inteiro, e a mais fácil de esquecer.

    Por fim, anote os números em algum lugar onde a próxima pessoa vá encontrá-los: a retenção que você escolheu, o motivo, e a data. Um comentário no topo do arquivo do logrotate já basta. A configuração acima é uma decisão, e uma decisão que ninguém consegue reconstruir daqui a um ano vira um padrão de novo.

SP·10 — PERGUNTAS FREQUENTES

Respostas rápidas

Zerar o último octeto realmente anonimiza um endereço IP?

Não, e vale a pena ser exato sobre isso em vez de reconfortante. Truncar para um /24 é pseudonimização: isso estreita um visitante para um bloco de até 256 endereços em vez de um só, o que em um provedor residencial é uma vizinhança inteira, e em uma rede de pequena empresa ainda pode ser uma única organização. Combine o /24 com um timestamp preciso e uma string de user-agent detalhada, e a reidentificação costuma ser possível para uma parte determinada.

O que isso realmente faz é remover o único campo que torna um arquivo de log trivialmente cruzável com qualquer outro dataset do mundo, e remove isso antes de a linha ser escrita, o que significa que o valor preciso nunca existiu no seu disco para ser apreendido, vazado ou intimado judicialmente. Essa é uma redução de risco grande e real. Não é anonimato, e qualquer ferramenta que chame isso de anonimato está vendendo peixe.

O fail2ban continua funcionando se o meu access log estiver truncado?

Não a partir do arquivo truncado — a captura <HOST> vai bater com 203.0.113.0 e banir um único endereço que nunca se conectou a você, deixando a origem real intocada. Quem anonimiza os próprios logs sem perceber isso desligou o fail2ban silenciosamente.

A correção é a divisão descrita acima: o nginx escreve um segundo arquivo, de vida curta, com endereços completos, e o fail2ban lê esse. Note que /var/log/nginx/error.log também carrega endereços completos e não pode ser reformatado, então jails baseadas nele (as de falha de autenticação, normalmente) continuam funcionando de qualquer forma. Para abuso volumétrico em vez de credential stuffing, o limit_req do nginx é melhor que os dois: ele age dentro do próprio processo, em microssegundos, e não escreve nada em lugar nenhum.

Como faço para depurar um 500 em produção sem o endereço do cliente?

Com um request ID, que normalmente é melhor do que o endereço já foi. O nginx gera $request_id por requisição; coloque isso no seu format de log, encaminhe para o upstream com proxy_set_header X-Request-ID $request_id;, e registre isso a partir da aplicação junto com o stack trace. Uma única string agora amarra a linha do access log, o erro de upstream e a exceção.

Isso vence o endereço em três quesitos: é único por requisição, em vez de por cliente, então duas falhas simultâneas não se misturam; sobrevive a um cliente móvel cujo endereço muda no meio da sessão; e é seguro mostrar ao usuário na página de erro, então um relatório de bug chega com o identificador exato de que você precisa. Para o raro caso que realmente precisa do endereço — um ataque direcionado, por exemplo — o log de segurança de retenção curta tem isso por um dia.

Sou obrigado por lei a guardar logs de servidor?

Para um operador de site ou um cliente de hospedagem na UE, em geral não. O regime de retenção geral que as pessoas meio que lembram veio da Diretiva de Retenção de Dados de 2006, que o Tribunal de Justiça da União Europeia invalidou em 2014; as leis nacionais que restam se aplicam principalmente a provedores de comunicações eletrônicas — operadoras de telecomunicações e provedores de internet — e não a quem roda um servidor web. O GDPR empurra na direção contrária, para guardar menos por menos tempo. Os Estados Unidos também não têm nenhum mandato geral de retenção para operadores de site, embora um pedido de preservação possa chegar e mudar o que você pode fazer com os dados que existem naquele momento.

Onde isso fica específico é por setor e por papel: processamento de pagamento, finanças reguladas, saúde e ser efetivamente um provedor de internet carregam deveres próprios, e algumas jurisdições impõem obrigações a operadores de wi-fi público ou de plataforma. O país em que a máquina está muda a resposta de forma relevante. Isso não é aconselhamento jurídico — se você está em uma dessas categorias, verifique a sua própria situação antes de encurtar qualquer coisa.

E os logs que a Cloudflare ou a minha CDN guardam?

Eles existem independentemente do que você faz na origem, e truncar o seu próprio arquivo não alcança isso retroativamente através da edge. Se a Cloudflare termina o TLS para você, ela vê a requisição completa e o endereço real do cliente antes de o seu servidor sequer entrar em cena, retém isso na própria agenda dela, e responde ao próprio processo legal dela. O mesmo vale para qualquer WAF ou scrubber de DDoS hospedado.

Você tem duas alavancas. Verifique o que o seu provedor de edge retém e por quanto tempo, e reduza tudo o que ele permitir reduzir. Ou rode uma edge que você controla — um VPS pequeno fazendo a terminação de TLS e o proxy de volta através de um túnel — e nesse caso tudo neste guia se aplica primeiro a essa máquina, já que é ela que recebe os endereços completos de fato.

Eu deveria simplesmente desativar os access logs por completo?

Normalmente não, e o motivo é prático, não filosófico. Sem access log nenhum, você não consegue responder uma denúncia de abuso, não consegue distinguir um crawler de um ataque, não consegue dimensionar a máquina, e não consegue ver que um deploy começou a retornar 404 uma hora atrás. Você também perde a capacidade de demonstrar o que aconteceu se alguém alegar que o seu servidor fez algo que não fez.

O meio-termo é o que este guia constrói: mantenha a linha, remova o identificador, adicione um ID de correlação, e deixe o único arquivo que guarda endereços completos expirar em um dia. O único lugar onde access_log off é diretamente correto é nos assets estáticos — imagens, CSS, fontes — que são puro volume e não dizem nada que você já não consiga obter da requisição HTML que veio antes deles. Lembre-se de que o error log continua registrando endereços de cliente em falhas de qualquer jeito, o que é mais um motivo para dar a ele uma retenção curta, em vez de simplesmente desligá-lo de vez.

Coloque em prática

VPS online em 15 min, dedicado entregue em 2–12 h. Recarregue a partir de $30.00 em cripto — sem identidade vinculada.

Implante um VPS