Ошибка fatal: detected dubious ownership стала привычной болью админов и девопсов: она всплывает после обновления Git, смены пользователя в пайплайне, переноса проекта на сервер, запуска деплоя через sudo git или при работе со shared runners. С одной стороны, это ломает автоматизацию. С другой — это осознанный hardening: Git защищает от работы с репозиторием в каталоге, где владелец и права не совпадают с контекстом выполнения команды.
Ниже — практическая шпаргалка: что именно проверяет Git, когда достаточно поправить владельца, когда уместен safe.directory, и как сделать это точечно, не превращая исключение в дыру.
Что такое dubious ownership и почему Git начал ругаться
Сообщение чаще всего выглядит так:
fatal: detected dubious ownership in repository at '/path/to/repo'
To add an exception for this directory, call:
git config --global --add safe.directory /path/to/repo
Смысл проверки: Git не хочет работать с репозиторием, который находится в дереве каталогов с «чужим» владельцем или подозрительными правами. Это защита от сценариев подмены рабочей копии и конфигурации: в реальном мире уязвимость возникает, когда вы запускаете Git под одним пользователем (часто root), а каталог репозитория контролируется другим пользователем или процессом.
Типовые причины в проде и CI
Деплой через
sudo: репозиторий в/var/www/appпринадлежитdeploy, а команды запускаются отroot(или наоборот).CI/CD меняет пользователя: checkout делает один UID, сборку запускают под другим.
Shared runners: рабочие каталоги и кеши могут жить в нестандартных местах и с нетипичными правами.
Docker bind-mount/volume: на хосте один UID/GID, в контейнере — другой.
NFS/SMB/кеши: владельцы и права «плывут» из-за особенностей монтирования и масок.
Если в пайплайне есть секреты (ключи, токены, доступ к реестрам/продакшену), то «разные пользователи трогают один и тот же workspace» — это повод остановиться и выровнять модель доступа.
Как работает safe.directory и где его настраивать
Настройка safe.directory — это список путей, которым Git разрешает доверять, даже если проверка владельца/прав сработала бы как «сомнительно». Исключение можно задавать на разных уровнях:
--global— для пользователя, под которым выполняется job/скрипт (самый частый вариант в CI).--system— для всей системы (обычно не нужно и часто опасно на многопользовательских серверах).
Локальная конфигурация репозитория (--local) в этом кейсе почти не помогает: Git может не «дойти» до чтения локального конфига, если уже отказался открывать репозиторий.
Почему нельзя делать safe.directory '*'
Иногда советуют «быстро починить» так:
git config --global --add safe.directory '*'
Технически это отключает смысл защиты: Git начнёт доверять любому каталогу. На shared runners и на серверах, где разные аккаунты или сервисы могут писать в общие директории, это увеличивает риск подмены репозитория и выполнения нежелательных действий в пайплайне.

Диагностика: быстро понять, что именно не так с правами
Перед тем как добавлять исключения, проверьте факты: кто запускает команду и кому принадлежит дерево каталогов. Часто проблема решается одним выравниванием владельца.
1) Под кем выполняется команда
id
whoami
2) Кому принадлежат репозиторий и родительские каталоги
pwd
ls -ld . ..
ls -ld /path/to/repo
ls -ld /path /path/to
3) Кому принадлежит .git
ls -ld .git
ls -l .git | head
Типичный кейс после ручных операций/rsync: каталог проекта принадлежит deploy, а .git внезапно root’у (или наоборот). Тогда Git-операции от «не того» пользователя закономерно ломаются.
Стратегии исправления (без отключения защиты)
Выбор стратегии зависит от контекста: сервер деплоя, корпоративный CI, контейнеры или shared runner.
Стратегия A: выровнять владельца и выполнять Git только одним пользователем
Для сервера деплоя это самый надёжный вариант: выделяете пользователя (например, deploy) и гарантируете, что все операции с репозиторием выполняются только им.
Аккуратно проверьте путь и затем выровняйте владельца:
sudo chown -R deploy:deploy /var/www/myapp
И запускайте Git так:
sudo -u deploy -H git -C /var/www/myapp status
sudo -u deploy -H git -C /var/www/myapp pull --ff-only
обычно не нужен
safe.directory;исчезает привычка делать
sudo git;проще аудит и повторяемость деплоя.
Если вы поднимаете деплой/раннер на сервере с нуля, проще и безопаснее делать это в изоляции на VDS, где вы контролируете пользователей, права на workspace и модель доступа.
Стратегия B: если «нужен sudo git» — меняем процесс, а не Git
Почти всегда «нужно запустить sudo git» означает, что Git используют не по назначению. Root должен заниматься только системными действиями (перезапуск сервиса, переключение симлинков, права на системные каталоги), а код и сборка — жить в пользовательской зоне.
Рабочий паттерн:
fetch/pull/checkout/build — под
deploy;копирование артефактов в привилегированные места — через минимальные
sudo-разрешения на конкретные команды;systemctl restart— отдельно и тоже через ограниченныйsudoers.
Если деплой у вас завязан на rsync/SSH и вы хотите меньше сюрпризов с правами, пригодится материал про практичный пайплайн: CI/CD деплой через GitHub Actions и rsync.
Стратегия C: точечный safe.directory (когда вы не можете выровнять владельца)
В CI/CD иногда действительно невозможно заставить UID/GID совпасть (особенности раннера, политика безопасности, volume из хоста). Тогда safe.directory допустим, но с правилами:
добавляйте конкретный путь, а не
*;делайте это в рамках job-а (в одноразовом окружении), а не «навсегда» на машине;
доверяйте только workspace конкретной job, а не общим каталогам.
Пример (добавить текущий каталог как safe):
git config --global --add safe.directory "$(pwd)"
Если в job используется подкаталог:
git config --global --add safe.directory "$(pwd)/project"
git -C project status
CI/CD: практические рецепты для shared runners и контейнеров
В shared runner вы обычно не контролируете хост целиком, поэтому цель — минимизировать область доверия и не оставлять «широких» исключений.
Рецепт 1: safe.directory в начале job-а
Добавьте workspace в исключения перед любыми Git-операциями в job-е:
git config --global --add safe.directory "$(pwd)"
git rev-parse --is-inside-work-tree
Рецепт 2: не шарить workspace между пользователями на своём раннере
Если раннер у вас собственный (на вашей инфраструктуре), проверьте базовую гигиену:
workspace принадлежит пользователю раннера;
в этот каталог не пишут посторонние сервисы;
кеши зависимостей отделены и имеют понятные права.
ls -ld /var/lib/runner /var/lib/runner/work
Рецепт 3: Docker bind-mount и mismatch UID/GID
Когда репозиторий монтируется в контейнер, а процесс внутри запускается под другим UID, Git может посчитать владение «сомнительным». Рабочие варианты:
запускать контейнер с UID/GID владельца файлов на хосте;
делать checkout внутри контейнера (не монтируя рабочую копию);
если модель доверия ясна — точечный
safe.directoryдля пути внутри контейнера.

Deploy на сервере: как не влететь в права и безопасность
На сервере деплоя ошибка чаще всего появляется из-за привычки «всё делать от root» и держать рабочую копию прямо в /var/www. Более безопасная схема: отдельный пользователь деплоя, отдельные каталоги релизов, а root — только для операций уровня сервиса.
Плохой сценарий: root делает git pull в /var/www
появляются root-owned файлы, которые ломают последующие обновления;
увеличивается ущерб при ошибке или подмене содержимого репозитория;
Git начинает закономерно показывать
dubious ownership.
Хороший сценарий: deploy-пользователь + ограниченный sudo
Минимальный принцип:
репозиторий, сборка и артефакты — только под
deploy;root — только перезапуск и переключение (например, симлинка), и только через ограниченные правила;
никаких «ALL:ALL» в
sudoersради удобства деплоя.
Если у вас деплой по SSH с возможностью отката, полезно держать под рукой схему с атомарным переключением и rollback: деплой по SSH с откатом релиза.
Security hardening: если вы всё-таки добавляете safe.directory
Если без safe.directory не обойтись, зафиксируйте модель доверия: кто может писать в каталог репозитория и кто запускает Git-команды. Дальше проверьте базовые ограничения.
Не используйте
safe.directory *на shared runners и многопользовательских серверах.Не добавляйте исключения в
--system, если узел не изолирован и не предназначен строго для одной задачи.Проверьте права на родительские каталоги: лишняя запись в
/var/wwwили в каталог workspace — частая первопричина.Минимизируйте root в CI: если job выполняется от root, цена ошибки резко выше.
Быстрые шпаргалки: что делать в популярных ситуациях
Repo в /var/www принадлежит deploy, а деплой-скрипт запускается от root
Запускайте Git от deploy:
sudo -u deploy -H git -C /var/www/myapp fetch --all
sudo -u deploy -H git -C /var/www/myapp reset --hard origin/main
CI падает на первой Git-команде с dubious ownership
Добавьте workspace в safe.directory в начале job-а:
git config --global --add safe.directory "$(pwd)"
git status
После rsync/копирования часть файлов стала root-owned
Выровняйте владельца дерева (обычно под пользователя деплоя):
sudo chown -R deploy:deploy /var/www/myapp
Итоги
git dubious ownership — это не «каприз Git», а сигнал, что у вас пересекаются контексты прав: один пользователь владеет/пишет, другой выполняет команды. В CI/CD это часто следствие shared runners и контейнеров; на сервере деплоя — симптом sudo git и неразделённых ролей.
Лучшее решение — выровнять владельца и выполнять Git под одним непривилегированным пользователем. Если это невозможно, используйте safe.directory точечно и только там, где модель доверия ясна.


