Os grupos de ransomware aprenderam há anos que cifrar servidores não chega: se a vítima restaurar a partir do backup, ninguém paga. Por isso a primeira coisa que um ataque sério faz, depois de obter acesso de administrador, é procurar os backups e destruí-los. A variante que descrevemos em o ransomware que já ataca o Proxmox diretamente retira a proteção dos backups e apaga-os antes de cifrar um único disco.
A lição é desconfortável. Um backup que a máquina comprometida consegue apagar não é um backup, é uma segunda cópia que desaparece com a primeira. E um backup que ninguém restaurou também não é: é uma esperança.
Este post mostra como construir o outro tipo, com comandos que corremos num laboratório: um servidor de backups append-only onde a produção pode escrever mas nunca apagar, uma cópia externa sob Object Lock que ninguém consegue destruir antes do fim da retenção, e um teste de restauro mensal que prova que os dados voltam e te avisa quando não voltam.
Porque é que os backups falham perante o ransomware
Quando um ataque de ransomware leva os backups, é quase sempre por uma de três razões:
- As mesmas credenciais. O job de backup do servidor tem uma chave que consegue apagar o que escreveu. O atacante que controla o servidor controla essa chave.
- Os mesmos administradores. O servidor de backups ligado ao mesmo diretório, gerido a partir do mesmo jump host, com as mesmas contas de administrador. Se o domínio cair, a consola de backups cai com ele.
- Restauros nunca testados. O backup ficava verde todas as noites e, no dia em que importava, o dump estava vazio, a password de cifra estava no servidor cifrado ou o restauro demorou quatro dias.
A solução para as duas primeiras é uma regra: a produção pode acrescentar backups, nunca removê-los. A solução para a terceira é um teste que corre quer alguém se lembre quer não.
A regra: a produção acrescenta, nunca remove
A clássica regra 3-2-1 (três cópias, em dois tipos de suporte, uma fora das instalações) continua válida, mas sozinha não diz nada sobre quem pode apagar as cópias. Acrescenta duas condições:
- Pelo menos uma cópia é imutável: nada a que a produção chegue consegue apagá-la ou sobrescrevê-la antes do fim de um período de retenção.
- Os restauros são testados, com o resultado registado e com alertas, não presumidos.
A seguir, duas formas de obter a cópia imutável, ambas com ferramentas open source. Usa a primeira na tua própria infraestrutura, a segunda para a cópia externa, ou as duas.
Opção 1: um servidor de backups append-only
O restic é uma ferramenta de backup open source, e o seu complemento rest-server pode correr em modo append-only: os clientes podem criar snapshots e lê-los, mas qualquer pedido para apagar ou sobrescrever um ficheiro é recusado. A limpeza de snapshots antigos só acontece no servidor, por alguém com sessão iniciada lá.
No servidor de backups, um repositório por host, com TLS:
rest-server --path /srv/restic --listen :8000 --append-only --private-repos \
--tls --tls-cert /etc/rest-server/cert.pem --tls-key /etc/rest-server/key.pem
# one user per host; --private-repos limits each user to its own repository
htpasswd -B -c /srv/restic/.htpasswd db01Em cada host de produção, o job de backup só precisa do URL do repositório e da respetiva password:
export RESTIC_REPOSITORY=rest:https://db01:USER_PASSWORD@backup.internal:8000/db01/
export RESTIC_PASSWORD_FILE=/root/.restic-repo-password
pg_dump -Fc -f /var/backups/postgres/app.dump app
restic backup --host db01 /var/backups/postgres /etcTestámos o que um atacante com as credenciais de backup de um host consegue fazer. Fazer backup funciona. Tentar remover snapshots falha, e os snapshots ficam:
$ restic forget --host web01 --keep-last 1 --prune
remove 1 snapshots:
...
Remove(<snapshot/55f0cb7a99>) failed: unexpected HTTP response (403): 403 Forbidden
unable to remove snapshot/55f0cb7a99... from the repositoryA retenção corre no próprio servidor de backups, sobre o caminho local, com uma política baseada em tempo:
restic -r /srv/restic/db01 forget --host db01 --keep-within 30d --pruneA política baseada em tempo não é um pormenor. A documentação do restic avisa que um atacante que consegue acrescentar snapshots pode acrescentar alguns vazios com datas ligeiramente mais recentes do que as reais, para que uma política por número, como "mantém os três últimos", apague por ti todos os snapshots legítimos. --keep-within 30d mantém tudo dos 30 dias anteriores ao snapshot mais recente, por isso os reais sobrevivem. Corre-a enquanto o último backup bom tiver menos de 30 dias, olha para a lista de snapshots antes de limpar e mantém o alerta de frescura do backup que está mais abaixo.
Mais dois pontos. A password do repositório protege a confidencialidade dos dados; guarda uma cópia num sítio que não seja nem o host de produção nem o servidor de backups, porque perdê-la é perder todos os backups. E o servidor de backups não deve partilhar domínio de administração, chaves SSH nem caminho de rede de gestão com a produção.
Opção 2: Object Lock para a cópia externa
O armazenamento de objetos compatível com S3, nas grandes clouds ou alojado por ti, suporta Object Lock: depois de escrita, uma versão de um objeto não pode ser apagada nem sobrescrita até à data de retenção. Em modo compliance, ninguém pode encurtar essa retenção, nem o administrador da conta. Em modo governance, os utilizadores com uma permissão especial podem. Para backups contra ransomware, usa o modo compliance e escolhe uma retenção cujo armazenamento estejas disposto a pagar.
Ativa o Object Lock ao criar o bucket: alguns fornecedores conseguem acrescentá-lo a um bucket com versionamento já existente, nem todos. Exige versionamento, e criar o bucket com o bloqueio liga o versionamento. Com o cliente de linha de comandos padrão aws, que funciona com qualquer endpoint compatível com S3:
aws s3api create-bucket --bucket backups --object-lock-enabled-for-bucket \
--endpoint-url https://s3.example.internal
aws s3api put-object-lock-configuration --bucket backups --endpoint-url https://s3.example.internal \
--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":30}}}'Depois, o restic escreve nele como em qualquer outro repositório (RESTIC_REPOSITORY=s3:https://s3.example.internal/backups/db01).
Esta é a parte que a maioria dos guias salta, e verificámo-la no laboratório. Um atacante com a chave de backup ainda consegue correr restic forget --prune, ou apagar o prefixo inteiro, e as duas coisas parecem resultar. Depois, o restic diz que nem sequer existe repositório. O que aconteceu de facto é que cada apagamento acrescentou um delete marker, que esconde o objeto, enquanto as versões bloqueadas continuaram exatamente onde estavam. Uma tentativa direta de apagar uma versão bloqueada foi recusada.
Portanto os dados estão seguros, mas tens de saber como os trazer de volta. Remove os delete markers sob o prefixo do repositório, com credenciais que o atacante nunca teve:
EP=https://s3.example.internal
aws s3api list-object-versions --bucket backups --prefix db01/ --endpoint-url "$EP" \
--query 'DeleteMarkers[?IsLatest].[Key,VersionId]' --output text |
while read -r key ver; do
aws s3api delete-object --bucket backups --key "$key" --version-id "$ver" --endpoint-url "$EP" >/dev/null
done
restic unlock --remove-all # stale locks come back with the markers
restic snapshots
restic check --read-dataNo nosso teste voltaram todos os snapshots, incluindo o que o "atacante" tinha removido, e o check --read-data não encontrou erros. Só corras unlock --remove-all quando tiveres a certeza de que não há nenhum backup a correr.
Duas notas práticas: dá à chave do job de backup apenas as permissões de que o restic precisa nesse bucket, e acrescenta uma regra de ciclo de vida que expire as versões não atuais depois do período de retenção, senão os dados apagados continuam a somar custo de armazenamento.
Não tens a certeza de que os teus backups sobreviveriam a um atacante com permissões de administrador? Pede uma avaliação gratuita de backups e preparação para ransomware: seguimos as credenciais que conseguem apagar cada cópia e testamos contigo um restauro.
Mantém as chaves fora do alcance
A imutabilidade falha em silêncio quando as credenciais que a controlam estão ao lado da produção:
- Identidades separadas. O servidor de backups e a conta de armazenamento de objetos são administrados com contas que não existem no diretório de produção, com MFA.
- Nenhum caminho a partir da produção. Os hosts de produção chegam à porta da API do servidor de backups e a mais nada. Sem SSH da produção para o servidor de backups, sem jump host partilhado.
- A password do repositório, as credenciais do administrador do bucket e os passos de recuperação também ficam guardados offline, por exemplo impressos num cofre, para que a recuperação não dependa de sistemas que podem estar cifrados.
- Os administradores do hipervisor e dos backups são pessoas diferentes, ou pelo menos contas diferentes. Quem controla o hipervisor chega aos discos de todas as máquinas virtuais e, muitas vezes, também à configuração dos backups.
O teste de restauro que ninguém faz
Um job de backup que termina com 0 prova que o job correu, não que os dados voltam. A única prova é um restauro, por isso automatiza-o. Este script restaura o último snapshot de um host, carrega o dump da base de dados num contentor PostgreSQL descartável, verifica que uma tabela importante tem linhas e publica o resultado para o Prometheus através do textfile collector do node_exporter. Corre-o todos os meses a partir de uma máquina sem permissões para apagar o que quer que seja:
#!/usr/bin/env bash
# Restore drill: restore the latest snapshot of one host, prove the database dump in it
# loads and holds data, and publish the result for Prometheus (node_exporter textfile
# collector). Run it from a machine whose credentials cannot delete from the repository.
# RESTIC_REPOSITORY and RESTIC_PASSWORD_FILE must be set; docker must be available.
# usage: restore-drill.sh HOST DUMP_PATH TABLE
# e.g. restore-drill.sh db01 /var/backups/postgres/app.dump orders
set -uo pipefail
HOST="$1" DUMP="$2" TABLE="$3"
METRICS="${METRICS_DIR:-/var/lib/node_exporter/textfile_collector}"
WORK=$(mktemp -d)
PG="drill-pg-$$"
trap 'docker rm -f "$PG" >/dev/null 2>&1; rm -rf "$WORK"' EXIT
drill() {
restic restore latest --host "$HOST" --include "$DUMP" --target "$WORK" --verify || return 1
[ -s "$WORK$DUMP" ] || { echo "no dump at $DUMP in the latest snapshot" >&2; return 1; }
docker run -d --name "$PG" -e POSTGRES_PASSWORD=drill -v "$WORK$DUMP:/drill.dump:ro" postgres:16-alpine >/dev/null || return 1
for _ in $(seq 1 60); do docker exec "$PG" pg_isready -q -U postgres && break; sleep 1; done
docker exec "$PG" pg_restore -U postgres -d postgres --no-owner --exit-on-error /drill.dump || return 1
rows=$(docker exec "$PG" psql -U postgres -tAc "SELECT count(*) FROM $TABLE") || return 1
[ "$rows" -gt 0 ] || { echo "table $TABLE is empty in the restored dump" >&2; return 1; }
echo "restored $rows rows from $TABLE"
}
start=$(date +%s)
if drill; then ok=1; else ok=0; fi
end=$(date +%s)
publish() { # file, content: write then rename, so the collector never reads half a file
printf '%s\n' "$2" > "$METRICS/$1.tmp" && mv "$METRICS/$1.tmp" "$METRICS/$1"
}
publish "restore_drill_$HOST.prom" "backup_restore_drill_success{host=\"$HOST\"} $ok
backup_restore_drill_duration_seconds{host=\"$HOST\"} $((end - start))"
# Only a success moves this timestamp, so a run of failures shows up as an overdue drill.
[ "$ok" = 1 ] && publish "restore_drill_last_success_$HOST.prom" \
"backup_restore_drill_last_success_timestamp_seconds{host=\"$HOST\"} $end"
exit $((1 - ok))Testámo-lo nos dois sentidos. Com um dump bom restaurou 500 linhas e publicou backup_restore_drill_success 1. Com um dump cuja tabela estava vazia, o tipo de backup que fica verde durante meses, falhou, publicou 0 e deixou intacto o timestamp do último sucesso. A métrica de duração é o teu tempo de restauro medido para esse sistema; compara-a com o tempo de recuperação que prometeste ao negócio.
Adapta a verificação ao que importa: uma contagem de linhas da tabela que mais custaria perder, uma lista de checksums para partilhas de ficheiros ou arrancar a aplicação com os dados restaurados.
Alertas sobre o que importa
Depois de cada backup, publica quando foi feito o snapshot mais recente:
ts=$(restic snapshots --host "$HOST" --latest 1 --json | jq -r '.[-1].time')
printf 'backup_last_snapshot_timestamp_seconds{host="%s"} %s\n' "$HOST" "$(date -d "$ts" +%s)" \
> "$METRICS/backup_$HOST.prom.tmp" && mv "$METRICS/backup_$HOST.prom.tmp" "$METRICS/backup_$HOST.prom"Com isso, três alertas do Prometheus cobrem as falhas que importam: nenhum backup novo, um teste falhado e um teste que já não passa há demasiado tempo:
groups:
- name: backups
rules:
- alert: BackupStale
expr: time() - backup_last_snapshot_timestamp_seconds > 26 * 3600
labels:
severity: critical
annotations:
summary: "No new backup snapshot for {{ $labels.host }} in 26 hours"
- alert: RestoreDrillFailed
expr: backup_restore_drill_success == 0
labels:
severity: warning
annotations:
summary: "The last restore drill for {{ $labels.host }} failed"
- alert: RestoreDrillOverdue
expr: time() - backup_restore_drill_last_success_timestamp_seconds > 35 * 86400
labels:
severity: warning
annotations:
summary: "No successful restore drill for {{ $labels.host }} in 35 days"Validámos as regras com promtool check rules e testámo-las com promtool test rules: o alerta de backup antigo dispara ao fim de 26 horas sem snapshot e não antes, e o de teste em atraso dispara no dia 35 depois do último teste com sucesso.
Uma rotina mensal
Põe isto no calendário, com um nome ao lado:
- 1Confirma que o teste de restauro passou em todos os sistemas críticos e lê as durações de restauro.
- 2Corre
restic check --read-data-subset 10%em cada repositório: todos os meses é relida e verificada uma décima parte aleatória dos dados guardados. - 3Lê a lista de snapshots de cada repositório antes de a limpeza correr no servidor. Snapshots que não esperavas são o primeiro sinal de que alguém tem as credenciais de backup.
- 4Uma vez por trimestre, restaura um sistema completo numa máquina limpa, só com a cópia externa e as instruções offline. É o único teste do procedimento que vais usar de facto.
A nossa prática de backup e recuperação de desastres configura e opera isto, e a nossa equipa de cibersegurança testa se um atacante com acesso de administrador ainda conseguiria chegar aos teus backups.
Avaliação gratuita de backups e preparação para ransomware
Se não tens a certeza de que os teus backups sobreviveriam a alguém com acesso de administrador, verificamos isso contigo:
- Caminhos de apagamento: cada credencial, host e conta de administrador que consegue apagar ou sobrescrever cada cópia de backup.
- Imutabilidade: se pelo menos uma cópia é append-only ou está sob Object Lock em modo compliance, e durante quanto tempo.
- Prova de restauro: um restauro real de um sistema crítico, cronometrado, com o resultado comparado com o tempo de recuperação que prometeste.
Recebes uma lista por escrito das lacunas e da ordem para as fechar, sem qualquer compromisso.
Pede a tua avaliação gratuita, ou vê como funciona a nossa prática de backup e recuperação de desastres.