Атаки на цепочку поставок npm: как защитить сервер
Узнайте, как вредоносные пакеты npm проникают на сервер через postinstall скрипты и typosquatting. Разберем методы защиты и правильную настройку процесса развертывания Node.js.
Что такое атака на цепочку поставок npm на вашем сервере
Атака на цепочку поставок npm проникает на ваш сервер через пакет, который вы решили установить. В этом процессе не задействованы открытые порты или этапы эксплуатации уязвимостей. npm (node package manager) устанавливает код, а установка кода означает его выполнение. Таким образом, небольшое приложение на Node подтягивает несколько сотен пакетов, которые вы никогда не просматривали, и любой из них может выпустить новую версию в ближайший час.
Ваш процесс развертывания загружает вредоносную версию, так как команда установки запрашивает самую новую подходящую версию. Этот код затем выполняется с привилегиями того пользователя, который запустил установку. Все, что описано ниже, вытекает из этих двух утверждений.
Типы атак упорядочены по частоте их воздействия на человека, развертывающего одно приложение на Node на одном VPS. Этот порядок отличается от того, который использовала бы крупная компания, так как у крупной компании есть внутренний реестр, команда проверки и зеркало публичного реестра. У вас есть только скрипт развертывания.
Сценарий 1: скомпрометирована учетная запись мейнтейнера, опубликован патч
Реестр npm не позволяет изменять содержимое уже существующей версии пакета. Злоумышленник, получивший доступ к учетной записи мейнтейнера через фишинг или кражу токена публикации, не может перезаписать 4.18.2. Вместо этого он публикует 4.18.3.
Изучите свой файл package.json. Строка вида "express": "^4.18.2" не означает жесткую фиксацию на версии 4.18.2. Символ каретки означает «любая версия 4.x, начиная с указанной и выше», а ~4.18.2 означает «любая версия 4.18.x». Команда npm install разрешает этот диапазон в момент выполнения, поэтому один и тот же git commit, развернутый дважды в течение одного дня, может установить два разных набора кода. Этот разрыв и является поверхностью атаки. Для её реализации не требуется компрометация вашей локальной машины.
О вредоносных релизах обычно сообщают, и их удаляют из реестра, но это происходит уже после того, как пользователи успели их установить. У всех, кто выполнил развертывание в этот промежуток времени, вредоносный код уже находится на диске. Конвейер сборки, который разрешает диапазоны версий при каждом запуске, автоматически попадает в этот промежуток несколько раз в неделю, даже без участия человека.
Вариант 2: установочный скрипт выполняется от имени пользователя, осуществляющего развертывание
В package.json пакета можно объявить preinstall, install, postinstall и prepare в блоке scripts. npm выполняет их в процессе установки. Они не изолированы в песочнице, и их никто не проверяет. Это обычные команды оболочки, которые запускаются от имени пользователя, введшего команду установки, в его домашнем каталоге, с его сетевым доступом и полным окружением текущей оболочки.
Поэтому правильный вопрос заключается не в том, что может сделать пакет, а в том, к каким данным у этого пользователя есть доступ на чтение. На обычном сервере развертывания ответ включает ~/.npmrc, где хранится токен реестра, ~/.ssh/id_ed25519, используемый как ключ развертывания для SSH (secure shell), ~/.aws/credentials, ~/.docker/config.json, а также все переменные окружения оболочки, где обычно находится DATABASE_URL.
Полезная нагрузка такого типа не требует закрепления в системе или повышения привилегий. Она считывает несколько файлов, отправляет их на хост по HTTPS и завершается с кодом 0. Вы ничего не заметите, так как npm по умолчанию скрывает вывод установочных скриптов. Отключите эту настройку, чтобы увидеть, что именно выполняется:
npm ci --foreground-scriptsforeground-scripts использует общие стандартные потоки ввода, вывода и ошибок с процессом npm, поэтому скрипты сборки выводят данные прямо в ваш терминал, а не в буфер, который npm очищает после успешной установки.
Типосквоттинг: опечатки и имена, которые вы не совсем верно ввели
Типосквоттинг — это публикация пакета под именем, похожим на популярный аналог, в расчете на опечатку или неверно скопированную команду установки. Механизм атаки заключается в самой команде, а не в коде, поэтому lock-файл здесь не поможет: вы один раз добавляете неверное имя, и с этого момента lock-файл добросовестно фиксирует его.
Вариант, который затрагивает команды, а не отдельных разработчиков, называется подменой зависимостей (dependency confusion). Ваш внутренний пакет называется billing-utils и находится в приватном реестре. Если в публичном реестре нет пакета с именем billing-utils, любой может его опубликовать. npm разрешает имена без области видимости (unscoped) через стандартный публичный реестр, поэтому публичная копия может оказаться приоритетнее. Решение заключается в использовании собственной области видимости (scope) и привязке реестра для этой области в файле .npmrc:
@yourorg:registry=https://npm.yourorg.example/
//npm.yourorg.example/:_authToken=${NPM_TOKEN}Теперь пакет @yourorg/billing-utils всегда будет загружаться только с этого хоста, так как привязка области к реестру проверяется до обращения к стандартному реестру. Внутреннее имя без области видимости не имеет такой привязки, а значит, и защиты.
Прежде чем добавлять новую зависимость, изучите её, а не просто смотрите на значок количества загрузок:
npm view some-lib repository.url maintainers time.created time.modifiedПакет, созданный в прошлом месяце и опубликованный аккаунтом, который невозможно связать с публичным репозиторием, представляет иной уровень риска, чем пакет с шестилетней историей. Ни один из этих фактов не является доказательством безопасности. Оба легко проверить.
Форма 4: зависимость, владелец которой сменился без уведомления
Сопровождающие передают пакеты другим лицам. Кто-то прекращает поддержку, появляется новый участник, права на публикацию переходят к нему, а проекты, зависящие от этого пакета, не получают никаких уведомлений. Компрометации не происходит. Доверие, которое вы оказали в 2021 году, теперь принадлежит другому человеку.
Это самая медленная и труднообнаружимая форма угрозы, и нет команды, которая ответила бы на этот вопрос напрямую. Две меры позволяют сузить круг поиска. Перед тем как внедрить пакет, проверьте, кто имеет право на его публикацию, с помощью строки npm view, приведенной выше. Затем изучайте diff, когда пакет, от которого вы действительно зависите, обновляется:
npm diff --diff-name-only --diff=some-lib@1.4.2 --diff=some-lib@1.4.3
npm diff --diff=some-lib@1.4.2 --diff=some-lib@1.4.3Первая форма выводит только измененные имена файлов, что достаточно быстро для выполнения при каждом обновлении важного для вас пакета. Патч-релиз, который затрагивает скрипт сборки, добавляет файл в корень пакета или изменяет блок scripts, стоит прочитать полностью, прежде чем он попадет на ваш сервер.
Сборка из зафиксированного lock-файла с помощью npm ci
package-lock.json записывает точную версию каждого пакета в дереве, URL источника, хеш целостности sha512 для каждого архива и информацию о том, какой пакет потребовал зависимость. Добавляйте этот файл в репозиторий. Это единственный файл, который подтверждает, что именно вы тестировали.
Затем выполняйте установку с помощью npm ci, а не npm install, на любой машине, которая не является ноутбуком разработчика:
npm ci --omit=dev --ignore-scriptsnpm ci отличается от npm install по ряду важных параметров. Команда требует наличия lock-файла. Она удаляет существующую папку node_modules перед началом работы, поэтому остатки предыдущего развертывания не попадут в текущее. Команда никогда не вносит изменения в package.json или в lock-файл, поэтому установка не может незаметно обновить версию пакета. Если lock-файл и package.json противоречат друг другу, команда завершается с ошибкой вместо попытки разрешить конфликт.
Эта ошибка — преимущество, а не помеха. Она означает, что изменение зависимостей должно прийти в виде коммита, который кто-то проверил, а не как побочный эффект развертывания в 02:00.
Хеш целостности проверяется при каждой загрузке. Если байты архива не совпадают с записанным хешем, установка прерывается с ошибкой code EINTEGRITY, и распаковка не выполняется. Важно понимать, что это дает: проверка подтверждает, что полученный файл соответствует файлу, зафиксированному в lock-файле. Это та же гарантия, которую дает проверка загрузок по контрольным суммам, и она имеет те же ограничения. Проверка не гарантирует, что зафиксированная версия не была вредоносной на момент публикации.
Одна деталь о --omit=dev: эти пакеты по-прежнему разрешаются и записываются в lock-файл. Они просто не размещаются на диске. Меньшее количество пакетов на диске означает меньше скриптов установки и меньше кода, загружаемого во время выполнения, поэтому это полезная практика. Данная опция не удаляет зависимость из вашего дерева.
Относитесь к скриптам установки как к коду и умейте их блокировать
Вы можете отключить выполнение скриптов установки. Добавьте следующие строки в файл .npmrc проекта и зафиксируйте их в системе контроля версий рядом с lock-файлом:
ignore-scripts=true
save-exact=trueПараметр ignore-scripts=true запрещает npm выполнять скрипты, объявленные в зависимостях. Параметр save-exact=true заставляет npm install some-lib записывать 1.4.2 в package.json вместо ^1.4.2, чтобы диапазон версий при разрешении зависимостей случайно не попал в ваш манифест.
Это может привести к сбоям, поэтому важно понимать последствия перед включением данных настроек. Пакеты, которые компилируют нативные модули или загружают готовые бинарные файлы, выполняют эту работу именно через скрипты установки. При отключенных скриптах установка завершится успешно, но ошибка проявится позже, во время выполнения, когда модуль не сможет загрузить свой бинарный файл. Решением является использование списка разрешенных пакетов (allowlist):
npm ci --ignore-scripts
npm rebuild better-sqlite3Параметр npm rebuild <package> запускает скрипты сборки только для указанного пакета. Теперь вы принимаете решение по каждому пакету индивидуально, вместо того чтобы предоставлять неограниченные права на выполнение кода сотням незнакомых разработчиков.
Чтобы узнать, насколько широк текущий список разрешений, выполните команду npm:
npm query ":attr(scripts, [postinstall])"Она выведет список всех пакетов в дереве зависимостей, содержащих скрипт postinstall. В типичном приложении этот список оказывается короче, чем ожидается, что и делает использование списка разрешенных пакетов практичным решением.
Разделение процесса сборки и процесса обслуживания трафика
Пользователю, выполняющему развертывание, необходимы права на запись в node_modules. Процессу, отвечающему на HTTP-запросы, они не нужны. Если это одна и та же учетная запись, код, выполняемый при установке, может перезаписать код, обслуживающий пользователей, а код, выполняемый во время работы, может сделать то же самое.
Разделите их. Выполняйте сборку от имени одного пользователя, обслуживайте запросы от имени другого и сделайте каталог с кодом доступным только для чтения для учетной записи, обслуживающей приложение:
sudo useradd --system --home-dir /srv/nodeapp --shell /usr/sbin/nologin nodeapp
sudo chown -R deploy:nodeapp /srv/nodeapp
sudo chmod -R o-rwx /srv/nodeappЗатем обеспечьте соблюдение этого правила через systemd. Создайте /etc/systemd/system/nodeapp.service:
[Unit]
Description=Node application
After=network-online.target
[Service]
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/nodeapp/current
EnvironmentFile=/etc/nodeapp/env
ExecStart=/usr/bin/node server.js
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/srv/nodeapp/shared
NoExecPaths=/srv/nodeapp/shared
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.targetProtectSystem=strict монтирует всю файловую систему в режиме только для чтения для этого сервиса, за исключением /dev, /proc, /sys и путей, которые вы укажете в ReadWritePaths. Поэтому попытка приложения записать данные в node_modules завершится ошибкой EROFS: read-only file system, которую вы сможете обнаружить в своих логах примерно через минуту. NoExecPaths охватывает каталог для загружаемых файлов: сервис может записывать туда файлы, но ядро запрещает их исполнение. Эта опция требует systemd версии 249 или новее; в Ubuntu 24.04 используется версия 255.
В этом unit-файле есть две ловушки. Во-первых, не добавляйте MemoryDenyWriteExecute=yes. Эта опция встречается в большинстве списков по усилению безопасности systemd, но она препятствует запуску Node, так как V8 компилирует JavaScript в машинный код во время выполнения и требует страницы памяти, которые одновременно доступны для записи и исполнения. Во-вторых, возьмите путь ExecStart из command -v node. Если Node был установлен через менеджер версий, он находится в домашнем каталоге пользователя, выполняющего развертывание; ProtectHome=yes скрывает этот каталог от сервиса, и юнит немедленно завершается с ошибкой status=203/EXEC и записью в логе о том, что исполняемый файл не найден.
Проверьте результат, вместо того чтобы полагаться на содержимое файла:
sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo systemd-analyze security nodeapp.service
sudo -u nodeapp touch /srv/nodeapp/current/probesystemd-analyze security выводит список всех настроек безопасности с указанием уровня их воздействия, поэтому вы можете увидеть, какие из них остались со значениями по умолчанию. Команда touch должна завершиться с ошибкой Permission denied, так как nodeapp не владеет ничем в current. Если она выполняется успешно, значит, права владения файлами настроены неверно, а настройки systemd лишь скрывают эту проблему.
Примечание по поводу EnvironmentFile: systemd считывает этот файл от имени root до того, как сменит пользователя на User=nodeapp, поэтому файл может иметь права root:root с режимом 600. Приложение все равно получит переменные окружения. Любой, у кого есть доступ к оболочке от имени nodeapp, по-прежнему может прочитать их из /proc/<pid>/environ, поэтому данный метод защищает секрет в состоянии покоя, а не в работающем процессе.
Храните учетные данные для развертывания вне среды сборки
Скрипты установки наследуют переменные окружения. Этот факт должен определять выбор места для сборки.
Наиболее надежный вариант — выполнять сборку не на продуктовом сервере, а на отдельной машине, после чего копировать готовый каталог. В таком случае на сборочном узле хранится только токен реестра с правами на чтение, и больше ничего. Никаких SSH-ключей для развертывания, ключей доступа к облаку, паролей от баз данных или учетных данных для реестра контейнеров.
npm token create --read-onlyТокен с правами на чтение позволяет загружать пакеты, но не позволяет публиковать их. Если такой токен будет украден из среды сборки, ущерб ограничится возможностью скачивания публичных пакетов.
Если сборка на сервере неизбежна, выполняйте её от имени пользователя deploy с намеренно ограниченным окружением, а секреты для времени выполнения храните в /etc/nodeapp/env, к которым у deploy нет доступа. Та же логика применима к системам автоматизации сборки, которые вы хостите самостоятельно: self-hosted GitHub Actions runner хранит токены и выполняет произвольный опубликованный код при каждой задаче, что делает его наиболее ценной целью в небольшой инфраструктуре. Любая программа, написанная не вами, которая получает доступ ко всему вашему окружению, относится к той же категории. Именно поэтому изоляция секретов от среды AI-агента — это та же самая проблема, просто с другой программой в качестве посредника.
Фиксация или вендоринг того, что невозможно проверить
Зафиксированная зависимость — это зависимость, версия которой не может измениться без фиксации изменений (commit). Файл блокировки (lockfile) уже обеспечивает это для всего дерева зависимостей. Однако в двух случаях требуются дополнительные меры.
Первый случай — транзитивные зависимости. Вы не контролируете, от чего зависят ваши зависимости. overrides в package.json принудительно задает версию для любого компонента в дереве:
{
"overrides": {
"some-transitive-lib": "1.4.2"
}
}Выполните npm install один раз после добавления, чтобы результат был записан в lockfile, а затем зафиксируйте оба файла.
Второй случай — пакет, который вы не можете проверить (audit) и не можете удалить. Используйте вендоринг (vendor). npm pack загружает именно тот архив (tarball), который предоставил бы реестр, а зависимость file: устанавливается из вашей локальной копии:
npm pack some-lib@1.4.2
mkdir -p vendor && mv some-lib-1.4.2.tgz vendor/{
"dependencies": {
"some-lib": "file:vendor/some-lib-1.4.2.tgz"
}
}Теперь архив находится в вашем репозитории и не может измениться без вашего ведома. Вы также берете на себя обязательство по его обновлению на неопределенный срок, поэтому используйте этот метод только для небольших заброшенных пакетов, от которых вы зависите, а не для основного веб-фреймворка.
Существует также период «охлаждения», который не требует затрат:
npm install --before=2026-08-01Опция before перестраивает дерево, используя только те версии, которые были опубликованы до указанной даты включительно. Установите дату на неделю или две назад при обновлении зависимостей, чтобы избежать периода, когда вредоносный релиз уже доступен, но о нем еще не сообщили. Это грубый инструмент, так как он также блокирует важные исправления безопасности. Используйте его для разрешения диапазонов версий, ознакомьтесь с изменениями, а затем зафиксируйте lockfile. Тот же подход стоит применять к инструментам командной строки, которые вы устанавливаете через npm, а не добавляете в зависимости: обычный вызов npx загружает то, что было выпущено сегодня утром, а фиксация точной версии dsh гарантирует, что на двух разных машинах будет выполняться идентичный код.
Как узнать, какая версия была фактически выпущена?
Файл блокировки в git указывает, что должно было быть установлено. Диск показывает, что установлено на самом деле. Только второй вариант является доказательством.
npm ls some-lib
node -e "console.log(require('./node_modules/some-lib/package.json').version)"npm ls считывает node_modules, поэтому утилита сообщает о том, что физически присутствует в системе, а не о том, что было запланировано в файле блокировки. Строка node -e считывает установленный манифест по пути, что работает даже для пакетов, чье поле exports блокирует импорт подпутей, и выводит одну версию без отрисовки дерева зависимостей.
Для второй части сравнения обратитесь к git:
git log --oneline -- package-lock.json
git show <commit>:package-lock.json | grep -A3 '"node_modules/some-lib"'Сделайте связь между ними постоянной, поместив коммит в структуру развертывания. Выпустите релиз в /srv/nodeapp/releases/<short commit sha> и укажите на него с помощью символической ссылки /srv/nodeapp/current. Ответ на вопрос «что запущено прямо сейчас» становится readlink /srv/nodeapp/current, и эта информация доступна в 03:00 любому человеку, даже если он не занимался развертыванием.
Наконец, проверьте, что подтверждает реестр:
npm audit signaturesЭта команда проверяет подписи реестра для пакетов в установленном дереве, а также проверяет аттестации происхождения для тех пакетов, у которых они есть. Происхождение связывает опубликованный архив с публичной сборкой в системе непрерывной интеграции (CI), которая его создала. Таким образом, проверенная аттестация означает, что вы можете проследить код до конкретного коммита, а не до неизвестного ноутбука. Покрытие не является всеобщим, поэтому отсутствие аттестации следует расценивать как «информация отсутствует», а не как «плохой пакет».
Что делать, если на сервер попал скомпрометированный релиз
Действуйте от того, что было запущено, и от имени какого пользователя это происходило.
Если код выполнялся во время установки, считайте, что всё, к чему имел доступ пользователь сборки, скомпрометировано. Смените токен реестра, SSH-ключи в этом домашнем каталоге, облачные учетные данные и любые секреты, которые были экспортированы в этой оболочке. Ротация — единственный честный ответ, так как вы не сможете доказать, что файл не был прочитан.
Если код выполнялся во время работы от имени ограниченной сервисной учетной записи, область доступа значительно меньше: это собственные переменные окружения приложения и всё, до чего оно может дотянуться по сети. В этом заключается основной аргумент в пользу запуска сервисов от имени непривилегированных пользователей на VPS. Это не предотвращает взлом. Это определяет, какая часть системы будет скомпрометирована и сохранится ли доступ после перезагрузки.
Затем пересоберите систему вместо очистки. Удалите node_modules, зафиксируйте версию затронутого пакета ниже проблемной в package.json, выполните npm install один раз для обновления lock-файла, закоммитьте изменения и разверните с помощью npm ci. Не пытайтесь исправлять дерево каталогов на месте. Вы не сможете перечислить всё, к чему обращался скрипт установки.
Также зафиксируйте временной интервал: момент первого развертывания, которое могло загрузить эту версию, и момент развертывания, которое её удалило. Этот диапазон укажет, какие именно логи нужно изучить, и это возможно только в том случае, если ваши релизы названы в соответствии с коммитами.
Что это не исправляет
Lockfile не делает зависимость безопасной. Он превращает момент, когда вы приняли эту зависимость, в зафиксированное и проверенное решение, а не в побочный эффект развертывания. Каждая из описанных выше практик выполняет то же самое преобразование: из случайности в осознанный выбор.
npm audit здесь не является защитой. Он сравнивает ваше дерево зависимостей с базой данных известных уязвимостей, поэтому находит проблемы, которые уже были опубликованы и получили описание. Атака на цепочку поставок остается безымянной на протяжении всего своего полезного жизненного цикла. Запускайте npm audit для поиска старых известных ошибок, но не ждите от него ничего в отношении релиза, который вышел четыре часа назад.
Сокращение количества зависимостей помогает больше, чем любой инструмент из этого руководства, и это самый непопулярный совет, который можно дать. Каждый пакет, который вы не добавили — это еще один разработчик, которого нельзя взломать от вашего имени, и еще один скрипт установки, который никогда не будет запущен от имени вашего пользователя для развертывания.
Ничего из этого не относится только к npm. Те же четыре сценария применимы к PyPI, RubyGems, образам контейнеров и менеджеру пакетов вашего дистрибутива. В npm проблема заметнее всего, потому что деревья зависимостей там глубже, а скрипты установки по умолчанию выполняются. Любое расширение уже используемого инструмента наследует ту же проблему. Поэтому определить, к чему может получить доступ плагин dsh, до его установки — это та же проверка, что и чтение скрипта postinstall, только вместо прав пользователя, выполняющего развёртывание, используются права вашего агента. То, какую часть окружающей системы вы должны защищать, зависит от места запуска. Это часть более общего вопроса о безопасности VPS-хостинга.
FAQ
Защищает ли npm ci от скомпрометированного пакета npm?
Он защищает от изменения версии без вашего ведома. npm ci устанавливает именно то, что зафиксировано в package-lock.json, проверяет каждый архив по его хешу целостности sha512 и завершается с ошибкой, если package.json и lock-файл не совпадают, вместо попытки разрешить конфликт. Это не гарантирует безопасность зафиксированной версии. Если вы закоммитите lock-файл, в котором закреплена вредоносная версия, npm ci будет добросовестно устанавливать её на каждый ваш сервер при каждом развёртывании.
Стоит ли устанавливать ignore-scripts=true для всего?
Установите его, а затем сформируйте список разрешённых пакетов. ignore-scripts=true в .npmrc проекта предотвращает выполнение скриптов установки зависимостей, что устраняет самый прямой путь от вредоносного пакета к учётным данным пользователя, от имени которого выполняется деплой. Пакетам, которые компилируют нативные дополнения или загружают готовые бинарные файлы, это действительно необходимо, и при отключении скриптов они завершатся с ошибкой во время выполнения из-за отсутствия файла привязки, а не на этапе установки. Запустите npm ci --ignore-scripts, а затем npm rebuild <package> для тех немногих пакетов, которым вы решили доверять. npm query ":attr(scripts, [postinstall])" показывает, сколько их на самом деле.
Как узнать, какая версия пакета фактически установлена на сервере?
Проверяйте диск, а не lock-файл. npm ls <package> сообщает о том, что находится в node_modules, а node -e "console.log(require('./node_modules/<package>/package.json').version)" выводит только строку версии. Lock-файл в git отвечает на другой вопрос: что должно было быть установлено, и сравнение этих двух источников — основная задача. Развёртывание в директорию, названную по хешу git-коммита, позволяет сохранить оба ответа доступными спустя месяцы, когда они вам понадобятся.
Находит ли npm audit атаки на цепочку поставок?
Нет. npm audit сопоставляет ваше дерево зависимостей с базой данных известных уязвимостей, поэтому он находит только те проблемы, которые уже были опубликованы и получили идентификатор. Вредоносный релиз остаётся необнаруженным в течение тех часов или дней, когда его установка наиболее опасна. npm audit signatures — более полезная команда: она проверяет подписи реестра во всём установленном дереве и проверяет подтверждения происхождения (provenance attestations), если издатель их предоставил. Это позволяет убедиться, что архив был получен в результате публичной сборки, а не с неизвестной машины.
Почему запуск приложения от непривилегированного пользователя важен, если атака происходит на этапе установки?
Потому что эти два типа сбоев имеют разный масштаб воздействия, и вы защищаетесь от обоих. Код, выполняемый при установке, работает от имени пользователя, осуществляющего деплой, и может прочитать его SSH-ключи, токены реестра и облачные учётные данные. Код, выполняемый во время работы приложения, работает от имени сервисной учётной записи, и при использовании User=nodeapp, ProtectSystem=strict и отсутствии учётных данных на диске, которые он мог бы прочитать, его возможности ограничены средой самого приложения и его базой данных. Разделение учётных записей также означает, что процесс, обрабатывающий трафик, не может перезаписать node_modules, поэтому компрометация во время выполнения исчезнет после следующего перезапуска, а не станет постоянной.