4. Права доступа и безопасность
Права доступа и безопасность в Linux: DAC, Capabilities и изоляция процессов
«Принцип наименьших привилегий: каждая программа и каждый пользователь системы должны обладать лишь минимальным объемом прав, строго необходимым для решения задачи».
— Джером Зальцер и Майкл Шредер
Когда мы проектируем бэкенд-сервис на Go, мы привыкли уделять пристальное внимание безопасности прикладного уровня: стойкому хэшированию паролей (bcrypt/argon2), защите от SQL-инъекций через подготовленные выражения и криптографической проверке JWT-токенов. Но все эти усилия мгновенно обесцениваются, если процесс в операционной системе Linux запущен с избыточными привилегиями.
Любая уязвимость прикладного уровня (будь то Remote Code Execution через неосторожный os/exec или чтение произвольного файла через path traversal) при работе от суперпользователя превращается в полную компрометацию всего физического сервера или кластера.
В операционной системе Linux безопасность базируется на двух фундаментальных механизмах: классической дискреционной модели Discretionary Access Control (DAC) (права владельца, группы и остальных) и современной гранулярной модели Linux Capabilities (разделение монолитного всемогущества root на независимые системные привилегии).
Фундамент: UID, GID и ядро
Для ядра Linux нет строковых имен пользователей вроде alice, bob или root. Ядро оперирует исключительно целыми беззнаковыми числами (обычно 32-битными).
Когда процесс запускается, ядро ассоциирует с ним набор учетных данных (credentials), хранящихся в структуре struct cred внутри дескриптора процесса task_struct:
- Real UID/GID (RUID/RGID): Идентификатор пользователя, запустившего процесс.
- Effective UID/GID (EUID/EGID): Идентификатор, с учетом которого ядро проверяет права доступа к файлам и ресурсам. Обычно совпадает с Real, но может меняться (например, при запуске бинарника с битом
setuid). - Saved UID/GID: Сохраненный EUID, чтобы процесс мог временно сбросить привилегии и вернуть их обратно.
Пользователь с UID 0 (root) — это не маг. Это просто процесс, чей EUID равен нулю. Ядро Linux содержит множество проверок вида if (capable(CAP_SYS_ADMIN) || uid_eq(cred->euid, GLOBAL_ROOT_UID)). Если вы root — проверки пропускются.
Классические права (DAC: rwx)
Модель DAC означает, что владелец объекта (файла) сам решает, кто к нему имеет доступ. Каждый файл и директория в Linux имеют атрибуты для трех категорий:
- User (Owner): Права владельца файла.
- Group: Права группы, которой принадлежит файл.
- Others: Права всех остальных.
В памяти Inode это хранится как 9-битная маска (например, rwxr-xr-- = 111 101 100 = 754).
Как ядро принимает решение о доступе? Алгоритм строго линеен:
flowchart TD
A["Процесс запрашивает доступ к файлу"] --> B{"EUID процесса равен 0?"}
B -->|"Да - Root"| C["Доступ разрешен - Проверка DAC обходится для базовых операций"]
B -->|"Нет"| D{"EUID совпадает с UID владельца файла?"}
D -->|"Да"| E["Проверка битов Owner - rwx"]
D -->|"Нет"| F{"EGID или дополнительные группы совпадают с GID файла?"}
F -->|"Да"| G["Проверка битов Group - rwx"]
F -->|"Нет"| H["Проверка битов Others - rwx"]
E --> I{"Нужный бит установлен?"}
G --> I
H --> I
I -->|"Да"| C
I -->|"Нет"| J["Доступ запрещен - ошибка EACCES"]
style A fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#fff
style C fill:#065f46,stroke:#10b981,color:#fff
style J fill:#881337,stroke:#f43f5e,color:#fff
Проблема Root и появление Capabilities
Исторически в Unix было два состояния: ты обычный пользователь (ограничен) или ты root (бог). Если вашему веб-серверу нужно было прибиндиться к 80 порту (порты < 1024 привилегированные), вам приходилось запускать весь процесс от root. Это означало, что бага в обработке HTTP-запроса давала хакеру права root на всей машине.
В Linux 2.2 появилась система Capabilities — дробление всемогущества root на маленькие, специфичные привилегии.
Вместо проверки if (uid == 0), ядро начало проверять конкретные флаги в структуре cred процесса. Некоторые из них критически важны для бэкендеров:
CAP_NET_BIND_SERVICE: Право биндиться на порты < 1024.CAP_NET_RAW: Право создавать сырые сокеты (ping, сканеры).CAP_SYS_PTRACE: Право инспектировать другие процессы (используется дебаггерами вроде Delve и профилировщиками вродеperf).CAP_CHOWN: Право менять владельца файлов.CAP_SETUID/CAP_SETGID: Право менять UID/GID процесса (для дропа привилегий).
Go и дроп привилегий (Privilege Dropping)
В классическом C-бэкенде (например, Nginx) популярный паттерн: запуститься от root, прибиндиться к 80/443 портам, а затем вызвать setuid/setgid для смены EUID на www-data.
В Go сделать это крайне сложно и неидиоматично.
Современный идиоматичный путь в Go: Не используйте дроп привилегий внутри приложения. Настройте изоляцию снаружи.
- В Docker/K8s запускайте контейнер от обычного пользователя.
- Используйте
CAP_NET_BIND_SERVICEили настройтеiptablesдля проброса портов. - Или (самый частый вариант) используйте Reverse Proxy (Nginx) на 80/443 портах, а Go-бэкенд слушайте на непривилегированном 8080 порту.
Безопасность контейнеров: Почему root — это зло
По умолчанию, если вы не указываете директиву USER в Dockerfile, контейнер запускается от root (UID 0).
# ПЛОХОЙ DOCKERFILE
FROM golang:1.22-alpine
WORKDIR /app
COPY myapp .
CMD ["./myapp"] # Запустится от root!
Если злоумышленник найдет RCE (Remote Code Execution) в вашем Go-приложении (например, через небезопасный os.Exec или уязвимость парсера), он получит shell внутри контейнера от имени root. А поскольку контейнеры делят ядро с хост-системой, уязвимости ядра (Container Escapes) позволят хакеру "выбраться" из контейнера и получить root-доступ к вашему физическому серверу.
Правильный Production-ready Dockerfile:
FROM golang:1.22-alpine AS builder
# Статическая сборка без CGO
ENV CGO_ENABLED=0
WORKDIR /app
COPY . .
RUN go build -ldflags="-s -w" -o /myapp .
FROM alpine:latest
# Устанавливаем ca-certificates для HTTPS запросов из Go
RUN apk --no-cache add ca-certificates
WORKDIR /app
# 1. Создаем непривилегированного пользователя и группу
RUN adduser -D -u 10001 appuser
# 2. Копируем бинарник
COPY --from=builder /myapp /app/myapp
# 3. Меняем владельца файлов
RUN chown -R appuser:appuser /app
# 4. ЯВНО указываем, что контейнер будет работать от этого пользователя
USER appuser
ENTRYPOINT ["./myapp"]
Umask: Молчаливый модификатор прав
Когда ваше Go-приложение создает файл (например, лог или загружает документ), оно передает системе вызов open с желаемыми правами (например, 0666 для файлов или 0777 для директорий). Но ядро применяет к этим правам побитовую маску Umask процесса.
Umask вычитает определенные биты из запрашиваемых прав. По умолчанию в Linux umask обычно 0022.
Если Go пытается создать файл с правами 0666 (rw-rw-rw-), ядро применит umask:
0666 & ~0022 = 0644 (rw-r--r--).
Это защищает систему от случайного создания "открытых для записи всем" файлов. В Go можно изменить umask для текущего процесса через syscall.Umask(0), но делать это в конкурентной среде небезопасно, так как umask — это свойство всего процесса (разделяемое между всеми потоками ОС), а не отдельной горутины. Лучше явно указывать права через os.OpenFile и os.Chmod, если вам нужны специфические доступы.
Итог
- DAC (rwx) — базовая модель, которая проверяется ядром по строгому алгоритму: Owner -> Group -> Others. Совпадение владельца перекрывает все остальные биты.
- Capabilities — замена монолитному root. Позволяют дать процессу ровно столько привилегий, сколько нужно (например, только
CAP_NET_BIND_SERVICE). - Дроп привилегий в Go — сложен из-за архитектуры G-M-P. Правильный путь — запускать процесс сразу от непривилегированного пользователя, используя инфраструктуру или прокси для портов < 1024.
- Контейнеры от root — смертный грех инфраструктуры. Всегда создавайте
USERв Dockerfile. - Umask — фильтр, который ядро применяет при создании файлов, отрезая "лишние" права на запись для группы и остальных.
Безопасность файловой системы — это не только права доступа, но и понимание того, как процессы стартуют, перезапускаются при падении и управляются в продакшене. В следующей статье мы разберем стандартный инструмент для управления сервисами в Linux — 5. Systemd и управление сервисами.