При чтении новостей о Docker может сложиться впечатление, что Docker по своей природе небезопасен и не готов к промышленной эксплуатации. А о проблемах, связанных с безопасным применением контейнеров, скажу лишь, что при правильном использовании контейнеры могут обеспечить более безопасную и эффективную работу, чем виртуальные машины или аппаратные решения.
### На что следует обратить особое внимание
* уязвимости в ядре – в отличие от виртуальной машины, ядро совместно используется всеми контейнерами и самим хостом, что увеличивает степень влияния любых уязвимостей, имеющихся в ядре. Если контейнер способен привести ядро в аварийное состояние (kernel panic), это выведет из строя весь хост. В виртуальных машинах ситуация намного лучше: атакующий должен сначала пройти через ядро виртуальной машины и гипервизор, прежде чем получит доступ к ядру хоста;
* атаки типа DoS (Denial-of-Service) – все контейнеры совместно используют ресурсы ядра. Если один контейнер способен монополизировать доступ к определенным ресурсам, например к оперативной памяти и к некоторым нематериальным ресурсам, таким как идентификаторы пользователей (UID), то другие контейнеры хоста окажутся «на голодном пайке», результатом чего станет полный отказ в обслуживании (denial of service), при котором пользователи лишаются доступа к некоторым компонентам системы или к системе в целом;
* взлом контейнеров – даже если атакующий получил доступ к одному контейнеру, то должна быть исключена возможность получения им доступа к другим контейнерам или к хосту. Так как пользователи не разделены по пространствам имен, любой процесс, «взломавший» контейнер, получит на хосте те же привилегии, которые были у него в контейнере: если в контейнере владельцем процесса был суперпользователь root, то и на хосте этот процесс будет иметь права суперпользователя root. Кроме того, это означает, что вы должны принять меры против атак с использованием повышения привилегий (privilege escalation), при которых пользователь захватывает более высокие права доступа, например привилегии root, чаще всего вследствие ошибок в коде приложений, требующих запуска с расширенными привилегиями. Учитывая юный возраст технологии контейнеров, следует организовать систему безопасности на основе предположения: ситуации взлома контейнеров маловероятны, но возможны;
* зараженные образы – как узнать, что используемые образы безопасны, что
они не поддельные, что они взяты из правильного источника? Если атакующий может обмануть вас и подменить образ, то опасности подвергаются и хост, и все ваши данные. Кроме того, необходимо всегда проверять актуальность запускаемых образов: они не должны содержать версий ПО с известными уязвимостями;
* нарушение защиты закрытых данных – если контейнер работает с базой данных или с сервисом, то наверняка потребуется использование некоторых закрытых данных, таких как API-ключ или имена пользователей и пароли. Когда атакующий узнает закрытые данные, он получает доступ ко всему сервису. Эта проблема крайне обостряется в архитектуре микросервисов, где контейнеры непрерывно останавливаются и запускаются, в отличие от архитектуры с малым количеством виртуальных машин с длинным жизненным циклом.
### Контейнеры и пространства имен
К ресурсам, к которым не применяются разграничения при помощи пространств имен, относятся:
• идентификатор пользователя (UID) – пользователи внутри контейнера имеют
тот же UID, что и на хосте. И если контейнер запущен с правами root, то при взломе этого контейнера атакующий получит права root на всем хосте.
• комплект ключей ядра (kernel keyring) – если ваше приложение или зависящие от него приложения используют комплект ключей ядра для обработки криптографических ключей и т. п., то очень важно знать об этом. Ключи разделены по UID, следовательно, контейнеры, запускаемые с одинаковым UID, получат доступ к одному и тому же набору ключей, как и соответствующий пользователь на хосте;
• само ядро и все модули ядра – если контейнер загружает модуль ядра (для этого требуются расширенные привилегии), то этот модуль становится доступным всем контейнерам на данном хосте.
• устройства – дисковые накопители, звуковые карты, устройства обработки графики (GPU);
• системное время – при изменении времени внутри любого контейнера изменяется системное время соответствующего хоста и всех прочих контейнеров. Такое изменение возможно только в тех контейнерах, для которых явно определен параметр SYS_TIME, который по умолчанию не разрешен.
### Рекомендации по обеспечению безопасности
#### Всегда определяйте пользователя
Никогда не запускайте приложения в режиме реальной эксплуатации с правами root внутри контейнера. Чтобы исключить запуск приложений с правами root, в файлах Dockerfile всегда должен создаваться непривилегированный пользователь, после чего необходимо переключиться на него с помощью инструкции USER или в скрипте для точки входа. Например:
```
RUN groupadd -r user_grp && useradd -r -g user_grp user
USER user
```
Инструкция USER будет действовать во всех последующих инструкциях, а также при запуске контейнера из данного образа. Иногда необходимо ввести инструкцию USER несколько позже, после того как в Dockerfile сначала будут выполнены действия, требующие привилегий root, например установка ПО.
#### Ограничения сетевой среды контейнеров
Контейнер должен открывать только те порты, которые действительно необходимы в режиме реальной эксплуатации, и доступ к этим портам следует предоставлять только тем контейнерам, которые будут действительно обращаться к ним.
Эту ситуацию можно изменить, запуская демон Docker с флагом --icc=false, который запрещает обмен данными между контейнерами. Это может защитить контейнеры от вмешательств извне, возможных при неограниченных соединениях между контейнерами. Для всех контейнеров, явно объявляющих о своих связях, сохраняется возможность обмена данными.
Docker управляет обменом информацией между контейнерами с помощью формируемых правил iptables (для этого при запуске демона необходимо установить флаг --iptables, который должен устанавливаться по умолчанию).
### Удаляйте бинарные файлы с установленными битами setuid/setgid
При возможности уда- ления или запрещения выполнения таких файлов резко снижается вероятность их использования в атаках, направленных на повышение привилегий.
Чтобы получить список бинарных (выполняемых) файлов с установленными битами setuid/setgid, выполните команду
```
find / -perm /+6000 -type f -exec ls -ld {} \;; 2> /dev/null
```
После этого можно «обезвредить» эти бинарные файлы командой `chmod a-s`, отменяющей установку бита suid. Например, можно создать менее опасный образ Debian с помощью следующего файла Dockerfile:
```
FROM debian:wheezy
RUN find / -perm /+6000 -type f -exec chmod a-s {} \; || true
```
### Ограничение использования оперативной памяти
Ограничение использования оперативной памяти защищает от DoS-атак и от приложений с утечками памяти, которые постепенно съедают всю доступную память на хосте (такие приложения могут автоматически перезапускаться, чтобы обеспечить требуемый уровень сервиса).
Флаги –m и --memory-swap в команде docker run ограничивают для контейнера объем оперативной памяти и свопа, который он может использовать. Аргумент --memory-swap устанавливает общий объем доступной памяти, то есть объем оперативной памяти плюс объем свопа, а не один только объем свопа, что приводит к небольшой путанице. По умолчанию ограничения памяти не устанавливаются. Если флаг -m используется без флага --memory-swap, то значение для --memory-swap принимается как удвоенное значение аргумента -m.
### Ограничение загрузки процессора
Если атакующий может захватить один контейнер или группу контейнеров, чтобы начать использование всех процессоров на данном хосте, то у него есть возможность лишить этого ресурса все прочие контейнеры хоста, то есть осуществить DoS-атаку.
В Docker совместное использование процессоров определяется относительным весовым коэффициентом со значением по умолчанию 1024, таким образом, по умолчанию все контейнеры получают одинаковое процессорное время.
Смысл относительного весового коэффициента заключается в том, чтобы не допустить захвата ресурса одним контейнером и лишения этого ресурса всех прочих контейнеров при параметрах, определенных по умолчанию. Тем не менее можно формировать группы контейнеров, которым будет отдаваться предпочтение при распределении процессорного ресурса, и в этом случае можно включать в эту группу контейнеры с пониженным коэффициентом по умолчанию, чтобы обеспечить справедливое распределение ресурса. При планировании совместного использования процессорного ресурса следует тщательно продумать значение весового коэффициента по умолчанию, чтобы контейнеры без явного указания этого коэффициента не ущемлялись другими контейнерами с высоким весовым коэффициентом.
Другим вариантом планирования совместного использования процессорного ресурса является применение абсолютно справедливого планировщика CFS (Completely Fair Scheduler) с помощью флагов --cpu-period и --cpu-quota. По этой методике для контейнеров устанавливается квота выделяемого процессорного ресурса (время, определяемое в микросекундах), которым они пользуются в течение заданного интервала времени. Если контейнер превышает квоту в рамках своего рабочего интервала, то он должен ждать следующего, для того чтобы продолжить свое выполнение. Например:
```
$ docker run –d --cpu-period=50000 --cpu-quota=25000 myimage
```
### Ограничение возможности перезапуска
Если контейнер постоянно останавливается и перезапускается, то он расходует большое количество системного времени и ресурсов, что может привести к отказу в обслуживании (DoS). Ситуация легко исправляется с помощью установки стратегии перезапуска on-failure вместо стратегии always. Например:
```
$ docker run -d --restart=on-failure:10 my-flaky-image
```
Docker получает указание перезапускать данный контейнер не более 10 раз.Текущее значение счетчика можно узнать из параметра .RestartCount в метаданных, возвращаемых командой docker inspect:
```
$ docker inspect -f "{{ .RestartCount }}" $(docker ps -lq)
```
### Ограничения файловых систем
Строгое запрещение записи в файл предотвращает некоторые типы атак и существенно снижает возможности взломщиков. Они не могут записать свой скрипт в файл и запустить его вместо вашего приложения или изменить важные данные или файлы конфигурации. Начиная с Docker версии 1.5, в команду docker run можно передавать флаг --read-only, полностью защищающий от записи файловую систему внутри контейнера.
Нечто похожее можно сделать и для томов, если добавить ключ :ro в конец аргумента тома:
```
$ docker run -v $(pwd):/pwd:ro debian touch /pwd/x
touch: cannot touch '/pwd/x': Read-only file system
```
Большинству приложений необходима запись в файлы, они не могут работать в среде с полной защитой от записи. В таких случаях можно определить каталоги и файлы, для которых необходима операция записи из приложения, и использовать отдельные тома для монтирования только этих файлов.
### Ограничение использования механизма Сapabilities.
В ядре Linux определены наборы привилегий, называемые Capabilities, которые могут назначаться процессам с целью ограничения их доступа к системным функциям.
По умолчанию контейнеры Docker запускаются с некоторым Capabilities. Например, контейнеру обычно запрещено использование таких устройств, как графический процессор (GPU) или звуковая карта, а также добавление модулей ядра. Чтобы предоставить контейнеру расширенные привилегии, нужно запустить его с аргументом --privileges в команде docker run.
С точки зрения обеспечения безопасности необходимо максимально ограничить количество Сapabilities, разрешенных для контейнеров. Управлять Сapabilities, доступными для конкретного контейнера, можно с помощью аргументов --cap-add и --cap-drop.
### Ограничение ресурсов (ulimits)
В ядре Linux определены ограничения ресурсов, которые могут применяться к процессам, например ограничение на количество создаваемых процессов-потомков и на количество дескрипторов открытых файлов. Эти ограничения можно использовать и для контейнеров Docker с помощью флага --ulimit в команде docker run или посредством установки ограничений по умолчанию при передаче аргумента --default-ulimit при запуске демона Docker. Аргумент включает два значения: мягкое ограничение и жесткое ограничение, разделенные двоеточием. Воздействие этого аргумента зависит от заданного ограничения. Если указано только одно значение, то оно используется и для мягкого, и для жесткого ограничений.
Полный набор возможных значений и их подробное описание приведено в руководстве man setrlimit (но следует отметить, что ограничение as не может
использоваться для контейнеров).
* cpu – ограничение использования процессорного времени определяется в секундах.
```
time docker run --ulimit cpu=12:14 amouat/stress stress --cpu 1
```
Аргумент ulimit позволяет принудительно завершить работу контейнера после 12-секундного интервала использования процессорного времени. Это может оказаться полезным для ограничения использования ресурса процессора контейнерами, запущенными другими процессами (например, при выполнении вычислений от имени пользователей).
* nofile – максимальное количество дескрипторов файлов, которые могут быть открыты в контейнере. И это ограничение можно использовать для защиты от DoS-атак, поскольку для атакующего ограничиваются операции чтения и записи в контейнере или на томах.
```
docker run --ulimit nofile=5 debian cat /etc/hostname
```
значением по умолчанию 1048576
* nproc – максимальное количество процессов, которое может быть создано пользователем данного контейнера. Это полезное ограничение, поскольку оно может защитить систему от форк-бомб и некоторых других типов атак. К сожалению, ограничение nproc не устанавливается для каждого отдельного контейнера, а накладывается на все процессы пользователя данного контейнера.
```
docker run --user 500 --ulimit nproc=2 -d debian sleep 100
```