Когда на одном физическом сервере работает несколько фоновых служб, приложению обычно требуется запись только в каталог состояния. Однако из-за слишком широких прав, заданных владельцем каталогов, оно может получить возможность изменять и файлы развёрнутого приложения. Отдельная учётная запись службы задаёт лишь первый уровень разграничения. Чтобы сузить область возможных ошибочных изменений, файловую систему в представлении службы следует сделать доступной только для чтения, оставив запись лишь в необходимых местах. Приёмка не должна ограничиваться проверкой успешного запуска: нужно также доказать, что запись в запрещённые места действительно невозможна.
1. Сначала определите, куда нужна запись, затем настройте песочницу
В примерах используется Ubuntu Server 24.04 LTS, а системными службами управляет systemd. Для выполнения действий нужны права администратора и Python 3. Все пути относятся к отдельному тестовому окружению; существующие рабочие юниты не изменяются. Данные для входа на выделенный сервер AmdMetal доступны в панели управления после оплаты. Подключение к Linux выполняется по SSH.
Сначала разделите код приложения, постоянное состояние, файлы времени выполнения и временные файлы. Код должна обновлять учётная запись, используемая для развёртывания, а службе достаточно доступа на чтение. Каталог состояния предназначен для данных, которые должны сохраняться после перезагрузки. Сокеты, файлы идентификаторов процессов и другие файлы времени выполнения, а также временные файлы нужно учитывать отдельно. Если журналы выводятся в стандартный поток вывода, их сбор можно поручить системе журналирования, не открывая для записи целое дерево каталогов журналов.
Основная настройка — ProtectSystem=strict: в пространстве имён монтирования службы она делает иерархию файловой системы доступной только для чтения, за исключением некоторых файловых систем API и других предусмотренных исключений. Затем ReadWritePaths сохраняет возможность записи в указанные каталоги. Эта настройка не меняет глобальное состояние монтирования на хосте и не обеспечивает полной изоляции процесса.
Режим «только чтение» не обеспечивает конфиденциальность. Доступные для чтения файлы не становятся автоматически скрытыми, а сетевой доступ не ограничивается сам по себе. Список разрешённых для записи путей также не заменяет права доступа к каталогам.
2. Подготовьте исходные права, позволяющие определить причину отказа
Для эксперимента нужны два каталога: один должен оставаться доступным для записи, а второй должен разрешать запись на уровне обычных прав, но блокироваться песочницей. Только так можно исключить ложноположительный результат из-за изначального отсутствия прав. Прежде чем продолжить, убедитесь, что приведённые ниже имя учётной записи и пути не используются существующими службами. Это тестовые объекты, а не стандартная учётная запись для входа на сервер.
sudo useradd --system --no-create-home \
--shell /usr/sbin/nologin sandbox-check
sudo install -d -o root -g root -m 0755 \
/srv/sandbox-check
sudo install -d -o sandbox-check -g sandbox-check -m 0750 \
/srv/sandbox-check/blocked \
/var/lib/sandbox-check
sudo runuser -u sandbox-check -- /usr/bin/python3 -c \
'import tempfile, os; f, p = tempfile.mkstemp(dir="/srv/sandbox-check/blocked"); os.close(f); os.unlink(p)'
Последняя команда должна завершиться успешно: она создаёт временный файл со случайным именем в каталоге, который планируется запретить для записи, и сразу удаляет его. Если команда завершается с ошибкой, сначала проверьте права прохода по каталогам, ACL и параметры нижележащего монтирования. Пока эта проблема не устранена, последующие отказы нельзя приписывать песочнице.
Владельцем развёрнутого скрипта должен быть администратор, чтобы учётная запись службы не могла изменить логику проверки. В тестовом каталоге намеренно назначен владелец с правом записи: это нужно только для подтверждения источника ограничения. В рабочей среде каталог кода должен оставаться недоступным для записи от имени службы; переносить это тестовое исключение в эксплуатацию нельзя.
3. Настройте разрешённые пути и отдельный проверочный скрипт
Сохраните следующий скрипт как /srv/sandbox-check/probe.py, назначьте владельцем администратора и установите права 0644. Запись в разрешённый путь должна проходить успешно. При обращении к запрещённому пути должна возвращаться ошибка файловой системы, доступной только для чтения; любая другая ошибка означает, что проверка не пройдена.
import errno
import os
import tempfile
def create_and_remove(directory):
fd, path = tempfile.mkstemp(prefix="probe-", dir=directory)
try:
with os.fdopen(fd, "w") as stream:
stream.write("sandbox-check\n")
stream.flush()
os.fsync(stream.fileno())
finally:
os.unlink(path)
create_and_remove("/var/lib/sandbox-check")
try:
create_and_remove("/srv/sandbox-check/blocked")
except OSError as exc:
if exc.errno != errno.EROFS:
raise
else:
raise RuntimeError("Write boundary was not enforced")
print("Allowed write passed; blocked write returned EROFS")
Случайные имена файлов позволяют не перезаписывать существующие данные. Вызов fsync проверяет цепочку операций записи, но не заменяет тестирование производительности или сохранности данных при отключении питания. Вывод скрипта содержит только результат проверки, без учётных данных и содержимого рабочих данных.
Создайте /etc/systemd/system/sandbox-check.service:
[Unit]
Description=Filesystem sandbox verification
[Service]
Type=oneshot
User=sandbox-check
Group=sandbox-check
ExecStart=/usr/bin/python3 /srv/sandbox-check/probe.py
ProtectSystem=strict
ReadWritePaths=/var/lib/sandbox-check
PrivateTmp=true
NoNewPrivileges=true
Параметр PrivateTmp=true предоставляет службе изолированное представление временных каталогов. Поэтому «запись разрешена только в один каталог постоянных данных» не означает, что у процесса вообще есть лишь одно доступное для записи место. Параметр NoNewPrivileges=true ограничивает получение новых привилегий при выполнении программ, но не отзывает уже предоставленные права доступа к файлам.
4. Проверьте и успешную запись, и отказ
Сначала проверьте юнит, затем загрузите его и запустите:
sudo systemd-analyze verify /etc/systemd/system/sandbox-check.service
sudo systemctl daemon-reload
sudo systemctl start sandbox-check.service
sudo systemctl show sandbox-check.service \
-p Result -p ExecMainStatus -p ProtectSystem -p ReadWritePaths
sudo journalctl -u sandbox-check.service -b --no-pager -n 30
Ожидаемый результат — Result=success, ExecMainStatus=0 и сообщение об успешной проверке в журнале. Неактивное состояние после завершения службы однократного выполнения — нормальное поведение: здесь не настроены ни постоянная работа, ни сохранение активного состояния после выхода.
Одного просмотра значений параметров недостаточно, чтобы подтвердить действие ограничения. Доказательства должны включать все три результата: успешную запись от имени той же учётной записи вне песочницы, успешную запись в разрешённый каталог внутри песочницы и отказ с EROFS при записи в запрещённый каталог. Ошибка EACCES подтверждает лишь отказ на уровне обычных прав или другой политики безопасности и не считается успешным прохождением этой проверки.
Для рабочей службы проверьте реальные сценарии приложения
После успешного выполнения проверочного скрипта подготовьте отдельную конфигурацию усиления защиты для реальной службы. Выполните тест рабочих операций, охватывающий загрузку файлов, создание кэша, сохранение состояния и очистку временных файлов. Если приложение обновляет данные через временный файл с последующим переименованием, сам целевой каталог тоже должен быть доступен для записи. Исключения только для одного файла часто недостаточно.
Также проверьте, куда в действительности ведут символические ссылки. Ссылка, которая в конфигурации выглядит как путь внутри каталога состояния, в итоге может указывать на другой путь, доступный только для чтения. Нельзя скрывать эту проблему, расширяя права на весь родительский каталог.
5. Диагностика, отмена изменений и протокол проверки
Если сбой происходит до запуска службы, сначала изучите журнал: причиной могут быть невозможность создать пространство имён монтирования, отсутствие пути или опечатка в конфигурации. Если ошибка возникает после запуска, разбирайтесь по месту файловой операции: не пытается ли приложение незаметно записать кэш в каталог кода, не зависит ли оно от общих временных файлов и не нужен ли ему дополнительный каталог времени выполнения.
Если рабочей службе нужен каталог времени выполнения, можно использовать RuntimeDirectory, чтобы systemd управлял его жизненным циклом, а затем проверить фактический доступ. Не добавляйте /var или /srv целиком в список разрешённых путей. Разрешайте запись отдельно для каждой подтверждённой потребности.
После изменения конфигурации команда daemon-reload лишь перечитывает определения юнитов и не применяет новую песочницу к уже работающим процессам. Для рабочей службы необходимо выполнить контролируемый перезапуск, чтобы создать новое окружение процесса, а затем повторить положительные и отрицательные тесты.
Чтобы отменить эксперимент, остановите юнит, удалите созданный для него файл и перечитайте определения. Тестовые каталоги данных и учётную запись очищайте отдельно, убедившись, что их не использует ни один процесс. Не применяйте универсальную команду рекурсивного удаления рабочих каталогов. Для рабочей службы восстановите сохранённую до изменений конфигурацию, перезапустите службу и проверьте результат.
В итоговом протоколе сохраните фактически применённую конфигурацию юнита, учётную запись службы, список разрешённых для записи путей, код завершения проверочного скрипта и результаты тестирования реальных рабочих операций. Цель приёмки — не более высокая оценка безопасности, а успешное выполнение каждой необходимой записи и объяснимый, воспроизводимый отказ для каждой записи, которая должна быть запрещена.
Часто задаваемые вопросы
Предоставляет ли ReadWritePaths недостающие права записи?
Нет. Параметр оставляет доступные для записи пути внутри файловой песочницы, но не меняет владельца, биты доступа или ACL. Учётной записи службы также нужны обычные права на каталог.
Защищает ли ProtectSystem=strict секреты от чтения?
Нет. Он прежде всего ограничивает запись, не скрывает автоматически доступные для чтения файлы и не запрещает сетевые соединения. Права на секреты, скрытие путей и сетевые ограничения настраиваются отдельно.
Выделенный сервер AMD EPYC 7543P
от $458.7/мес.