Compare commits
6 Commits
7be5dd910b
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 8597a75abb | |||
| 81f4ea764e | |||
| 39cf9e83e3 | |||
| aecd708bb5 | |||
| b0913ab7ef | |||
| d3559ac6a0 |
@@ -49,7 +49,7 @@ Filesystem Inodes IUsed IFree IUse% Mounted on
|
|||||||
/dev/mapper/vg02-opt 262141952 486209 261655743 1% /opt
|
/dev/mapper/vg02-opt 262141952 486209 261655743 1% /opt
|
||||||
```
|
```
|
||||||
|
|
||||||
Деталь, которая зацепила взгляд: всего 67424 инода на 29-гигабайтном томе - по привычке принял это за фиксированное значение, заданное при создании файловой системы, как это работает в ext4. При 99% занятости с таким скромным лимitom легко вылететь по инодам даже без аномального количества файлов. Отложил мысль на потом - и, как выяснится в финале статьи, отложил с неверной гипотезой в основе.
|
Деталь, которая зацепила взгляд: всего 67424 инода на 29-гигабайтном томе - по привычке принял это за фиксированное значение, заданное при создании файловой системы, как это работает в ext4. При 99% занятости с таким скромным лимитом легко вылететь по инодам даже без аномального количества файлов. Отложил мысль на потом - и, как выяснится в финале статьи, отложил с неверной гипотезой в основе.
|
||||||
|
|
||||||
## Первая ошибка: du без -x
|
## Первая ошибка: du без -x
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ Codename: noble
|
|||||||
|
|
||||||
**Про пользователя:** Ubuntu при установке создаёт непривилегированного пользователя с sudo. Если устанавливал с таким пользователем - используй его. Если получил root-only сервер (облако, VPS) - создадим пользователя в разделе SSH.
|
**Про пользователя:** Ubuntu при установке создаёт непривилегированного пользователя с sudo. Если устанавливал с таким пользователем - используй его. Если получил root-only сервер (облако, VPS) - создадим пользователя в разделе SSH.
|
||||||
|
|
||||||
Все команды выполняются с `sudo` если не указано иное. Если работаешь от root - `sudo` можно опустить.
|
> [!info] ВАЖНО
|
||||||
|
> Все команды выполняются с `sudo` если не указано иное. Если работаешь от root - `sudo` можно опустить.
|
||||||
|
|
||||||
## Обновление системы
|
## Обновление системы
|
||||||
|
|
||||||
@@ -260,7 +261,7 @@ Ubuntu 24.04 хранит конфигурацию SSH в `/etc/ssh/sshd_config.
|
|||||||
Создай файл с настройками:
|
Создай файл с настройками:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo nano /etc/ssh/sshd_config.d/99-hardening.conf
|
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -296,6 +297,8 @@ AcceptEnv LANG LC_*
|
|||||||
AddressFamily inet
|
AddressFamily inet
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Важно:** Ubuntu 24.04 хранит файлы конфигурации SSH в `/etc/ssh/sshd_config.d/` и применяет **первое** вхождение каждой директивы. Облачные образы Ubuntu часто поставляются с файлом `50-cloud-init.conf` который содержит `PasswordAuthentication yes` - он загружается раньше `99-hardening.conf` и его значение побеждает. Файл `00-hardening.conf` загружается первым и корректно перекрывает дефолты cloud-init.
|
||||||
|
|
||||||
**Важно:** Ubuntu 24.04 использует socket-активацию через `ssh.socket` - порт слушает не sshd напрямую, а сокет-юнит. Обычный `systemctl restart ssh` перезапускает только сервис, порт остаётся прежним. Для применения нового порта нужно перезапускать именно сокет.
|
**Важно:** Ubuntu 24.04 использует socket-активацию через `ssh.socket` - порт слушает не sshd напрямую, а сокет-юнит. Обычный `systemctl restart ssh` перезапускает только сервис, порт остаётся прежним. Для применения нового порта нужно перезапускать именно сокет.
|
||||||
|
|
||||||
**КРИТИЧНО:** Перед перезапуском открой вторую сессию и не закрывай её.
|
**КРИТИЧНО:** Перед перезапуском открой вторую сессию и не закрывай её.
|
||||||
@@ -330,7 +333,7 @@ LISTEN 0 4096 0.0.0.0:2222
|
|||||||
sudo systemctl cat ssh.socket
|
sudo systemctl cat ssh.socket
|
||||||
```
|
```
|
||||||
|
|
||||||
Строка `ListenStream=0.0.0.0:2222` подтверждает что порт применился.
|
Строка `ListenStream=2222` подтверждает что порт применился.
|
||||||
|
|
||||||
В новом терминале проверь подключение:
|
В новом терминале проверь подключение:
|
||||||
|
|
||||||
@@ -355,7 +358,7 @@ sudo nano /etc/ssh/banner
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
```
|
```
|
||||||
|
|
||||||
Добавь в `/etc/ssh/sshd_config.d/99-hardening.conf`:
|
Добавь в `/etc/ssh/sshd_config.d/00-hardening.conf`:
|
||||||
|
|
||||||
```
|
```
|
||||||
Banner /etc/ssh/banner
|
Banner /etc/ssh/banner
|
||||||
@@ -460,59 +463,11 @@ sudo nano /etc/fail2ban/jail.local
|
|||||||
|
|
||||||
```ini
|
```ini
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
# Бан на 1 час
|
|
||||||
bantime = 3600
|
bantime = 3600
|
||||||
# За 10 минут
|
|
||||||
findtime = 600
|
findtime = 600
|
||||||
# После 3 неудачных попыток
|
|
||||||
maxretry = 3
|
maxretry = 3
|
||||||
# Не банить свои IP
|
|
||||||
ignoreip = 127.0.0.1/8 ::1
|
ignoreip = 127.0.0.1/8 ::1
|
||||||
|
|
||||||
[sshd]
|
|
||||||
enabled = true
|
|
||||||
port = 2222
|
|
||||||
filter = sshd
|
|
||||||
logpath = /var/log/auth.log
|
|
||||||
maxretry = 3
|
|
||||||
bantime = 3600
|
|
||||||
```
|
|
||||||
|
|
||||||
Замени `2222` на свой SSH порт.
|
|
||||||
|
|
||||||
**Особенность Ubuntu 24.04:** `systemd-journald` по умолчанию не пишет в `/var/log/auth.log`. Если файл отсутствует или пустой:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ls -la /var/log/auth.log
|
|
||||||
```
|
|
||||||
|
|
||||||
Включи запись в файл:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/rsyslog.d/50-default.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
Убедись что строка не закомментирована:
|
|
||||||
|
|
||||||
```
|
|
||||||
auth,authpriv.* /var/log/auth.log
|
|
||||||
```
|
|
||||||
|
|
||||||
Перезапусти rsyslog:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart rsyslog
|
|
||||||
```
|
|
||||||
|
|
||||||
Или используй journald backend в fail2ban - он умеет читать логи напрямую из journald без файлов:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/fail2ban/jail.local
|
|
||||||
```
|
|
||||||
|
|
||||||
Добавь в секцию `[sshd]`:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[sshd]
|
[sshd]
|
||||||
enabled = true
|
enabled = true
|
||||||
port = 2222
|
port = 2222
|
||||||
@@ -522,7 +477,9 @@ maxretry = 3
|
|||||||
bantime = 3600
|
bantime = 3600
|
||||||
```
|
```
|
||||||
|
|
||||||
При `backend = systemd` параметр `logpath` не нужен - fail2ban читает journald напрямую.
|
Замени `2222` на свой SSH порт.
|
||||||
|
|
||||||
|
`backend = systemd` — обязательно для Ubuntu 24.04. На этом дистрибутиве journald является основным хранилищем логов, файл `/var/log/auth.log` может отсутствовать или не заполняться. При `backend = systemd` параметр `logpath` не нужен — fail2ban читает journald напрямую.
|
||||||
|
|
||||||
Запусти:
|
Запусти:
|
||||||
|
|
||||||
@@ -1064,8 +1021,9 @@ sudo ufw status verbose
|
|||||||
# Статус fail2ban
|
# Статус fail2ban
|
||||||
sudo fail2ban-client status sshd
|
sudo fail2ban-client status sshd
|
||||||
|
|
||||||
# Состояние диска
|
# Определить имя диска и проверить его состояние (только физический сервер)
|
||||||
sudo smartctl -H /dev/sda
|
lsblk -d -o NAME,TYPE | grep disk
|
||||||
|
sudo smartctl -H /dev/ИМЯ_ДИСКА
|
||||||
|
|
||||||
# Swap
|
# Swap
|
||||||
free -h
|
free -h
|
||||||
|
|||||||
@@ -0,0 +1,796 @@
|
|||||||
|
---
|
||||||
|
title: "Ubuntu 24.04: настройка Docker-хоста для production"
|
||||||
|
date: 2026-07-29
|
||||||
|
draft: false
|
||||||
|
description: "Пошаговая настройка Ubuntu 24.04 LTS как Docker-хоста для production: установка Docker, hardening daemon, UFW + Docker интеграция, systemd-resolved и DNS, изоляция контейнеров, docker.sock."
|
||||||
|
summary: "Пошаговая настройка Ubuntu 24.04 LTS как Docker-хоста для production: установка Docker, hardening daemon, UFW + Docker интеграция, systemd-resolved и DNS, изоляция контейнеров, docker.sock."
|
||||||
|
tags: ["ubuntu 24.04", "docker", "security", "hardening", "ufw", "production", "containers", "linux"]
|
||||||
|
categories: ["Безопасность"]
|
||||||
|
series: ["Linux Hardening"]
|
||||||
|
series_order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ubuntu 24.04: настройка Docker-хоста для production
|
||||||
|
|
||||||
|
Статья предполагает что базовый hardening уже сделан по статье: SSH на нестандартном порту, UFW включён, fail2ban настроен. Если нет - сначала туда.
|
||||||
|
{{< article link="/posts/ubuntu-24-base-hardening/">}}
|
||||||
|
|
||||||
|
В этой статье - Docker и всё что нужно чтобы контейнеры не стали дырой в безопасности хоста.
|
||||||
|
|
||||||
|
**Что получишь на выходе:**
|
||||||
|
- Docker установлен из официального репозитория (не snap)
|
||||||
|
- daemon.json настроен безопасно - без userns-remap и icc:false по умолчанию
|
||||||
|
- UFW и Docker работают вместе
|
||||||
|
- systemd-resolved и DNS в контейнерах - рабочее решение
|
||||||
|
- docker.sock - понимание риска и контроль доступа
|
||||||
|
- Контейнеры изолированы по сетям, ограничены по ресурсам
|
||||||
|
- Образы проверяются на уязвимости
|
||||||
|
|
||||||
|
## Исходные данные
|
||||||
|
|
||||||
|
- Ubuntu 24.04 LTS с выполненным базовым hardening
|
||||||
|
- Минимум 2GB RAM, 20GB диска
|
||||||
|
- Пользователь с sudo
|
||||||
|
|
||||||
|
Проверь версию:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lsb_release -a
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Distributor ID: Ubuntu
|
||||||
|
Description: Ubuntu 24.04.4 LTS
|
||||||
|
Release: 24.04
|
||||||
|
Codename: noble
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!info] ВАЖНО!
|
||||||
|
> Все команды выполняются с `sudo` если не указано иное.
|
||||||
|
> Если работаешь от root - `sudo` можно опустить.
|
||||||
|
|
||||||
|
## Установка Docker
|
||||||
|
|
||||||
|
> [!danger] ОЧЕНЬ ВАЖНО!
|
||||||
|
> Не используй snap версию в production!
|
||||||
|
|
||||||
|
В Ubuntu можно поставить Docker через snap (`sudo snap install docker`).
|
||||||
|
|
||||||
|
Snap-версия Docker работает в собственном confinement-окружении со своими путями (`/var/snap/docker/`), хуже интегрируется с системным `iptables`/`UFW`, и обновляется по графику snap, а не когда нужно тебе. Используй официальный репозиторий Docker - там стандартный путь `/var/lib/docker`, предсказуемое поведение, полный контроль над версией.
|
||||||
|
|
||||||
|
Проверь что snap-версии нет:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
snap list | grep docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Если есть - удали:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo snap remove docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### Удаление старых версий
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt remove -y docker docker-engine docker.io containerd runc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Установка из официального репозитория
|
||||||
|
|
||||||
|
Зависимости
|
||||||
|
```bash
|
||||||
|
sudo apt install -y ca-certificates curl gnupg lsb-release
|
||||||
|
```
|
||||||
|
|
||||||
|
Создать директорию для ключей
|
||||||
|
```bash
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавить GPG ключ Docker
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
|
||||||
|
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавить репозиторий
|
||||||
|
```bash
|
||||||
|
echo \
|
||||||
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||||
|
https://download.docker.com/linux/ubuntu \
|
||||||
|
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
Обновить индекс и установить
|
||||||
|
```bash
|
||||||
|
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверь версию:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker --version
|
||||||
|
```
|
||||||
|
Ожидаем:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Docker version 29.x.x, build xxxxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверь daemon:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl status docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаем:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
● docker.service - Docker Application Container Engine
|
||||||
|
Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: enabled)
|
||||||
|
Active: active (running) since Wed 2026-07-29 MSK; 3h 38min ago
|
||||||
|
TriggeredBy: ● docker.socket
|
||||||
|
Docs: https://docs.docker.com
|
||||||
|
Main PID: 182611 (dockerd)
|
||||||
|
Tasks: 8
|
||||||
|
Memory: 23.4M (peak: 24.0M)
|
||||||
|
CPU: 2.661s
|
||||||
|
CGroup: /system.slice/docker.service
|
||||||
|
└─182611 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
### Добавление пользователя в группу docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
```
|
||||||
|
|
||||||
|
Выйди и зайди снова (Выполни `newgrp docker` для текущей сессии без перелогина). Проверь:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm hello-world
|
||||||
|
```
|
||||||
|
|
||||||
|
## docker.sock - главный риск
|
||||||
|
|
||||||
|
Прежде чем настраивать daemon - разберёмся с самым опасным местом.
|
||||||
|
|
||||||
|
`/var/run/docker.sock` - Unix сокет Docker daemon. Любой процесс с доступом к нему имеет **полный root на хосте**. Через один вызов API можно смонтировать корень хоста и получить туда доступ:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Пример атаки - одна команда и ты root на хосте
|
||||||
|
docker run --rm -v /:/host alpine chroot /host
|
||||||
|
```
|
||||||
|
|
||||||
|
**Правила работы с docker.sock:**
|
||||||
|
|
||||||
|
> [!info] ВАЖНО
|
||||||
|
> Никогда не монтируй docker.sock в контейнер без явной необходимости.
|
||||||
|
|
||||||
|
Если сервис требует его (Portainer, Watchtower, Traefik с автообнаружением) - этот сервис имеет root-доступ к хосту.
|
||||||
|
|
||||||
|
Проверь права на сокет:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la /var/run/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
srw-rw---- 1 root docker 0 Jul 29 11:38 /var/run/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверь кто в группе docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
getent group docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Только доверенные пользователи должны быть в этой группе - фактически это равно правам root.
|
||||||
|
|
||||||
|
Проверь какие контейнеры монтируют сокет:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps -q | xargs -I {} docker inspect {} \
|
||||||
|
--format '{{.Name}}: {{range .Mounts}}{{if eq .Source "/var/run/docker.sock"}}DOCKER_SOCK_MOUNTED{{end}}{{end}}' \
|
||||||
|
2>/dev/null | grep DOCKER_SOCK_MOUNTED
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hardening daemon.json
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/docker/daemon.json
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"log-driver": "json-file",
|
||||||
|
"log-opts": {
|
||||||
|
"max-size": "50m",
|
||||||
|
"max-file": "3"
|
||||||
|
},
|
||||||
|
"live-restore": true,
|
||||||
|
"userland-proxy": false,
|
||||||
|
"no-new-privileges": true,
|
||||||
|
"storage-driver": "overlay2",
|
||||||
|
"default-ulimits": {
|
||||||
|
"nofile": {
|
||||||
|
"Name": "nofile",
|
||||||
|
"Hard": 65536,
|
||||||
|
"Soft": 65536
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Что и почему:**
|
||||||
|
|
||||||
|
`log-driver` + лимиты - без них логи контейнеров забивают `/var/lib/docker/containers/` за дни на активном сервисе.
|
||||||
|
|
||||||
|
`live-restore: true` - контейнеры продолжают работать пока daemon перезапускается.
|
||||||
|
|
||||||
|
`userland-proxy: false` - iptables вместо userland proxy, быстрее, меньше процессов.
|
||||||
|
|
||||||
|
`no-new-privileges: true` - контейнеры не поднимают привилегии через setuid/setgid бинарники.
|
||||||
|
|
||||||
|
`storage-driver: overlay2` - зафиксирован явно, хотя на Ubuntu 24.04 он и так используется по умолчанию.
|
||||||
|
|
||||||
|
### Почему нет userns-remap
|
||||||
|
|
||||||
|
Root в контейнере мапится на непривилегированный UID на хосте (например 100000). Звучит безопасно, но ломает bind mounts: файлы на хосте принадлежат UID 1000, контейнер видит UID 100000 и получает `Permission denied`. Каждый сервис с записью на хост требует `chown -R 100000:100000`, и пересчёт нужен для каждого нового сервиса.
|
||||||
|
|
||||||
|
> [!info] ВАЖНО
|
||||||
|
> Включай только если осознанно готов управлять UID маппингом для каждого тома.
|
||||||
|
|
||||||
|
### Почему нет icc:false
|
||||||
|
|
||||||
|
Глобальный `icc: false` ломает общение контейнеров внутри одного docker-compose стека - Compose создаёт свою bridge сеть, и без `icc` контейнеры в ней не видят друг друга без явных `--link`.
|
||||||
|
|
||||||
|
Правильная изоляция - через отдельные сети Docker, не через глобальный флаг daemon. Раздел ниже.
|
||||||
|
|
||||||
|
Примени:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверь:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker info | grep -E "Storage Driver|Logging Driver|Live Restore"
|
||||||
|
```
|
||||||
|
|
||||||
|
## sysctl для Docker-хоста
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/sysctl.d/99-docker.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
# IP forwarding - обязательно для Docker
|
||||||
|
net.ipv4.ip_forward = 1
|
||||||
|
|
||||||
|
# IPv6 forwarding - только если используешь IPv6 в контейнерах
|
||||||
|
# net.ipv6.conf.all.forwarding = 1
|
||||||
|
|
||||||
|
# Лимиты inotify
|
||||||
|
fs.inotify.max_user_instances = 512
|
||||||
|
fs.inotify.max_user_watches = 524288
|
||||||
|
|
||||||
|
# Для Java приложений (Elasticsearch и подобных) в контейнерах
|
||||||
|
vm.max_map_count = 262144
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo sysctl -p /etc/sysctl.d/99-docker.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
## systemd-resolved и DNS в контейнерах
|
||||||
|
|
||||||
|
Это самая частая проблема на Ubuntu специфично.
|
||||||
|
Ubuntu 24.04 использует `systemd-resolved` - DNS резолвер слушает на `127.0.0.53`. Контейнер не может достать этот адрес - `127.0.0.53` это loopback хоста, контейнер находится в своём network namespace и видит свой собственный loopback, не хостовый.
|
||||||
|
|
||||||
|
Симптом:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm alpine nslookup google.com
|
||||||
|
# ;; connection timed out; no servers could be reached
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение 1 (быстрое, для отдельных контейнеров):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --dns 8.8.8.8 --dns 8.8.4.4 alpine nslookup google.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение 2 (для всех контейнеров через daemon.json):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/docker/daemon.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавь:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dns": ["8.8.8.8", "8.8.4.4"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
**Решение 3 (если нужен внутренний DNS, не публичный):**
|
||||||
|
|
||||||
|
Узнай реальный upstream DNS на хосте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
resolvectl status | grep "DNS Server"
|
||||||
|
```
|
||||||
|
|
||||||
|
Используй именно его IP (не `127.0.0.53`) в `daemon.json`.
|
||||||
|
|
||||||
|
Docker Compose поддерживает то же самое на уровне сервиса:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
dns:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
|
```
|
||||||
|
|
||||||
|
## UFW + Docker интеграция
|
||||||
|
|
||||||
|
Та же проблема что и на любом дистрибутиве - Docker пишет свои iptables правила напрямую, обходя UFW.
|
||||||
|
|
||||||
|
**Подход 1 (рекомендуемый): порты только на localhost**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Плохо - обходит UFW
|
||||||
|
docker run -p 80:80 nginx
|
||||||
|
|
||||||
|
# Хорошо
|
||||||
|
docker run -p 127.0.0.1:80:80 nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Reverse proxy на хосте слушает 0.0.0.0:443, UFW разрешает только 443, контейнеры недоступны напрямую.
|
||||||
|
|
||||||
|
**Подход 2: DOCKER-USER chain**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/ufw/after.rules
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавь в конец:
|
||||||
|
|
||||||
|
```
|
||||||
|
# BEGIN UFW AND DOCKER
|
||||||
|
*filter
|
||||||
|
:DOCKER-USER - [0:0]
|
||||||
|
|
||||||
|
-A DOCKER-USER -s 10.0.0.0/8 -j RETURN
|
||||||
|
-A DOCKER-USER -s 172.16.0.0/12 -j RETURN
|
||||||
|
-A DOCKER-USER -s 192.168.0.0/16 -j RETURN
|
||||||
|
|
||||||
|
-A DOCKER-USER -j ufw-user-forward
|
||||||
|
|
||||||
|
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
|
||||||
|
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
|
||||||
|
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
|
||||||
|
|
||||||
|
-A DOCKER-USER -j RETURN
|
||||||
|
COMMIT
|
||||||
|
# END UFW AND DOCKER
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Для большинства случаев достаточно **подхода 1**.
|
||||||
|
|
||||||
|
## Сетевая изоляция контейнеров
|
||||||
|
|
||||||
|
Вместо глобального `icc: false` - отдельные сети под каждое приложение:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create --driver bridge app_network
|
||||||
|
|
||||||
|
docker run -d --name app --network app_network myapp
|
||||||
|
docker run -d --name db --network app_network postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
Контейнеры из разных сетей не видят друг друга. В docker-compose сети создаются автоматически для каждого стека - это и есть рабочая изоляция.
|
||||||
|
|
||||||
|
## Безопасность образов
|
||||||
|
|
||||||
|
### Trivy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | \
|
||||||
|
gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
|
||||||
|
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] \
|
||||||
|
https://aquasecurity.github.io/trivy-repo/deb \
|
||||||
|
$(lsb_release -sc) main" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/trivy.list > /dev/null
|
||||||
|
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y trivy
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
trivy image nginx:alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
Перед деплоем:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
trivy image --exit-code 1 --severity CRITICAL,HIGH nginx:alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
Правило: CRITICAL уязвимости - образ не идёт в production.
|
||||||
|
|
||||||
|
### Выбор образов
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Большой attack surface
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
# Минимальный
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
# Ещё меньше - нет shell, нет пакетного менеджера
|
||||||
|
FROM gcr.io/distroless/static-debian12
|
||||||
|
```
|
||||||
|
|
||||||
|
Конкретные теги, не `latest`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull nginx:1.27-alpine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Изоляция контейнеров
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Read-only filesystem
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --read-only --tmpfs /tmp --tmpfs /var/run nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seccomp
|
||||||
|
|
||||||
|
Docker применяет дефолтный seccomp профиль автоматически:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker info | grep seccomp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Никогда не использовать `--privileged`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# НИКОГДА в production - отключает всю изоляцию
|
||||||
|
docker run --privileged nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ресурсные ограничения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Memory с soft limit
|
||||||
|
docker run -m 512m --memory-reservation 256m nginx
|
||||||
|
|
||||||
|
# CPU
|
||||||
|
docker run --cpus="0.5" nginx
|
||||||
|
|
||||||
|
# PID limit - защита от fork bomb
|
||||||
|
docker run --pids-limit 200 nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка OOM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dmesg | grep -i "oom\|killed process"
|
||||||
|
docker inspect CONTAINER --format='{{.State.OOMKilled}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose с hardening
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:1.27-alpine
|
||||||
|
container_name: web
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:80"
|
||||||
|
|
||||||
|
dns:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 256M
|
||||||
|
reservations:
|
||||||
|
memory: 128M
|
||||||
|
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
- /var/cache/nginx
|
||||||
|
- /var/run
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
pids_limit: 100
|
||||||
|
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "50m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: db
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1.0"
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- SETUID
|
||||||
|
- SETGID
|
||||||
|
- DAC_OVERRIDE
|
||||||
|
- CHOWN
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
pids_limit: 200
|
||||||
|
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "50m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
app:
|
||||||
|
image: myapp:1.2.3
|
||||||
|
container_name: app
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
pids_limit: 100
|
||||||
|
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "50m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend:
|
||||||
|
driver: bridge
|
||||||
|
backend:
|
||||||
|
driver: bridge
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db_data:
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
db_password:
|
||||||
|
file: ./secrets/db_password.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
`internal: true` для backend - база данных без доступа к интернету.
|
||||||
|
|
||||||
|
## Регулярные задачи обслуживания
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Что занимает место
|
||||||
|
docker system df
|
||||||
|
|
||||||
|
# Очистка неиспользуемого
|
||||||
|
docker system prune -f
|
||||||
|
|
||||||
|
# Полная очистка включая неиспользуемые образы
|
||||||
|
docker system prune -a -f
|
||||||
|
|
||||||
|
# Volumes - ОСТОРОЖНО, удаляет данные безвозвратно
|
||||||
|
docker volume prune -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Cron:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo crontab -e
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
0 4 * * 0 docker system prune -f >> /var/log/docker-prune.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Метрики:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stats --no-stream
|
||||||
|
```
|
||||||
|
|
||||||
|
### Аудит через auditd
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install -y auditd
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/audit/rules.d/docker.rules
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
-w /usr/bin/docker -p rwxa -k docker
|
||||||
|
-w /var/lib/docker -p rwxa -k docker
|
||||||
|
-w /etc/docker -p rwxa -k docker
|
||||||
|
-w /var/run/docker.sock -p rwxa -k docker
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable auditd
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart auditd
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ausearch -k docker | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### DNS не работает в контейнере
|
||||||
|
|
||||||
|
См. раздел "systemd-resolved и DNS в контейнерах" выше - это специфика Ubuntu, самая частая проблема на этом дистрибутиве.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Docker обходит UFW
|
||||||
|
|
||||||
|
**Причина:** Docker пишет напрямую в iptables.
|
||||||
|
|
||||||
|
**Решение:** `127.0.0.1:port:port` или DOCKER-USER chain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Permission denied на bind mount
|
||||||
|
|
||||||
|
**Причина:** UID процесса в контейнере не совпадает с владельцем файлов на хосте.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm IMAGE id
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
sudo chown -R UID:GID /path/to/mount
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### OOM killer убивает контейнер
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dmesg | grep -i "oom\|killed process"
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
docker inspect CONTAINER --format='{{.State.OOMKilled}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Увеличить `memory` limit или оптимизировать приложение.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### newgrp docker не сработал после usermod
|
||||||
|
|
||||||
|
**Симптом:** `docker: permission denied` несмотря на `usermod -aG docker $USER`.
|
||||||
|
|
||||||
|
**Причина:** Группа применяется только к новым сессиям. Текущая сессия не знает о изменении.
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
newgrp docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Или просто выйти и зайти заново по SSH.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Диск забит логами
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo find /var/lib/docker/containers/ -name "*.log" -exec du -sh {} \; | sort -rh | head -10
|
||||||
|
```
|
||||||
|
|
||||||
|
Экстренно:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo truncate -s 0 /var/lib/docker/containers/CONTAINER_ID/CONTAINER_ID-json.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Постоянно - `max-size`/`max-file` в daemon.json (уже настроено).
|
||||||
|
|
||||||
|
### Финальная проверка
|
||||||
|
```bash
|
||||||
|
sudo systemctl status docker
|
||||||
|
docker info | grep -E "Storage Driver|Logging Driver|Live Restore"
|
||||||
|
docker run --rm alpine nslookup google.com
|
||||||
|
ss -tulnp | grep docker
|
||||||
|
docker system df
|
||||||
|
```
|
||||||
|
|
||||||
|
## Отличия от Debian 12
|
||||||
|
|
||||||
|
| Аспект | Debian 12 | Ubuntu 24.04 |
|
||||||
|
|--------|-----------|--------------|
|
||||||
|
| Snap версия Docker | Не существует | Существует, избегать |
|
||||||
|
| DNS resolver | Обычно прямой `/etc/resolv.conf` | systemd-resolved (127.0.0.53), требует явной настройки DNS в Docker |
|
||||||
|
| Группа docker | `newgrp docker` работает | `newgrp docker` работает, та же команда |
|
||||||
|
| UFW | Нужно устанавливать | Предустановлен |
|
||||||
|
| GPG ключ репозитория Docker | `download.docker.com/linux/debian/gpg` | `download.docker.com/linux/ubuntu/gpg` |
|
||||||
|
|
||||||
|
## Что дальше
|
||||||
|
|
||||||
|
Docker-хост готов к production нагрузке.
|
||||||
|
|
||||||
|
- Reverse proxy - Traefik или nginx
|
||||||
|
- Мониторинг - Prometheus + cAdvisor + Grafana
|
||||||
|
- Централизованные логи - Loki + Promtail
|
||||||
|
- Секреты - HashiCorp Vault или Docker Secrets
|
||||||
|
|
||||||
|
**Стек этой статьи:** Ubuntu 24.04 LTS (Noble Numbat) · Docker CE · UFW · systemd-resolved · auditd · Trivy · docker-compose
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
{{ $coffeeOn := site.Params.buymeacoffee.globalWidget | default false }}
|
||||||
|
{{ $coffeePos := lower (site.Params.buymeacoffee.globalWidgetPosition | default "right") }}
|
||||||
|
{{ $coffeeOnRight := and $coffeeOn (eq $coffeePos "right") }}
|
||||||
|
{{ $scrollTopOn := site.Params.footer.showScrollToTop | default true }}
|
||||||
|
|
||||||
|
{{ $side := "right" }}
|
||||||
|
{{ $bottomPx := 24 }}
|
||||||
|
{{ if $coffeeOnRight }}
|
||||||
|
{{ $side = "left" }}
|
||||||
|
{{ else if $scrollTopOn }}
|
||||||
|
{{ $bottomPx = 90 }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<button id="survey-trigger" type="button" aria-label="Пройти опрос"
|
||||||
|
style="{{ $side }}: 20px; bottom: {{ $bottomPx }}px;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" aria-hidden="true"><path fill="currentColor" d="M152.1 38.2c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 113C-2.3 103.6-2.3 88.4 7 79s24.6-9.4 33.9 0l22.1 22.1 55.1-61.2c8.9-9.9 24-10.7 33.9-1.8zm0 160c9.9 8.9 10.7 24 1.8 33.9l-72 80c-4.4 4.9-10.6 7.8-17.2 7.9s-12.9-2.4-17.6-7L7 273c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22.1 22.1 55.1-61.2c8.9-9.9 24-10.7 33.9-1.8zM224 96c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zm0 160c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32zM160 416c0-17.7 14.3-32 32-32H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H192c-17.7 0-32-14.3-32-32zM48 368a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/></svg>
|
||||||
|
<span>Опрос</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script src="https://forms.yandex.ru/_static/embed.js"></script>
|
||||||
|
|
||||||
|
<div id="survey-modal" class="survey-modal" hidden>
|
||||||
|
<div class="survey-modal__overlay" data-survey-close></div>
|
||||||
|
<div class="survey-modal__box">
|
||||||
|
<button class="survey-modal__close" data-survey-close aria-label="Закрыть">×</button>
|
||||||
|
<iframe
|
||||||
|
data-src="https://forms.yandex.ru/cloud/6a6de0cc90290299827b1dca?iframe=1"
|
||||||
|
name="ya-form-6a6de0cc90290299827b1dca"
|
||||||
|
frameborder="0"
|
||||||
|
width="100%"
|
||||||
|
class="survey-modal__iframe"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var article = document.querySelector('.article-content');
|
||||||
|
if (!article) return;
|
||||||
|
|
||||||
|
var DISMISS_KEY = 'survey-dismissed-until';
|
||||||
|
var DISMISS_DAYS = 14;
|
||||||
|
var SCROLL_THRESHOLD = 0.55;
|
||||||
|
|
||||||
|
var trigger = document.getElementById('survey-trigger');
|
||||||
|
var modal = document.getElementById('survey-modal');
|
||||||
|
var iframe = modal ? modal.querySelector('iframe') : null;
|
||||||
|
if (!trigger || !modal) return;
|
||||||
|
|
||||||
|
if (Date.now() < Number(localStorage.getItem(DISMISS_KEY) || 0)) return;
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
var rect = article.getBoundingClientRect();
|
||||||
|
if (rect.height > 0 && (window.innerHeight - rect.top) / rect.height >= SCROLL_THRESHOLD) {
|
||||||
|
trigger.classList.add('is-visible');
|
||||||
|
window.removeEventListener('scroll', onScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
|
onScroll();
|
||||||
|
|
||||||
|
trigger.addEventListener('click', function () {
|
||||||
|
if (iframe && !iframe.src) { iframe.src = iframe.dataset.src; }
|
||||||
|
modal.hidden = false;
|
||||||
|
});
|
||||||
|
modal.querySelectorAll('[data-survey-close]').forEach(function (el) {
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
modal.hidden = true;
|
||||||
|
localStorage.setItem(DISMISS_KEY, Date.now() + DISMISS_DAYS * 86400000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -6,12 +6,82 @@
|
|||||||
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||||
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
||||||
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=106995329', 'ym');
|
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=106995329', 'ym');
|
||||||
|
|
||||||
ym(106995329, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
|
ym(106995329, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
|
||||||
</script>
|
</script>
|
||||||
<noscript><div><img src="https://mc.yandex.ru/watch/106995329" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
<noscript><div><img src="https://mc.yandex.ru/watch/106995329" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||||
<!-- /Yandex.Metrika counter -->
|
<!-- /Yandex.Metrika counter -->
|
||||||
|
|
||||||
<!-- Dzen.Metrika counter -->
|
<!-- Dzen.Metrika counter -->
|
||||||
<meta name="zen-verification" content="GDc1yxlsRy03jcI1t8NJ8OTleUE3f4I4PEmfOBjz4KBCezoDX1WWctO3l8Z2HGEx" />
|
<meta name="zen-verification" content="GDc1yxlsRy03jcI1t8NJ8OTleUE3f4I4PEmfOBjz4KBCezoDX1WWctO3l8Z2HGEx" />
|
||||||
<!-- /Dzen.Metrika counter -->
|
<!-- /Dzen.Metrika counter -->
|
||||||
|
<!-- Survey widget: floating trigger + modal styles -->
|
||||||
|
<style>
|
||||||
|
#survey-trigger {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 40;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 22px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
background: #FF5A36;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px) scale(0.9);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .25s ease, transform .25s ease, background .2s ease;
|
||||||
|
box-shadow: 0 4px 14px rgba(255,90,54,.45);
|
||||||
|
}
|
||||||
|
#survey-trigger:hover { background: #E8481F; }
|
||||||
|
#survey-trigger.is-visible { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
|
||||||
|
#survey-trigger svg { width: 20px; height: 20px; flex-shrink: 0; }
|
||||||
|
.survey-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.survey-modal[hidden] { display: none; }
|
||||||
|
.survey-modal__overlay { position: absolute; inset: 0; background: rgba(0,0,0,.5); }
|
||||||
|
.survey-modal__box {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 680px;
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
max-height: calc(100dvh - 32px);
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 44px 8px 8px;
|
||||||
|
}
|
||||||
|
.survey-modal__iframe {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 420px;
|
||||||
|
border: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.survey-modal__close {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
background: #f2f2f2;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.survey-modal__close:hover { background: #e5e5e5; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user