Files
AWG2_harness/docs/CONFIGURATION.md
T
founder 322f67f69a fix(swap): bind ownership to device+inode, restrict swap.path to root-owned roots [P0]
Two defects that together made the previous ownership model unsafe, and reverse an earlier
deviation that the follow-up audit correctly rejected.

1. The sidecar marker identified a path, not a file.

   Ownership was `<swap.path>.awg2-harness-owned` sitting next to the swapfile, and the
   check was whether that sidecar existed and started with our marker. Nothing tied it to
   the file it was supposed to describe. The sequence that loses data: our swapfile is
   removed by hand, the sidecar survives, an operator puts an unrelated file at the same
   path - and the next reconcile calls it managed and, if the size differs, deletes it.
   The claim in the previous commit that a wrong path "can now only fail, never destroy"
   was wrong.

   Ownership now lives in /var/lib/awg2-harness/swap-ownership.conf, a root-only directory
   created 0700, and records version, path, device and inode. A path is not an identity: a
   file can be deleted and another created in its place, but (device, inode) cannot be
   reused while our file exists. Classification gained StaleOwnership for "right path,
   different file", which refuses exactly like Foreign but says which situation the
   operator is actually in. Only ManagedValid and ManagedWrongSize ever authorise a
   delete, and that is asserted directly.

   The record is written after the file exists, from its real metadata, so until it lands
   the swapfile is not considered managed. The old sidecar is now only ever removed, never
   read.

   classify_ownership is a pure function taking the recorded state and the observed
   identity, so the cases that matter - stale record, record for another path, no record -
   are unit-testable rather than requiring a Linux filesystem.

2. An arbitrary swap.path was still exploitable.

   The previous commit kept arbitrary absolute paths and argued the ownership marker was
   sufficient protection. It is not, and the audit is right to reject it. Between
   assert_swap_path_is_safe and fallocate/dd/mkswap running, a user who can write the
   parent directory replaces the final component with a symlink and redirects a root-owned
   operation. That is a TOCTOU; stat-ing before use cannot close it, because the check and
   the use are separate syscalls. Paths like /tmp/awg.swap or /home/user/awg.swap made this
   reachable by any local user.

   swap.path is now restricted to /swapfile or a file directly under
   /var/lib/awg2-harness/. Both are root-owned, so the window does not exist rather than
   being detected. /swapfile is kept so existing deployments need no config change. The
   deny-list it replaces only ruled out the paths someone had thought of; what actually
   matters is who can write the parent.

Eleven tests: the five classification states, a stale record, a record for another path,
the invariant that no refusal state is managed, record round-trip, malformed and
unsupported-version records, the newly allowed and newly rejected path shapes.

cargo test --workspace: 176 + 4 passed. clippy -D warnings clean.
2026-08-07 04:08:52 +05:00

17 KiB
Raw Blame History

Конфигурация AWG2 Harness

AWG2 Harness разрабатывался для self-hosted AmneziaWG/AWG 2.0 и совместимого legacy-сценария AmneziaWG/AWG 1.5. Конфиг живёт в /etc/awg2-harness/config.toml. Он общий для всего пакета и должен содержать только эксплуатационно значимые параметры.

Создание runtime config

Пакет намеренно не создаёт /etc/awg2-harness/config.toml автоматически. Это снижает риск случайно применить firewall с неверным SSH-портом или неверным Amnezia-портом.

После установки .deb создайте runtime config вручную:

install -d -m 0755 /etc/awg2-harness
cp /usr/share/doc/awg2-harness/repo/config.example.toml /etc/awg2-harness/config.toml
nano /etc/awg2-harness/config.toml

Перед первым apply-firewall/apply-limits оператор обязан сверить:

  • host_firewall.ssh_port — должен совпадать с реально слушающим sshd портом;
  • host_firewall.amnezia_port — должен совпадать с published Docker/Amnezia UDP port;
  • amnezia.peer_ipv4_cidr — должен совпадать с реальной peer-подсетью;
  • anti_torrent.peer_ipv4_cidr — обычно тот же CIDR.

Discovery-команды:

docker port amnezia-awg2 2>/dev/null || docker port amnezia-awg3 2>/dev/null || docker port amnezia-awg

C="$(docker ps --format '{{.Names}}' | grep -E '^(amnezia-awg2|amnezia-awg3|amnezia-awg)$' | head -n1)"
docker exec "$C" ip -4 addr
docker exec "$C" awg show

Рекомендуемый config.toml

[amnezia]
container_names = ["amnezia-awg2", "amnezia-awg3", "amnezia-awg"]
interface_candidates = ["awg0", "wg0", "amnezia0", "tun0"]
interface_patterns = ["awg*", "wg*", "amnezia*", "tun*"]
wg_binaries = ["awg", "wg"]
peer_ipv4_cidr = "10.8.1.0/24"

[traffic]
enabled = true
peer_limit = "50mbit"
root_rate = "500mbit"
reconcile_interval_secs = 15
container_ready_timeout_secs = 120
class_start_id = 10
ifb_name = "ifb0"

[sysctl_tuning]
enabled = true
apply_on_daemon_start = true
ip_forward = true
tcp_bbr = "auto"          # off | auto | require
default_qdisc = "fq"      # none | fq | fq_codel
rmem_max = 67108864
wmem_max = 67108864
netdev_backlog_enabled = true
netdev_max_backlog = 50000
udp_rmem_min_enabled = true
udp_rmem_min = 8192

[host_firewall]
enabled = true
ssh_port = 2323
amnezia_port = 443
amnezia_protocols = ["udp"]
allow_icmp = true
default_input_policy = "drop"

[anti_torrent]
enabled = true
max_connections_per_peer = 150
peer_ipv4_cidr = "10.8.1.0/24"
apply_in_amnezia_netns = true

[reboot]
enabled = true
window_start_utc = "01:00"
window_end_utc = "02:00"

[swap]
enabled = true
path = "/swapfile"
size_mb = 2048
priority = 1
swappiness = 1

Пояснения

amnezia.container_names — имена контейнеров, которые считаются Amnezia/AWG. Порядок важен, сопоставление точное. По текущему production-снимку основной вариант — amnezia-awg2, fallback — amnezia-awg.

amnezia-awg3 перечислен на будущее и не является подтверждённым официальным именем: в проверенной версии AmneziaVPN 5.0.0.5 поддержка AWG 3.0 добавлена, но варианты контейнера по-прежнему представлены как Awg и Awg2, отдельного типа Awg3 нет. Имя оставлено в примере потому, что несовпавшее имя не стоит ничего — поиск просто идёт дальше по списку, — тогда как отсутствующее в списке имя даст молчаливое «контейнер не найден». Если Amnezia введёт другое имя, его нужно будет дописать сюда.

amnezia.interface_candidates — точные имена интерфейса, которые пробуются первыми.

amnezia.interface_patterns — fallback-паттерны для устойчивости к обновлениям Amnezia. Они нужны для снижения риска переименования интерфейса между awg0, wg0, amnezia0, tun0.

amnezia.wg_binaries — утилита чтения peer state внутри контейнера, пробуется по порядку до первого успеха. Default — ["awg", "wg"]. В официальном образе AmneziaWG реальный бинарь /usr/bin/awg, а /usr/bin/wg — symlink на него, поэтому awg идёт первым; wg остаётся для контейнеров с чистым WireGuard. Значения выполняются внутри контейнера и валидируются как имена команд: непустые, без пробелов и без разделителей пути. Параметр опциональный — конфиги, созданные до его появления, продолжают работать с default.

Хостовый wireguard-tools не нужен и не проверяется в preflight. Peer state читается только через docker exec: AmneziaWG — userspace-реализация, её UAPI-сокет находится в RUNSTATEDIR/amneziawg/ внутри mount namespace контейнера, а nsenter -n входит только в network namespace, поэтому хостовый wg до него не достаёт.

amnezia.peer_ipv4_cidr — подсеть VPN peers. Production default — 10.8.1.0/24; значение конфигурируемое.

traffic.peer_limit — per-peer лимит в обе стороны. Текущий production default — 50mbit.

traffic.root_rate — верхний HTB-класс, в текущем production default — 500mbit.

traffic.reconcile_interval_secs — период фоновой проверки. Он нужен не вместо Docker events, а вместе с ними.

traffic.container_ready_timeout_secs — сколько ждать появления интерфейса после старта контейнера.

traffic.class_start_id — первый class id. Production default — 10, что даёт классы 1:10, 2:10 и дальше.

traffic.ifb_name — имя IFB-интерфейса для ingress shaping. Production default — ifb0. Этот параметр можно оставить в конфиге, потому что бинарь реально создаёт/удаляет этот интерфейс.

sysctl_tuning — отдельный host-level модуль управляемого сетевого baseline через sysctl:

  • persistent-файл: /etc/sysctl.d/90-awg2-harness-vpn.conf;
  • модуль swap отдельно управляет /etc/sysctl.d/90-awg2-harness-swap.conf (только vm.swappiness); прежний путь /etc/sysctl.d/99-awg2-harness.conf удаляется при первом запуске новой версии, если несёт старый маркер;
  • дополнительный managed-файл /etc/modules-load.d/90-awg2-harness-bbr.conf создаётся только при фактически активном BBR и удаляется, когда BBR неактивен;
  • оба managed-файла несут в первой строке маркер # Managed by awg2-harness sysctl_tuning; файл без маркера не перезаписывается и не удаляется, вместо этого возвращается ошибка;
  • net.ipv4.tcp_congestion_control и net.core.default_qdisc управляются только при активном BBR;
  • runtime apply: только sysctl -w и только для drifted keys;
  • на установке .deb модуль не применяется;
  • на старте daemon модуль применяется, если enabled=true и apply_on_daemon_start=true;
  • tcp_bbr = auto не валит запуск при недоступном BBR (warning), require — валит;
  • для sysctl_tuning: modprobe обязателен только для tcp_bbr = "require".

Отдельно для всего пакета:

  • traffic-модуль требует modprobe для IFB при traffic.enabled = true.

Совместимость обновлений:

  • если в существующем runtime-конфиге отсутствует блок [sysctl_tuning], он не включается автоматически (enabled=false по default для missing block);
  • для новых установок рекомендуется явный блок [sysctl_tuning] из примера, где enabled=true.

Ограничения модуля:

  • не использует /etc/sysctl.conf;
  • не запускает sysctl --system;
  • не меняет nftables, Docker bridge и container-local sysctl;
  • не управляет net.ipv4.udp_wmem_min (в Linux документации параметр указан как не имеющий эффекта).

При drift orchestrator всегда делает controlled rebuild owned topology.

anti_torrent.enabled — включает nftables connection-count guard против high-peer-count P2P-нагрузки. Это отдельный AWG2 Harness module и часть текущего production-контракта.

host_firewall.enabled — включает host-level INPUT firewall в отдельной AWG2-owned таблице inet awg2_host_firewall.

host_firewall.ssh_port — SSH порт хоста, который обязательно должен быть открыт. Production default/example: 2323. Значение обязано совпадать с реально слушающим sshd портом до применения apply-firewall.

host_firewall.amnezia_port — внешний UDP/TCP порт Amnezia, который AWG2 открывает в host INPUT (в зависимости от host_firewall.amnezia_protocols). В текущей целевой конфигурации default — 443; значение должно совпадать с реально опубликованным Amnezia UDP/TCP port.

Важно: host_firewall.amnezia_port — это transport-порт AmneziaWG/AWG (например, 443/udp). Это не порт AmneziaDNS.

  • Не задавайте amnezia_port = 53.
  • Не добавляйте DNS/53 в amnezia_protocols.
  • Не открывайте 53/tcp или 53/udp в AWG2 host firewall для ожидаемого production-профиля.

В expected production layout AmneziaDNS работает как внутренний Docker/VPN сервис и не публикуется на public host interface.

Для диагностического блока Docker/Amnezia port path в status приоритет сопоставления такой: сначала ищется published UDP binding с HostPort == host_firewall.amnezia_port, затем fallback на ключ "<amnezia_port>/udp" как container port, затем (если UDP binding ровно один) берётся он. Если UDP bindings несколько и однозначного совпадения нет, статус показывает published_udp_port: unknown.

Диагностика FORWARD в этом блоке является best-effort: сначала используется iptables -S FORWARD, затем fallback через nft list ruleset; результаты только отображаются в status и не изменяют host FORWARD/Docker NAT.

host_firewall.amnezia_protocols — протоколы для порта Amnezia (udp, tcp или оба).

Важно: awg2-harness не управляет host FORWARD/Docker NAT-цепочками. Проект сознательно не добавляет универсальные правила в FORWARD и не меняет Docker-managed forwarding path.

host_firewall.allow_icmp — разрешать ICMP/ICMPv6 на входе.

host_firewall.default_input_policy — политика INPUT chain (drop или accept). Для production рекомендуется drop.

anti_torrent.max_connections_per_peer — максимум одновременных conntrack flows на один VPN peer IP. Текущий пример использует 150: это мягче, чем строгий 100, и обычно безопаснее как первый production baseline. 100 можно использовать как более жёсткий режим; 200/250 — более permissive варианты.

anti_torrent.peer_ipv4_cidr — подсеть, на которую распространяется правило. Обычно должна совпадать с amnezia.peer_ipv4_cidr.

anti_torrent.apply_in_amnezia_netns — выбор namespace для применения anti-torrent правил:

  • true (default): применять в network namespace контейнера Amnezia;
  • false: применять в host namespace.

Для production рекомендуется true, если peer source IP (10.8.1.x) видны именно внутри netns контейнера.

apply-firewall в текущей модели делает оба reconcile: host firewall (host namespace) и anti-torrent firewall (configured namespace).

reboot.enabled — включает плановую перезагрузку.

reboot.window_start_utc / reboot.window_end_utc — UTC-окно плановой перезагрузки. Production default/example — 01:00–02:00 UTC.

swap.enabled — включает setup-задачу swap.

swap.path, swap.size_mb, swap.priority, swap.swappiness — параметры managed swap. При swap.enabled=true пакет приводит swap-state к exclusive состоянию: активен ровно один swap (swap.path), остальные выключаются и swap-entries в /etc/fstab пересобираются в одну managed строку. Рекомендуемый low priority — 1, swappiness = 1.

Ограничения swap.path: путь должен быть либо /swapfile, либо файлом непосредственно в /var/lib/awg2-harness/. Дополнительно: абсолютный, без компонентов . и .., без пробелов, управляющих символов и обратных слэшей; на этапе применения проверяется, что ни один родительский каталог не является symlink.

Ограничение корней обязательно, а не косметика. Если родительский каталог доступен на запись кому-то кроме root, между проверкой пути и запуском fallocate/mkswap этот пользователь успевает подменить файл на symlink и перенаправить операцию, выполняемую от root. Оба разрешённых корня принадлежат root, поэтому окна не возникает. /swapfile оставлен для совместимости с уже развёрнутыми конфигурациями.

Владение фиксируется в /var/lib/awg2-harness/swap-ownership.conf по device + inode + пути, а не по маркеру рядом с файлом. Файл, которого нет в этой записи, или файл с тем же путём, но другими device/inode (то есть подменённый), считается чужим: он никогда не пересоздаётся и не удаляется, вместо этого возвращается ошибка. Чужие активные swap только выключаются — их файлы пакет не удаляет.

Что не выносить в конфиг

Не нужно выносить:

  • handles 1:, 2:, ffff:;
  • имена внутренних Rust-модулей;
  • формат команд tc;
  • retry micro-tuning;
  • формат логов;
  • путь lock-файла, если нет эксплуатационной причины;
  • hardening systemd;
  • nftables hook/priority/table names, если нет осознанной причины.

Handles и структура HTB + fq_codel + ingress + ifb — это contract traffic module, а не настройка оператора.