Новинка Виртуальный VDS сервер в Нидерландах от 490р
Выберите продукт

Linux capabilities: как слушать 80 и 443 без запуска приложения от root

Показываем, как разрешить приложению bind privileged ports 80 и 443 без запуска от root: через setcap, cap_net_bind_service и systemd AmbientCapabilities, с проверкой, диагностикой и безопасными ограничениями.
Linux capabilities: как слушать 80 и 443 без запуска приложения от root

Порты 80 и 443 исторически относятся к привилегированным: обычный пользователь Linux не может открыть TCP-сокет на порту ниже 1024. Поэтому у администраторов часто появляется выбор: запускать приложение от root, ставить перед ним Nginx/HAProxy/Caddy или выдать только одно точечное право — слушать нужный порт. Для этой задачи как раз подходят Linux capabilities.

Capabilities разбивают полномочия суперпользователя на отдельные части. Вместо полного набора прав root можно выдать только CAP_NET_BIND_SERVICE. Эта capability разрешает bind privileged ports, то есть привязку к портам ниже 1024, включая 80 и 443. При правильной настройке приложение работает от отдельного пользователя, не может менять системные файлы, управлять сетевыми интерфейсами или читать чужие данные, но способно открыть веб-порт.

Главная идея: не «как бы запустить сервис от root безопаснее», а «как вообще не запускать его от root, если ему нужно только слушать 80/443».

Когда capabilities действительно нужны

В классической схеме веб-сервер запускает master-процесс от root, открывает 80/443, а затем отдаёт обработку рабочим процессам от непривилегированного пользователя. Так работают Nginx и Apache в стандартных пакетах большинства дистрибутивов. Для них вручную добавлять cap_net_bind_service обычно не нужно: модель уже отработана, а конфиги, logrotate, reload и TLS-интеграция ожидают именно такой режим.

Capabilities чаще нужны для приложений, которые сами хотят слушать публичный порт: Go API, Rust-сервис, Node.js-приложение, Java-приложение, небольшой reverse proxy, самописный webhook endpoint, gRPC/HTTP-сервис или демон на Python. Особенно это полезно на VDS, где вы контролируете systemd, пользователей, firewall и можете выстроить минимально достаточные привилегии без запуска всего приложения от администратора.

Есть и альтернативы. Можно оставить приложение на 127.0.0.1:3000 и проксировать через Nginx. Можно изменить системный порог привилегированных портов через net.ipv4.ip_unprivileged_port_start. Можно использовать socket activation в systemd. Но у каждого варианта своя цена. Reverse proxy удобен для TLS, логов и лимитов, но добавляет ещё один слой. Глобальный sysctl разрешает низкие порты всем пользователям, что редко хорошо с точки зрения безопасности. Socket activation хороша, но требует, чтобы приложение корректно принимало уже открытый сокет. Capabilities занимают середину: точечно, понятно и без полного root.

Что такое CAP_NET_BIND_SERVICE

CAP_NET_BIND_SERVICE — capability ядра Linux, которая позволяет процессу привязываться к Internet domain sockets на портах ниже 1024. Это не даёт процессу права читать /etc/shadow, менять маршруты, загружать модули ядра, слушать чужой трафик или обходить права файловой системы. Только bind на privileged ports.

В контексте веб-сервисов это значит: если бинарник или systemd unit получил CAP_NET_BIND_SERVICE, приложение от пользователя myapp сможет сделать bind на 0.0.0.0:80, [::]:80, 0.0.0.0:443 или [::]:443. Все остальные ограничения остаются обычными: доступ к сертификатам, логам, рабочей директории и сокетам БД нужно настраивать отдельно через Unix-права, группы, ACL или параметры systemd.

У capability есть разные наборы: permitted, effective, inheritable, bounding, ambient. Для практической настройки чаще всего встречаются два подхода:

  • file capabilities через setcap: capability записывается в расширенный атрибут исполняемого файла;
  • systemd capabilities через AmbientCapabilities и CapabilityBoundingSet: capability выдаётся процессу при старте сервиса.

Оба варианта рабочие, но использовать их стоит по-разному. setcap удобен для статически собранного бинарника, который вы контролируете. systemd AmbientCapabilities безопаснее и удобнее для сервисов, особенно если приложение запускается через интерпретатор, часто обновляется или разворачивается CI/CD-пайплайном.

Схема наборов capabilities для Linux-сервиса

Устанавливаем инструменты для работы с capabilities

Для команд setcap, getcap, getpcaps и capsh нужен пакет libcap. Названия немного отличаются между семействами дистрибутивов.

Debian и Ubuntu

sudo apt update
sudo apt install libcap2-bin

AlmaLinux, Rocky Linux, CentOS Stream, Oracle Linux и Fedora

sudo dnf install libcap

Если у вас старый RHEL-based дистрибутив с yum, команда будет такой:

sudo yum install libcap

Дальше команды setcap и getcap одинаковы для популярных Linux-дистрибутивов. В FreeBSD есть Capsicum, но это другая модель безопасности, и прямого аналога Linux file capabilities для bind на 80/443 там нет. Поэтому в этой инструкции FreeBSD отдельно не рассматриваем.

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды

Вариант 1: setcap для отдельного бинарника

Допустим, у нас есть приложение /usr/local/bin/myapi. Оно собрано в один исполняемый файл, запускается от пользователя myapp и должно слушать порт 443 напрямую. Сначала проверим, что без дополнительных прав оно действительно не стартует на низком порту. Типичная ошибка выглядит так: bind: permission denied или listen tcp :443: bind: permission denied.

Выдаём capability файлу:

sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapi

Проверяем:

getcap /usr/local/bin/myapi

Ожидаемый вывод:

/usr/local/bin/myapi cap_net_bind_service=ep

Теперь бинарник при запуске получает право открыть порт ниже 1024. Важно: capability привязана именно к файлу. Если вы заменили файл при деплое, пересобрали бинарник, перезаписали его через rsync, установили новую версию пакета или переключили symlink на другой release, capability может исчезнуть. Поэтому после деплоя полезно явно проверять getcap или повторять setcap в post-deploy шаге.

Снять capability можно так:

sudo setcap -r /usr/local/bin/myapi

А затем снова проверить:

getcap /usr/local/bin/myapi

Если вывода нет, file capability удалена.

Почему не стоит делать setcap на Python, Node.js или Java

Самая частая опасная ошибка — выдать cap_net_bind_service не своему приложению, а общему интерпретатору: /usr/bin/python3, /usr/bin/node, /usr/bin/java. Технически это может сработать, но вы расширяете права всех сценариев, которые запускаются этим интерпретатором. Если любой пользователь или процесс сможет выполнить произвольный Python-скрипт через такой бинарник, он тоже получит возможность слушать 80/443.

Для интерпретируемых приложений обычно лучше использовать systemd: capability выдаётся конкретному сервису, с конкретным пользователем, рабочей директорией, ограничениями файловой системы и списком доступных путей. Это проще аудировать и безопаснее сопровождать.

Вариант 2: systemd AmbientCapabilities

Systemd умеет выдавать capability процессу на старте сервиса. Это удобно: не нужно менять атрибуты бинарника, capability не теряется при обновлении файла, а политика безопасности описана рядом с unit-файлом. Параметр AmbientCapabilities добавляет capability в ambient-набор процесса, а CapabilityBoundingSet ограничивает максимальный набор capabilities, доступный сервису.

Если отдельного пользователя ещё нет, создайте его заранее. Имя и домашний каталог подберите под своё приложение:

sudo useradd --system --home-dir /opt/myapp --shell /usr/sbin/nologin myapp
sudo mkdir -p /opt/myapp /var/lib/myapp /var/log/myapp
sudo chown -R myapp:myapp /opt/myapp /var/lib/myapp /var/log/myapp

Пример unit-файла для приложения /opt/myapp/myapi, которое работает от пользователя myapp и слушает 80 или 443:

[Unit]
Description=My API service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapi
Restart=on-failure
RestartSec=3
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp /var/log/myapp

[Install]
WantedBy=multi-user.target

Сохраняем unit, например в /etc/systemd/system/myapi.service, затем перечитываем конфигурацию и запускаем сервис:

sudo systemctl daemon-reload
sudo systemctl enable --now myapi.service

Если unit уже существовал и вы добавляете capabilities через override, удобнее не редактировать пакетный файл напрямую:

sudo systemctl edit myapi.service

В override можно добавить только секцию [Service] с нужными параметрами:

[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

После изменения override:

sudo systemctl daemon-reload
sudo systemctl restart myapi.service

У этого подхода есть важное преимущество: capability получает не «любой запуск бинарника», а именно процесс, стартующий как systemd service. Если администратор вручную выполнит /opt/myapp/myapi от пользователя myapp в shell, без systemd unit приложение не получит ambient capability и не сможет открыть 80/443.

Как проверить, что сервис слушает 80/443 без root

Начните с проверки systemd-статуса и логов:

sudo systemctl status myapi.service
sudo journalctl -u myapi.service -n 100 --no-pager

Проверить, кто слушает порты 80 и 443, можно через ss:

sudo ss -ltnp 'sport = :80 or sport = :443'

В выводе обратите внимание на имя процесса и PID. Затем убедитесь, что процесс действительно работает не от root:

ps -o user,group,pid,comm -p 12345

Замените 12345 на PID из вывода ss. Для просмотра capabilities процесса используйте getpcaps:

sudo getpcaps 12345

Ещё один способ — посмотреть эффективные capabilities через /proc и расшифровать их:

grep Cap /proc/12345/status
capsh --decode=0000000000000400

Значение в CapEff будет отличаться в зависимости от набора прав процесса. Для CAP_NET_BIND_SERVICE часто встречается битовая маска 0000000000000400, но лучше не запоминать её вручную, а использовать capsh --decode.

Проверка портов и capabilities процесса в Linux

Типовые ошибки и диагностика

bind: permission denied остался после настройки

Проверьте, каким способом вы выдаёте capability. Если через setcap, убедитесь, что capability стоит на реальном исполняемом файле, который запускается, а не на старой копии. Команда getcap должна показывать cap_net_bind_service=ep именно для текущего пути из ExecStart.

Если используете systemd, проверьте итоговую конфигурацию unit:

systemctl cat myapi.service
systemctl show myapi.service -p AmbientCapabilities -p CapabilityBoundingSet -p User -p Group

После любого изменения unit или override нужен systemctl daemon-reload, а затем перезапуск сервиса. Обычный reload приложения не всегда пересоздаёт процесс и применяет новые capabilities.

setcap сработал, но после обновления всё сломалось

Это нормальное поведение: file capabilities хранятся в extended attributes конкретного файла. Пакетный менеджер или деплой часто заменяет файл новым inode, и атрибут исчезает. Для самописных бинарников добавьте setcap в post-deploy. Для пакетных приложений лучше перейти на systemd AmbientCapabilities, чтобы обновление пакета не влияло на право bind privileged ports.

Operation not supported при setcap

Обычно причина в файловой системе или mount-опциях. File capabilities требуют поддержки extended attributes. Также проверьте, не смонтирован ли раздел с nosuid: на Linux эта опция может отключать не только setuid, но и file capabilities. Для приложений в контейнерах или на нестандартных overlay-файловых системах такое встречается особенно часто.

Сервис слушает порт, но TLS-сертификат не читается

CAP_NET_BIND_SERVICE не даёт права читать приватные ключи. Если приложение само терминирует TLS на 443, настройте права на файлы сертификата отдельно. Хорошая практика — хранить приватный ключ с доступом для root и отдельной группы, включить пользователя сервиса в эту группу или использовать systemd-параметры для передачи credentials. Не делайте приватный ключ world-readable ради быстрого запуска. Если сертификат ещё не выпущен, для публичного сервиса можно заранее подобрать подходящие SSL-сертификаты и продумать схему обновления ключей.

Порт уже занят

Capabilities решают только вопрос прав, но не конфликтов. Если порт 80 или 443 уже слушает Nginx, Apache, Caddy, HAProxy, контейнер или старый экземпляр приложения, новый процесс получит ошибку вроде address already in use. Проверяйте занятые порты так:

sudo ss -ltnp 'sport = :80 or sport = :443'

Если процесс неочевиден, дополнительно посмотрите units:

systemctl list-units --type=service --state=running

setcap или systemd: что выбрать

Для production-сервиса под systemd чаще всего выбирайте AmbientCapabilities. Это легче поддерживать, проще проверять в systemctl cat, безопаснее для интерпретируемых приложений и устойчивее к обновлениям бинарника. setcap хорош, когда у вас один понятный статический бинарник, путь стабилен, а деплой контролируемый.

Короткое практическое правило такое:

  • Go/Rust/C/C++ single binary с редкими заменами — можно setcap, но не забывайте проверку после деплоя;
  • Node.js, Python, Ruby, Java, PHP CLI daemon — лучше systemd AmbientCapabilities, а не setcap на общий интерпретатор;
  • обычный сайт за Nginx или Apache — чаще оставьте приложение на высоком локальном порту и используйте reverse proxy;
  • несколько приложений на одном сервере — reverse proxy обычно удобнее, потому что 80/443 всё равно должны распределяться по Host/SNI/path;
  • одно приложение на минимальном сервере — systemd capability может быть самым чистым вариантом.

Почему не стоит просто запускать от root

Запуск веб-приложения от root кажется быстрым решением: порт открылся, ошибка исчезла, можно идти дальше. Но если в приложении появится RCE, небезопасная загрузка файлов, уязвимость в dependency или ошибка в обработчике архивов, атакующий сразу окажется в процессе с максимальными системными правами. Это радикально увеличивает ущерб: от чтения секретов и изменения конфигов до закрепления в системе.

Отдельный пользователь сервиса плюс минимальная capability резко сужают поверхность. Даже если приложение скомпрометировано, процесс остаётся ограниченным пользователем. Да, он может слушать низкий порт, но не получает автоматически права управлять системой. Если дополнить это ProtectSystem=strict, ProtectHome=true, PrivateTmp=true, NoNewPrivileges=true, ограниченными ReadWritePaths и нормальными правами на секреты, получается гораздо более здоровая модель эксплуатации.

Capabilities — не замена firewall, обновлениям и безопасной разработке. Это один слой защиты. По-прежнему нужно закрывать лишние порты, следить за владельцами файлов, регулярно обновлять зависимости, проверять systemd unit, не хранить секреты в world-readable конфигурациях и анализировать логи.

Про net.ipv4.ip_unprivileged_port_start: почему это не то же самое

В Linux есть sysctl net.ipv4.ip_unprivileged_port_start. Если выставить его в 0, непривилегированные пользователи смогут слушать любые TCP/UDP-порты, включая 80 и 443. На первый взгляд это проще capabilities:

sysctl net.ipv4.ip_unprivileged_port_start

Но настройка глобальная для network namespace. Она не говорит: «только этот сервис может открыть 443». Она говорит шире: «низкие порты больше не являются привилегированными в этом namespace». Для локальной разработки или контейнерной песочницы это иногда удобно. Для production-сервера с несколькими пользователями и демонами лучше выдавать точечное право через CAP_NET_BIND_SERVICE или держать bind на 80/443 за reverse proxy.

Мини-чеклист безопасной настройки

  • Создайте отдельного системного пользователя для приложения, не используйте root.
  • Выберите способ выдачи права: setcap для контролируемого бинарника или AmbientCapabilities для systemd-сервиса.
  • Не ставьте setcap на общие интерпретаторы вроде python3, node или java.
  • Ограничьте unit через CapabilityBoundingSet, NoNewPrivileges, ProtectSystem, ProtectHome и ReadWritePaths.
  • Проверьте, что порт слушает нужный процесс и что он работает не от root.
  • Настройте права на TLS-ключи, конфиги и директории логов отдельно.
  • После деплоя проверяйте getcap, если используете file capabilities.
  • Не забывайте про firewall: право слушать порт не означает, что порт должен быть открыт для всех сетей.

Итог

Linux capabilities позволяют решить типовую задачу аккуратно: приложению нужен bind на 80 или 443, но полный root ему не нужен. Для этого достаточно cap_net_bind_service. Если приложение — отдельный стабильный бинарник, можно использовать setcap. Если это production-сервис под systemd, особенно на Node.js, Python, Java или другом runtime, практичнее и безопаснее настроить systemd AmbientCapabilities.

Хорошая конфигурация выглядит так: сервис работает от отдельного пользователя, получает только CAP_NET_BIND_SERVICE, имеет ограниченный доступ к файловой системе, пишет только в свои каталоги, а администратор может быстро проверить всё через systemctl, ss, getcap и getpcaps. Такой подход не усложняет эксплуатацию, но убирает одну из самых неприятных привычек — запускать веб-приложение от root только ради портов 80 и 443.

Поделиться статьей

Вам будет интересно

Linux sudoers.d и visudo: безопасный sudo для deploy user OpenAI Статья написана AI (GPT 5)

Linux sudoers.d и visudo: безопасный sudo для deploy user

Показываем, как выдать deploy user только нужные sudo-права для выкладки приложения: создать правило в sudoers.d, проверить его че ...
Linux ACL и setgid для deploy directory: деплой без chmod 777 OpenAI Статья написана AI (GPT 5)

Linux ACL и setgid для deploy directory: деплой без chmod 777

Разберём безопасную схему прав для каталога деплоя: кто владеет файлами, как работают setgid и ACL, какие права нужны Nginx/Apache ...
Docker Compose healthcheck и depends_on: чем заменить sleep при ожидании базы OpenAI Статья написана AI (GPT 5)

Docker Compose healthcheck и depends_on: чем заменить sleep при ожидании базы

Если контейнер приложения стартует раньше PostgreSQL, MySQL или Redis, не спешите добавлять sleep. Разберём, как правильно описать ...