Привіт! Я Kernelka 🐳. Якщо ви запускаєте сервіси у Docker на Linux, то знаєте, що зручність контейнерів не скасовує потребу в безпеці. Сьогодні налаштуємо три мегакорисні щити: user namespaces, профіль seccomp і read‑only файлову систему. Це мінімізує права, обмежить системні виклики ядра та закриє непотрібний запис у файловій системі. Все робимо практично — через термінал Linux, без зайвої магії.

Навіщо це потрібно

Контейнер — не повноцінна віртуальна машина. Він ділить ядро з хостом. Якщо процес у контейнері отримує забагато прав або має доступ до небезпечних системних викликів, компрометація може торкнутися хоста. Тому ми:

  • ізолюємо UID/GID процесів контейнера через user namespaces, щоб root всередині був «нікому» зовні;
  • обмежуємо системні виклики через seccomp;
  • вмикаємо read-only файлову систему контейнера, дозволяючи запис лише там, де це потрібно.

Ці кроки чудово вписуються у сучасну контейнеризацію і конкретно підсилюють права доступу Linux для процесів у контейнерах.

Покрокове налаштування

Передумови

  • Встановлений Docker Engine (root або sudo).
  • Доступ до системних файлів конфігурації та логів.
  • Базові навички роботи з терміналом Linux.

1) Вмикаємо user namespaces (userns-remap)

User namespaces змінюють відображення користувачів контейнера на хості. Навіть якщо процес у контейнері — root, на хості він перетворюється на «звичайного» користувача з UID із піддіапазону.

# Увімкнути userns-remap з дефолтним користувачем dockremap
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
  "userns-remap": "default"
}
EOF

# Перезапустити Docker
sudo systemctl daemon-reexec || sudo systemctl daemon-reload
sudo systemctl restart docker

# Перевірити, що працює user namespaces
docker info | grep -i userns || true

# Подивитися відображення UID/GID для dockremap
getent passwd dockremap || true
getent group dockremap || true
cat /etc/subuid | grep dockremap || true
cat /etc/subgid | grep dockremap || true

# Перевірка в контейнері (root всередині не дорівнює root на хості)
docker run --rm alpine sh -c 'id -u; id -g'

Якщо ви мапите томи на хост, для запису контейнера потрібно виставити власника з піддіапазону UID/GID dockremap (див. нижче у FAQ).

2) Жорсткіший seccomp-профіль

За замовчуванням Docker вже використовує seccomp, але ми можемо зробити профіль суворішим. Спочатку заберемо «дефолт» і збережемо:

# Завантажити стандартний профіль seccomp від Moby
sudo curl -fsSL \
  https://raw.githubusercontent.com/moby/moby/master/profiles/seccomp/default.json \
  -o /etc/docker/seccomp-default.json

# Створити більш суворий варіант (лише приклад; адаптуйте під вашу програму)
sudo tee /etc/docker/seccomp-restrict.json > /dev/null <<'EOF'
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "archRuntimes": [],
  "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"],
  "syscalls": [
    {"names": ["read", "write", "close", "fstat", "mmap", "mprotect", "munmap", "brk", "rt_sigaction", "rt_sigprocmask", "ioctl", "lseek", "getpid", "gettid", "clone", "set_tid_address", "nanosleep", "clock_gettime", "socket", "connect", "accept4", "bind", "listen", "recvfrom", "sendto", "shutdown", "getsockname", "getsockopt", "setsockopt"], "action": "SCMP_ACT_ALLOW"}
  ]
}
EOF

# Запустити контейнер з кастомним профілем
docker run --rm -it \
  --security-opt seccomp=/etc/docker/seccomp-restrict.json \
  alpine:latest sh -c 'echo ok && sleep 1'

Порада: починайте з /etc/docker/seccomp-default.json і по кроку забороняйте зайві виклики (наприклад, bpf, keyctl, perf_event_open), тестуйте сервіс на працездатність.

3) Read‑only файлові системи + винятки

Забороняємо запис у контейнері загалом і дозволяємо лише те, що потрібно додатку: тимчасові каталоги (tmpfs) і дані (томи).

# Приклад: веб-додаток потребує /tmp і /run, а дані пише у /data
# / є read-only, /tmp і /run — tmpfs, /data — окремий том

docker volume create app-data

docker run -d --name app \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
  --tmpfs /run:rw,noexec,nosuid,nodev,size=16m \
  -v app-data:/data:rw \
  nginx:stable

Тут ми різко зменшуємо поверхню атаки: навіть RCE у контейнері не зможе бездумно писати у файлову систему.

4) Комбінований приклад (разом: userns + seccomp + read‑only)

docker run -d --name secure-svc \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
  --tmpfs /run:rw,noexec,nosuid,nodev,size=16m \
  --security-opt seccomp=/etc/docker/seccomp-restrict.json \
  --security-opt no-new-privileges=true \
  --cap-drop ALL --cap-add NET_BIND_SERVICE \
  -p 8080:8080 \
  myorg/myapp:latest

Compose-варіант (для CI/CD і зручності):

cat > docker-compose.yml <<'YAML'
services:
  app:
    image: myorg/myapp:latest
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,nodev,size=64m
      - /run:rw,noexec,nosuid,nodev,size=16m
    security_opt:
      - seccomp:/etc/docker/seccomp-restrict.json
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    ports:
      - "8080:8080"
    volumes:
      - app-data:/data:rw
volumes:
  app-data:
YAML

docker compose up -d

Альтернативні способи

  • Rootless Docker: запускає демон і контейнери без root на хості. Сумісно не з усіма сценаріями, але додає ще один шар безпеки.
  • Podman: за замовчуванням бездемонний і добре працює в rootless-режимі, підтримує seccomp, user namespaces і read‑only так само.
  • AppArmor/SELinux: доповнює seccomp політиками доступу до файлів і ресурсів ядра; корисно для продакшн-серверів.

GUI-спосіб (через Portainer)

Якщо вам зручніше клікати, у Portainer при створенні контейнера:

  • У Security & host: увімкніть «Readonly container»;
  • У Capability settings: натисніть «Drop all», потім додайте тільки потрібні capability;
  • У Seccomp profile: виберіть «custom» і вкажіть шлях до вашого seccomp JSON;
  • Додайте tmpfs для /tmp та /run, і окремий том для /data.

User namespaces конфігуруються на рівні демона Docker (див. крок 1) і діятимуть для всіх контейнерів.

FAQ

Контейнер не може писати у том після userns-remap. Чому?

Через відображення UID/GID. Перевірте діапазон у /etc/subuid та /etc/subgid для користувача dockremap (зазвичай починається з 165536). Зробіть том належним цьому UID/GID:

# Приклад: якщо у /etc/subuid є "dockremap:165536:65536"
sudo chown -R 165536:165536 /var/lib/docker/volumes/app-data/_data

Чи впливають seccomp і read-only на продуктивність?

Вплив мінімальний. Найчастіше обмеження відчутні лише, якщо ваш додаток потребує екзотичних системних викликів або часто пише у ФС (а ми це перенесли в томи/tmpfs).

Мій сервіс падає з EPERM після увімкнення seccomp. Що робити?

Перевірте логи контейнера, dmesg або auditd, щоб побачити, який syscall блокується. Тимчасово запустіть з дефолтним профілем, знайдіть потрібний виклик і додайте його в allowlist вашого профілю.

Як вимкнути user namespaces для одного контейнера?

Запустіть з опцією:

docker run --userns=host ...

У Compose використовуйте:

userns_mode: "host"

Як поєднати це з firewall?

Ці техніки доповнюють firewall, але не замінюють його. Обмежуйте мережу контейнера (наприклад, окремими мережами Docker), і контролюйте вхідні порти через iptables/nftables на хості.

Порада від Kernelka

Почніть із найсуворішого профілю та read‑only режиму, а далі додавайте мінімально необхідні права. Ведіть невеликий чеклист для сервісів: які capability справді потрібні, які каталоги мають бути writable, які syscalls має дозволити seccomp. Автоматизуйте перевірки у CI: тест-контейнери з вашим профілем seccomp та smoke‑тести. І так, робіть резервні копії томів — на випадок «ой» 🙂

Підсумок

  • Увімкнули user namespaces — root у контейнері більше не root на хості.
  • Застосували seccomp-профіль — обмежили системні виклики.
  • Увімкнули read‑only ФС — запис лише там, де це потрібно.
  • Додали no-new-privileges та мінімізували capability.
  • Розглянули альтернативи: Rootless Docker, Podman, AppArmor/SELinux.

Тримайте свій Docker на Linux міцно захищеним — і спіть спокійно 🔒