--read-only робить кореневу файлову систему контейнера доступною лише для читання. Запис дозволено тільки в явно змонтовані томи й tmpfs.
docker run --rm --read-only alpine touch /x
# touch: /x: Read-only file system
docker run --rm --read-only --tmpfs /tmp alpine sh -c 'touch /tmp/y && echo ok'
# ok
Навіщо:
- зламаний застосунок не може змінити себе: зловмисник, що отримав виконання коду, не запише веб-шелл у каталог з кодом, не підмінить бінарні файли, не встановить інструменти через пакетний менеджер;
- незмінність: контейнер гарантовано відповідає образу - жодного «дрейфу» конфігурації через ручні зміни;
- видно всі місця запису: кожен каталог, куди застосунок пише, треба оголосити явно - це документує поведінку застосунку.
Що зазвичай потрібно для запису й чим це закрити:
- тимчасові файли (
/tmp,/var/run, кеш PHP-сесій) -tmpfs(у пам'яті, зникає при зупинці); - дані, що мають жити - іменовані томи;
- для Laravel:
storage/framework(кеш, сесії, скомпільовані шаблони),storage/logs,bootstrap/cache- tmpfs або томи. Логи краще писати вstderr(LOG_CHANNEL=stderr), тоді каталог логів не потрібен.
services:
app:
read_only: true
tmpfs:
- /tmp
- /var/www/html/storage/framework:uid=1000,gid=1000
volumes:
- uploads:/var/www/html/storage/app
Особливості tmpfs:
- зберігається в пам'яті й зараховується до ліміту пам'яті контейнера - великі тимчасові файли (обробка відео) там не місце;
- вміст зникає при зупинці контейнера;
- права й розмір задаються опціями монтування.
Поєднання захисних прапорців - стандартний «посилений» запуск:
read_only: true
user: "1000:1000"
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
Це закриває більшість сценаріїв закріплення зловмисника в контейнері після злому застосунку.