Файлы и права доступа: chmod, chown и всё такое
Файлы и права доступа: chmod, chown и всё такое
Ты написал деплой-скрипт, запускаешь - Permission denied. Знакомо? Это права доступа. В Linux каждый файл имеет владельца и набор разрешений.
Как читать права
Права видно в первой колонке вывода ls -la - той же команды, которой листаешь каталоги:
ls -la
# -rw-r--r-- 1 user group 1234 Apr 22 10:00 config.yml
# drwxr-xr-x 2 user group 4096 Apr 22 10:00 scripts/
Разберём -rw-r--r--:
- rw- r-- r--
│ │ │ │
│ │ │ └── другие (other): только чтение
│ │ └─────── группа (group): только чтение
│ └──────────── владелец (user): чтение + запись
└─────────────── тип: - файл, d каталог, l ссылка
Три права: r (read), w (write), x (execute).
Права на каталог значат не то же, что на файл
Самая частая путаница: rwx у каталога читают так же, как у файла. Смысл битов другой:
| Бит | У файла | У каталога |
|---|---|---|
r | прочитать содержимое | получить список имён (ls) |
w | изменить содержимое | создать, удалить, переименовать файл внутри |
x | запустить как программу | войти внутрь, обратиться к файлу по имени |
Отсюда два неочевидных следствия.
Без x каталог бесполезен, даже если есть r.
mkdir -p /tmp/lab && echo secret > /tmp/lab/data.txt
chmod 444 /tmp/lab # r--r--r--, бит x убрали
ls /tmp/lab # data.txt - имена видны
cat /tmp/lab/data.txt # Permission denied - войти внутрь нельзя
Обратный случай тоже рабочий: chmod 711 на каталог даёт открыть /tmp/lab/data.txt, если знаешь имя, но ls вернёт отказ. Заходить можно, подглядывать нельзя.
Удаление разрешает каталог, а не файл. Право удалить файл - это бит w у каталога, в котором файл лежит. Свой файл с правами 444 ты удалишь (rm только переспросит), а чужой файл 666 в каталоге, куда тебе нет записи, - не удалишь.
Логика станет понятнее, если знать, что rm вообще не трогает содержимое файла, а правит запись в каталоге. Разбор ниже: почему удалённый файл не исчезает.
Sticky bit: почему /tmp не превращается в свалку
/tmp доступен на запись всем, иначе программы не смогли бы создавать временные файлы. По правилу выше это значит, что любой пользователь мог бы удалять чужие файлы в /tmp. Спасает sticky bit:
ls -ld /tmp
# drwxrwxrwt 2 root root 4096 Aug 16 10:00 /tmp
# ^ t вместо x - это sticky bit
chmod +t /var/shared # поставить
chmod 1777 /var/shared # то же в восьмеричной: ведущая 1
Со sticky bit удалить файл может только владелец файла, владелец каталога или root. Поднимаешь общий каталог для нескольких сервисов - ставь 1777, а не 777, иначе один сервис подчистит файлы другого.
chmod - меняем права
chmod +x deploy.sh # добавить право на выполнение
chmod 755 deploy.sh # rwxr-xr-x (стандарт для скриптов)
chmod 644 config.yml # rw-r--r-- (стандарт для конфигов)
chmod 600 .env # rw------- (только владелец)
Числовая нотация:
| Число | Права | Значение |
|---|---|---|
| 7 | rwx | всё |
| 6 | rw- | чтение + запись |
| 5 | r-x | чтение + выполнение |
| 4 | r-- | только чтение |
| 0 | - | ничего |
chmod 600 .env защищает секреты только от соседей по серверу. Чтобы файл не попал в образ и не утёк в реестр, его подсовывают контейнеру снаружи - через env_file и volumes, как в уроке про переменные окружения Docker.
umask: откуда берутся 644 и 755
Ты не выставлял права новому файлу, а он всё равно получился rw-r--r--. Это работа umask - маски, которая вычитает биты при создании файла.
umask # 0022 - типичное значение
umask -S # u=rwx,g=rx,o=rx - то же символьно
Система запрашивает 666 для файлов и 777 для каталогов, а umask убирает лишнее:
файл: 666 (rw-rw-rw-) без 022 = 644 (rw-r--r--)
каталог: 777 (rwxrwxrwx) без 022 = 755 (rwxr-xr-x)
Заодно это ответ на «почему созданный файл не исполняемый»: ядро вообще не предлагает бит x для обычных файлов, максимум 666. Поэтому после echo ... > deploy.sh всегда нужен chmod +x - без него bash-скрипт остаётся просто текстом.
umask 077 # всё создаётся как 600/700 - «только я»
umask 002 # 664/775 - работа группой над общим каталогом
777 не «даёт права», а снимает защиту
chmod -R 777 выглядит универсальным лекарством и почти всегда делает хуже. Три конкретные причины.
Программы сами отказываются работать с распахнутыми правами. SSH проверяет права на приватные ключи и молча игнорирует слишком доступные:
chmod 777 ~/.ssh/id_ed25519
ssh git@gitlab.com
# WARNING: UNPROTECTED PRIVATE KEY FILE!
# Permissions 0777 for '/home/user/.ssh/id_ed25519' are too open.
# This private key will be ignored.
Так же ведут себя visudo с /etc/sudoers и PostgreSQL с каталогом данных: он просто не запустится, если PGDATA доступен группе или всем.
777 делает файл исполняемым. Бит x на конфиге, логе или загруженной картинке не нужен никому, кроме атакующего.
777 не лечит причину. Если Permission denied из-за неверного владельца, 777 замаскирует симптом, а следующий файл приложение создаст по своему umask - и ошибка вернётся завтра. Правильный порядок: посмотреть, от какого пользователя работает процесс (ps aux | grep backend), и править владельца или группу.
chown - меняем владельца
chown user:group file.txt # сменить владельца и группу
chown -R www-data:www-data /var/www # рекурсивно для каталога
chown root:root /etc/nginx/nginx.conf
Типичная ситуация: nginx работает от пользователя www-data, а файлы принадлежат root. Решение - chown.
Кто вообще имеет право менять права
chmod и chown подчиняются разным правилам, и это регулярно ломает деплой-скрипты:
| Операция | Кто может |
|---|---|
chmod на файл | владелец файла и root |
chown - смена владельца | только root |
chgrp - смена группы | владелец, но лишь на группу, в которой сам состоит; root - на любую |
«Отдать» свой файл другому пользователю нельзя даже добровольно: иначе можно было бы подсунуть человеку гигабайтный файл и съесть его дисковую квоту. Отсюда классический отказ в скрипте, который запускается не от root:
chown www-data:www-data /var/app/uploads
# chown: changing ownership of '/var/app/uploads': Operation not permitted
Лечится либо sudo, либо тем, что каталог сразу создаётся правильным владельцем.
Отдельный случай - bind mount в Docker. В контейнере appuser может иметь UID 1000, а на хосте под UID 1000 сидит другой пользователь: ядро сверяет числовые UID, имена ему не важны. Поэтому файл, созданный контейнером в примонтированном каталоге, на хосте выглядит «чужим», и наоборот - контейнер не может писать в каталог хоста. Разбор монтирования - в уроке про переменные окружения, volumes и сети.
Пользователи и sudo
whoami # кто я
id # подробно: uid, gid, группы
sudo command # выполнить от root
sudo -u postgres psql # выполнить от другого пользователя
В контейнере ровно та же проблема: процесс по умолчанию идёт от root, поэтому в Dockerfile создают отдельного пользователя и переключаются на него инструкцией USER - см. сборку образа по шагам.
Символические ссылки
ln -s /opt/myapp/current /opt/myapp/latest # симлинк
ls -la /opt/myapp/latest # покажет → current
Симлинки используются повсеместно: /usr/bin/php → конкретная версия (php8.2), конфиги nginx в sites-enabled → sites-available.
Права у самого симлинка всегда lrwxrwxrwx, и это не дыра в безопасности: при обращении ядро проверяет права цели, а не ссылки. Из этого три практических правила:
chmod 600 /opt/myapp/latest # изменит права ЦЕЛИ, а не ссылки
chown -h appuser link # -h - поменять владельца именно ссылки
readlink -f link # куда ведёт после раскрытия всех ссылок
find /opt/myapp -xtype l # найти все битые («висячие») симлинки
Второй подвох - относительный путь против абсолютного. ln -s ../releases/v2 current переживёт переезд всего каталога, а ln -s /opt/myapp/releases/v2 current сломается, если проект перенесли в /srv. Битая ссылка при этом выглядит в ls нормально, и только cat по ней вернёт No such file or directory.
Третий подвох - завершающий слеш. rm current удалит ссылку, а rm -r current/ пойдёт в каталог-цель и вычистит настоящий релиз. Удаляй симлинки без слеша.
Разбор инцидента: nginx отдаёт 403 на существующий файл
Файл на месте, права 644, владелец верный - а nginx возвращает 403 Forbidden. Причина почти всегда в промежуточном каталоге: чтобы дойти до файла, процессу нужен бит x на каждом каталоге пути. Проверяется одной командой:
namei -l /home/deploy/app/public/index.html
# f: /home/deploy/app/public/index.html
# drwxr-xr-x root root /
# drwxr-xr-x root root home
# drwx------ deploy deploy deploy <- здесь www-data останавливается
# drwxr-xr-x deploy deploy app
# drwxr-xr-x deploy deploy public
# -rw-r--r-- deploy deploy index.html
Домашний каталог 700 - нормальная настройка по умолчанию, но www-data через него не пройдёт. Правильное лечение - chmod 711 /home/deploy (войти можно, листинг нельзя) или перенести приложение в /opt, где такой проблемы нет. Неправильное - chmod 755 /home/deploy, после чего домашний каталог со всеми черновиками читает любой пользователь сервера.
Дать сервису доступ к каталогу: четыре способа и их цена
Задача: демону под www-data нужно писать в /var/app/uploads, каталог принадлежит deploy.
| Способ | Команда | Цена |
|---|---|---|
| Сменить владельца | chown -R www-data /var/app/uploads | deploy теряет запись, деплой-скрипт начинает падать |
| Общая группа | chgrp -R webdata dir плюс chmod -R 2775 dir | нужен setgid (ведущая 2), чтобы новые файлы наследовали группу; настройка сложнее, зато переживает деплой |
| ACL | setfacl -Rm u:www-data:rwx dir | точечно, без смены владельца, но права не видны в ls -la (только + в конце) и о них забывают при переносе |
chmod 777 | chmod -R 777 dir | доступ получают все пользователи и все процессы сервера; в аудите это находка уровня «критично» |
В 90% случаев верный выбор - вторая строка: общая группа плюс setgid на каталог. Именно она не отваливается после следующего деплоя.
Почему удалённый файл не исчезает
rm secret.env - файла нет. Кажется, данные стёрты. Не стёрты.
rm вызывает unlink и снимает с данных имя. Это как снять с коробки на складе наклейку и написать «место свободно». Коробка стоит где стояла, внутри всё на месте. Просто теперь система считает, что сюда можно поставить новую - и когда-нибудь поставит.
Отсюда и восстановление удалённых файлов. Никакой магии: на их место ещё ничего не записали.
Имя и данные лежат отдельно
Содержимое файла живёт в inode: там данные, права, времена и счётчик имён. Имя в каталоге - просто запись «строка → номер inode».
echo "данные" > original.txt
ls -li original.txt
# 1234567 -rw-r--r-- 1 user user 14 Aug 26 12:00 original.txt
# ^ номер inode ^ счётчик имён
ln original.txt copy.txt # хардлинк: второе имя тому же inode
ls -li *.txt
# 1234567 -rw-r--r-- 2 user user 14 ... copy.txt
# 1234567 -rw-r--r-- 2 user user 14 ... original.txt
Номер inode один, счётчик стал 2. Это один файл с двумя именами, а не копия - в отличие от символической ссылки, которая хранит путь и ломается, если цель переехала.
rm original.txt
cat copy.txt # данные
rm уменьшил счётчик с 2 до 1. Данные освободятся, когда он дойдёт до нуля.
Имя, которого не видно в ls
Счётчик считает не только записи в каталогах. Открытый дескриптор процесса держит inode так же крепко.
Поэтому rm huge.log при живом сервисе не возвращает место на диске: имени нет, а дескриптор есть. df показывает 100%, du ничего не находит. Разбор с lsof и лечение - в уроке про процессы.
Обратная сторона того же механизма - способ спасти лог, который снесли по ошибке:
ls -l /proc/1234/fd/3
# 3 -> /var/log/app.log (deleted)
cp /proc/1234/fd/3 /var/log/app-restored.log # пока процесс жив
Перезапустишь сервис - последняя ссылка закроется, и данные уйдут по-настоящему.
В коде ровно то же
Функции удаления в языках - это тот же системный вызов. В PHP она даже названия не меняла: unlink().
f, err := os.Create("/tmp/report.csv")
if err != nil {
return err
}
defer f.Close()
// снимаем имя сразу, дескриптор остаётся рабочим
if err := os.Remove("/tmp/report.csv"); err != nil {
return err
}
// в каталоге файла уже нет, а писать в него можно
if _, err := f.WriteString("id,total\n1,42\n"); err != nil {
return err
}
// после f.Close() последняя ссылка закроется и данные исчезнут
$f = fopen('/tmp/report.csv', 'w');
if ($f === false) {
throw new RuntimeException('не открыть файл');
}
// снимаем имя сразу, дескриптор остаётся рабочим
unlink('/tmp/report.csv');
// в каталоге файла уже нет, а писать в него можно
fwrite($f, "id,total\n1,42\n");
// после fclose() последняя ссылка закроется и данные исчезнут
fclose($f);
Приём рабочий: временный файл, который гарантированно не переживёт процесс, даже если тот упадёт. Остальные способы работы с файлами - в уроке про файлы в Go.
Как стереть по-настоящему
На HDD - перезаписать поверх:
shred -u -n 3 secret.env # 3 прохода случайными данными, затем unlink
На SSD этот способ не работает. Контроллер сам решает, в какую физическую ячейку положить данные, и выравнивает износ, раскидывая записи по диску. «Перезапись поверх» уходит в другое место, а старая копия остаётся в ячейке, к которой файловая система больше не обращается. Через ФС её не видно, физически она есть.
Что работает:
- полнодисковое шифрование, включённое заранее: стёр ключ - весь диск разом стал шумом;
- сброс до заводских настроек на телефоне и ноутбуке делает именно это;
- для отдельного диска - ATA Secure Erase или NVMe Format, командой самой прошивке.
Полезные команды для файлов
file document.pdf # определить тип файла
du -sh /var/log/ # размер каталога
df -h # свободное место на дисках
find / -name "*.log" -size +100M # логи больше 100MB
which nginx # где лежит бинарник
Мини-задание
- Создай скрипт
hello.shс содержимым#!/bin/bashиecho "Hello" - Попробуй запустить
./hello.sh- получиPermission denied - Дай права:
chmod +x hello.shи запусти снова - Проверь права через
ls -la hello.sh - Убери бит
xу каталога (chmod 444 /tmp/lab) и убедись, чтоlsработает, аcatвнутри - нет - Выполни
umaskи посчитай на бумаге, какие права получат новый файл и новый каталог - Посмотри
ls -ld /tmpи найди буквуtв конце прав - Прогони
namei -lпо пути до любого файла в своём домашнем каталоге - Сделай хардлинк через
ln, посмотриls -liдо и после, удали первое имя и убедись, что данные на месте - Запусти
tail -fна файле, удали файл, найди его черезls -l /proc/$(pgrep -n tail)/fd
Итог
- Каждый файл имеет владельца, группу и три набора прав (user/group/other).
chmodменяет права,chownменяет владельца. Числовая нотация - быстрее.600для секретов,644для конфигов,755для скриптов.777- не используй.
Типичная ошибка
Лечить Permission denied через chmod 777 или sudo. Это как открыть все двери, потому что не нашёл нужный ключ. Разберись, какому пользователю нужен доступ, и дай минимально необходимые права.
Мини-практика
Создай каталог /tmp/permissions-lab. Внутри создай три файла: public.txt (644), secret.env (600), deploy.sh (755). Проверь через ls -la, что права выставлены верно. Попробуй прочитать secret.env от другого пользователя (через sudo -u nobody cat secret.env) - должен получить отказ.