MAATRIX / Блог / Деплой без git: как понять, откуда на сервер вообще приезжает код

Деплой без git: как понять, откуда на сервер вообще приезжает код

MAATRIX

Сайт или сервис на унаследованном сервере регулярно обновляется — вы это видите по датам файлов и по тому, что баги иногда чинятся сами. Но git log в директории с кодом молчит: либо .git нет вообще, либо есть, да только последний коммит датирован позапрошлым годом, а файлы новее. Значит, код приезжает откуда-то ещё — и пока вы не поняли, откуда именно, любое серьёзное изменение на этом сервере — риск: вы можете поправить файл, а через час его перезапишет процесс, о существовании которого вы не подозревали. Разберём, как методично найти реальный канал доставки кода: от логов FTP/SFTP до скрытых cron-скриптов и симлинков на чужие директории.

Зачем вообще искать канал деплоя, если код и так работает

Соблазн понятен: сайт открывается, ошибок нет, зачем копать. Но на унаследованном сервере без документации это разведка боем, и вот почему она окупается почти всегда.

Во-первых, вы не сможете безопасно ничего поменять в коде, если не знаете, что его перезаписывает. Классическая ловушка: администратор правит файл руками через SSH, тестирует — работает. Через полчаса ночной cron перекладывает свежий архив с внешнего FTP поверх директории, и правка исчезает без следа. Разбираться потом, «почему исправление не сохранилось», можно часами, хотя ответ был в crontab всё это время.

Во-вторых, канал деплоя — это часто и канал компрометации. Если код приезжает через FTP с логином и паролем, который знают три бывших подрядчика, это дыра размером с саму площадку. Если скрипт деплоя запускается из cron от root и тянет архив по HTTP без проверки подписи — тем более. Понимание механизма — первый шаг к тому, чтобы его закрыть или хотя бы ограничить.

В-третьих, без понимания канала невозможно оценить риск простого «ничего не трогать». Если код приезжает с ноутбука уволившегося разработчика через закреплённый SSH-ключ и ручной rsync, следующий деплой либо не случится вообще, либо случится хаотично. Это стоит знать заранее, а не в момент, когда клиент попросит «быстро поправить цену на сайте». Это частный случай более общей задачи — разбора незнакомого сервера, доставшегося без документации: деплой обычно один из первых вопросов, которые встают после инвентаризации сервисов.

Наконец, каналов часто оказывается несколько, и они конфликтуют друг с другом — например, часть файлов кладёт FTP из панели хостинга, а часть перетирает cron-скрипт, который тянет из давно заброшенного git-репозитория. Пока это не задокументировано, любое обновление — лотерея.

Первый источник: логи FTP и SFTP

Если сервер вообще принимает FTP или SFTP-соединения, логи этих служб — самый прямой источник: там видно логин, IP и точное время каждой сессии передачи.

Для ванильного vsftpd и proftpd лог обычно лежит в /var/log/vsftpd.log или /var/log/proftpd/proftpd.log, но на серверах с панелями управления (ISPmanager, FastPanel, aaPanel, cPanel) путь чаще нестандартный — ищите так:

find / -iname "*ftp*log*" -mtime -365 2>/dev/null
grep -i "ftpd\|proftpd\|vsftpd\|pure-ftpd" /var/log/syslog /var/log/messages 2>/dev/null | tail -50

Полезная деталь: если FTP запущен через xinetd или systemd, а не как отдельный демон, часть событий подключения попадает в journalctl, даже если файловый лог ротировался и потёрся:

journalctl -u vsftpd --since "2026-06-01" | grep -i "login\|upload\|STOR"

Команда STOR в логе — это именно загрузка файла на сервер, её и ищите. Если видите регулярные STOR от одного и того же IP каждый вторник в 3 ночи — вот и обнаружился канал, который вы искали, а заодно расписание релизов.

SFTP чуть сложнее: если он работает через встроенный SSH-сервер (Subsystem sftp), отдельного лога передач может не быть — только запись о самом SSH-подключении. Проверьте конфиг:

grep -i sftp /etc/ssh/sshd_config

Если там указан internal-sftp с -l INFO или выше, лог событий SFTP (открытие, запись, закрытие файлов) пишется через auth.log/secure вместе с обычными SSH-сессиями — ищите строки sftp-server и open "..." flags WRITE.

Отдельно проверьте домашние директории пользователей — иногда FTP настроен через отдельного системного юзера с chroot, и его домашняя папка сама по себе подсказка:

cat /etc/passwd | grep -v nologin
ls -la /home/*/

Пользователь deploy или ftpupload с шеллом /bin/false, но свежими файлами в домашней папке — почти наверняка и есть канал доставки. Если сам FTP-сервер настроен нестандартно или барахлит, разбор частых ошибок FTP уже описан отдельно — см. частые ошибки FTP-сервера и их решения.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Второй источник: ручная загрузка через панель управления

Если на сервере стоит панель (ISPmanager, cPanel, FastPanel, aaPanel, Plesk), в ней обычно есть встроенный файловый менеджер и своя история операций — независимая от системных логов ОС.

У большинства панелей есть отдельный журнал действий администратора:

  • ISPmanager — раздел «Журнал» в интерфейсе, плюс лог /usr/local/mgr5/var/ispmgr.log или похожий путь в зависимости от версии.
  • cPanel/usr/local/cpanel/logs/access_log и /usr/local/cpanel/logs/login_log фиксируют не только вход, но и обращения к File Manager.
  • FastPanel/aaPanel — своя БД истории операций, обычно доступная через раздел «Логи» в самой панели, файлы стоит поискать в /usr/local/fastpanel2/ или /www/server/panel/logs/.

Если панельного журнала нет или он короткий (многие панели хранят историю ограниченное время), косвенный, но надёжный признак — владелец и группа файлов. Файлы, залитые через панель, часто принадлежат системному пользователю панели (www-data, apache, специфичному юзеру виртуального хоста), а не тому, от чьего имени вы заходите по SSH:

ls -la /var/www/site/ | head -20
stat /var/www/site/index.php

Обратите внимание на Access, Modify и Change время в выводе stat — панельная загрузка через веб-интерфейс обычно даёт Change время (изменение метаданных inode), совпадающее до секунды с Modify, тогда как rsync с сохранением атрибутов (-a) может сохранить старое время модификации файла, но обновить Change. Это не стопроцентный маркер, но в сочетании с логами панели помогает восстановить хронологию.

Третий источник: скрипты деплоя, спрятанные в cron и systemd timers

Даже если ручного FTP или панели не видно, код может обновляться автоматически — просто скрипт лежит не там, где вы привыкли искать. Проверьте не только crontab -l для текущего пользователя, а все источники периодических задач:

for user in $(cut -f1 -d: /etc/passwd); do
  crontab -u "$user" -l 2>/dev/null && echo "^-- $user"
done
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/
systemctl list-timers --all

Отдельно посмотрите системные unit-файлы — на серверах с systemd деплой иногда реализован как таймер плюс сервис, а не как классический cron:

grep -rl "deploy\|rsync\|git pull\|scp\|curl.*tar" /etc/systemd/system/*.service 2>/dev/null

Если находите такой скрипт — прочитайте его целиком, не полагаясь на название. Часто внутри deploy.sh обнаруживается цепочка: скрипт стягивает архив с внешнего хранилища (S3-совместимого бакета, а иногда просто с другого VPS по wget), распаковывает во временную директорию и симлинком переключает current на новый релиз — знакомая схема, оставшаяся от прежней команды без единой строчки README.

Проверьте также /var/spool/cron/ напрямую — там лежат raw-файлы crontab’ов пользователей, и иногда задача, отредактированная вручную через crontab -e от имени пользователя, которого давно нет в /etc/passwd с шеллом (но запись осталась), не всплывает в обычном crontab -l под текущей учёткой:

ls -la /var/spool/cron/crontabs/ 2>/dev/null || ls -la /var/spool/cron/

Если скрипт деплоя тянет код по HTTP без токена и без проверки контрольной суммы — это стоит зафиксировать отдельно как риск: любой, кто подменит источник (взломанный DNS, скомпрометированный промежуточный сервер), получит возможность подложить что угодно в следующий релиз.

Четвёртый источник: симлинки и точки монтирования, ведущие в сторону

Директория с кодом может оказаться не тем, чем выглядит. Прежде чем делать выводы по путям модификации файлов, проверьте, не смотрит ли документ-рут веб-сервера через симлинк или bind-mount в совсем другое место:

ls -la /var/www/
readlink -f /var/www/site/current
mount | grep -i www
findmnt --output TARGET,SOURCE,FSTYPE | grep -i www

Схема, где current — симлинк на releases/2026-08-14-153022/, а рядом лежат ещё пять таких же датированных папок — прямое доказательство скриптового деплоя с историей релизов, даже если самого деплой-скрипта в стандартных местах не нашлось (возможно, он запускается вручную с ноутбука разработчика через ssh и там же и остался).

Отдельно проверьте, не смонтирован ли каталог с кодом через NFS или sshfs с другого сервера — в таком случае реальный код и вовсе физически лежит не на этой машине, а «деплой» — это просто изменение файлов на удалённом хранилище, которое сюда примонтировано:

mount -t nfs,nfs4,fuse.sshfs
cat /etc/fstab | grep -v "^#"

Если находите там что-то похожее на 10.0.x.x:/srv/releases /var/www/site nfs ... — вы нашли не деплой, а вообще другой сервер, который стоит обследовать отдельно.

Пятый источник: даты и права файлов как окаменелости

Когда прямых логов нет вообще (ротация всё стёрла, панель не вела журнал, cron почищен), остаётся археология по метаданным файловой системы. Метод грубее, но часто единственный доступный.

Соберите распределение файлов по времени модификации, чтобы увидеть волны обновлений:

find /var/www/site -type f -printf '%TY-%Tm-%Td %p\n' | sort | uniq -c -w10 | sort -rn | head -30

Кластеры дат с сотнями файлов, обновившихся в одну секунду — это, как правило, один акт деплоя: либо распаковка архива, либо rsync, либо git checkout без сохранения .git. Одиночные файлы с уникальными временными метками между такими кластерами — вероятно, ручные правки через редактор прямо на проде.

Владелец и права тоже подсказывают канал. Сравните типичные комбинации:

ПризнакВероятный канал
Владелец www-data/apache, права 644, даты кластерамиДеплой через панель управления или веб-загрузчик
Владелец конкретного системного пользователя (deploy, ftpuser), даты по расписаниюАвтоматический скрипт из cron/systemd timer
Владелец совпадает с личной учёткой администратора, права 664/600, даты вразнобойРучное редактирование через SSH/SFTP-клиент с рабочей станции
Файлы во временных директориях типа releases/, current — симлинкСкриптовый деплой с версионированием релизов
SUID/SGID биты на скриптах в директории кодаСтоит проверить отдельно — почти всегда лишнее и небезопасное

Похожая техника по датам и метаданным применяется и для восстановления истории правок конфигов — если задача расширяется с деплоя кода на весь /etc, пригодится отдельный разбор: как восстановить историю изменений конфигов без git.

Ещё один источник — время создания inode (crtime), доступное через debugfs или stat -c %W (поддерживается не на всех ФС): оно честнее, чем mtime, который легко подделать командой touch. А если сервер — виртуалка со снапшотами у провайдера, сравнение файловой системы между двумя снапшотами с разницей в неделю прямо покажет, что менялось за этот период — иногда быстрее, чем ковыряться в логах вручную.

Как задокументировать найденный канал, чтобы не искать заново

Найдя механизм, сразу зафиксируйте его — иначе через полгода придётся повторять расследование с нуля, потому что память команды короче, чем жизнь сервера.

Минимальный файл DEPLOY.md в корне проекта (даже если самого git-репозитория нет — просто текстовый файл рядом с кодом) должен отвечать на четыре вопроса:

# Как код попадает на этот сервер

- Канал: [FTP / SFTP / панель / cron-скрипт / ручной SSH]
- Источник: [IP, домен, путь к репозиторию, кто инициирует]
- Расписание: [по требованию / cron `* * * * *` / вручную кем-то конкретным]
- Где смотреть логи: [путь к файлу лога или журналу панели]
- Дата последней проверки этой схемы: 2026-08-XX

Если оказалось, что каналов несколько и они пересекаются (например, часть каталогов трогает FTP, а конфиги правит вручную конкретный человек по SSH) — распишите по каталогам, кто чем владеет. Это резко снижает риск, что кто-то из команды случайно перезапишет чужие изменения следующим релизом.

Заодно, пока вы уже внутри директории с кодом и смотрите на даты и права файлов, имеет смысл сразу проверить, не завелось ли там что-то постороннее — методика во многом та же, что и для поиска канала деплоя: как проверить сайт на закладки, если исходников нет в git.

Дальше стоит решить, оставлять ли найденную схему как есть или переводить сервер на что-то предсказуемое. Если деплой шёл через голый FTP с паролем — минимум, что стоит сделать в ближайшее время, — перейти на SFTP с ключами или закрыть FTP вообще, оставив только внутренний канал. Если схема оказалась рабочей, но без версионирования (архив просто перезаписывает файлы поверх старых) — рассмотрите промежуточный шаг с релизными директориями и симлинком, даже без полноценного CI/CD: это даёт возможность откатиться на предыдущую версию за секунды, а не восстановлением из бэкапа.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Логи FTP уже сгорели по ротации, а найти канал деплоя всё равно нужно — что делать?

Смотрите на владельца и права файлов, кластеры дат модификации через find -printf, а также в конфиг самого FTP-демона — там есть путь к директории пользователя, и часто по одному этому уже видно, какой каталог сайта он обслуживает. Если сервер у хостинг-провайдера со снапшотами, сравнение снапшотов за период тоже помогает восстановить картину без логов.

В .git есть история, но она явно устарела — файлы новее последнего коммита. Это баг или тут два канала деплоя сразу?

Почти всегда это означает, что git когда-то использовался, а потом от него отказались в пользу более простого способа (панель, ручной FTP), но директорию .git забыли удалить. Проверьте дату последнего коммита и сравните с датами модификации файлов — если разрыв больше пары недель, считайте .git архивным артефактом, а не источником правды, и ищите реальный канал по остальным признакам из статьи.

Как отличить деплой скриптом от ручной правки одного файла через SFTP-клиент, если аккаунт один и тот же?

Смотрите на масштаб и одновременность изменений: скриптовый деплой почти всегда трогает десятки-сотни файлов за секунды, а ручная правка — один-два файла с меткой, заметно отличающейся от соседних кластеров. Дополнительно помогает auth.log: долгая SSH/SFTP-сессия на несколько минут больше похожа на человека за клавиатурой, чем на автоматизацию.

Нашли скрипт деплоя, который тянет архив по обычному HTTP без токена — насколько это серьёзная проблема?

Серьёзная, особенно если скрипт запускается от root или от пользователя с широкими правами. Любой, кто может вклиниться в сетевой путь между сервером и источником архива (некорректный DNS, скомпрометированный промежуточный узел, устаревший TLS без проверки сертификата в самом скрипте), потенциально может подменить содержимое следующего релиза. Минимум — перевести источник на HTTPS с проверкой сертификата и добавить проверку контрольной суммы или подписи архива перед распаковкой.

Стоит ли сразу переводить унаследованный сервер на git и CI/CD, раз уж всё равно разбираться с деплоем?

Не обязательно сразу и не всегда целиком. Если код меняется редко и команда маленькая, промежуточный вариант — оставить существующий канал, но добавить релизные директории с симлинком и элементарный лог каждого деплоя (кто, когда, что залил) — уже закроет большую часть рисков с минимальными трудозатратами. Полноценный переход на git и автодеплой стоит планировать отдельно, когда понятна вся текущая схема и её не жалко заменить целиком.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →