O daemon cron é uma ferramenta familiar para qualquer administrador de sistemas Linux. Ele é responsável por executar tarefas agendadas, como backups, limpeza de arquivos temporários e execução de scripts esquecidos. No entanto, a aparente simplicidade do cron esconde um potencial perigoso: uma única linha mal configurada no crontab pode levar à perda de dados, esgotamento do disco, criação de fork bombs ou abertura de brechas de segurança para hackers. Neste artigo, detalharemos como e quando o cron se torna uma ameaça e quais comandos podem ajudar a mantê-lo sob controle.
Origem do Cron
O cron tem suas raízes no Unix clássico. Introduzido no Version 7 Unix em 1979, ele era executado durante a transição para o modo multiusuário. O agendador de tarefas do sistema verificava o arquivo /usr/lib/crontab a cada minuto, comparando as entradas com o tempo atual e executando os comandos correspondentes. As primeiras versões incluíam um único crontab do sistema, algumas tarefas regulares e a manutenção do próprio sistema. Com o aumento do número de usuários nas máquinas Unix, o daemon começou a falhar. Para resolver isso, a Universidade de Purdue desenvolveu uma versão do cron com uma fila de eventos e crontabs de usuário, que mais tarde foi incorporada ao Unix System V, BSD e sistemas Unix comerciais.
No final dos anos 1980, Paul Vixie lançou sua implementação do agendador, que é amplamente associada ao formato que vemos hoje no Linux: crontab, cinco campos de tempo, o comando no final da linha, variáveis de ambiente dentro do agendamento, @reboot, @daily e intervalos como */5. Atualmente, sistemas baseados em Red Hat geralmente utilizam cronie, enquanto Debian e Ubuntu empregam Vixie cron 3.0pl1 com patches. Essa diferença, no entanto, tem pouco impacto prático. Atualmente, o cron é usado para backups, limpeza de /tmp, rotação de logs customizada, atualização de cache, verificação de certificados, exportação de relatórios e pequenos scripts de serviço. Com o tempo, linhas de comando são adicionadas por novas funcionalidades, outras vêm com pacotes e algumas permanecem após migrações, resultando em um crontab com muitas entradas obscuras e frequentemente não limpas. No entanto, o problema não se limita a essas entradas; exploraremos cada ponto "perigoso" desta ferramenta.
Erros no Cron São Invisíveis
Quando o cron executa um processo, ele não possui um terminal ou uma sessão de shell interativa. Se um script falhar, o erro não será visível. Tudo o que o programa escreve em stdout ou stderr é enviado pelo cron ao proprietário do crontab ou a um endereço especificado em MAILTO. Antigamente, isso funcionava bem, pois servidores tinham agentes de e-mail locais configurados, a correspondência do sistema era lida e as mensagens de erro não se perdiam. Em VPS modernos, é raro ter um MTA (Mail Transfer Agent) instalado, e as caixas de e-mail raramente são verificadas. Como resultado, os erros simplesmente desaparecem. Portanto, cada tarefa do cron deve ter uma saída clara. Inicialmente, pode-se redirecionar stdout e stderr para um log separado: * * * * * /path/to/script.sh >> /var/log/my-task.log 2>&1. Isso já é suficiente para entender o que está acontecendo. No entanto, existem pré-requisitos: o usuário que executa a tarefa deve ter permissão para escrever em /var/log/my-task.log, e o próprio arquivo deve ser rotacionado. Caso contrário, em pouco tempo, o cron deixará de ser um problema, pois o disco cheio se tornará a primeira preocupação. Para um log simples do cron, pode-se adicionar um arquivo em /etc/logrotate.d/my-task:
/var/log/my-task.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
O parâmetro copytruncate é adequado para a maioria das tarefas do cron – o script é executado rapidamente, grava a saída e termina. Se os logs se tornarem volumosos, eles devem ser coletados centralmente. É mais conveniente enviar mensagens diretamente para o syslog ou journald. Para isso, a utilidade padrão logger é suficiente: * * * * * /path/to/script.sh 2>&1 | logger -t my-task. Após isso, as mensagens estarão disponíveis via journald: journalctl -t my-task --since "1 hour ago".
O local de armazenamento dos logs depende da distribuição. Em Debian e Ubuntu, as entradas do cron geralmente estão em /var/log/syslog. Em RHEL, AlmaLinux, Rocky Linux e outros sistemas baseados em Red Hat, um journal separado é mais comum. Se o cron for executado via systemd, o nome do serviço dependerá da distribuição. É possível verificar ambas as opções:
bashjournalctl -u cron -S today journalctl -u crond -S today
O método com logger tem uma peculiaridade: o código de retorno dessa linha será o código do último comando no pipe, ou seja, logger, e não do seu script. Por isso, para backups, exportações e tarefas com dados, é melhor usar um wrapper que salve separadamente o log, o código de saída e um bloqueio contra execuções repetidas. Nesses casos, um pequeno script wrapper é preferível. Ele permite coletar convenientemente tudo o que é geralmente necessário para a operação: bloqueio de execução repetida, logging e verificação do código de término.
bash#!/bin/bash set -Eeuo pipefail LOCKFILE="/run/lock/my-task.lock" LOGFILE="/var/log/my-task.log" exec 200>"$LOCKFILE" if ! flock -n 200; then echo "$(date -Is): already running, exiting" >> "$LOGFILE" exit 0 fi echo "$(date -Is): starting" >> "$LOGFILE" /path/to/actual/script.sh >> "$LOGFILE" 2>&1 rc=$? if [ "$rc" -ne 0 ]; then echo "$(date -Is): failed with exit code $rc" >> "$LOGFILE" exit "$rc" fi echo "$(date -Is): finished OK" >> "$LOGFILE"
Nesse caso, o crontab terá apenas uma linha: * * * * * /usr/local/bin/cronjobs/my-task-wrapper.sh. Se você deseja manter a saída por e-mail, é melhor especificar um endereço exato. Mas primeiro, verifique se o e-mail do servidor está sendo enviado:
bashecho "cron mail test" | mail -s "cron test" admin@example.com
Se o e-mail não chegar, o MAILTO no crontab não salvará seus backups. Nesses casos, é melhor escrever diretamente em um arquivo com rotação, syslog, journald ou um healthcheck externo. Para uma verificação rápida do próprio cron, você pode adicionar temporariamente uma linha de teste: * * * * * date -Is >> /tmp/cron-test.log 2>&1. Após alguns minutos, verifique o arquivo:
bashcat /tmp/cron-test.log
Após a verificação, remova a linha. Tarefas temporárias de cron podem facilmente permanecer no servidor para sempre. Outro ponto: a entrada no log do sistema mostra apenas o início do comando. Ela não prova que o backup foi criado, que o arquivo foi enviado para o armazenamento e que as cópias antigas foram removidas sem erros. Para tarefas de recuperação, é melhor deixar um indicador separado de conclusão bem-sucedida:
bash/path/to/backup.sh >> /var/log/backup.log 2>&1 rc=$? if [ "$rc" -eq 0 ]; then date -Is > /var/lib/backup/last-success fi exit "$rc"
Este arquivo pode ser verificado por monitoramento:
bashstat /var/lib/backup/last-success
Ele pode ser integrado ao Prometheus, Zabbix, seu próprio healthcheck ou qualquer outro sistema de monitoramento. O importante é que a tarefa do cron tenha um rastro verificável de execução bem-sucedida, e não apenas o fato de ter sido iniciada.
O Cron Executa Tarefas Fora do Seu Ambiente
A segunda questão é que, frequentemente, um comando funciona durante os testes, mas falha quando executado pelo cron. Geralmente, o problema está no ambiente, pois, por padrão, a tarefa tem apenas um conjunto mínimo de variáveis. SHELL é geralmente especificado em /bin/sh, HOME e LOGNAME são obtidos do registro do usuário em /etc/passwd, e PATH costuma ser mais curto do que no console. Devido a isso, um script pode iniciar inesperadamente com o interpretador errado, o Node.js pode não ser encontrado, ou o mysqldump pode ser obtido de um diretório diferente. Por isso, antes de adicionar uma tarefa ao cron, é melhor executá-la em um ambiente limpo. Por exemplo:
bashenv -i SHELL=/bin/sh HOME=/root PATH=/usr/bin:/bin /bin/sh -c '/path/to/command'
Para um usuário de serviço, é melhor testar diretamente como ele:
bashsudo -u backup env -i \ SHELL=/bin/sh \ HOME=/home/backup \ PATH=/usr/bin:/bin \ /bin/sh -c '/usr/local/bin/backup-wrapper.sh'
Isso não é uma cópia completa do cron, pois diferentes implementações podem adicionar suas próprias variáveis e considerar o PAM, mas o teste captura problemas muito grosseiros. Se um comando não funciona em um ambiente limpo, ele também não funcionará no crontab. No próprio crontab, é melhor definir o ambiente com precisão, especialmente para backups, exportações ou tarefas que rodam há meses:
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=
admin@example.com
15 2 * * * /usr/local/bin/cronjobs/backup-wrapper.sh
Para Python e Node.js, é melhor não depender de gerenciadores de versão de uma sessão interativa. Se a tarefa deve ser executada através de um ambiente virtual, especifique o caminho completo para o interpretador: 30 * * * * /opt/myapp/.venv/bin/python /opt/myapp/jobs/recalculate.py.
Para Node.js, especifique o binário que deve ser executado no servidor: */10 * * * * /usr/bin/node /opt/myapp/jobs/sync.js.
Se o aplicativo precisar de variáveis de um arquivo separado, carregue-as no script wrapper. Assim, é mais fácil entender de onde vem a configuração e não depender de variáveis aleatórias:
bash#!/bin/bash set -u set -a . /etc/myapp/cron.env set +a exec /opt/myapp/.venv/bin/python /opt/myapp/jobs/sync.py
No entanto, o arquivo com as variáveis não deve ser acessível a todos, especialmente se contiver tokens, senhas ou chaves:
bashsudo chown root:myapp /etc/myapp/cron.env sudo chmod 640 /etc/myapp/cron.env
Lembre-se separadamente sobre /bin/sh. Em Debian e Ubuntu, geralmente é dash, não bash. Em sistemas baseados em Red Hat, /bin/sh pode apontar para bash, mas ele é executado como sh e se comporta de forma diferente. Portanto, construções como [[ ... ]], arrays e process substitution devem ser evitadas diretamente no crontab. A lógica deve ser transferida para um script, onde o shell é especificado: */5 * * * * /usr/local/bin/cronjobs/app-run-wrapper.sh.
O próprio wrapper:
bash#!/bin/bash set -u if [[ -f /tmp/flag ]]; then exec /opt/app/run.sh fi
Antes de enviar tal script para o cron, é útil executá-lo através do shellcheck:
bashshellcheck /usr/local/bin/cronjobs/app-run-wrapper.sh
Ele não substitui uma revisão, mas detecta aspas esquecidas, variáveis não inicializadas e problemas de execução.
O Cron Não Verifica se a Tarefa Anterior Terminou
Outro problema surge quando uma tarefa leva mais tempo do que seu intervalo. Isso acontece porque o cron não monitora se a execução anterior foi concluída. Para verificar rapidamente se tais processos estão se acumulando, use pgrep: pgrep -a -f 'sync-job' ou pgrep -c -f 'sync-job'. Esta é uma verificação grosseira, pois pgrep -f pode capturar o wrapper, o shell e comandos semelhantes, mas é suficiente para uma avaliação. Se você vir dezenas de processos em vez de um, o cron já começou a multiplicar a tarefa. A maneira mais simples de impedir a execução repetida é usar flock do util-linux: */5 * * * * flock -n /run/lock/sync-job.lock /usr/local/bin/cronjobs/sync-job-wrapper.sh.
A opção -n indica que flock não esperará que o arquivo de lock seja liberado. Se a cópia anterior ainda estiver em execução, a nova será encerrada imediatamente. Para tarefas importantes, eu manteria o flock dentro do script wrapper. Assim, o log, o código de retorno e um comportamento compreensível em caso de execução repetida permanecem juntos:
bash#!/bin/bash set -u LOCKFILE="/run/lock/sync-job.lock" LOGFILE="/var/log/sync-job.log" exec 200>"$LOCKFILE" if ! flock -n 200; then echo "$(date -Is): previous run is still active, skipping" >> "$LOGFILE" exit 0 fi echo "$(date -Is): started" >> "$LOGFILE" /opt/app/bin/sync-job >> "$LOGFILE" 2>&1 rc=$? echo "$(date -Is): finished with exit code $rc" >> "$LOGFILE" exit "$rc"
No crontab, após isso, resta apenas a chamada usual do wrapper: */5 * * * * /usr/local/bin/cronjobs/sync-job-wrapper.sh. Quando o lock já está ocupado, eu frequentemente retorno 0. Se pular uma execução for um problema, você pode retornar 1 e configurar um alerta separado para tais eventos. Existe outra situação em que a tarefa não apenas demora, mas trava. Nesse caso, o lock protege contra novas cópias, mas a antiga continuará pendente. Aqui, você pode adicionar um timeout: */5 * * * * flock -n /run/lock/sync-job.lock timeout 10m /usr/local/bin/cronjobs/sync-job-wrapper.sh.
O timeout encerrará o processo após o tempo especificado, mas não substitui o tratamento de erros normal dentro da própria tarefa.
O Cron Tem Dificuldade com o Horário Local
O cron obtém o agendamento do horário do sistema do servidor. Em máquinas com fuso horário local, surgem problemas, por isso, para servidores, é mais fácil manter o UTC: sudo timedatectl set-timezone UTC e timedatectl status. Após isso, o agendamento deve ser escrito considerando o UTC. No entanto, se você não administra sozinho, não se esqueça de avisar a equipe. Há outro problema, relacionado a downtime. O cron comum não executa uma tarefa que foi perdida devido ao desligamento ou reinicialização do servidor. Se a máquina esteve indisponível às 02:15 e o backup estava agendado para esse horário, ele geralmente não será iniciado. Para servidores que são ocasionalmente desligados, historicamente usava-se o anacron. Em sistemas com systemd, uma tarefa semelhante é frequentemente resolvida através de um timer com Persistent=true:
ini[Timer] OnCalendar=*-*-* 02:15:00 Persistent=true
Esse timer lembrará o início perdido e executará a tarefa após a próxima ativação do timer. Mas ele também não substitui a verificação do próprio backup.
O Cron Facilmente Esconde Tarefas Extras
O cron é conveniente não apenas para administradores, mas também para hackers. Uma linha no crontab sobrevive a reinicializações, é executada sem intervenção do usuário e não se destaca. A propósito, uma tarefa de cron suspeita geralmente parece banal. Por exemplo, curl ou wget em uma única linha, execução de /tmp ou /dev/shm, base64, bash -c, @reboot, um domínio desconhecido e um script no diretório pessoal de um usuário antigo. Você pode começar a auditoria com tarefas do sistema:
bashsudo ls -la \ /etc/crontab \ /etc/cron.d \ /etc/cron.hourly \ /etc/cron.daily \ /etc/cron.weekly \ /etc/cron.monthly 2>/dev/null
Em seguida, verifique os crontabs de usuário. Para evitar escrever arquivos temporários diretamente em /tmp, é mais conveniente criar um diretório temporário separado:
bashtmpdir="$(mktemp -d)" for user in $(getent passwd | cut -d: -f1); do if sudo crontab -u "$user" -l > "$tmpdir/cron-$user" 2>/dev/null; then echo "=== $user ===" cat "$tmpdir/cron-$user" fi done rm -rf "$tmpdir"
A propósito, em servidores corporativos antigos, após essa verificação, tarefas de usuários que não fazem mais parte da equipe são frequentemente encontradas. Mudanças recentes também podem ser visualizadas com find: sudo find /etc/cron* /var/spool/cron* -type f -mtime -7 -ls 2>/dev/null.
Para uma busca rápida de linhas suspeitas, grep comum é suficiente: sudo grep -RnsE '(@reboot|curl|wget|base64|bash -c|/tmp/|/dev/shm|nc[[:space:]])' /etc/cron* /var/spool/cron* 2>/dev/null.
Verifique separadamente as permissões. Arquivos de cron e os scripts que ele executa não devem ser acessíveis a todos. sudo find /etc/cron* /var/spool/cron* -type f -perm /022 -ls 2>/dev/null.
Após isso, revise os próprios scripts do agendamento. O crontab -e manual em produção deve ser evitado, exceto em casos de emergência. Para um servidor único, você pode pelo menos conectar o etckeeper para que as alterações em /etc não desapareçam sem deixar rastros:
bashsudo apt-get install etckeeper sudo etckeeper init sudo etckeeper commit "initial /etc snapshot"
Lembre-se que o cron não possui revisão integrada, histórico de edições ou verificação do sentido dos comandos – tudo isso deve ser feito externamente.
O Que Verificar Antes de Adicionar uma Tarefa ao Cron
É melhor gastar cinco minutos em uma verificação mínima do que sofrer depois. Primeiro, certifique-se de que todos os comandos são chamados pelos caminhos completos. Em uma sessão interativa, mysqldump, python, node, flock ou logger podem ser encontrados através do seu PATH, mas no cron esse PATH não existirá. Verifique com command -v mysqldump, command -v python3, command -v flock, command -v logger.
Em seguida, o comando deve ser executado pelo mesmo usuário que o executará no cron. Se for uma conta separada, teste especificamente com ela: sudo -u backup /usr/local/bin/cronjobs/backup-wrapper.sh.
O próximo passo é a execução em um ambiente quase limpo. É nesta etapa que surgem problemas com PATH, variáveis, diretório pessoal e dependência de .bashrc: sudo -u backup env -i SHELL=/bin/sh HOME=/home/backup PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin /bin/sh -c '/usr/local/bin/cronjobs/backup-wrapper.sh'.
Se o wrapper for escrito em bash, isso deve ser indicado na primeira linha do arquivo: #!/bin/bash. O próprio arquivo, antes de ser adicionado ao cron, deve ser verificado pelo menos basicamentente: bash -n /usr/local/bin/cronjobs/backup-wrapper.sh e shellcheck /usr/local/bin/cronjobs/backup-wrapper.sh.
Em VPS mínimas, o shellcheck pode não estar presente, então ele precisará ser instalado do repositório da distribuição. Para Debian e Ubuntu, geralmente é feito assim: sudo apt-get update && sudo apt-get install shellcheck. Para sistemas RHEL-like, o comando depende dos repositórios conectados, mas geralmente se parece com: sudo dnf install ShellCheck.
Depois, não se esqueça de verificar as permissões. O cron não deve executar um script que qualquer usuário no servidor possa editar. Especialmente se a tarefa for executada como root: ls -l /usr/local/bin/cronjobs/backup-wrapper.sh e namei -l /usr/local/bin/cronjobs/backup-wrapper.sh.
Para um script root comum, as permissões podem ser:
bashsudo chown root:root /usr/local/bin/cronjobs/backup-wrapper.sh sudo chmod 750 /usr/local/bin/cronjobs/backup-wrapper.sh
Se o script grava um log, o diretório e o arquivo também devem ser verificados com antecedência. Caso contrário, a tarefa será executada, falhará ao gravar no log, e você procurará o erro no lugar errado: sudo mkdir -p /var/log/cronjobs, sudo touch /var/log/cronjobs/backup.log, sudo chown backup:backup /var/log/cronjobs/backup.log, sudo chmod 640 /var/log/cronjobs/backup.log.
Após isso, você pode fazer uma entrada de teste no crontab para um horário próximo. Assim, você verá imediatamente que o cron realmente executa a tarefa: * * * * * /usr/local/bin/cronjobs/backup-wrapper.sh. Após alguns minutos, verifique o log: tail -100 /var/log/cronjobs/backup.log e o log do sistema do cron: journalctl -u cron -S "10 minutes ago" ou journalctl -u crond -S "10 minutes ago".
Em Debian e Ubuntu, você pode verificar adicionalmente /var/log/syslog: grep CRON /var/log/syslog | tail -50. Em sistemas RHEL-like, geralmente há um arquivo separado /var/log/cron: sudo tail -50 /var/log/cron.
Quando o teste for bem-sucedido, a linha temporária deve ser removida e o agendamento normal definido: 15 2 * * * /usr/local/bin/cronjobs/backup-wrapper.sh.
E o último item, que é melhor fazer imediatamente. Deixe um comentário ao lado da tarefa:
# owner: infra, daily database backup, writes to /var/log/cronjobs/backup.log
15 2 * * * /usr/local/bin/cronjobs/backup-wrapper.sh
Daqui a um ano, este comentário pode economizar mais tempo do que toda a configuração inicial.
O Que Lembrar
O cron em si não é ruim, pois faz exatamente o que foi projetado para fazer. Portanto, para tarefas simples, não recomendo substituí-lo por Systemd timers, Cronie ou Jobber. Para tarefas importantes, como backups, já vale a pena considerar alternativas. Quanto ao cron, a disciplina é fundamental. Com ela, não haverá problemas. Compartilhe nos comentários quais acidentes com cron você já presenciou e como se livrou deles. Escreva também se precisar de uma compilação de alternativas.





