<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Блог &#8211; REMADMIN</title>
	<atom:link href="https://remadmin.com/blog/feed/" rel="self" type="application/rss+xml" />
	<link>https://remadmin.com</link>
	<description>Удалённый системный администратор</description>
	<lastBuildDate>Wed, 21 Oct 2026 18:00:18 +0000</lastBuildDate>
	<language>ru-RU</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>
	<item>
		<title>Патчи PHP вышли: 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 — обновляйтесь сейчас</title>
		<link>https://remadmin.com/blog/vebmasteru/patchi-php-vyshli-8-5-12-8-4-27-8-3-36-8-2-35-obnovlyajtes-sejchas/</link>
					<comments>https://remadmin.com/blog/vebmasteru/patchi-php-vyshli-8-5-12-8-4-27-8-3-36-8-2-35-obnovlyajtes-sejchas/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Wed, 21 Oct 2026 18:00:18 +0000</pubDate>
				<category><![CDATA[Вебмастеру]]></category>
		<category><![CDATA[CVE-2026-109618]]></category>
		<category><![CDATA[CVE-2026-109619]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[RCE]]></category>
		<category><![CDATA[безопасность]]></category>
		<category><![CDATA[Уязвимость]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7445</guid>

					<description><![CDATA[Официальный security-релиз PHP от 21 октября 2026: CVE-2026-109618 и CVE-2026-109619 закрыты. Какие версии ставить, кого бьёт, временные меры и проверка после обновления.]]></description>
										<content:encoded><![CDATA[<p>Вчера PHP предупредил про две критические дыры и попросил готовиться к экстренному релизу. Сегодня, 21 октября 2026, патчи уже вышли. Официально: <a href="https://www.php.net/archive/2026.php" target="_blank" rel="noopener nofollow">Security advisory на php.net</a> и релизы <strong>8.5.12</strong>, <strong>8.4.27</strong>, <strong>8.3.36</strong>, <strong>8.2.35</strong>.</p>
<p>Коротко: это не «обновите, когда удобно». Обе CVE — unauthenticated RCE. На Apache <code>mod_php</code> эксплойт показали до запуска кода вашего сайта. Полных временных костылей нет — только обновление. Ниже — что именно починили, кого бьёт и как быстро проверить, что вы уже на патче.</p>
<h5>Какие версии ставить</h5>
<table>
<tbody>
<tr>
<th>Ветка</th>
<th>Минимальный fixed-релиз</th>
</tr>
<tr>
<td>PHP 8.5</td>
<td><strong>8.5.12</strong></td>
</tr>
<tr>
<td>PHP 8.4</td>
<td><strong>8.4.27</strong></td>
</tr>
<tr>
<td>PHP 8.3</td>
<td><strong>8.3.36</strong></td>
</tr>
<tr>
<td>PHP 8.2</td>
<td><strong>8.2.35</strong></td>
</tr>
</tbody>
</table>
<p>PHP 8.1 и старше upstream больше не патчит. Для 7.4 / 8.0 / 8.1 выложили минимальные source-backport’ы — если собираете сами или ждёте пакет от LTS-вендора. На Remi PHP-команда прямо написала: сегодня обновлений в <code>rpms.remirepo.net</code> не будет, берите дистрибутивные пакеты или собирайте из исходников.</p>
<p>Проверка после обновления:</p>
<pre><code>php -v
# и обязательно через веб — тот же php-fpm/mod_php, что обслуживает сайт
&lt;?php echo PHP_VERSION;</code></pre>
<p>CLI может уже быть новым, а пул FPM — старым. После пакета — reload/restart воркеров.</p>
<h5>CVE-2026-109618 — RCE через OOM</h5>
<p>Advisory: <a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" target="_blank" rel="noopener nofollow">GHSA-w8hw-w9fx-j79w</a>. CVSS v4.0 — <strong>9.2</strong>.</p>
<p>Суть простым языком: при нехватке памяти PHP раньше уходил через <code>longjmp()</code>, обрывая обработку запроса. Иногда это случалось, когда внутренняя структура запроса ещё не до конца инициализирована. Cleanup потом смотрел на это «полусобранное» состояние как на нормальное — отсюда порча памяти и путь к RCE.</p>
<p>OOM можно спровоцировать не только «тяжёлым скриптом»: multipart без нормального Content-Length, куча заголовков внутри part’ов, пустые file-поля, глубокая вложенность имён полей — всё это раздувает память сильнее, чем кажется по <code>post_max_size</code>.</p>
<p>Самое неприятное: на <strong>mod_php</strong> атака возможна <em>до</em> любого кода приложения. FPM по анализу тоже в зоне риска (полный публичный эксплойт под FPM на момент advisory не демонстрировали, но «мы на nginx+php-fpm — нам норм» — плохой вывод).</p>
<p>В патче: отказ от опасного <code>longjmp()</code> на OOM, soft/hard OOM через нормальные точки остановки, shutdown больше не видит битое состояние.</p>
<h5>CVE-2026-109619 — RCE через «протухшее» состояние</h5>
<p>Advisory: <a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" target="_blank" rel="noopener nofollow">GHSA-6wmf-4rv5-p3rp</a>. Тоже <strong>9.2</strong>.</p>
<p>Здесь другая история: внутреннее состояние, привязанное к запросу, может пережить освобождение памяти. Следующий запрос на том же воркере успевает опереться на stale state, пока нормальная инициализация его не сбросила. Классический use-after-free на уровне рантайма — снова без логина.</p>
<p>Обе дыры закрыты одним пакетом релизов. Если обновились — закрыли обе.</p>
<h5>Кто в зоне риска</h5>
<p>По словам PHP Security Team — <strong>все установки PHP, которые отдают веб-запросы</strong>:</p>
<ul>
<li>PHP-FPM</li>
<li>Apache <code>mod_php</code> (риск считают наивысшим)</li>
<li>php-cgi</li>
<li>FrankenPHP</li>
<li>LiteSpeed / LSAPI</li>
</ul>
<p>Не только «публичный WordPress». Ещё админки, панели, контейнеры, appliances, где PHP вшит и про него часто забывают. WordPress, Битрикс, Laravel, самопис — всё на одном интерпретаторе.</p>
<h5>Как обновить на практике</h5>
<p><strong>Debian / Ubuntu (ondrej или вендорский репозиторий):</strong></p>
<pre><code>sudo apt update
sudo apt install --only-upgrade php8.3-fpm php8.3-cli   # подставьте свою ветку
sudo systemctl restart php8.3-fpm
# если Apache+mod_php:
sudo systemctl restart apache2</code></pre>
<p><strong>RHEL / Alma / Rocky:</strong> пакеты от дистрибутива или AppStream; не ждите сегодня Remi.</p>
<p><strong>Docker:</strong> пересобрать образ на <code>php:8.x.y</code> с fixed-тегом / digest, задеплоить заново. Один <code>docker pull</code> без recreate контейнера не спасёт старый процесс.</p>
<p><strong>Shared-хостинг:</strong> тикет хостеру + проверка <code>phpinfo()</code> / версии в панели. «Мы обновим на выходных» для этой дыры — плохой ответ.</p>
<p>После рестарта убедитесь, что все пулы/vhost’ы реально на новой версии, а не только дефолтный.</p>
<h5>Если обновить прямо сейчас нельзя</h5>
<p>PHP честно пишет: <strong>полного mitigation нет</strong>. Ниже — только снижение отдельных векторов, пока катите пакет.</p>
<p>Для CVE-2026-109618:</p>
<ul>
<li><code>memory_limit=-1</code> через <code>php_admin_value</code> в FPM/Apache так, чтобы приложение не вернуло лимит обратно;</li>
<li>низкий <code>post_max_size</code> (например 8M без аплоадов, 32M если аплоады нужны), <code>upload_max_filesize</code> ниже него;</li>
<li>лимиты на размер/число multipart/headers на nginx/прокси;</li>
<li>не использовать <code>RLIMIT_AS</code> / <code>RLIMIT_DATA</code> как «защиту» — они как раз могут спровоцировать уязвимый путь.</li>
</ul>
<p>Для CVE-2026-109619:</p>
<ul>
<li><code>session.upload_progress.enabled=0</code> и рестарт всех воркеров (не через <code>ini_set</code> / <code>.user.ini</code>);</li>
<li>на PHP 8.5 — <code>fatal_error_backtraces=0</code>;</li>
<li>убрать странные per-request deprecated settings, которые сыплют диагностикой до инициализации.</li>
</ul>
<p>WAF в одиночку PHP не считает достаточной защитой. Временные меры — мост на часы/сутки, не замена патчу.</p>
<h5>После патча — не забыть про компрометацию</h5>
<p>Окно между утечкой (20 октября) и массовым обновлением уже открыто. Обновление закрывает вход — уже залитый шелл не удалит. Имеет смысл:</p>
<ul>
<li>логи на странные multipart/огромные POST, падения воркеров, всплески 5xx;</li>
<li>новые <code>.php</code> в uploads / странных каталогах;</li>
<li>левые пользователи в CMS, правки cron, mu-plugins, неожиданные процессы от www-data.</li>
</ul>
<p>Тот же урок, что после wp2shell и массовых взломов Битрикса: сначала чистка, если уже задели, потом спокойная жизнь на патче.</p>
<h5>Чеклист на сегодня</h5>
<ol>
<li>Узнать текущую ветку PHP на каждом сервере/контейнере.</li>
<li>Поставить fixed-релиз своей линии (или пакет вендора с бэкпортом).</li>
<li>Перезапустить php-fpm / Apache / контейнеры.</li>
<li>Проверить версию именно через HTTP, не только CLI.</li>
<li>Если сидите на 7.4/8.0/8.1 — ускорить миграцию или бэкпорт вендора.</li>
<li>Пробежать логи и файлы на следы компрометации.</li>
</ol>
<p>Исходники и Windows-сборки — на <a href="https://www.php.net/downloads.php" target="_blank" rel="noopener nofollow">downloads.php.net</a>. Полный разбор — в advisory на архиве новостей и в двух GHSA выше.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/vebmasteru/patchi-php-vyshli-8-5-12-8-4-27-8-3-36-8-2-35-obnovlyajtes-sejchas/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Критические дыры в PHP: экстренные патчи CVE-2026-109618 и CVE-2026-109619</title>
		<link>https://remadmin.com/blog/vebmasteru/kriticheskie-dyry-v-php-ekstrennye-patchi-cve-2026-109618-i-cve-2026-109619/</link>
					<comments>https://remadmin.com/blog/vebmasteru/kriticheskie-dyry-v-php-ekstrennye-patchi-cve-2026-109618-i-cve-2026-109619/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Wed, 21 Oct 2026 08:11:49 +0000</pubDate>
				<category><![CDATA[Вебмастеру]]></category>
		<category><![CDATA[CVE-2026-109618]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[RCE]]></category>
		<category><![CDATA[безопасность]]></category>
		<category><![CDATA[Уязвимость]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7440</guid>

					<description><![CDATA[PHP предупредил о двух критических RCE (CVSS 9.2). Информация утекла до disclosure. Что известно, какие версии ждать и что делать админам WordPress/Битрикс прямо сейчас.]]></description>
										<content:encoded><![CDATA[<p>20 октября 2026 PHP Security Team выложила редкое для себя предупреждение: готовьтесь к экстренному security-релизу. Две критические дыры, обе по CVSS v4.0 на <strong>9.2</strong>. Релиз назначен на <strong>21 октября 2026, 14:00 UTC</strong> (17:00 по Москве). Полных технических деталей пока нет — и это как раз тот случай, когда «подожду changelog» может дорого стоить.</p>
<p>Повод ускориться неприятный: информация об уязвимостях <strong>утекла до официального disclosure</strong>. Сначала пост на Hacker News (потом удалили), затем подтверждение от исследователей watchTowr. PHP прямо написали: имеют право выпустить патч раньше, если детали станут публичными или появится эксплуатация в дикой природе.</p>
<p>Официальный анонс: <a href="https://www.php.net/archive/2026.php" target="_blank" rel="noopener nofollow">Upcoming Critical PHP Security Release</a>. Разбор ситуации: <a href="https://www.bleepingcomputer.com/news/security/php-warns-admins-of-two-critical-flaws-emergency-patches-coming-tomorrow/" target="_blank" rel="noopener nofollow">BleepingComputer</a>.</p>
<h5>Что уже известно</h5>
<p>Пока официально PHP подтвердили только рейтинг и дату релиза. CVE-номера и «это RCE» в их коротком анонсе не расписаны — детали обещают вместе с патчами.</p>
<p>Из параллельных источников (watchTowr → BleepingComputer, CERT-EU, ACSC):</p>
<ul>
<li>идентификаторы: <strong>CVE-2026-109618</strong> и <strong>CVE-2026-109619</strong>;</li>
<li>тип: remote code execution (удалённое выполнение кода);</li>
<li>watchTowr оценивает как CVSS 10, PHP официально ставит каждой <strong>9.2</strong> по CVSS v4.0 — расхождение в шкалах, не в том, что «не страшно»;</li>
<li>по оценке исследователей, затронуты ветки PHP <strong>5 / 6 / 7 / 8</strong> (то есть фактически «почти всё, что ещё крутится в проде»);</li>
<li>на момент предупреждений <strong>активной эксплуатации в wild не видели</strong> — но окно после утечки и до патча как раз самое опасное.</li>
</ul>
<p>Ожидаемые fixed-релизы (по сообщениям security-комьюнити, не финальный список от PHP до момента публикации патчей): <strong>8.5.12</strong>, <strong>8.4.27</strong>, <strong>8.3.36</strong>, <strong>8.2.35</strong>. EOL-ветки вроде PHP 7.4 / 8.0 / 8.1 патча могут и не получить — рассчитывать на «дистр когда-нибудь подтянет» для мёртвой ветки наивно.</p>
<h5>Почему это важно именно вам</h5>
<p>PHP — это не «какой-то фреймворк». На нём сидят WordPress, Битрикс, Laravel, Magento, куча самописных ЛК и API. Если дыра в интерпретаторе и она RCE — компрометируется не «один плагин», а процесс php-fpm / apache-модуль. Дальше классика: шелл, бэкдор в <code>wp-content</code>, кража <code>wp-config</code> / <code>.env</code>, рассылка, ботнет.</p>
<p>После историй с wp2shell и массовыми взломами Битрикса сценарий знакомый: disclosure → через сутки сканеры по интернету → через неделю «у нас странный админ и левый плагин». Здесь ещё и leak до релиза — значит, кто умеет, уже мог готовить эксплойт, пока вы читаете новость.</p>
<h5>Что сделать прямо сейчас</h5>
<p><strong>1. Инвентаризация.</strong> Пройдитесь по серверам и узнайте, где какой PHP:</p>
<pre><code>php -v
# или на сайте
&lt;?php echo PHP_VERSION;</code></pre>
<p>Отдельно — контейнеры, shared-хостинг клиентов, «забытые» лендинги на старом 7.4. Список версий и хостов лучше иметь до того, как пакеты появятся в репозитории.</p>
<p><strong>2. Подготовить обновление.</strong> На Debian/Ubuntu заранее проверить, откуда тянете PHP (ondrej/php, вендорский репозиторий, remi на RHEL). На VPS с ручной сборкой — план: скачать релиз → подменить → reload php-fpm. На shared — тикет хостеру: «когда выкатите security-релиз PHP от 21.10».</p>
<p><strong>3. Как только пакеты вышли — ставить без отлагательств.</strong> Для поддерживаемых веток цель простая: оказаться на fixed-версии своей линии (или новее). После обновления:</p>
<pre><code>systemctl reload php*-fpm
# или
systemctl restart php8.3-fpm apache2</code></pre>
<p>И проверить, что сайт реально отвечает уже новым бинарником, а не старым php-fpm из другого pool.</p>
<p><strong>4. EOL — отдельно.</strong> Если сайт на PHP 7.x или 8.0/8.1 и вендор патч не выпустит — это не «подождём», а миграция на поддерживаемую ветку. Временные костыли (WAF, закрыть наружу) — только мост, не лечение.</p>
<p><strong>5. Пока патча нет / нельзя сразу обновить.</strong> CERT-EU прямо советует: где нельзя пропатчить мгновенно — по возможности изолировать интернет-facing приложения. На практике: усилить WAF/Cloudflare, убрать лишний PHP с публичных vhost’ов, закрыть ненужные entrypoint’ы, не торчать phpMyAdmin в мир. Это не замена патчу.</p>
<p><strong>6. После патча — быстрый чек на компрометацию.</strong> Особенно если сервер торчал в интернет всё окно утечки. Смотрите новые PHP в <code>uploads</code> / странных каталогах, левых пользователей в CMS, неожиданные cron, правки <code>.htaccess</code> / nginx, неизвестные процессы от www-data. Патч закрывает вход — бэкдор, уже залитый до обновления, сам не исчезнет (мы это уже проходили на Битриксе и WordPress).</p>
<h5>Чего пока делать не стоит</h5>
<ul>
<li>Не гонять по себе чужие «PoC с GitHub», пока нет официального advisory — половина будет фейком или сама шеллом.</li>
<li>Не путать CVSS 9.2 от PHP и «CVSS 10» из твитов: это разные формулировки одной критичности, а не две разные дыры.</li>
<li>Не ждать, что «WordPress сам обновит PHP» — CMS ядро обновляет, интерпретатор на сервере — ваша зона.</li>
</ul>
<h5>Что дальше</h5>
<p>После 14:00 UTC 21 октября на <a href="https://www.php.net/" target="_blank" rel="noopener nofollow">php.net</a> должны появиться changelog’и и список affected/fixed. Как только будут точные версии и описание вектора — имеет смысл свериться ещё раз: не все сборки дистрибутивов выходят в ту же минуту, что upstream.</p>
<p>Если коротко: сегодня это не «интересная CVE для чтения на выходных», а окно, в котором нужно знать свои версии PHP и быть готовым нажать обновление в первые часы после релиза. WordPress, Битрикс, самопис — неважно: дыра в PHP бьёт по всему стеку.</p>
<p>Нужна помощь — пробежать серверы, обновить PHP на VPS, проверить сайты после патча — пишите через <a href="https://remadmin.com/podderzhka-sajtov/">контакты</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/vebmasteru/kriticheskie-dyry-v-php-ekstrennye-patchi-cve-2026-109618-i-cve-2026-109619/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SSH ProxyJump вместо проброса ключа на сервер: когда безопаснее agent forwarding</title>
		<link>https://remadmin.com/blog/linux/ssh-proxyjump-vmesto-probrosa-kljucha-na-server-kogda-bezopasnee-agent-forwarding/</link>
					<comments>https://remadmin.com/blog/linux/ssh-proxyjump-vmesto-probrosa-kljucha-na-server-kogda-bezopasnee-agent-forwarding/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Fri, 09 Oct 2026 07:45:41 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[bastion]]></category>
		<category><![CDATA[ProxyJump]]></category>
		<category><![CDATA[ssh]]></category>
		<category><![CDATA[ssh-agent]]></category>
		<category><![CDATA[безопасность]]></category>
		<category><![CDATA[шпаргалка]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7436</guid>

					<description><![CDATA[Копировать ключ на bastion или включить ForwardAgent «чтобы удобно» — разные дыры. Как я хожу через прыжок без ключа на джампе и когда агент всё-таки нужен.]]></description>
										<content:encoded><![CDATA[<p>Типичная заявка: «на прод с ноутбука не пускает, только через bastion, я скопировал свой <code>id_ed25519</code> на прыжок, теперь захожу в два ssh». Вариант «культурнее»: «включили <code>ForwardAgent</code>, чтобы не вводить пароль от ключа дважды, и Ansible сам прыгает». Через месяц на джампе кто-то с root снимает сокет агента и ходит на прод вашим ключом. Ключ при этом «не крали» — его и не надо красть, агент сам подписывает.</p>
<p>Я не таскаю закрытый ключ на промежуточную машину. Прыжок для меня — дырка в TCP, не второй дом для <code>ssh-agent</code>. Ниже — чем <code>ProxyJump</code> отличается от проброса агента, когда агент всё-таки оставляю, и чем проверяю, что ключ с ноутбука никуда не уехал.</p>
<p>Хосты в примерах вымышленные: <code>jump.example.net</code> (документационный <code>203.0.113.40</code>), внутренний <code>prod-web-01.internal</code>, пользователь <code>ops</code>. Свои имена и ключи подставляйте, чужие из чатов не копируйте.</p>
<h5>Две привычки, которые путают с «удобным прыжком»</h5>
<p>Первая: положить тот же закрытый ключ на bastion. «Ну я же сам туда захожу, чего его беречь». Bastion — это машина, на которую смотрит интернет. На ней чаще стоят общие учётки, старый sudo, чужой tmux. Если ключ от прода лежит файлом в <code>~/.ssh</code> на такой коробке, компрометация прыжка равна компрометации всего, куда этот ключ пускают. Пароль на ключ тут слабое утешение: его снимут вместе с файлом, из памяти агента или из вашей же сессии.</p>
<p>Вторая: <code>ForwardAgent yes</code> глобально в <code>~/.ssh/config</code> или в системном <code>ssh_config</code>. Так делают, чтобы со прыжка открыть следующий ssh «как с ноутбука». Удобно. И ровно поэтому опасно. На время сессии на джампе появляется unix-сокет агента. Любой, кто может читать этот сокет — root, ваша скомпрометированная учётка, чужой процесс под тем же uid — может пользоваться ключами, пока вы онлайн. Ключ с диска не копировали. Подпись от вашего агента уже достаточно.</p>
<p>Есть ещё гибрид: ключ на ноутбуке, а на прыжок его один раз залили «на всякий случай» и забыли. Потом ротировали ключ дома, на джампе старый остался. Я такие находки встречаю чаще, чем хотелось бы: <code>ls -l ~/.ssh</code> на bastion и два <code>id_*</code>, один из которых уже «не наш».</p>
<h5>Что именно делает агент, когда его пробросили</h5>
<p><code>ssh-agent</code> на ноутбуке — нормальная вещь. Он держит расшифрованные ключи в памяти, чтобы не тыкать passphrase на каждый <code>git fetch</code>. Плохо не агент. Плохо отдавать его сокет чужой машине.</p>
<p>При <code>ForwardAgent yes</code> клиент поднимает на удалённой стороне канал и выставляет <code>SSH_AUTH_SOCK</code>. Дальше <code>ssh prod-web-01</code>, запущенный уже <em>на прыжке</em>, ходит в этот сокет. Подпись делает ваш ноутбук. Выглядит безопасно: «ключ же дома». Но решение «подписать вот этот challenge» принимает процесс на bastion. Если bastion врет, вы подпишете не то соединение, о котором думаете.</p>
<p>Практическая картинка, которую я показываю заказчику:</p>
<ul>
<li>Вы в сессии на <code>jump.example.net</code>.</li>
<li><code>echo $SSH_AUTH_SOCK</code> даёт путь вроде <code>/tmp/ssh-XXXX/agent.1234</code>.</li>
<li>С правами на этот сокет можно сделать <code>ssh-add -L</code> и увидеть ваши ключи.</li>
<li>Дальше — обычный <code>ssh ops@prod-web-01.internal</code>. Прод видит ваш ключ. В authorized_keys это тот же самый ключ, которым вы заходите «сами».</li>
</ul>
<p>Root на джампе для этого не всегда нужен. Достаточно вашей же утёкшей сессии, crон-скрипта под тем же uid или «временного» бинарника в <code>$PATH</code>. Поэтому фраза «ну у нас же на bastion только свои» меня не греет: свои тоже ошибаются, а агент не спрашивает, кто именно попросил подпись.</p>
<p>Отдельно: <code>ssh-add -c</code> на ноутбуке чуть лучше, чем голый агент. Перед каждой подписью всплывает confirm. Это не броня, но если сокет увели тихо, вы хотя бы увидите лишний запрос в момент, когда сами ничего не открывали.</p>
<h5>ProxyJump: прыжок без вашего ключа на той стороне</h5>
<p><code>ProxyJump</code> (ключ <code>-J</code>) делает другое. Клиент на ноутбуке открывает ssh на bastion и просит его прокинуть TCP до цели. Аутентификация на <code>prod-web-01</code> идёт с ноутбука, напрямую в этом тоннеле. На прыжке нет вашего закрытого ключа, нет сокета агента, нет второго <code>ssh</code> от вашего имени.</p>
<p>Грубо: bastion видит, что с вашего IP пришёл ssh и что вы попросили соединиться с <code>prod-web-01:22</code>. Bastion не видит, каким ключом вы представились проду, и не может этот ключ переиспользовать через минуту после того, как вы отключились.</p>
<p>Минимальный <code>~/.ssh/config</code>, с которого я начинаю:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">Host jump
    HostName jump.example.net
    User ops
    IdentityFile ~/.ssh/id_ed25519_jump
    IdentitiesOnly yes
    ForwardAgent no

Host prod-web-01
    HostName prod-web-01.internal
    User ops
    ProxyJump jump
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes
    ForwardAgent no</pre>
<p>Два ключа — не паранойя. Ключ на интернет-джамп я считаю расходным: его проще отозвать. Ключ на прод на прыжок не кладу и в агент для сессии на jump не пихаю, если можно не пихать. <code>IdentitiesOnly yes</code> обязателен: иначе клиент переберёт все ключи из агента, и вы сами не вспомните, чем открыли хост. Плюс лишние попытки ключей иногда заканчиваются <code>Too many authentication failures</code>.</p>
<p>Одноразово без конфига:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ssh -J ops@jump.example.net ops@prod-web-01.internal</pre>
<p>Два и больше прыжка — список через запятую:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ssh -J jump.example.net,dmz-gw.example.net ops@prod-web-01.internal</pre>
<p>В конфиге то же самое: <code>ProxyJump jump,dmz-gw</code>. Старый <code>ProxyCommand ssh -W %h:%p jump</code> я оставляю только там, где клиент древний и <code>ProxyJump</code> ещё нет. Смысл тот же: проброс TCP, не проброс агента.</p>
<h5>Как я проверяю, что агент не уехал</h5>
<p>После захода на цель смотрю три вещи, не «ну вроде работает».</p>
<p>На ноутбуке, до входа:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ssh -G prod-web-01 | egrep -i 'proxyjump|proxycommand|forwardagent|identityfile|identitiesonly'</pre>
<p>Хочу увидеть свой <code>ProxyJump</code>, <code>forwardagent no</code>, нужные <code>IdentityFile</code> и <code>identitiesonly yes</code>. Если <code>forwardagent yes</code> приехало из <code>/etc/ssh/ssh_config</code> или из <code>Host *</code> в конце файла — чиню там, а не добавляю ещё один <code>Host</code> выше, который потом перебьют.</p>
<p>Вход с отладкой:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ssh -v prod-web-01 2&gt;&amp;1 | egrep -i 'proxy|jump|forwarding|agent|Offering|Authentication succeeded'</pre>
<p>Ищу, что клиент идёт через jump и что нет строк про agent forwarding. «Offering public key» должно относиться к ключу прода, не к ключу джампа. Если клиент предлагает пять ключей подряд — <code>IdentitiesOnly</code> не сработал.</p>
<p>На самой цели и на прыжке, уже внутри сессии:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">echo "SSH_AUTH_SOCK=${SSH_AUTH_SOCK:-}"
ssh-add -l</pre>
<p>На <code>prod-web-01</code> при чистом ProxyJump сокет агента обычно пустой, <code>ssh-add -l</code> отвечает, что агента нет. На прыжке при обычном заходе на сам jump — то же самое, если я не просил проброс. Если сокет вдруг есть, а я его не включал, ищу кто: <code>Host *</code>, Include-info в конфиге, обёртка вроде <code>assh</code>/<code>sshpass</code>, корпоративный «улучшатель» ssh.</p>
<p>Ещё одна проверка, которую люди пропускают: ключи на диске прыжка.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ls -l ~/.ssh
find ~/.ssh -type f \( -name 'id_*' -o -name '*.pem' \) ! -name '*.pub'</pre>
<p>Закрытых ключей там быть не должно. Если есть — снимаю с машины, смотрю <code>authorized_keys</code> на проде, не торчит ли тот же pubkey, и ротирую. Pubkey на прыжке (чтобы вы сами на него заходили) — нормально. Приватник от прода — нет.</p>
<h5>Когда агент всё-таки нужен — и как его не размазать</h5>
<p>Иногда без проброса не обойтись. Типичные случаи, которые я оставляю точечно:</p>
<ul>
<li>С удалённой машины нужно сходить в git по SSH вашим ключом, а ключ на эту машину класть нельзя. Тогда агент — меньшее зло, чем файл в <code>~/.ssh</code>.</li>
<li>Старый сценарий, который сам делает <code>ssh</code> со прыжка на пачку хостов и не умеет <code>ProxyJump</code>. Это чинят, но не в пять минут посреди инцидента.</li>
<li>Промежуточный хост должен подписать что-то ещё вашим ключом (редко, и я сначала пытаюсь перенести это действие на ноутбук).</li>
</ul>
<p>Правило одно: агент включаю на конкретный <code>Host</code>, не на <code>*</code>. И только на время задачи.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">Host git-build.internal
    HostName git-build.internal
    User ops
    ProxyJump jump
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes
    ForwardAgent yes</pre>
<p>На ноутбуке ключ в агент кладу с подтверждением:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ssh-add -c ~/.ssh/id_ed25519_prod</pre>
<p>Сессию короче держу. После работы <code>ssh-add -d</code> этот ключ из агента. На прыжке в <code>sshd_config</code> я по возможности ставлю <code>AllowAgentForwarding no</code> — тогда даже ошибочный <code>ForwardAgent yes</code> у клиента не поднимет сокет. Если агент нужен на одну машину за прыжком, форвардинг агента разрешаю только там, не на bastion.</p>
<p>Ansible отдельным абзацем. Плейбук, который заходит на прод через bastion, не обязан пробрасывать агент. В инвентаре достаточно:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ansible_ssh_common_args: '-o ProxyJump=ops@jump.example.net'</pre>
<p>или <code>ansible_ssh_extra_args</code> с тем же. Ключ остаётся на контрольной машине. Если вам «без <code>ForwardAgent</code> плейбук не едет», чаще виноват не Ansible, а то, что роли сами делают <code>ssh</code>/<code>rsync</code>/<code>git</code> уже на целевом хосте вашим ключом. Это другая задача: либо ключ деплоя на целевой машине, либо отдельный deploy key в git, либо вынести шаг на ноутбук.</p>
<h5>Что должно быть на самом прыжке</h5>
<p>Bastion я собираю скучно. Чем меньше на нём живёт, тем меньше соблазна «раз уж зашли, давайте тут ключи».</p>
<ul>
<li>Отдельный ключ только для входа на jump. Этот ключ не пускает на прод.</li>
<li><code>PermitRootLogin no</code>, вход по паролю выключен, по возможности отдельный порт или хотя бы geo/fail2ban — как принято у заказчика, не догма.</li>
<li><code>AllowTcpForwarding yes</code> (иначе ProxyJump не взлетит), <code>AllowAgentForwarding no</code>, <code>X11Forwarding no</code>.</li>
<li>Если sshd умеет <code>PermitOpen</code> — ограничиваю, куда вообще можно прыгать: только рабочие подсети, не «любой порт мира через нас».</li>
<li>Нет общих <code>ops</code> с одним ключом на пятерых. Если ключ общий, вы не отзовёте одного человека.</li>
<li>На диске нет <code>id_*</code> без <code>.pub</code>. Нет <code>authorized_keys</code> с ключами прода «для удобства».</li>
</ul>
<p>Кусок <code>sshd_config</code> на прыжке, от которого я отталкиваюсь:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">AllowAgentForwarding no
AllowTcpForwarding yes
X11Forwarding no
PermitTunnel no
GatewayPorts no
# если политика позволяет — режем направления:
# PermitOpen prod-web-01.internal:22 prod-db-01.internal:22</pre>
<p><code>PermitOpen</code> легко забыть и потом час ловить <code>administratively prohibited</code>. Сначала проверяю, что ProxyJump жив без него, потом сужаю. Логи sshd на прыжке смотрю не «красиво ли», а кто и куда просил forward: если из сессии <code>ops</code> вдруг полезли на адрес, которого в заявке не было, это уже не удобство, а инцидент.</p>
<h5>Мелочи, из-за которых «прыжок не работает»</h5>
<p>Частая жалоба: «с <code>-J</code> меня не пускает, а два ручных ssh работают». Почти всегда одно из трёх.</p>
<p>Первое — разные пользователи. На jump вы <code>ops</code>, на проде <code>deploy</code>, а в команде указали одного. В конфиге это два <code>User</code>. В <code>-J</code> — явно <code>ops@jump</code> и <code>deploy@prod</code>.</p>
<p>Второе — DNS. Ноутбук не резолвит <code>prod-web-01.internal</code>, а bastion резолвит. <code>ProxyJump</code> соединяет туда, куда сказал <em>клиент</em>. Если клиент не знает имя, пишу в <code>HostName</code> адрес, который виден с прыжка, либо завожу имя в локальный <code>/etc/hosts</code> только как метку. Обратная ловушка: в <code>HostName</code> поставили внешний IP прода, которого с прыжка нет — ручной второй ssh с jump шёл на внутреннее имя, а ProxyJump пытается в белый адрес.</p>
<p>Третье — ключи и агент. Клиент предлагает ключ джампа на прод, прод отвечает «не тот» несколько раз и отваливается. Лечится <code>IdentitiesOnly</code> и отдельным <code>IdentityFile</code>. Если ключ в агенте один, а файлов много, агент всё равно может предложить не то: поэтому для прод-хостов я указываю файл явно.</p>
<p>Четвёртое, реже: <code>ControlMaster</code>. Мультиплекс на jump удобен, но старый сокет иногда держит чужие опции. Если после правки конфига поведение «как раньше», сношу сокеты <code>~/.ssh/cm-*</code> и проверяю ещё раз.</p>
<h5>Что я отказываюсь считать решением</h5>
<p>Не считаю решением скопировать закрытый ключ на bastion «только на время работ». Файл переживает работы. Его найдёт следующий, кто зайдёт под той же учёткой.</p>
<p>Не считаю решением глобальный <code>ForwardAgent yes</code> «потому что так в wiki». Wiki часто писали, когда <code>ProxyJump</code> ещё не было в клиенте по умолчанию. Сейчас клиент это умеет давно.</p>
<p>Не считаю решением один ключ на всё: ноутбук, jump, прод, git, CI. Отзыв после утечки тогда равен «перевыпустить жизнь». Два-три ключа с понятной ролью дешевле нервов.</p>
<p>Не считаю решением «у нас на джампе свои люди, нам не надо». Свои люди оставляют tmux, ставят тестировать бинарник, дают sudo на час. Агент в этот час работает на всех.</p>
<p>Считаю решением короткую схему, которую можно проговорить вслух: ключ прода живёт на ноутбуке, прыжок только прокидывает TCP, агент выключен, кроме одной явно названной машины, и на диске bastion нет ни одного <code>id_*</code> без суффикса <code>.pub</code>. Это скучно. Зато когда bastion однажды вскроют — а интернет-машину вскрывают — прод не откроется тем же ключом из чужой сессии.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/linux/ssh-proxyjump-vmesto-probrosa-kljucha-na-server-kogda-bezopasnee-agent-forwarding/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ИБП и удалёнка: порядок выключения NAS/ПК, чтобы не словить порчу ФС</title>
		<link>https://remadmin.com/blog/umnyj-dom/ibp-i-udaljonka-poryadok-vykljucheniya-nas-pk-chtoby-ne-slovit-porchu-fs/</link>
					<comments>https://remadmin.com/blog/umnyj-dom/ibp-i-udaljonka-poryadok-vykljucheniya-nas-pk-chtoby-ne-slovit-porchu-fs/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Tue, 22 Sep 2026 20:31:17 +0000</pubDate>
				<category><![CDATA[Умный дом]]></category>
		<category><![CDATA[NAS]]></category>
		<category><![CDATA[NUT]]></category>
		<category><![CDATA[ИБП]]></category>
		<category><![CDATA[сеть]]></category>
		<category><![CDATA[файловая система]]></category>
		<category><![CDATA[шпаргалка]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7431</guid>

					<description><![CDATA[ИБП без сценария выключения — просто отложенный hard power-off. Как я гашу клиенты раньше NAS, кто должен держать USB, и что проверить утром после обрыва 220.]]></description>
										<content:encoded><![CDATA[<p>Типичная заявка звучит так: «свет моргнул, я был в отъезде, утром NAS не монтирует том, Windows просит chkdsk, а в логах SMB куча обрывов». Вариант похуже: том поднялся, фото на месте, зато база 1С или iSCSI-лун после ночи «как будто обрезали посередине записи». Ещё чаще люди ставят ИБП «на всякий случай», вешают на него NAS, ПК и чайник, а сценарий выключения не прописывают вообще. Батарея держит минуты, потом ИБП сам щёлкает нагрузку — и это уже не «мягкое выключение», а обычный hard power-off, только с задержкой.</p>
<p>Я не продаю «умный дом, который сам всё спасёт». Мне нужно одно: когда я не дома, машины должны успеть сами размонтировать диски и погаснуть в правильном порядке. Ниже — как я это собираю, что ломается, если порядок перепутать, и что смотреть утром, если ночью свет всё-таки ушёл.</p>
<p>Хосты в примерах вымышленные: <code>nas.home.lan</code>, <code>pc-office</code>, <code>nut-master</code>. Свои имена, VLAN и модель ИБП подставляйте.</p>
<h5>Почему «ИБП стоит» ≠ «файловая система целая»</h5>
<p>Журнал ext4, NTFS dirty bit, ZFS intent log — это подушки. Они уменьшают шанс полной смерти тома, но не отменяют простой факт: если в момент отключения шёл сброс кэша, дописывался RAID-страйп или клиент держал открытый файл по SMB, вы ловите как минимум долгий fsck и «тихую» порчу одного файла. На NAS это особенно обидно: локальный диск ПК ещё можно пережить, а общий том — уже заявка «откати бэкап, но не вчерашний».</p>
<p>ИБП даёт время. Не бесконечное. Бытовой line-interactive на NAS + роутер + коммутатор часто держит от нескольких минут до четверти часа под реальной нагрузкой. Если за это время никто не начал shutdown, батарея просто кончится. Тогда ИБП либо уйдёт в bypass и потухнет, либо сам отрубит розетки. Для дисков разницы почти нет: питание пропало на работающей ФС.</p>
<p>Отдельная ловушка — «умные» розетки и сценарии вроде «через 60 секунд после пропадания 220 отключить NAS, чтобы экономить батарею». Это не shutdown. Это тот же hard power-off, только реле сработало красиво. Резать питание можно только после того, как ОС сама погасла.</p>
<h5>Порядок, который я держу в голове</h5>
<p>Клиенты с сетевыми дисками выключаются раньше хранилища. Хранилище — раньше, чем сеть, по которой ему пришла команда. Монитор ИБП (тот, у кого USB в ИБП) гаснет последним из компьютеров. Коммутатор и роутер живут до последнего: без них команда shutdown до NAS не доедет.</p>
<p>На пальцах для типичной квартиры/небольшого офиса:</p>
<ul>
<li>Короткий моргающий свет (секунды) — ничего не выключаю. Только пишу в лог <code>ONBATT</code> и, если настроено, шлю уведомление.</li>
<li>Питание так и не вернулось, батарея ушла в «мало» или осталось несколько минут рантайма — начинаю сценарий.</li>
<li>Сначала ПК, тонкие клиенты, виртуалки, которые держат SMB/NFS/iSCSI на NAS. Им нужно сбросить кэш и отпустить файлы.</li>
<li>Потом сам NAS: unmount, flush, штатный poweroff. DSM/TrueNAS на это могут занять пару минут, не секунды.</li>
<li>Потом хост, который видит ИБП по USB (NUT master). Он же в конце может дать ИБП команду снять нагрузку.</li>
<li>Роутер и коммутатор не трогаю сценарием. Они умрут сами, когда ИБП снимет розетки — к этому моменту компьютеры уже должны быть off.</li>
</ul>
<p>Перепутать два места легко. Первое: NAS выключился, а <code>pc-office</code> ещё дописывает проект на <code>\\nas\share</code>. Клиент получит обрыв, локальный кэш SMB может соврать, а на томе останется недописанный файл. Второе: выключили коммутатор «чтобы экономить батарею», и NAS, который должен был принять команду по сети, её уже не услышит. Дальше NAS живёт до нуля батареи и падает как есть.</p>
<h5>Кто вообще имеет право нажимать shutdown</h5>
<p>USB от ИБП физически торчит в один хост. Не в два сразу: hid-драйвер не шарится. Этот хост становится источником правды: он читает <code>OL</code>/<code>OB</code>/<code>LB</code>, считает рантайм и рассылает остальным «пора гаснуть».</p>
<p>Три рабочие схемы, которые я встречаю:</p>
<ul>
<li>USB в NAS (Synology/TrueNAS). NAS сам гаснет по своему «UPS». Остальные машины либо клиенты NUT/Synology UPS Server, либо их вообще нет в сценарии. Минус: если ПК с примонтированными шарами не клиент, он переживёт NAS и умрёт позже на пустой батарее.</li>
<li>USB в отдельный Linux («<code>nut-master</code>»). Классика Network UPS Tools: master видит ИБП, slaves слушаются. Порядок я тогда рисую скриптом на master, а не надеюсь, что все slaves погаснут «примерно одновременно».</li>
<li>Вендорский агент на Windows (PowerChute и аналоги). Работает, пока в контуре один ПК. Как только появляется NAS с шарами, Windows-агент про NAS не думает. Либо добавляю NUT/SSH со стороны Linux, либо сознательно оставляю NAS единственным, кто умеет гаснуть.</li>
</ul>
<p>Правило, которое я записываю заказчику в одну строку: кто держит диски с чужими сессиями — тот не имеет права умереть первым. NAS с открытыми SMB — не первый. Хост с USB — не первый, если он должен ещё раздать команду.</p>
<h5>NUT: master, slave и почему «все slave» — плохой порядок</h5>
<p>На <code>nut-master</code> минимум такой каркас. Имена и пароли вымышленные, в прод так не копировать.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># /etc/nut/ups.conf
[officeups]
    driver = usbhid-ups
    port = auto
    desc = "office line-interactive"

# /etc/nut/upsd.conf
LISTEN 127.0.0.1 3493
LISTEN 192.168.10.10 3493

# /etc/nut/upsd.users
[upsmon]
    password = "replace-me"
    upsmon master

# /etc/nut/upsmon.conf
MONITOR officeups@localhost 1 upsmon replace-me master
MINSUPPLIES 1
SHUTDOWNCMD "/sbin/shutdown -h now"
POWERDOWNFLAG /etc/killpower
FINALDELAY 5</pre>
<p><code>LISTEN</code> только во внутреннюю сеть, не на белый IP. <code>upsd</code> — не веб-морда.</p>
<p>На NAS или втором Linux slave выглядит так:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">MONITOR officeups@192.168.10.10 1 upsmon replace-me slave
SHUTDOWNCMD "/sbin/shutdown -h now"</pre>
<p>Если и <code>pc-office</code>, и <code>nas.home.lan</code> оба slave, NUT при FSD скажет им гаснуть почти вместе. Для меня этого мало. Клиенту нужно 20–40 секунд на unmount, NAS — минуты на сброс томов. Поэтому на master я не полагаюсь на одновременный FSD, а вешаю свой <code>NOTIFYCMD</code>: сначала гашу клиентов по SSH, жду, потом NAS, и только потом локальный shutdown.</p>
<p>Грубый скелет, без прикрас:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">#!/bin/bash
# /usr/local/sbin/ups-shutdown-order.sh
# вызывается из NOTIFYCMD на ONBATT+LB или FSD, не на каждый ONBATT
set -eu
clients="pc-office.home.lan htpc.home.lan"
nas="nas.home.lan"

for h in $clients; do
  ssh -o ConnectTimeout=5 "shutdown@$h" 'sudo /sbin/shutdown -h +0' || true
done
sleep 45

ssh -o ConnectTimeout=8 "shutdown@$nas" 'sudo /sbin/shutdown -h now' || true
# DSM: часто удобнее synoshutdown / shutdown из их sudoers, не systemd
sleep 90

/sbin/shutdown -h now</pre>
<p>Таймауты не универсальные. На живом NAS я один раз засекаю, сколько он гаснет от команды до того, как погасли дисковые LED. Это и есть нижняя граница <code>sleep</code> плюс запас. Если DSM выключается 2,5 минуты, <code>sleep 30</code> — способ убить том почти наверняка: master погас, ИБП снял нагрузку, NAS ещё писал.</p>
<p>Доступ <code>shutdown@</code> — отдельная учётка с sudo только на <code>shutdown -h</code>, ключ без пароля, только с <code>nut-master</code>. Не админ «на все команды».</p>
<h5>Когда начинать: ONBATT — ещё не повод рубить всё</h5>
<p>Короткий провал 220 я не превращаю в массовый reboot. Иначе каждый сварочный сосед и каждый ввод резерва на ТП будут по ночам гасить офис.</p>
<p>Смотрю статус так:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">upsc officeups@localhost
# важные поля:
# ups.status     OL / OB / LB / FSD / CHRG
# battery.charge
# battery.runtime</pre>
<p><code>OL</code> — сеть есть. <code>OB</code> — работаем от батареи. <code>LB</code> — мало. <code>FSD</code> — forced shutdown уже идёт.</p>
<p>Практическая схема, которой я пользуюсь:</p>
<ul>
<li><code>ONBATT</code> без <code>LB</code> — лог, письмо или Telegram, ничего не гашу. Жду 30–120 секунд: свет часто возвращается.</li>
<li>Порог по рантайму, не по красивому проценту. «20%» на старой батарее может быть одной минутой. Смотрю <code>battery.runtime</code> и засечённое время реального гашения контура.</li>
<li>Старт сценария, когда остатка хватает на: ожидание клиентов + shutdown NAS + запас. Если весь контур гасится 3 минуты, порог «осталось 4 минуты» — уже игра в кости.</li>
<li>Калибровку батареи и «сколько реально держит» делаю раз в сезон, локально, не в первую командировку.</li>
</ul>
<p>Вендорские галочки в DSM вроде «выключить при 15%» без замера рантайма я не доверяю. Процент врёт чаще, чем секунды.</p>
<h5>Что именно вешать на батарейные розетки</h5>
<p>На «battery backup», не на «surge only»:</p>
<ul>
<li>роутер и коммутатор, через который NAS получает команду;</li>
<li>сам NAS;</li>
<li>хост с USB в ИБП;</li>
<li>ПК, которые обязаны штатно погаснуть, а не «ну переживём chkdsk».</li>
</ul>
<p>Не вешаю лазерный принтер, обогреватель, большой монитор «ну раз розетка свободна». Они съедают минуты, которые нужны дискам. PoE-коммутатор с кучей камер может сожрать батарею быстрее NAS — тогда камеры либо на отдельный маленький ИБП, либо сознательно не входят в контур «спасти ФС».</p>
<p>Отдельный камень: модем провайдера. Если он не на ИБП, уведомление «свет пропал» из дома вы не получите: канал умрёт вместе с 220. На shutdown это не влияет — сценарий локальный. На ваше спокойствие в поездке — влияет. Я либо питаю ONT/модем от того же ИБП, либо не жду, что телега доедет.</p>
<h5>Windows, DSM, TrueNAS — где люди обычно срезают угол</h5>
<p>Windows по умолчанию не слушает NUT. Варианты, которые работают у меня без вендорской простыни: WinNUT-Client как slave, либо SSH/WinRM с master и штатный <code>shutdown /s /t 0 /f</code>. <code>/f</code> закрывает приложения принудительно — для ночного обрыва питания это меньшее зло, чем диалог «сохранить документ» до конца батареи. Для интерактивного ПК днём — неприятно, поэтому порог LB, а не первый ONBATT.</p>
<p>Synology: «Панель управления → Оборудование и питание → ИБП». Если USB в NAS, включаю сетевой UPS-сервер Synology, чтобы другой их же NAS или NUT-совместимый клиент получил сигнал. Если USB в Linux — DSM настраиваю как NUT slave на <code>nut-master</code>. Проверяю, что команда реально гасит DSM, а не пишет «событие ИБП» в журнал и живёт дальше. Разница видна один раз, если посидеть рядом с LED дисков.</p>
<p>TrueNAS: служба UPS — тот же NUT. Не забываю <code>SHUTDOWNCMD</code> и что jail/VM должны уехать до zpool. Иначе пул экспортируется, пока виртуалка ещё пишет в zvol.</p>
<p>systemd на Linux: <code>shutdown -h</code> лучше, чем <code>systemctl halt</code> без понимания, уйдёт ли питание. Для NUT master ещё важен <code>POWERDOWNFLAG /etc/killpower</code> и проверка <code>upsmon -K</code> на старте: ИБП после пачки shutdown должен снять нагрузку, а когда 220 вернётся — подать её снова, чтобы машины включились. Без этого батарея догорит в ноль на уже выключенных ПК, и утром часть железа не встанет, пока кто-то не нажмёт кнопку на ИБП.</p>
<h5>Как машины должны просыпаться, когда свет вернулся</h5>
<p>Выключение — половина. В BIOS/UEFI хоста и NAS смотрю «Restore AC Power Loss» / «After Power Loss»:</p>
<ul>
<li>NAS — обычно Always On. Хранилище должно подняться само.</li>
<li><code>nut-master</code> — тоже Always On. Иначе следующий обрыв никто не отследит.</li>
<li>Офисный ПК — Last State или Off. Мне редко нужно, чтобы он сам ревел в пустой комнате.</li>
</ul>
<p>Wake-on-LAN тут ни при чём: его некому послать, пока вы спите в другом городе. Это про поведение блока питания после возврата 220.</p>
<p>Роутер на ИБП сам поднимется. Если у провайдера ONT долго регистрируется, это не повод задерживать shutdown: к моменту возврата света компьютеры уже должны быть холодные.</p>
<h5>Как я это проверяю, не надеясь на «первый настоящий обрыв»</h5>
<p>Первый тест — только локально, днём, с руками на кнопке ИБП и с копией важных томов, которая не на этом же NAS.</p>
<ul>
<li><code>upsc</code> в норме: <code>OL</code>, рантайм похож на правду, USB не отваливается в dmesg.</li>
<li>Выдёргиваю входную вилку ИБП из стены, не нагрузку. Статус должен стать <code>OB</code>. Клиенты ещё живы.</li>
<li>Жду порог или форсирую сценарий. Смотрю, что <code>pc-office</code> погас первым, шары отпустились, потом погас NAS, потом master.</li>
<li>Засекаю время от старта сценария до момента, когда на NAS нет активности дисков. Это число кладу в <code>sleep</code> и в порог батареи.</li>
<li>Втыкаю вилку обратно. ИБП берёт сеть, поднимает нагрузку, NAS стартует сам.</li>
</ul>
<p>Форсировать FSD командой NUT можно, но на чужом контуре в пятницу вечером я этого не делаю. Слишком легко получить тот самый обрыв, от которого писали статью.</p>
<p>После теста:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># Linux / TrueNAS
journalctl -u nut-monitor -u nut-server --since today
zpool status
dmesg -T | tail

# DSM: Storage Manager / журналы ИБП, не «всё зелёное в виджете»

# Windows
wevtutil qe System /q:"*[System[EventID=6008 or EventID=6006 or EventID=41]]" /c:5 /f:text</pre>
<p><code>6008</code> — неожиданное выключение. Если он есть после «успешного» сценария, Windows погасили питанием, не <code>shutdown /s</code>. Сценарий врёт.</p>
<h5>Утро после настоящего обрыва, когда меня не было дома</h5>
<p>Короткий список, без магии:</p>
<ul>
<li>Поднялся ли NAS сам. Если нет — сначала ИБП и «Restore AC», не fsck.</li>
<li>Тома смонтировались без ручного repair. На ZFS — <code>zpool status</code>, нет ли FAULTED/UNAVAIL после внезапного питания. На DSM — Storage Manager, не «папка открывается в File Station».</li>
<li>Клиенты, которые писали ночью (бэкап, 1С, камеры на шару). Открыть не «файл на месте», а хвост: последняя запись, целостность архива, не обрезан ли контейнер.</li>
<li>Журнал NUT: был ли <code>OB</code> → сценарий → shutdown, или сразу смерть на <code>OL</code> (тогда ИБП не участвовал: выбили автомат после ИБП, сдох USB, уехал драйвер).</li>
<li>Рантайм в логе. Если от <code>OB</code> до смерти прошло меньше, чем длится shutdown NAS — порог надо поднимать или разгружать ИБП.</li>
<li>Батарея после серии обрывов. Старый аккумулятор «держит на экране 12 минут» и отдаёт три — это уже не настройка NUT, а замена батареи. Цены тут не считаю: если ИБП пищит и рантайм сдулся, это расходник, не «ещё чуть крутануть процент».</li>
</ul>
<p>Если том всплыл, а один каталог странный — не чиню «на горячую» и не запускаю агрессивный repair, пока нет копии. Сначала снимок/бэкап, потом fsck. ИБП спасает от внезапного питания, но не от желания починить живой диск первой попавшейся галочкой.</p>
<h5>Что я отказываюсь считать решением</h5>
<p>Не считаю решением плагин умного дома «выключить розетку NAS через минуту». Не считаю решением «поставим ИБП побольше и не будем ничего гасить» — батарея всё равно кончится в длинный обрыв, плюс вы греете комнату зря. Не считаю решением только уведомление в мессенджер: в 03:00 я могу не успеть, а канал может умереть вместе с модемом.</p>
<p>Считаю решением короткую цепочку, которую можно проговорить вслух: кто видит USB, кто гаснет первым, сколько минут это занимает, сколько батарея реально держит, и что должно включиться само, когда 220 вернётся. Это скучно. Зато утром NAS монтирует том, а не просит «ваш диск был извлечён неправильно».</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/umnyj-dom/ibp-i-udaljonka-poryadok-vykljucheniya-nas-pk-chtoby-ne-slovit-porchu-fs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Application Passwords vs XML-RPC: что оставлять и как не открыть лишнюю дыру</title>
		<link>https://remadmin.com/blog/vebmasteru/application-passwords-vs-xml-rpc-chto-ostavlyat-i-kak-ne-otkryt-lishnjuju-dyru/</link>
					<comments>https://remadmin.com/blog/vebmasteru/application-passwords-vs-xml-rpc-chto-ostavlyat-i-kak-ne-otkryt-lishnjuju-dyru/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Tue, 22 Sep 2026 20:30:05 +0000</pubDate>
				<category><![CDATA[Вебмастеру]]></category>
		<category><![CDATA[Application Passwords]]></category>
		<category><![CDATA[REST API]]></category>
		<category><![CDATA[Wordpress]]></category>
		<category><![CDATA[xmlrpc]]></category>
		<category><![CDATA[безопасность]]></category>
		<category><![CDATA[логи]]></category>
		<category><![CDATA[Уязвимость]]></category>
		<category><![CDATA[шпаргалка]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7429</guid>

					<description><![CDATA[XML-RPC и Application Passwords — разные входы. Как я закрываю xmlrpc.php на nginx, почему фильтр xmlrpc_enabled не считается закрытием, и как не оставить токен приложения с правами администратора.]]></description>
										<content:encoded><![CDATA[<p>Типичная заявка: «поставил плагин безопасности, галочка Disable XML-RPC стоит, 2FA на админке включена, а в логах всё равно долбят xmlrpc.php». Вариант помягче: «выдал подрядчику логин администратора, чтобы скрипт заливал новости, потом сменил пароль — скрипт отвалился, а через неделю на сайте чужие черновики». Ещё чаще путают двери: закрыли xmlrpc, зато в профиле пользователя висит Application Password с именем «test», который никто не отзывал полгода.</p>
<p>Это не одна настройка. XML-RPC — старый вход в PHP-файл. Application Passwords — отдельные пароли к REST API (и, если xmlrpc не закрыт, ещё и к нему же). Двухфакторка на <code>wp-login.php</code> вторую дверь не закрывает. Ниже — как я решаю, что оставлять, что резать на nginx, и где «плагин запретил xmlrpc» оставляет дыру, которой хватает боту.</p>
<p>Хосты в примерах вымышленные: <code>shop.example</code>. Свои пути, пользователя и префикс логов подставляйте.</p>
<h5>Две двери, не «один API WordPress»</h5>
<p>На живом сайте обычно торчат три способа что-то сделать «не из браузера»:</p>
<ul>
<li><code>/xmlrpc.php</code> — XML-RPC. Методы вроде <code>wp.getUsersBlogs</code>, <code>wp.newPost</code>, pingback. Клиент шлёт логин и пароль в теле XML на каждый запрос.</li>
<li><code>/wp-json/</code> — REST API. Gutenberg, приложение, ваш скрипт, куча плагинов. С WordPress 5.6 туда ходят Application Passwords через HTTP Basic Auth.</li>
<li><code>/wp-login.php</code> — человек в браузере. Сюда Application Password <em>не подходит</em>: это не замена пароля учётки для формы входа.</li>
</ul>
<p>Путаница начинается здесь. Человек включает 2FA и считает, что «без телефона никто не войдёт». Для формы логина — да. Для xmlrpc, если он принимает основной пароль пользователя, второго фактора нет. Бот не открывает браузер и не вводит код из приложения.</p>
<p>Вторая путаница: «раз есть Application Passwords, xmlrpc больше не нужен, WordPress сам его выключит». Не выключит. Это разные стеки. Пароль приложения, если xmlrpc жив, подходит и туда: в документации ядра прямо сказано, что Application Password можно подставить вместо обычного пароля в XML-RPC. То есть вы завели «безопасный» токен для REST, а старый файл его тоже съест.</p>
<p>Третья: галочка в плагине «Disable XML-RPC». Часто это фильтр <code>xmlrpc_enabled</code>. Он отключает методы, которым нужна авторизация. Файл при этом открывается, PHP крутится, pingback и часть методов без логина могут отвечать. Для бота, которому нужно занять php-fpm, этого достаточно. Для меня критерий «закрыто» — nginx/apache не отдают этот URI в PHP, а не «в админке галочка зелёная».</p>
<h5>Зачем xmlrpc вообще существует в 2026-м</h5>
<p>Когда-то это был нормальный способ писать с телефона и цеплять Jetpack. Сейчас на большинстве витрин, которые мне отдают, в access.log по <code>xmlrpc.php</code> нет ни одного своего User-Agent. Есть словари паролей, <code>system.multicall</code> и pingback на чужие сайты.</p>
<p>Две атаки, из-за которых я не оставляю файл «на всякий случай»:</p>
<ul>
<li>Брутфорс пачкой. <code>system.multicall</code> в одном HTTP-запросе прогоняет десятки <code>wp.getUsersBlogs</code> с разными паролями. Лимитер по запросам на IP видит один POST. Лимит без закрытия multicall — полумера: словарь укладывается в один хит.</li>
<li>Pingback как чужой молоток. Сайт принимает <code>pingback.ping</code> и сам идёт на URL, который подсунули. Либо участвует в отражённой нагрузке, либо светит внутренние адреса, если кто-то целится в SSRF. Даже без успешного логина файл уже не бесплатный.</li>
</ul>
<p>Jetpack, официальное приложение WordPress старых версий, часть сервисов рассылки и редкие десктопные клиенты xmlrpc ещё используют. Поэтому я не ставлю <code>deny all</code> вслепую на проде, пока не посмотрел неделю логов. Но «вдруг пригодится» — не причина держать вход, который умеет перебирать пароль администратора без 2FA.</p>
<h5>Что такое Application Passwords на практике</h5>
<p>С WordPress 5.6 у пользователя в профиле появляется блок «Пароли приложений». Вы даёте имя («backup-bot», «магазин-контент», «телефон редактора»), ядро показывает 24 символа один раз, дальше пароль лежит хешем в user meta. Пробелы в отображении можно не копировать — перед проверкой их срезают.</p>
<p>Зачем это, если «и так есть пароль админа»:</p>
<ul>
<li>Основной пароль не разъезжается по скриптам, телефону и подрядчику. Сменили пароль учётки после увольнения — бот с Application Password продолжает жить, пока его явно не отозвали. Это и плюс, и минус: ротация учётки дыру не закрывает.</li>
<li>Отзыв точечный. Удалили один пароль — отвалилось одно приложение, редактор в браузере не вылетел.</li>
<li>2FA на логине можно держать жёсткой. Скрипту второй фактор не нужен, человеку — нужен. Раньше из-за xmlrpc люди 2FA не включали: «приложение не зайдёт».</li>
<li>В профиле видно, когда пароль последний раз использовали и с какого IP. Не в реальном времени до секунды, с задержкой примерно до суток, но «висит полгода, last used — никогда» уже повод снести.</li>
</ul>
<p>Ходят они по HTTPS Basic Auth: логин учётки плюс этот пароль. Без HTTPS ядро по умолчанию блок даже не показывает: иначе пароль утечёт на первой же кофейне. Если сайт за прокси и WordPress думает, что он на http, в профиле пусто. Тогда сначала чинят <code>HTTPS</code>/<code>X-Forwarded-Proto</code>, а не «включают фильтр разрешить без SSL». Разрешить Application Passwords по http — сознательно оставить секрет в открытую на участке до прокси.</p>
<p>Права у такого пароля — права пользователя, не «только читать посты». Завели под администратором — получили администратора. Для заливки черновиков мне хватает роли автора или редактора и отдельной учётки, не «ещё один токен на admin».</p>
<h5>Как я смотрю, кто чем пользуется</h5>
<p>Пока нет картины за несколько дней, я ничего не отключаю на живом магазине. Сначала логи, потом решение.</p>
<p>Nginx, если access_log в обычном combined:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash"># пути к логам подставьте свои
awk '$7 ~ /xmlrpc\.php/ {print $1,$4,$9,$11}' /var/log/nginx/shop.example.access.log | tail -n 50
awk '$7 ~ /xmlrpc\.php/ {c[$1]++} END {for (i in c) print c[i], i}' /var/log/nginx/shop.example.access.log | sort -n | tail
awk '$7 ~ /wp-json\// {print $1,$9,$7}' /var/log/nginx/shop.example.access.log | awk '{print $3}' | sort | uniq -c | sort -n | tail</pre>
<p>Что я хочу увидеть:</p>
<ul>
<li>По xmlrpc: либо пусто, либо одни и те же /24 из сканеров, статус 200 на POST. Свой телефон обычно один-два IP, предсказуемый интервал, не 400 запросов в минуту.</li>
<li>User-Agent. Jetpack и приложение WordPress не маскируются под <code>Mozilla/5.0</code> с кривой кодировкой. Сканеры часто пишут <code>xmlrpc</code>, пусто или «WordPress» без версии.</li>
<li>По REST: Gutenberg с админки будет, это норма. Смотрю, есть ли POST/PUT с внешних IP на <code>/wp-json/wp/v2/posts</code> или media. Если есть — это живой клиент, его нельзя убить вместе с xmlrpc.</li>
</ul>
<p>Проверка «файл вообще отвечает», не из лога:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">curl -sS -D - -o /dev/null https://shop.example/xmlrpc.php
curl -sS https://shop.example/xmlrpc.php \
  -H 'Content-Type: text/xml' \
  -d '&lt;?xml version="1.0"?&gt;&lt;methodCall&gt;&lt;methodName&gt;system.listMethods&lt;/methodName&gt;&lt;/methodCall&gt;' | head</pre>
<p>200 и список методов — дверь открыта, даже если «плагин безопасности установлен». 403/404 с nginx без <code>X-Powered-By</code> и без тела PHP — уже похоже на правду.</p>
<p>REST и доступность Application Passwords:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">curl -sS https://shop.example/wp-json/ | python3 -m json.tool | head
# в корне смотрю ключ authentication.application-passwords
curl -sS -u 'editor:xxxx xxxx xxxx xxxx xxxx xxxx' \
  https://shop.example/wp-json/wp/v2/users/me</pre>
<p>Если в корне <code>authentication</code> пустой — либо HTTP, либо фича выключена фильтром, либо что-то по дороге срезает заголовок <code>Authorization</code>. Последнее на nginx с PHP-FPM встречается: CGI не пробрасывает Authorization, пока не сказать.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="nginx"># внутри location ~ \.php$
fastcgi_param HTTP_AUTHORIZATION $http_authorization;</pre>
<p>Без этой строки «пароль приложения не работает, а обычный логин в браузере живой» — не баг WordPress, а FPM не видит Basic Auth.</p>
<h5>Если xmlrpc никому не нужен — режу на веб-сервере</h5>
<p>Это мой дефолт для витрины без Jetpack и без старого мобильного клиента. Фильтр в теме — вторым слоем, не вместо.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="nginx"># server { ... }
location = /xmlrpc.php {
    access_log /var/log/nginx/shop.example.xmlrpc.denied.log;
    return 403;
}</pre>
<p><code>deny all</code> или <code>return 444</code> тоже работают. Мне удобнее 403 и отдельный лог: сразу видно, что сканеры не исчезли, просто больше не крутят PHP. На Apache в корне сайта или в конфиге хоста:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="apache">&lt;Files xmlrpc.php&gt;
    Require all denied
&lt;/Files&gt;</pre>
<p>Правило на WAF (если он есть) дублирует, не заменяет. Когда человек правит только Cloudflare и забывает origin, прямой IP сервера xmlrpc снова открыт.</p>
<p>После рестарта nginx проверяю тем же <code>curl</code>: не 200 со списком методов, а 403 без <code>text/html</code> от WordPress. Затем на час оставляю отдельный лог — если вдруг вылезет свой клиент, это будет видно по IP, а не «через неделю редактор звонит, что приложение молчит».</p>
<p>Плагины «выключить xmlrpc» я не ставлю как основную защиту. Лишний PHP на периметре, плюс половина из них делает только <code>xmlrpc_enabled</code>.</p>
<h5>Почему xmlrpc_enabled я не считаю закрытием</h5>
<p>В <code>functions.php</code> или mu-plugin часто копируют:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_filter( 'xmlrpc_enabled', '__return_false' );</pre>
<p>Это отключает авторизованные методы. Запрос всё равно доходит до WordPress: php-fpm воркер занят, плагины загрузились, объектный кеш дёрнули. Pingback на части сборок остаётся в списке. Сканер с <code>system.multicall</code> получает отказ, но уже после входа в PHP.</p>
<p>Если xmlrpc по какой-то причине оставляю, вычищаю хотя бы multicall и pingback:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
// mu-plugins/disable-xmlrpc-extras.php
add_filter( 'xmlrpc_methods', function ( $methods ) {
    unset( $methods['system.multicall'], $methods['system.multitask'] );
    unset( $methods['pingback.ping'], $methods['pingback.extensions.getPingbacks'] );
    return $methods;
} );</pre>
<p>Это не «можно не закрывать файл». Это минимум, если Jetpack реально нужен и закрыть <code>location</code> нельзя. Плюс лимит запросов на этот URI отдельно от <code>admin-ajax.php</code> — иначе снова либо дыра, либо свои ловят 429.</p>
<h5>Когда xmlrpc оставляю</h5>
<p>Короткий список, не «все интеграции»:</p>
<ul>
<li>Jetpack и связанные сервисы Automattic, если они включены сознательно, а не «поставили и забыли».</li>
<li>Старое официальное приложение WordPress, которое ещё ходит в xmlrpc, а не в REST. Новые клиенты умеют Application Passwords и REST — их я перевожу.</li>
<li>Редкий десктопный клиент публикации, который заказчик не готов менять сегодня.</li>
</ul>
<p>Если оставляю, пароль приложения завожу под отдельным пользователем с нужной ролью, основной пароль этой учётки длинный и никуда не светится, 2FA на человеческих админах включена. xmlrpc тогда принимает Application Password, и утечка основного пароля админки не даёт боту этот вход. Обратное тоже верно: утечка пароля приложения — это вход в API с правами этой учётки, пока не отозвали.</p>
<p>Белый список IP на <code>xmlrpc.php</code> лучше, чем открытый мир. Для Jetpack это набор адресов Automattic, он плавает; без WAF с готовым правилом я не рисую его руками «по памяти» и не выкладываю чужие схемы. Практичнее: либо официальное правило у CDN, либо закрыть xmlrpc и жить на REST.</p>
<h5>Application Passwords: заводить, не размножать, уметь выключить</h5>
<p>Правила, без которых я токен не создаю:</p>
<ul>
<li>Одно приложение — один пароль. Не «один на все скрипты», чтобы потом не гадать, какой cron его светит.</li>
<li>Имя в профиле человеческое: <code>rsync-report bot</code>, <code>iPhone редактор</code>, не <code>test</code> и не <code>1</code>.</li>
<li>Учётка с минимальной ролью. Для черновиков не нужен администратор. Для медиа — часто хватает автора.</li>
<li>Пароль копирую в секрет хранилища один раз. В чат, в тикет и в тело статьи его не кладут. В логах nginx он не должен появиться: Basic Auth лучше не писать в access_log.</li>
<li>Раз в квартал смотрю last used. Не использовался — удаляю, не «вдруг пригодится».</li>
</ul>
<p>Через WP-CLI, если SSH есть:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">cd /var/www/shop.example/public_html
wp user list --fields=ID,user_login,roles
wp user application-password list editor --fields=uuid,name,created,last_used,last_ip
# создать и сразу забрать только секрет, без таблицы
wp user application-password create editor 'content-bot' --porcelain
# отозвать по uuid из списка
wp user application-password delete editor 00000000-0000-4000-8000-000000000000</pre>
<p>Если Application Passwords на сайте никто не использует (нет мобильного клиента, нет внешних скриптов, черновики пишут руками), я их выключаю. Иначе любой администратор может завести себе токен, а через дырявый плагин или XSS это делают уже не только администраторы.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
// mu-plugins/disable-application-passwords.php
add_filter( 'wp_is_application_passwords_available', '__return_false' );</pre>
<p>Точечно, только выбранным ролям:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
add_filter( 'wp_is_application_passwords_available_for_user', function ( $allow, $user ) {
    if ( $user instanceof WP_User &amp;&amp; in_array( 'administrator', (array) $user-&gt;roles, true ) ) {
        return $allow;
    }
    return false;
}, 10, 2 );</pre>
<p>Выключить и забыть отозвать уже выданные пароли — мало. Фильтр закрывает новые и использование, но я всё равно прохожусь по списку и удаляю старые: меньше сюрпризов, когда фильтр кто-то снимет «чтобы приложение заработало».</p>
<h5>REST я не вырубаю «за компанию»</h5>
<p>После статьи про xmlrpc находится человек, который в плагине безопасности ставит Disable REST API. Gutenberg после этого рисует белый экран, кастомайзер полуживой, часть блоков не сохраняется. REST — не xmlrpc. Это штатный транспорт админки.</p>
<p>Что я ограничиваю, не выключая REST целиком:</p>
<ul>
<li>Публичный список пользователей. На многих сайтах <code>GET /wp-json/wp/v2/users</code> без авторизации отдаёт логины. Для брутфорса это подарок: не надо угадывать <code>admin</code>. Плагин безопасности или маленький mu-plugin, который на <code>rest_authentication_errors</code> закрывает <code>users</code> для анонимов, — нормальная гигиена. Слепой «REST off» — нет.</li>
<li>Лимит на <code>/wp-json/</code> на краю, отдельно от html, если вижу перебор. Не общий <code>location ~ \.php$</code>.</li>
<li>Не торчит ли создание Application Passwords для всех ролей, если фича нужна только админу.</li>
</ul>
<p>Проверка, светит ли сайт логины:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">curl -sS 'https://shop.example/wp-json/wp/v2/users' | python3 -m json.tool | head</pre>
<p>Если в ответе массив с <code>slug</code> — аноним уже знает, кого подбирать. Это не «дыра нулевого дня», это дефолт, который я закрываю в тот же вечер, что и xmlrpc.</p>
<h5>Какие комбинации я считаю нормальными</h5>
<p>Свожу к трём схемам, без зоопарка галочек.</p>
<ol>
<li>Витрина, правки из браузера, нет Jetpack, нет внешних ботов. xmlrpc на nginx 403. Application Passwords выключены фильтром. 2FA на администраторах. REST живой, список users для гостя закрыт. Это большинство сайтов, которые мне отдают «просто посмотрите безопасность».</li>
<li>Нужен скрипт или телефон. xmlrpc всё равно 403, если клиент умеет REST. Одна учётка с ролью по задаче, один Application Password, HTTPS доведён до ядра, Authorization проброшен в FPM. Основной пароль этой учётки не совпадает с паролем приложения и никуда не записан в cron.</li>
<li>Jetpack или старый клиент, без них пока никак. xmlrpc открыт, multicall и pingback сняты, лимит на URI, по возможности ограничение по источнику на WAF. Пароли приложений для этого клиента, не основной пароль админа. Остальным пользователям Application Passwords не светятся.</li>
</ol>
<p>Схема, которую я снимаю сразу: 2FA «для галочки», xmlrpc открыт на мир, в cron лежит основной пароль администратора, в профиле три пароля приложения с именами test/test2/asdf, REST отдаёт users. Это не «чуть-чуть не донастроили». Это три входа с одним секретом и одним без второго фактора.</p>
<h5>Что проверить за ночь, а не сразу закрыть ноутбук</h5>
<p>После изменений я не верю одному <code>curl</code> с ноутбука. За ночь видно, не отвалился ли свой клиент и не начал ли сканер долбить соседнюю дверь.</p>
<ul>
<li>Отдельный лог 403 по xmlrpc: есть чужие POST, нет 200 в php-fpm slowlog по этому файлу.</li>
<li>В access.log нет всплеска 401/403 на <code>/wp-json/wp/v2/</code> с офисного IP. Если есть — вы закрыли не то, или срезали Authorization.</li>
<li>Админка открывается, запись сохраняется, медиа грузится. Gutenberg ходит в REST; если его прибили, узнаете утром от редактора, не из мониторинга.</li>
<li><code>wp user application-password list</code> по всем, у кого не пусто: имена понятные, last_ip свой, лишние удалены.</li>
<li>Повторный <code>curl</code> на <code>/wp-json/wp/v2/users</code> без куки — не список логинов.</li>
<li>Плагин безопасности не включил обратно xmlrpc своим «режимом совместимости» и не выключил REST. После обновления плагинов это проверяю отдельно.</li>
</ul>
<p>Если за ночь в xmlrpc-логе вдруг свой IP приложения — не открываю файл обратно «как было», а перевожу клиент на REST и Application Password. Старый xmlrpc ради одного телефона на витрине с заказами я оставляю только когда перевод реально нельзя сделать сегодня, и тогда это временная схема с датой, а не новая норма.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/vebmasteru/application-passwords-vs-xml-rpc-chto-ostavlyat-i-kak-ne-otkryt-lishnjuju-dyru/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Миграция домена WordPress без поломки serialized-данных</title>
		<link>https://remadmin.com/blog/vebmasteru/migraciya-domena-wordpress-bez-polomki-serialized-dannyh/</link>
					<comments>https://remadmin.com/blog/vebmasteru/migraciya-domena-wordpress-bez-polomki-serialized-dannyh/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Tue, 22 Sep 2026 20:29:06 +0000</pubDate>
				<category><![CDATA[Вебмастеру]]></category>
		<category><![CDATA[Mysql]]></category>
		<category><![CDATA[Wordpress]]></category>
		<category><![CDATA[wp-cli]]></category>
		<category><![CDATA[миграция]]></category>
		<category><![CDATA[Резервное копирование]]></category>
		<category><![CDATA[шпаргалка]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7426</guid>

					<description><![CDATA[Почему REPLACE по дампу оставляет рабочий siteurl и мёртвые виджеты. Как я меняю домен через WP-CLI с учётом сериализации, какие варианты URL не забыть и что проверить за ночь.]]></description>
										<content:encoded><![CDATA[<p>Типичная заявка: «переехали на новый домен, главная открывается, в админку пускает, а меню пустое, виджеты слетели, Elementor открывается белым экраном, в кастомайзере ошибка». Иногда мягче: «часть картинок с старого адреса, часть с нового», «письма клиентам уходят со старой ссылкой», «после смены домена пропал футер».</p>
<p>Я почти всегда нахожу одно и то же. Кто-то сделал в phpMyAdmin <code>REPLACE</code> по <code>option_value</code> или прогнал дамп через «найти и заменить» в редакторе. Поля <code>siteurl</code> и <code>home</code> в <code>wp_options</code> — обычные строки, они меняются и сайт «как будто живой». Виджеты, theme_mods, ACF, настройки кэша и куча plugin-опций лежат как PHP-сериализация. Наивная замена портит длину строк внутри этой каши. <code>unserialize()</code> возвращает <code>false</code>, WordPress считает «настроек нет» и рисует пустые сайдбары.</p>
<p>Ниже — как я перевожу домен, чтобы не чинить потом сериализацию руками. Хосты в примерах вымышленные: <code>old.example</code> → <code>shop.example</code>. Свои домены, префикс таблиц и путь к сайту подставляйте, чужой дамп из интернета на прод не накатывайте.</p>
<h5>Почему обычный REPLACE ломает сайт</h5>
<p>PHP пишет строку не как «текст», а как «длина плюс текст». Упрощённо:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">s:19:"https://old.example";</pre>
<p>«19» — это число байт внутри кавычек, не «примерно длина». Если в дампе или SQL вы меняете <code>old.example</code> на <code>shop.example</code>, букв становится больше, а префикс <code>s:19:</code> остаётся. Для интерпретатора это уже не валидная сериализация. Массив вокруг тоже разваливается: один битый кусок — и весь option «не читается».</p>
<p>Отсюда картина, которую путают с «плагин не перенёсся»:</p>
<ul>
<li>Админка работает: логин, список записей, даже медиатека. Это не сериализация, это обычные таблицы.</li>
<li>Виджеты пустые или «сбросились на дефолт». Классика — <code>sidebars_widgets</code> и <code>widget_*</code>.</li>
<li>Кастомайзер/меню выглядят как новый сайт на чистой теме: <code>theme_mods_*</code> не распаковался.</li>
<li>Страница открывается, визуальный редактор — нет. У конструкторов URL ещё и в JSON/CSS, не только в serialize.</li>
<li>В PHP-логах за ночь: <code>unserialize(): Error at offset …</code> на <code>option_value</code> или postmeta. Это не «хостер глючит», это вы вчера поправили домен в БД.</li>
</ul>
<p><code>siteurl</code>/<code>home</code> я вообще не считаю критерием успеха. Их можно поменять руками за минуту. Критерий — живые виджеты, меню, конструктор и отсутствие старого хоста в опциях, которые WordPress реально читает.</p>
<h5>Что я снимаю до любой замены</h5>
<p>Пока не лежит откат, я замену не запускаю. Испорченная сериализация откатывается из дампа за две минуты и чинится «на глаз» полдня.</p>
<p>Минимум:</p>
<ul>
<li>Дамп базы <em>до</em> замены, не «сейчас сниму, если что». И файл не в <code>wp-content/uploads</code> этого же сайта.</li>
<li>Архив файлов: тема, <code>uploads</code>, <code>mu-plugins</code>. Конструкторы после смены домена часто оставляют CSS со старым хостом уже в файлах, не в MySQL.</li>
<li>Текущие <code>siteurl</code> и <code>home</code>, плюс константы из <code>wp-config.php</code>. Если заданы <code>WP_HOME</code>/<code>WP_SITEURL</code>, база может врать, а сайт всё равно ходить на старый адрес — или наоборот, «замена в БД не действует».</li>
<li>Список вариантов URL, которые реально встречаются. Не один «красивый» https.</li>
</ul>
<p>Варианты, которые я выписываю в блокнот до команды, не в процессе:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">https://old.example
http://old.example
https://www.old.example
http://www.old.example
//old.example
old.example</pre>
<p>Слеш на конце — отдельная история. В опциях чаще без него, в контенте и в JSON конструктора бывает с ним. Менять «с слэшем» и «без» двумя проходами безопаснее, чем надеяться, что один проход поймает оба.</p>
<p>Голый <code>old.example</code> без схемы я оставляю на последний проход и смотрю отчёт: он может задеть e-mail вида <code>shop@old.example</code>, CDN, упоминания в тексте. Если почта на этом домене остаётся — голый хост не трогаю или меняю точечно.</p>
<p>Префикс таблиц не угадываю:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash"># каталог сайта подставьте свой
cd /var/www/example-site/public_html
wp config get table_prefix
wp option get siteurl
wp option get home
wp config get WP_HOME
wp config get WP_SITEURL</pre>
<p>Если две последние константы заданы и расходятся с тем, куда вы переезжаете, сначала правлю <code>wp-config.php</code>. Иначе search-replace в базе вы сделаете, а WordPress продолжит считать каноническим старый URL.</p>
<h5>WP-CLI: основной путь</h5>
<p>На VPS и нормальном хостинге с SSH я не открываю phpMyAdmin для этой задачи. <code>wp search-replace</code> ходит по ячейкам, понимает PHP-сериализацию (и в свежих версиях — JSON), первичные ключи не трогает.</p>
<p>Сначала сухой прогон, чтобы увидеть масштаб, а не «сразу на прод»:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">cd /var/www/example-site/public_html

wp search-replace 'https://old.example' 'https://shop.example' \
  --all-tables-with-prefix \
  --precise \
  --dry-run \
  --report-changed-only</pre>
<p><code>--all-tables-with-prefix</code> нужен потому, что дефолт — таблицы, которые WordPress зарегистрировал в <code>$wpdb</code>. У Magento-в-WP это не про нас, а у WooCommerce, форм, кэша, самописных плагинов таблица <code>wp_something_extra</code> легко остаётся со старым хостом. <code>--all-tables</code> без префикса я на шареде с общей MySQL не включаю: можно вылезти в чужую базу на том же пользователе.</p>
<p><code>--precise</code> гоняет замену через PHP, не через быстрый SQL. Медленнее, зато меньше шансов проскочить колонку, где сериализация не похожа на «очевидную». На магазине в сотни мегабайт дампа это не мгновенно — зато не потом ночью ловить пустую корзину.</p>
<p>Если сухой прогон выглядит здраво (есть попадания в <code>wp_options</code>, postmeta, может быть post content — и нет сюрприза в <code>wp_users.user_email</code> на тысячи строк), снимаю ещё один свежий дамп и запускаю без <code>--dry-run</code>.</p>
<p>GUID записей я обычно пропускаю:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">wp search-replace 'https://old.example' 'https://shop.example' \
  --all-tables-with-prefix \
  --precise \
  --skip-columns=guid \
  --report-changed-only</pre>
<p><code>guid</code> в WordPress — идентификатор, не «красивая ссылка». Менять его при переезде домена не обязательно, а фиды и импорты от этого иногда едут. Если у вас старый зоопарк, где в GUID руками писали permalink — смотрите отчёт dry-run по колонке и решайте точечно, не «всем REPLACE».</p>
<p>Дальше те же команды для <code>http://</code>, <code>www</code> и протоколо-относительного <code>//old.example</code>. Один проход по «основному» https почти никогда не вычищает всё: редирект на https могли включить год назад, а в виджете 2019 года лежит http.</p>
<p>Если WP-CLI есть на старом сервере, а на новом ещё нет, удобен экспорт уже с заменой:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">wp search-replace 'https://old.example' 'https://shop.example' \
  --all-tables-with-prefix \
  --precise \
  --skip-columns=guid \
  --export=/root/shop-example-after-replace.sql</pre>
<p>Накатываю этот SQL на новое место, а не «сырой дамп плюс sed». <code>sed</code> по <code>.sql</code> — тот же класс ошибки, что REPLACE в phpMyAdmin, только в файле на 400 МБ её ещё и не видно.</p>
<h5>Если WP-CLI нет</h5>
<p>На дешёвом shared без SSH люди ставят плагин вроде Better Search Replace. Под капотом та же идея, что у interconnect/it: идти по ячейкам и пересобирать serialize. Это лучше, чем правка дампа. Это хуже, чем WP-CLI, потому что плагин работает из-под веба, на больших таблицах упирается в timeout, и его ещё надо не забыть удалить.</p>
<p>Правила, без которых я этот путь не рекомендую:</p>
<ul>
<li>Сначала дамп. Плагин «умеет dry-run» — dry-run обязателен, как у CLI.</li>
<li>Ставить, прогнать, снести. Search-Replace, оставленный в корне сайта, — готовая дыра: это скрипт с доступом к базе, который не должен торчать в интернет ни минуты после работы.</li>
<li>Не мешать в одном заходе «домен» и «путь на диске». Это разные строки. Путь <code>/home/u1234/public_html</code> в сериализации тоже бывает, его меняют отдельно, когда переезд ещё и на другой аккаунт хостинга.</li>
</ul>
<p>Перенос «архивом плагина миграции» (Duplicator и родственники) сериализацию обычно переживает, потому что замену они делают своим движком, не вашим REPLACE. Имеет смысл, когда нет SSH. Не имеет смысла, когда вы уже на VPS и можете одну команду. Я не тащу лишний плагин на прод ради смены домена.</p>
<h5>Что search-replace не видит</h5>
<p>База — не весь сайт. После удачной замены в MySQL у меня регулярно всплывает старый хост в файлах.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">cd /var/www/example-site/public_html
grep -R --binary-files=without-match -l 'old.example' \
  wp-content/themes wp-content/plugins wp-content/mu-plugins \
  wp-content/uploads wp-config.php 2&gt;/dev/null | head</pre>
<p>Типичные находки:</p>
<ul>
<li><code>WP_HOME</code>/<code>WP_SITEURL</code> в <code>wp-config.php</code> — перекрывают базу, спор «я же заменил в опциях» решается здесь.</li>
<li>CSS/JSON конструктора в <code>wp-content/uploads/elementor/</code> (и аналоги у других билдеров). Это файлы. Их WP-CLI не трогает. У Elementor после смены домена я ещё регенерирую CSS из админки или, если есть их CLI, отдельной командой замены URL, не надеясь только на MySQL.</li>
<li>Жёстко прошитый адрес в дочерней теме: логотип, пиксель, кастомный endpoint. Это уже код, не контент.</li>
<li>Объектный кэш. Redis/Memcached продолжает отдавать старые сериализованные blobs. После замены — flush. Иначе «в базе уже новый домен, в браузере старый» до истечения TTL, и вы начнёте чинить то, что уже починено.</li>
</ul>
<p>Мультисайт — не тот же чеклист. Там <code>wp_blogs</code>, <code>wp_site</code>, домены блогов, и CLI нужно звать с <code>--url</code> конкретного сайта плюс <code>--network</code>, когда меняете сразу сеть. Если у вас не мультисайт, не копируйте чужие однострочники с <code>--network</code> «на всякий случай».</p>
<h5>Как понять, что сериализация жива</h5>
<p>Первая проверка — не «главная открылась». Я смотрю то, что сериализация обычно убивает, и то, что REPLACE обычно забывает.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash"># канонические URL — уже новый домен, без редиректа в голове
wp option get siteurl
wp option get home

# остатки старого хоста по всем таблицам с префиксом
wp db search 'old.example' --all-tables-with-prefix --table_column_once

# виджеты реально читаются, не «таблица на месте»
wp widget list sidebar-1
wp option get sidebars_widgets</pre>
<p>Если <code>sidebars_widgets</code> печатает массив с именами сайдбаров — сериализация этого option жива. Если пусто, <code>false</code> или PHP Notice на unserialize — откатились к дампу, не «восстанавливаем виджеты руками».</p>
<p>Грубый проход по опциям, которые WordPress считает сериализованными, но уже не может распаковать:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">wp eval '
foreach ( wp_load_alloptions() as $name =&gt; $value ) {
  if ( ! is_serialized( $value ) ) { continue; }
  $restored = @unserialize( $value );
  if ( $value === "b:0;" || $restored !== false || $value === serialize( false ) ) { continue; }
  echo $name, PHP_EOL;
}
'</pre>
<p>Пустой вывод — хороший знак. Имена вроде <code>widget_block</code>, <code>theme_mods_…</code>, <code>external_updates</code> в этом списке — плохой: именно они после «успешного» REPLACE приезжают битыми.</p>
<p>В админке руками:</p>
<ul>
<li>Внешний вид → Виджеты / редактор блоков виджетов: блоки на месте, не пустой холст.</li>
<li>Меню и расположения меню. «Меню существует, но нигде не назначено» часто значит, что theme_mods не прочитался.</li>
<li>Медиа: открыть пару старых записей, не главную. В контенте <code>https://old.example/wp-content/uploads/…</code> после нормальной замены быть не должно.</li>
<li>Конструктор: открыть на редактирование страницу, которую точно верстали не вчера. Белый экран при живой публичной странице — почти всегда JSON/CSS со старым хостом или битая meta.</li>
</ul>
<p>Смешанный контент (главная по https, картинки по http старого домена) я проверяю не глазами, а по исходнику и по логам. Браузер может дорисовать из кэша.</p>
<h5>Что смотреть overnight</h5>
<p>Днём вы видите то, что кликнули. Ночью всплывает cron, письма и боты.</p>
<p>За ночь мне нужны:</p>
<ul>
<li>Письма сайта: сброс пароля, WooCommerce, формы. В теле и в заголовке <code>From</code>/ссылках не должен торчать старый хост. Это часто не <code>siteurl</code>, а отдельная опция плагина или константа SMTP.</li>
<li>Cron: <code>wp cron event list</code> и системный crontab. Старый URL в HTTP-cron (wget на <code>wp-cron.php</code>) продолжает дергать прежний домен, пока DNS жив.</li>
<li>Карта сайта и robots: плагины SEO пишут абсолютные URL. Rank Math/Yoast после переезда я открываю и пересохраняю карту, не надеясь, что «само подхватит».</li>
<li>Редирект со старого домена. Пока он не 301 на тот же путь нового, Google и закладки клиентов будут кормить оба хоста. Сам search-replace редирект не настраивает — это nginx/хостер.</li>
<li>Логи PHP на unserialize и на mixed content в error_log. Одна-две строки offset — повод не «подождать», а свернуть на дамп и прогнать замену нормально.</li>
<li>REST и превью: <code>/wp-json/</code> должен отдавать новый <code>home</code> в <code>url</code>/<code>home</code>. Иначе мобильные приложения и часть плагинов продолжают стучать на старое имя.</li>
</ul>
<p>Если старый домен ещё резолвится в тот же DocumentRoot без редиректа, вы будете неделю ловить «то новый, то старый» в кэше и в сериализации, которую кто-то снова сохранит из админки, зайдя по старому имени. Схема одна: один канонический хост, второй только 301, константы и опции смотрят на канон.</p>
<p>Переезд домена в WordPress — это не «поменять две опции в wp_options». Две опции как раз переживают любой REPLACE. Ломается то, что вы не видите в phpMyAdmin с первого экрана: длины строк внутри serialize. Я меняю URL инструментом, который эти длины пересчитывает, потом отдельно ищу хвосты в файлах и кэше, и только после живых виджетов и пустого поиска по старому хосту считаю переезд состоявшимся — не в момент, когда открылась главная.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/vebmasteru/migraciya-domena-wordpress-bez-polomki-serialized-dannyh/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Разбор заражённого WordPress: порядок действий без магии одного плагина</title>
		<link>https://remadmin.com/blog/vebmasteru/razbor-zarazhjonnogo-wordpress-poryadok-dejstvij-bez-magii-odnogo-plagina/</link>
					<comments>https://remadmin.com/blog/vebmasteru/razbor-zarazhjonnogo-wordpress-poryadok-dejstvij-bez-magii-odnogo-plagina/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Fri, 11 Sep 2026 14:01:48 +0000</pubDate>
				<category><![CDATA[Вебмастеру]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[Wordpress]]></category>
		<category><![CDATA[безопасность]]></category>
		<category><![CDATA[бэкдор]]></category>
		<category><![CDATA[вредоносный код]]></category>
		<category><![CDATA[логи]]></category>
		<category><![CDATA[Уязвимость]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7421</guid>

					<description><![CDATA[Как я разбираю взломанный WordPress: снимок и стоп, сверка ядра, mu-plugins и drop-in, PHP в uploads, база и cron, поиск дыры по логам. Почему одного сканера мало и что проверить за ночь.]]></description>
										<content:encoded><![CDATA[<p>Типичная заявка звучит так: «сайт редиректит на казино, в админке куча неизвестных плагинов, Wordfence ничего не нашёл / Wordfence нашёл 400 файлов, я нажал Repair, через сутки опять». Иногда без редиректа: просто «гугл пометил», «хостер прислал abuse», «в Search Console странные URL», «письма клиентам уходят с левыми ссылками».</p>
<p>Я не начинаю с установки ещё одного антивируса в плагины. Сканер внутри заражённого WordPress — это программа, которой уже могли подменить файлы, отключить хуки и спрятать строки в базе. Он полезен как подсказка. Он не является разбором инцидента.</p>
<p>Ниже — порядок, которым я хожу по взломанному WP, когда нужно не «почистить то, что нашёл сканер», а понять, чем держится зараза, откуда зашли и что сделать, чтобы после смены пароля админа сайт не поднял шелл снова через час. Примеры путей и имён — типовые, не с конкретного клиента. Хосты и IP в логах подставляйте свои, чужие из интернета копировать вместе с «готовым лечением» не стоит.</p>
<h5>Сначала стоп, снимок, доступ мимо wp-admin</h5>
<p>Первое желание — зайти в админку и «поудалять левых пользователей». Если в теме или в <code>mu-plugins</code> сидит стилер куку, вы только подтвердите злоумышленнику, что хозяин на сайте, и отдадите свежую сессию.</p>
<p>Пока сайт публичный и пишет в те же файлы, вы затираете улики. Поэтому в первые минуты я делаю три вещи, и только потом открываю редактор.</p>
<p>Снять снимок, который можно откатить. На VPS — снапшот диска у хостера плюс архив каталога сайта и дамп базы на свою машину, не «бэкап плагином на этот же wp-content/uploads». На shared-хостинге — хотя бы <code>tar</code> корня сайта и <code>mysqldump</code> через панель или SSH. Если снимка нет, любое удаление файла потом не докажет, что именно он слал спам.</p>
<p>Посадить сайт на заглушку для посетителей, админку не оставлять открытой в интернет. Варианты, которые работают:</p>
<ul>
<li>В nginx/apache — отдать 503 на все PHP, кроме своего IP. Не «maintenance mode плагином»: его как раз и обходят.</li>
<li>Временно подменить <code>index.php</code> в корне на статическую заглушку, оригинальный файл сохранить как <code>index.php.orig</code> рядом. Корень WP трогать аккуратно: не путать с <code>index.php</code> темы.</li>
<li>На хостинге без SSH — закрыть каталог через панель / <code>.htaccess</code> по IP, если хостер это умеет. Если не умеет — хотя бы сменить пароли FTP и панели <em>до</em> того, как начнёте качать файлы на домашний ПК.</li>
</ul>
<p>Дальше захожу по SSH или SFTP. wp-admin использую только когда уже понял, что сессия оттуда не исполняет чужой PHP. Почту, Telegram-ботов сайта и SMTP-плагин на время инцидента тоже считаю скомпрометированными: с них часто уходит фишинг от имени магазина.</p>
<p>Пока не закрыли вход, параллельно снимаю «кто ещё живой»: соседние сайты на том же аккаунте, cron пользователя, панели ISPmanager/cPanel. Один шелл в соседнем каталоге <code>/var/www/site2</code> через час снова положит «уже почищенный» WP.</p>
<h5>Что считать уликой, а не ощущением</h5>
<p>«Выглядит странно» — плохой критерий. Мне нужны время, путь и факт изменения. Минимальный набор, который я забираю целиком, до того как начну удалять:</p>
<ul>
<li>Корневой <code>wp-config.php</code>, <code>.htaccess</code> в корне, в <code>wp-admin</code> и в <code>uploads</code> (их часто дописывают отдельно).</li>
<li>Drop-in файлы в <code>wp-content</code>: <code>object-cache.php</code>, <code>advanced-cache.php</code>, <code>db.php</code>, <code>sunrise.php</code>. Их сканеры ядра не считают «ядром», и туда любят класть одну строку <code>include</code>.</li>
<li>Каталог <code>wp-content/mu-plugins</code> — must-use подключается всегда, без галочки в админке. Если зараза там, «деактивировать плагины» ничего не делает.</li>
<li>Список пользователей ОС и crontab: <code>crontab -l</code>, <code>/etc/cron.d/</code>, <code>/var/spool/cron/</code>. PHP-шелл, который раз в пять минут дописывает себя в <code>index.php</code>, переживает любую «очистку файлов сайта».</li>
<li>Логи веб-сервера за неделю-две, не за последний час. Нужны access и error, плюс PHP-fpm slowlog, если есть. POST на странный <code>.php</code> в uploads — классика.</li>
</ul>
<p>По файлам смотрю не «все php в uploads», а свежие и с чужим владельцем:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># каталог сайта подставьте свой, не копируйте путь с чужого сервера
SITE=/var/www/example-site/public_html

find "$SITE" -type f -mtime -14 -printf '%TY-%Tm-%Td %TH:%TM %u:%g %p\n' | sort

find "$SITE/wp-content/uploads" -type f \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' -o -name '.*.php' \) -print

find "$SITE" -type f \( -name '.*.ico' -o -name 'wp-*.php' -o -name '*timthumb*' \) -path '*/uploads/*' -print

ls -la "$SITE/wp-content/" "$SITE/wp-content/mu-plugins" 2&gt;/dev/null
ls -la "$SITE/wp-content/"object-cache.php "$SITE/wp-content/"advanced-cache.php "$SITE/wp-content/"db.php 2&gt;/dev/null</pre>
<p>Свежий <code>wp-login.php</code> с датой «сегодня» при том, что ядро вы не обновляли — уже повод сверить контрольные суммы, а не «ну это же ядро, его не трогают». Свежий файл в <code>wp-includes</code> с именем вроде <code>wp-vcd.php</code> / <code>class-wp-http-proxy.php</code> рядом с настоящим классом — ещё чаще.</p>
<p>Владелец тоже врёт, если зараза работает от php-fpm того же пользователя, что и сайт. Тогда все файлы «как надо». Смотрите дату и содержимое, не только <code>ls -l</code>.</p>
<h5>Ядро сверяю с эталоном, плагины — с zip из репозитория</h5>
<p>WordPress умеет сам сказать, какие файлы ядра не совпали. С заражённой админки этой кнопке я не доверяю: подменённый <code>update-core.php</code> вам улыбнётся и покажет зелёное. С хоста, где есть wp-cli и исходящий HTTPS, или просто скачав zip той же версии:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># версию брать из wp-includes/version.php, не «ну наверное 6.4»
grep wp_version "$SITE/wp-includes/version.php"

# на чистой машине:
# curl -fsSLO "https://wordpress.org/wordpress-X.Y.Z.zip"
# unzip и rsync -avnc --delete ядра БЕЗ wp-content и без wp-config.php

wp core verify-checksums --path="$SITE"
wp plugin verify-checksums --all --path="$SITE"
wp theme verify-checksums --all --path="$SITE"</pre>
<p><code>verify-checksums</code> по плагину работает только для того, что лежит в wordpress.org. Платный Elementor, самопис, «нулёвая» тема с warez-сайта — для них эталона нет. Нулёвая тема — отдельный красный флаг: в ней бэкдор часто был <em>с момента установки</em>, и «вирус пришёл сам» тут ни при чём.</p>
<p>Что делать с расхождениями в ядре: не править файл руками «вырезать eval». Заменить пакет ядра целиком из официального zip. Исключения — <code>wp-config.php</code> и каталог <code>wp-content</code>. Если правили сами <code>wp-config.php</code> (соль, префиксы, константы) — файл не из zip, его сверяют глазами, не перезаписывают молча.</p>
<p>Плагин с расхождением: снимаю копию, ставлю тот же zip с wordpress.org. Если плагин платный — zip с кабинета разработчика, не «с форума, он тот же». Самопис, который «нам верстальщик оставил», читаю целиком. Там же часто лежит загрузчик файлов без проверки типа.</p>
<h5>Где прячут PHP, когда «в файлах чисто»</h5>
<p>Поиск по <code>eval(base64_decode</code> — полезный, но наивный. Нормальный PHP тоже бывает некрасивым. Я ищу сочетание: обфускация + место, куда CMS не должна писать исполняемое.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">grep -R --include='*.php' -nE 'eval\s*\(|assert\s*\(|gzinflate\s*\(|gzuncompress\s*\(|str_rot13\s*\(|create_function\s*\(|preg_replace\s*\(.+/e' "$SITE" | head

grep -R --include='*.php' -nE 'FilesMan|c99shell|WSO\s|wp_vcd|wp-tmp|predli|\$GLOBALS\[.as.\]' "$SITE" | head

# одна строка в конце легитимного файла — классика
find "$SITE" -name '*.php' -size -8k -mtime -14 -print

# php внутри картинки / «.ico»
find "$SITE/wp-content/uploads" -type f -name '*.php.*' -o -name '*.jpg.php' -o -name '*.png.php'</pre>
<p>Типовые гнёзда, которые я открываю всегда, даже если grep молчит:</p>
<ul>
<li><code>wp-content/uploads</code> и кэш плагинов: <code>*.php</code> там не должен жить. Исключение — редкие плагины, которые сами кладут PHP в uploads; это повод такой плагин выкинуть, а не «ну так задумано».</li>
<li><code>wp-content/upgrade</code>, <code>wp-content/upgrade-temp-backup</code>, <code>wp-content/blog.dir</code> на мультисайте — мусор после обновлений, его не смотрят.</li>
<li>Старая неактивная тема. «Мы на Astra, Twenty Twenty лежит на всякий» — в Twenty часто и сидит инжект в <code>functions.php</code>. Неактивная тема всё равно исполняется, если её кто-то дернул напрямую по URL.</li>
<li><code>wp-config.php</code> выше корня (правильная схема) <em>и</em> копия <code>wp-config.php</code> в корне/бэкапах <code>*.bak</code>, <code>wp-config.php.old</code>, <code>config.php.save</code>. Через них читают ключи БД без всякого шелла.</li>
</ul>
<p>Отдельно — <code>.htaccess</code>. Не только редирект на чужой домен. Ещё <code>auto_prepend_file</code> / <code>auto_append_file</code>, php_value, RewriteRule на левый файл в uploads, закрытие <code>.php</code> «для всех кроме». Я сравниваю текущий файл с тем, что должен быть у выбранных постоянных ссылок. Если permalinks — «название записи», в корневом htaccess должен быть стандартный кусок WP, а не три килограмма чужих правил.</p>
<p>База. Файловый сканер её не видит. Смотрю:</p>
<ul>
<li>Лишние администраторы в <code>wp_users</code> + <code>wp_usermeta</code> (<code>wp_capabilities</code> = administrator). Пользователь <code>wp_update</code> / <code>wpadmin1</code> / почта на одноразовом домене — не «сотрудник хостера».</li>
<li><code>siteurl</code> и <code>home</code> в <code>wp_options</code>. Если там чужой домен — редирект может быть не в файлах.</li>
<li><code>active_plugins</code>, <code>recently_activated</code>, опции с автозагрузкой <code>autoload=yes</code> и телом в десятки килобайт: туда кладут PHP, который потом <code>eval</code>ят из темы одной строкой.</li>
<li>Контент записей и виджетов: <code>iframe</code>, <code>eval</code>, короткие ссылки, спам-страницы в черновиках и ревизиях. Ревизии чистить отдельно: «удалил пост» не удаляет <code>wp_posts</code> с <code>post_type=revision</code>.</li>
<li>Cron внутри WP: <code>cron</code> в <code>wp_options</code> или <code>wp cron event list</code>. Хук, которого нет ни в одном вашем плагине, раз в 10 минут дергает URL — это не «оптимизация».</li>
</ul>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">wp user list --role=administrator --path="$SITE"
wp option get siteurl --path="$SITE"
wp option get home --path="$SITE"
wp plugin list --path="$SITE"
wp cron event list --path="$SITE"
wp db query "SELECT option_name, LENGTH(option_value) AS sz, autoload FROM wp_options ORDER BY sz DESC LIMIT 30;" --path="$SITE"</pre>
<p>Префикс таблиц берите из <code>wp-config.php</code>, не считайте что он всегда <code>wp_</code>.</p>
<h5>Дыру ищу до «лечения», иначе она зашьёт файл обратно</h5>
<p>Удалить шелл и не закрыть вход — значит пригласить того же бота через пару часов. Пока файлы ещё на месте, по логам видно, <em>какой URL принял POST до появления файла</em>.</p>
<p>Ищу в access-логе не «все 404», а успешные запросы к тому, чего в чистом WP нет, и POST на плагины с известными дырами. Имена ниже — примеры шаблона, не диагноз конкретного сайта:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># nginx: подставьте свой лог
grep -E 'POST /(wp-admin/admin-ajax\.php|xmlrpc\.php|wp-login\.php)' /var/log/nginx/example.access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head

grep -E ' 200 | 302 ' /var/log/nginx/example.access.log | grep -Ei 'uploads/.+\.php|wp-content/plugins/.+/upload|filemanager|wp-setup|radio\.php|about\.php'

# php в uploads, который уже отработал
grep 'wp-content/uploads/.*\.php' /var/log/nginx/example.access.log | grep -E ' 200 | 500 '</pre>
<p>Что чаще всего оказывается входом на обычных магазинах и лендингах, которые мне приносят:</p>
<ul>
<li>Слабый пароль администратора + открытый <code>xmlrpc.php</code> с <code>system.multicall</code>. Брут идёт не через форму логина, лимит на <code>wp-login.php</code> его не видит.</li>
<li>Устаревший плагин формы, слайдера, «добавить товар из Excel», файловый менеджер, который кто-то поставил «на пять минут».</li>
<li>Нулёвая тема / «пак плагинов». Бэкдор там с завода.</li>
<li>Утекла сессия админа: тот же пароль, что в панели хостинга, что в FTP, что в почте. После взлома пароль «тот же, я его недавно менял» меня не успокаивает.</li>
<li>Соседний сайт в том же пользователе UNIX. Дыра была не в этом WP.</li>
</ul>
<p>Версии плагинов фиксирую сразу, до обновления: <code>wp plugin list</code> + даты файлов. Обновить всё «на всякий случай» до разбора — значит затереть следы, какой именно компонент принял запрос. Обновлять надо, но <em>после</em> снимка и записи версий.</p>
<p>Если вход не находится за час — это нормально. Тогда я всё равно меняю секреты и убираю известные дыры, но в отчёте пишу честно: persistence сняли, вектор не доказан. Не пишу «всё закрыто», если в логах дырка.</p>
<h5>Чистить на месте или накатывать чистый слепок</h5>
<p>Правило, которым я пользуюсь. Если есть проверенный бэкап <em>до</em> даты взлома — проще поднять его на чистый каталог, накатить контент (uploads без PHP, дамп таблиц контента без левых пользователей) и заново поставить ядро и плагины из официальных zip. «Лечить» живой каталог имеет смысл, когда бэкапа нет или взлом свежий, и заказчик не может быть сутки в 503.</p>
<p>Чего не делаю:</p>
<ul>
<li>Не ставлю поверх заразы «свежий WordPress» в тот же каталог. Старый <code>mu-plugin</code> и <code>object-cache.php</code> переживут.</li>
<li>Не верю кнопке Repair у сканера как единственному действию. Она вычищает известные сигнатуры. Не вычищает новый include в <code>wp-blog-header.php</code> из трёх символов.</li>
<li>Не оставляю «карантин» плагина внутри <code>wp-content</code>, если этот карантин всё ещё исполняется по прямому URL.</li>
<li>Не правлю обфусцированный файл «вырезать кусок». Файл целиком из эталона или удалить.</li>
</ul>
<p>Порядок очистки на месте, если слепка до взлома нет:</p>
<ol>
<li>Снимок, логи, список админов, crontab, drop-in, mu-plugins — уже сняты.</li>
<li>Ядро заменить официальным zip той же (или уже новой, если дыра в ядре) версии.</li>
<li>Каждый плагин и тему — либо zip с официального источника, либо удалить. Неактивное — тоже удалить с диска, не «выключить».</li>
<li>Из uploads удалить любой PHP/phtml/phar и файлы с двойным расширением. Картинки не «лечить антивирусом», а не исполнять: в nginx для uploads — <code>location ~ \.php$</code> deny.</li>
<li>База: лишние админы, option с длинным autoload, спам-посты и ревизии, подмена siteurl, чужие cron-хуки.</li>
<li>Секреты: ключи в <code>wp-config.php</code> (<code>AUTH_KEY</code> и остальные — сгенерировать заново), пароли всех админов, пароль БД, FTP, панели хостинга, SMTP, токены платёжек и Telegram, application passwords, ключи REST. Сессии: сменить соли, чтобы старые куки умерли.</li>
<li>Только после этого обновлять плагины до текущих версий и включать сайт для своих IP на проверку.</li>
</ol>
<p>Запрет исполнения PHP в uploads — это не «оптимизация», это то, без чего чистка часто бессмысленна:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="nginx">location ~* ^/wp-content/uploads/.*\.php$ {
    deny all;
}</pre>
<p>На apache — в <code>uploads/.htaccess</code> что-то в духе <code>php_flag engine off</code> плюс <code>&lt;FilesMatch&gt;</code> на php. Точный синтаксис зависит от того, mod_php у вас или php-fpm через cgi. Если не уверены — лучше сломать загрузку шелла, чем угадать директиву и оставить выполнение. Проверка простая: положить свой <code>test.php</code> с <code>echo 1;</code> в uploads, открыть в браузере, получить 403/скачивание, файл удалить.</p>
<h5>Проверка, что не всплыло через ночь</h5>
<p>Сайт открываю с своего IP. Смотрю исходник главной и пары внутренних: нет ли внешнего скрипта с левого домена, нет ли <code>display:none</code> простыни ссылок, не подменён ли каноникал. В базе ещё раз <code>siteurl</code>/<code>home</code>. В файлах — даты за последние часы: если после «чистки» снова появились php в uploads, persistence жива (cron, соседний сайт, украденный FTP, незакрытая дыра).</p>
<p>Технически полезно:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># повтор через несколько часов, не сразу
find "$SITE" -type f -mmin -180 -printf '%TY-%Tm-%Td %TH:%TM %p\n' | sort
crontab -l
wp user list --role=administrator --path="$SITE"
wp plugin list --path="$SITE"</pre>
<p>Снаружи: Search Console на странные URL, письмо хостера про исходящий спам, очередь почты на сервере (<code>mailq</code>, логи exim/postfix). Исходящий спам после «уже чистого сайта» почти всегда значит, что PHP-шелл или SMTP-креды живы.</p>
<p>Сканер после этого можно поставить. Один. Как датчик на будущее, не как доказательство чистоты. Я смотрю на него так же, как на fail2ban: хорошо, что пишет, плохо, если им заменяют работу руками.</p>
<p>И ещё: пароли, которые «и так сложные», после инцидента всё равно меняю. В том числе в панели домена и у регистратора. Бывали случаи, когда после чистки сайта через сутки меняли NS — это уже не WordPress, это украденный аккаунт регистратора, и файловый grep этого не увидит.</p>
<h5>Что я не обещаю одним плагином</h5>
<p>Плагин-сканер не видит crontab ОС, соседний vhost, подмену NS и письмо, которое уже ушло ночью. Он плохо видит инжект в базе и drop-in на одну строку. Он может быть сам заражён, если его поставили в уже дырявый WP.</p>
<p>Разбор — это снимок, сверка ядра, поиск persistence, закрытие входа, смена секретов и проверка через несколько часов. Скучно, зато сайт не «зеленеет» в сканере и не краснеет у клиентов на следующий день.</p>
<p>Если нужно разобрать конкретный случай — файлы, логи и доступ по SSH, не «скрин админки и надежда». Это обычная работа по сопровождению, без магии и без прайса на каждый найденный <code>eval</code>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/vebmasteru/razbor-zarazhjonnogo-wordpress-poryadok-dejstvij-bez-magii-odnogo-plagina/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>WireGuard офис ↔ VPS: маршруты, DNS, split-tunnel и проверка что трафик в туннеле</title>
		<link>https://remadmin.com/blog/linux/wireguard-ofis-vps-marshruty-dns-split-tunnel-i-proverka-chto-trafik-v-tunnele/</link>
					<comments>https://remadmin.com/blog/linux/wireguard-ofis-vps-marshruty-dns-split-tunnel-i-proverka-chto-trafik-v-tunnele/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Tue, 08 Sep 2026 20:37:34 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[DNS]]></category>
		<category><![CDATA[VPN]]></category>
		<category><![CDATA[VPS]]></category>
		<category><![CDATA[WireGuard]]></category>
		<category><![CDATA[безопасность]]></category>
		<category><![CDATA[сеть]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7418</guid>

					<description><![CDATA[Как я собираю WireGuard между офисом и VPS: AllowedIPs как маршрут, split-tunnel против 0.0.0.0/0, DNS который не утекает мимо, и проверка tcpdump что рабочий трафик реально в wg0.]]></description>
										<content:encoded><![CDATA[<p>Типичная заявка: «подняли WireGuard на VPS, рукопожатие есть, сайты открываются, а в офис до внутренней админки как не ходили, так и не ходят». Или наоборот: «весь интернет офиса теперь через виртуалку, YouTube тормозит, трафик на тарифе хостера улетел в космос, а до NAS в соседней комнате пакеты почему-то идут через Франкфурт».</p>
<p>Смотрю конфиг — почти всегда одно и то же. На пире стоит <code>AllowedIPs = 0.0.0.0/0</code> «чтобы точно работало», DNS прописан 8.8.8.8, на VPS <code>ip_forward</code> выключен, а проверку сделали командой <code>ping 10.66.66.1</code>. Пинг до адреса туннеля проходит. Это доказывает только то, что два пира видят друг друга. Это не доказывает, что рабочий трафик идёт в туннель, что DNS не утекает мимо, и что обратный путь из офисной сети существует.</p>
<p>Ниже — как я собираю офис <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> VPS так, чтобы маршруты, DNS и split-tunnel не врали, и чем проверяю, что пакет реально ушёл в <code>wg0</code>, а не красиво показал handshake в <code>wg show</code>. Без «поставьте GUI» и без схемы «все в 0.0.0.0/0 и будет счастье».</p>
<h5>Два разных туннеля, которые путают в одном конфиге</h5>
<p>Мне на стол приносят три задачи, и их пытаются решить одним <code>[Peer]</code>:</p>
<ul>
<li>С ноутбука в офисе ходить на сервисы на VPS: SSH, панель, приватный git, админка, которая слушает только внутренний адрес.</li>
<li>С VPS ходить в офисную сеть: NAS, принтер, контроллер, камера, которой «интернет не нужен».</li>
<li>Выгнать весь офисный интернет через VPS, «чтобы как на работе».</li>
</ul>
<p>Это не один туннель с разными галочками. Это разный <code>AllowedIPs</code>, разный форвардинг и разный DNS. Первые две задачи я почти всегда делаю split-tunnel: в туннель уезжает только нужная подсеть. Третья — полный туннель, и за неё отдельно платят CPU, трафиком и сломанным Zoom, если MTU не поправили.</p>
<p>Адреса в примерах ниже документационные, не боевые. Публичный VPS — <code>203.0.113.10</code>, белый офиса — <code>198.51.100.20</code>, туннель — <code>10.66.66.0/24</code>, LAN офиса — <code>192.168.40.0/24</code>. Свои цифры подставьте, чужие из интернета не копируйте вместе с ключами.</p>
<h5>AllowedIPs — это маршрут, а не «список друзей»</h5>
<p>В WireGuard <code>AllowedIPs</code> делает две вещи сразу. Снаружи это криптоключ: пакет с таким источником примут только от этого пира. Изнутри <code>wg-quick</code> по этому списку ставит маршруты. Написали <code>0.0.0.0/0</code> — получите дефолт через туннель (у <code>wg-quick</code> ещё и отдельная таблица, обычно <code>51820</code>). Написали <code>10.66.66.0/24</code> — в туннель пойдёт только эта сеть.</p>
<p>Поэтому «handshake есть, а сайт на VPS открывается по белому IP» — нормально для split-tunnel и баг, если вы хотели полный туннель. Браузер пошёл на <code>203.0.113.10</code> напрямую, UDP 51820 при этом живой. Туннель тут ни при чём.</p>
<p>Смотрю, что система думает про маршрут, не что написано в комментарии к конфигу:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">wg show
ip -4 addr show dev wg0
ip -4 route show
ip -4 route show table all | grep -E 'wg0|51820'
ip -4 route get 10.66.66.1
ip -4 route get 203.0.113.10
ip -4 route get 8.8.8.8</pre>
<p><code>ip route get</code> врёт меньше, чем ощущения. Если до внутреннего адреса VPS маршрут не через <code>wg0</code>, дальше можно не гадать — трафик в туннель не собирался.</p>
<h5>Split-tunnel: офис ходит на VPS, интернет офиса не трогаем</h5>
<p>Это тот вариант, который я ставлю по умолчанию. Офисный пир (ноутбук или маленький шлюз) держит туннель, в <code>AllowedIPs</code> только сеть туннеля и, если надо, приватные сети на стороне VPS. Исходящий веб офиса идёт как раньше, через местного провайдера.</p>
<p>VPS:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="ini">[Interface]
Address = 10.66.66.1/24
ListenPort = 51820
PrivateKey = &lt;ключ VPS&gt;

[Peer]
PublicKey = &lt;ключ офиса&gt;
AllowedIPs = 10.66.66.2/32</pre>
<p>Офис (за NAT, поэтому keepalive):</p>
<pre class="EnlighterJSRAW" data-enlighter-language="ini">[Interface]
Address = 10.66.66.2/24
PrivateKey = &lt;ключ офиса&gt;

[Peer]
PublicKey = &lt;ключ VPS&gt;
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.66.66.0/24
PersistentKeepalive = 25</pre>
<p>Если на VPS сервисы торчат в docker-сети или во втором внутреннем интерфейсе, в <code>AllowedIPs</code> офиса добавляю <em>эту</em> сеть, не «весь мир». И на VPS тогда уже нужен форвардинг и фильтр, иначе пакет дойдёт до ядра и умрёт в <code>FORWARD</code>.</p>
<p>На VPS для split-tunnel NAT наружу обычно не нужен. Нужен, когда офис должен ходить не на сам VPS, а <em>через</em> него дальше. Пока задача «SSH на 10.66.66.1» — без маскарада.</p>
<p><code>PersistentKeepalive = 25</code> ставлю на том конце, который за NAT или с динамическим белым. Без него UDP-маппинг у провайдера офиса схлопывается, handshake в <code>wg show</code> становится старше пары минут, и «туннель встал сам» ровно до первой попытки зайти с другой стороны.</p>
<h5>Когда VPS должен видеть LAN офиса</h5>
<p>Вторая заявка: бэкап с VPS на NAS, мониторинг контроллера, SSH на станок, который «в интернет не смотрит». Тогда на стороне VPS в пире офиса мало <code>/32</code> туннельного адреса. Нужна офисная подсеть:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="ini"># на VPS, peer = офисный шлюз
[Peer]
PublicKey = &lt;ключ офисного шлюза&gt;
AllowedIPs = 10.66.66.2/32, 192.168.40.0/24</pre>
<p>Офисный шлюз должен форвардить. Это уже не «WireGuard на ноутбуке бухгалтера». Ноутбук не маршрутизатор: пакет с VPS на <code>192.168.40.15</code> дойдёт до ноутбука и останется там, NAS его не увидит, а ответ NAS пойдёт в локальный шлюз офиса и умрёт, потому что шлюз про <code>10.66.66.0/24</code> ничего не знает.</p>
<p>На шлюзе офиса:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">sysctl -w net.ipv4.ip_forward=1
# постоянное место — /etc/sysctl.d/, не «в сессии до ребута»

iptables -C FORWARD -i wg0 -o eth0 -s 10.66.66.0/24 -d 192.168.40.0/24 -j ACCEPT 2&gt;/dev/null || \
iptables -A FORWARD -i wg0 -o eth0 -s 10.66.66.0/24 -d 192.168.40.0/24 -j ACCEPT
iptables -C FORWARD -i eth0 -o wg0 -s 192.168.40.0/24 -d 10.66.66.0/24 -j ACCEPT 2&gt;/dev/null || \
iptables -A FORWARD -i eth0 -o wg0 -s 192.168.40.0/24 -d 10.66.66.0/24 -j ACCEPT</pre>
<p>Имена интерфейсов свои. <code>eth0</code> в статье — заглушка. На машине смотрю <code>ip -br link</code>, не копирую с чужого скриншота.</p>
<p>Обратный маршрут в LAN часто забывают. NAS отвечает на запрос с <code>10.66.66.1</code> через дефолтный шлюз офиса. Шлюз этот пакет не ждал — в лучшем случае дроп, в худшем NAT в интернет. Варианты, которые работают:</p>
<ul>
<li>Статический маршрут на роутере офиса: <code>10.66.66.0/24 via 192.168.40.2</code>, где <code>.2</code> — шлюз с WireGuard.</li>
<li>Или маскарад на офисном WG-хосте в сторону LAN, если роутер трогать нельзя. Работает, в логах NAS все запросы с одного адреса шлюза. Для бэкапа терпимо, для разбора «кто стучится» — нет.</li>
</ul>
<p>Пересечение подсетей ловлю сразу. Офис на <code>192.168.1.0/24</code>, docker на VPS тоже <code>192.168.1.0/24</code> — туннель поднимется, маршруты будут врать по очереди. Одну из сетей надо сменить. Спорить, «чья 192.168.1 важнее», бесполезно: важнее та, которую проще перенумеровать.</p>
<h5>Полный туннель: 0.0.0.0/0, таблица 51820 и NAT на VPS</h5>
<p><code>AllowedIPs = 0.0.0.0/0</code> на офисном пире означает: весь IPv4, кроме того что <code>wg-quick</code> сам вычтет для Endpoint, должен идти в туннель. <code>wg-quick</code> для этого поднимает policy routing, не просто <code>ip route add default</code>. Поэтому «я убрал default из main, а туннель живой» — ещё не победа. Смотреть надо <code>ip rule</code> и таблицу, которую создал WireGuard:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ip rule
ip route show table 51820
ip route get 8.8.8.8
ip route get 203.0.113.10</pre>
<p>До Endpoint маршрут обязан остаться в физический интерфейс. Иначе туннель пытается нести сам себя и тихо умирает после первого же пересчёта маршрутов.</p>
<p>На VPS для чужого интернета нужны три вещи, не одна:</p>
<ul>
<li><code>net.ipv4.ip_forward=1</code></li>
<li>NAT с туннеля в внешний интерфейс, иначе провайдер VPS не пропустит чужие RFC1918 как source</li>
<li>FORWARD в файрволе в обе стороны, не только INPUT на 51820/udp</li>
</ul>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># на VPS, внешний интерфейс уточнить: ip -br addr
sysctl -w net.ipv4.ip_forward=1

iptables -t nat -C POSTROUTING -s 10.66.66.0/24 -o ens3 -j MASQUERADE 2&gt;/dev/null || \
iptables -t nat -A POSTROUTING -s 10.66.66.0/24 -o ens3 -j MASQUERADE

iptables -C FORWARD -i wg0 -o ens3 -j ACCEPT 2&gt;/dev/null || iptables -A FORWARD -i wg0 -o ens3 -j ACCEPT
iptables -C FORWARD -i ens3 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2&gt;/dev/null || \
iptables -A FORWARD -i ens3 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT</pre>
<p>Правила в статье — эскиз под iptables. На машине с nftables я пишу в ту таблицу, которая уже живая, а не добавляю второй файрвол «рядом». <code>ufw route allow</code> и <code>iptables -A FORWARD</code> одновременно — классический способ получить «у меня же разрешено», когда пакет режет другая цепочка.</p>
<p>Полный туннель я не ставлю «на всякий случай». Он нужен, когда офисный канал дырявый, нужен выход с фиксированного белого VPS, или политика «весь трафик только через нас». Для доступа к одной админке на VPS это дорогой способ сломать DNS, MTU и чужой Zoom.</p>
<h5>DNS: туннель живой, а имена резолвятся мимо</h5>
<p>Самая частая ложь после <code>wg show</code>. Туннель есть, <code>curl https://10.66.66.1</code> работает, а <code>curl https://panel.internal</code> идёт неизвестно куда или не идёт вовсе. Причина почти всегда в резолвере, не в маршрутах.</p>
<p><code>DNS =</code> в <code>wg-quick</code> — не магия. На systemd-resolved это попытка прикрутить домены к интерфейсу. На машине без resolved это может записаться в <code>/etc/resolv.conf</code> и затереть офисный DNS. На Windows-клиенте это отдельная история с «использовать DNS этого адаптера». Я не верю строке в конфиге, пока не увижу, куда реально уходит запрос.</p>
<p>Проверка с офисного пира:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">resolvectl status
resolvectl query panel.internal
getent hosts panel.internal

# куда уходит UDP/53, пока стучите в имя
tcpdump -ni wg0 port 53
tcpdump -ni eth0 port 53</pre>
<p>Если дамп на физическом интерфейсе показывает запросы на <code>8.8.8.8</code>, а на <code>wg0</code> тихо — split-tunnel сделал ровно то, что вы написали: маршрут до публичного DNS прямой. Внутреннее имя при этом либо не резолвится, либо резолвится в публичный адрес, и вы снова обходите туннель.</p>
<p>Рабочая схема для split-tunnel, которой я пользуюсь:</p>
<ul>
<li>На VPS крутится DNS, который знает внутренние имена. Слушает <code>10.66.66.1</code>, не <code>0.0.0.0</code> в интернет.</li>
<li>У офисного пира <code>DNS = 10.66.66.1</code>, и этот адрес уже входит в <code>AllowedIPs</code>.</li>
<li>Если нужен только суффикс <code>internal</code>, в resolved указываю домен на интерфейс туннеля, а не подменяю весь резолвер офиса.</li>
</ul>
<p>Для полного туннеля публичный DNS тоже должен быть достижим <em>через</em> туннель. Иначе получаете курицу и яйцо: чтобы поднять туннель, нужен DNS Endpoint, а DNS вы уже отправили в туннель, которого нет. Поэтому Endpoint я пишу IP, не именем. Имя в Endpoint оставляю только там, где белый VPS прыгает и без DNS туннель не собрать — тогда DNS офиса должен резолвить этот хост <em>до</em> подъёма <code>wg0</code>.</p>
<h5>IPv6 утекает отдельно, MTU ломает «сайты открываются через раз»</h5>
<p>Настроили IPv4, про IPv6 забыли. Браузер берёт AAAA, пакеты идут мимо туннеля в родной канал. <code>curl -4</code> при этом красивый. Если полный туннель — в <code>AllowedIPs</code> нужен и <code>::/0</code>, и на VPS форвардинг/NAT6, либо IPv6 на офисном пире надо выключить осознанно, не «само как-нибудь».</p>
<p>Для split-tunnel я проверяю оба стека:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ip -6 route get 2001:db8::1
curl -4 -s https://ifconfig.me
curl -6 -s https://ifconfig.me</pre>
<p>Документационный <code>2001:db8::1</code> в <code>route get</code> нужен, чтобы увидеть, в какой интерфейс ядро отправит «чужой» v6. Если полного туннеля нет, прямой маршрут — ожидаемо. Если полный туннель заявлен, а v6 уходит в <code>eth0</code> — дыра.</p>
<p>MTU. UDP + WireGuard + внешний Ethernet обычно просит 1420, на части каналов — ниже. Симптом не «туннель мёртв», а «SSH живой, HTTPS висит, крупные POST отваливаются, мессенджеры отваливаются на картинках». Ловлю так:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">ip link show wg0
ping -M do -s 1372 10.66.66.1
ping -M do -s 1200 10.66.66.1</pre>
<p>Если 1372 с DF не проходит, опускаю MTU на <code>wg0</code>, пока не начнёт. На VPS с jumbo или с PPPoE за NAT цифры другие — подбираю, не копирую 1280 «потому что все так пишут», если канал нормально держит больше.</p>
<h5>Проверка, что рабочий трафик в туннеле, а не только handshake</h5>
<p>Чеклист, которым я пользуюсь после <code>wg-quick up</code>. По порядку, не выборочно.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># 1. пир живой
wg show
# handshake секунды-минуты, не часы; transfer растёт, когда вы сами генерируете трафик

# 2. адрес и маршрут
ip route get 10.66.66.1
# ожидаю dev wg0

# 3. ICMP внутри туннеля — необходимое, но не достаточное
ping -c 3 10.66.66.1

# 4. TCP на сервис, который слушает только внутренний адрес
curl -v --connect-timeout 5 http://10.66.66.1:8080/
ss -tlnp | grep 8080   # это уже на VPS: сервис не на 0.0.0.0:80 случайно?</pre>
<p>Дальше — дамп. Это единственный способ перестать спорить. На офисном пире в одном окне генерирую трафик, в двух других:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">tcpdump -ni wg0 host 10.66.66.1
tcpdump -ni eth0 udp port 51820
tcpdump -ni eth0 host 203.0.113.10 and not udp port 51820</pre>
<p>Нормальная картина split-tunnel, когда я хожу на внутренний сервис: на <code>wg0</code> видны пакеты до <code>10.66.66.1</code>, на физическом интерфейсе до VPS — только UDP 51820. Если на физическом интерфейсе всплыл обычный TCP на белый <code>203.0.113.10:443</code>, вы открыли публичный nginx, не туннель.</p>
<p>Для полного туннеля добавляю:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># с офисного пира
curl -4 -s https://ifconfig.me
# должен вернуться белый VPS, не белый офиса

ip route get 1.1.1.1
# dev wg0 или таблица 51820, не eth0

tcpdump -ni eth0 tcp port 443
# во время curl на внешний сайт здесь не должно быть голого TCP
# только UDP 51820</pre>
<p>Если <code>ifconfig.me</code> показал белый офиса — трафик мимо. Если показал белый VPS, но на <code>eth0</code> всё равно виден TCP 443 — смотрю второй интерфейс, IPv6, браузерный DoH, «умный» антивирус со своим туннелем. Бывает, что <code>curl</code> в туннеле, а Firefox живёт своей жизнью.</p>
<p>На самой VPS в это время:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">tcpdump -ni wg0
tcpdump -ni ens3 udp port 51820
# conntrack, если ping есть, а TCP нет
conntrack -L | grep 10.66.66</pre>
<p>Ещё одна ловушка: сервис на VPS слушает <code>127.0.0.1</code> или только публичный адрес. Туннель доставляет пакет на <code>10.66.66.1</code>, nginx его не принимает. Это не «WireGuard сломался». Это <code>ss -tlnp</code> до настройки туннеля, не после.</p>
<h5>Что я проверяю, когда «подключилось, но не работает»</h5>
<p>Короткий разбор по симптомам, без перебора всего man.</p>
<ul>
<li>Handshake старше нескольких минут, transfer ноль — NAT, файрвол хостера на UDP 51820, неверный Endpoint, часы сильно разъехались реже, но бывает. С офиса: <code>nc -u -v 203.0.113.10 51820</code> ничего не доказывает (UDP без ответа), нужен дамп на VPS в момент <code>wg-quick up</code>.</li>
<li>Handshake живой, ping до <code>10.66.66.x</code> нет — AllowedIPs не покрывает адрес, или адреса туннеля пересеклись у двух пиров. Два клиента с <code>10.66.66.2/24</code> одновременно — классика копипаста.</li>
<li>Ping есть, TCP нет — MTU, или сервис не слушает адрес туннеля, или FORWARD/фильтр режет не ICMP, а tcp.</li>
<li>До VPS по внутреннему IP есть, до NAS в офисе нет — на VPS в AllowedIPs нет LAN, на офисе нет forward, на роутере офиса нет обратного маршрута.</li>
<li>Браузер открывает сайт по домену «не туда» — DNS мимо туннеля или A-запись смотрит на белый, а вы думали, что ходите внутрь.</li>
<li>После сна ноутбука туннель «есть», трафика нет — keepalive, stale UDP mapping, помогает <code>wg-quick down; wg-quick up</code>, а постоянно — keepalive и иногда <code>NetworkManager</code> dispatcher, не вечная сессия с 2023 года.</li>
</ul>
<p>Ключи и чужие IP в тикет не кладу. В логах для разбора достаточно последних байт публичного ключа из <code>wg show</code> и документационных адресов. Боевой Endpoint заказчика в статью и в переписку с подрядчиком не тащу.</p>
<h5>Что оставляю в конфиге, когда уже заработало</h5>
<p>Минимальный набор, который я не выкидываю «потом допишем»:</p>
<ul>
<li>Endpoint только IP, порт явно 51820 или тот, что реально открыт у хостера. У части облаков UDP на дефолтном порту режут, тогда слушаю другой и это же пишу в Endpoint.</li>
<li><code>AllowedIPs</code> по задаче, не максимальный. Split-tunnel — сети, которые правда надо унести. Полный туннель — осознанный <code>0.0.0.0/0</code> плюс IPv6-решение.</li>
<li><code>PersistentKeepalive</code> на NAT-стороне.</li>
<li>DNS только тот, который достижим через получившиеся маршруты.</li>
<li>Проверка дампом один раз после подъёма, не «пинг прошёл — закрываем задачу».</li>
</ul>
<p>Конфиг без этой проверки — это VPN на скриншоте. Скриншот <code>wg show</code> с handshake я в отчёт тоже прикладываю, но рядом кладу <code>ip route get</code> до рабочего адреса и кусок tcpdump, где видно UDP 51820 снаружи и целевой трафик внутри. Если второго нет, туннеля для работы тоже нет — есть только красивый интерфейс <code>wg0</code>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/linux/wireguard-ofis-vps-marshruty-dns-split-tunnel-i-proverka-chto-trafik-v-tunnele/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Скандинавский свет на Opel Astra H: UEC Variant 5 + REC Variant 1</title>
		<link>https://remadmin.com/blog/auto/skandinavskij-svet-na-opel-astra-h-uec-variant-5-rec-variant-1/</link>
					<comments>https://remadmin.com/blog/auto/skandinavskij-svet-na-opel-astra-h-uec-variant-5-rec-variant-1/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Sun, 06 Sep 2026 13:25:30 +0000</pubDate>
				<category><![CDATA[Авто]]></category>
		<category><![CDATA[Daytime Running Light]]></category>
		<category><![CDATA[OP-COM]]></category>
		<category><![CDATA[Opel Astra H]]></category>
		<category><![CDATA[REC]]></category>
		<category><![CDATA[UEC]]></category>
		<category><![CDATA[скандинавский свет]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7413</guid>

					<description><![CDATA[Как включить скандинавский свет на Astra H 2008 через OP-COM: почему одной таблицы из интернета мало, зачем отдельно программировать UEC и REC, и какая связка вариантов реально зажигает ближний и задние габариты после запуска двигателя.]]></description>
										<content:encoded><![CDATA[<p>На Opel Astra H скандинавский свет включается не реле, как на старых Zafira A, а программированием блоков через OP-COM / Tech2. Казалось бы — открыл таблицу вариантов Daytime Running Light, выбрал нужную строку, и готово. На практике у нас (Astra H, 2008, универсал, бензин) всё оказалось чуть хитрее: таблица есть почти в каждом гайде, а задние габариты сами по себе всё равно не загорались. Разбираю, почему так и какой связкой UEC + REC в итоге получилось то, что нужно.</p>
<p>Нужный результат простой: крутилка света в «0» (или Auto), при включении зажигания ничего лишнего не горит, после запуска двигателя автоматически ближний спереди и габариты сзади. Штатный ручной свет при этом остаётся как был.</p>
<h5>Та самая таблица, которую все копируют</h5>
<p>В интернете по Astra H / Zafira B гуляет одна и та же схема вариантов DRL — «Стандарт» и Version 1–5 в разрезе «зажигание» / «мотор заведён» и положения переключателя. Её перепечатывают на Drive2, в FAQ по OP-COM, на чешских и немецких форумах. Мы тоже от неё оттолкнулись.</p>
<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="960" height="566" class="wp-image-7411" src="https://remadmin.com/wp-content/uploads/2026/09/astra-h-drl-variants-table.jpg" alt="Таблица вариантов скандинавского света Opel Astra H — Стандарт и Version 1–5" srcset="https://remadmin.com/wp-content/uploads/2026/09/astra-h-drl-variants-table.jpg 960w, https://remadmin.com/wp-content/uploads/2026/09/astra-h-drl-variants-table-300x177.jpg 300w, https://remadmin.com/wp-content/uploads/2026/09/astra-h-drl-variants-table-768x453.jpg 768w" sizes="(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px" /><figcaption>Классическая таблица вариантов DRL для Astra H / Zafira B — её копируют почти во всех гайдах.</figcaption></figure>
<p>По таблице кажется, что достаточно один раз выбрать Version 1 или Version 5 — и поведение «всё включится как надо». На деле таблица описывает <em>логику варианта</em>, а не то, что перед и зад программируются в разных блоках. Из‑за этого половина инструкций заканчивается на UEC, а про REC либо вскользь, либо никак.</p>
<h5>Что получилось у нас на Version 1 и 5</h5>
<p>Пробовали классику — ставили одинаковый вариант и смотрели глазами.</p>
<ul>
<li><strong>Version 1:</strong> при включении зажигания уже горели передние габариты; после запуска двигателя они оставались, плюс включался ближний. Для «дневного» режима это слишком рано: ещё не завёл — уже светится.</li>
<li><strong>Version 5:</strong> при зажигании ничего, после запуска двигателя зажигается ближний..</li>
</ul>
<p>И главное: <strong>задние габариты автоматически не включались ни в одном из этих режимов</strong>, пока трогали только то, что обычно трогают первым — передний блок. Спереди картина менялась, сзади — тишина. Вот тут и всплывает архитектура Astra H.</p>
<h5>Почему перед и зад настраиваются отдельно</h5>
<p>На Astra H (и близкой по электрике Zafira B) свет и кузовная периферия разнесены по двум «центрам»:</p>
<ul>
<li><strong>UEC</strong> (Underhood Electrical Center, иногда FZM / Front Zone Module) — подкапотный блок. Передние фары, часть передней кузовной логики, датчик дождя/света и параметр <em>Daytime Running Light</em> именно для передка.</li>
<li><strong>REC</strong> (Rear Electrical Center, RZM / Rear Zone Module) — задний блок (обычно слева сзади). Задние фонари, центральный замок, охранка и <em>свой</em> такой же пункт Daytime Running Light.</li>
</ul>
<p>Это не «одна настройка на всю машину». У каждого модуля свой Variant Configuration и свой CarPass-ввод. Можно поставить на UEC Version 5, а на REC оставить Disabled / Standard — и получите ровно нашу картину: спереди DRL ожил, сзади габариты ждут ручной крутилки. Таблица из интернета этого почти не объясняет: там одна строка «версия», как будто она глобальная.</p>
<p>Отсюда и редкий, но рабочий трюк: <strong>разные варианты на перед и зад</strong>. Мы не изобретали велосипед с нуля — в FAQ по OP-COM давно пишут, что DRL надо прописывать и в UEC, и в REC. А вот комбинацию «5 спереди + 1 сзади» под конкретное поведение мало где разжёвывают.</p>
<h5>Что понадобится</h5>
<ul>
<li>Диагностика с поддержкой Opel: OP-COM (качественный клон/оригинал) или Tech2 + CANdi.</li>
<li><strong>CarPass</strong> — четыре цифры security code с карточки, которую выдавали с машиной. Без него Program Variant Configuration не пустит. Потеряли — только через дилера / сервис с доступом к базе (это не «взлом», а штатная защита кодировки).</li>
<li>Зажигание включено (шина живая), аккумулятор не на нуле.</li>
<li>Желательно сохранить текущий конфиг кнопкой Save в OP-COM, если она есть — чтобы откатить, если вариант не зайдёт.</li>
</ul>
<h5>Как мы запрограммировали перед (UEC) — Variant 5</h5>
<p>Путь в OP-COM:</p>
<p><code>Diagnostics → год выпуска → Astra-H → Body → UEC → Programming → Program Variant Configuration → CarPass (4 цифры) → Daytime Running Light → Variant 5 → Program</code></p>
<p>Variant 5 на передке как раз ближе к «скандинавскому» дневном режиму: не тянет габариты и приборку так, как ранние версии при одном только зажигании, и даёт ближний уже в логике DRL. Сам по себе он нам ещё не закрыл задачу — но как половинка связки сработал отлично.</p>
<p>Нюансы из заводских/форумных заметок по Version 5 (имейте в виду): пока DRL активен, подсветка дисплея/кнопок может вести себя иначе, чем в ручном ближнем; дальний по таблице включается только из положения переключателя «ближний»; при крутилке на габаритах DRL обычно глушится. Если стоит ксенон — на Version 2–4 в гайдах часто советуют не заигрывать (свет может гореть ещё до запуска мотора).</p>
<h5>Как настроили зад (REC) — Variant 1</h5>
<p>Тот же ритуал, другой блок:</p>
<p><code>Diagnostics → год выпуска → Astra-H → Body → REC → Programming → Program Variant Configuration → CarPass → Daytime Running Light → Variant 1 → Program</code></p>
<p>Именно <strong>Variant 1 на REC</strong> у нас наконец поднял задние габариты в автоматике. Пока REC оставался без своего DRL-варианта (или на «стандарте»), сзади тишина — сколько ни крути UEC.</p>
<p>После Program иногда помогает коротко выключить зажигание и снова завести, чтобы модули перечитали кодировку.</p>
<h5>Итог на нашей машине</h5>
<p>Связка <strong>UEC = Variant 5</strong> + <strong>REC = Variant 1</strong> на Astra H 2008 универсал бензин дала то, что хотели:</p>
<ul>
<li>зажигание включено, мотор не заведён — ничего не горит;</li>
<li>двигатель запущен — автоматически ближний спереди и задние габариты;</li>
<li>ручной режим света как штатный, без сюрпризов.</li>
</ul>
<p>Если у вас другой год / хэтч / GTC / ксенон / датчик света — цифры вариантов могут понадобиться другие, но принцип тот же: <strong>не программируйте только UEC и не ждите чудес от задних фонарей</strong>. Сначала таблица из интернета, потом обязательно REC, и уже подбором (или нашей связкой 5+1) ловите нужную картину.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/auto/skandinavskij-svet-na-opel-astra-h-uec-variant-5-rec-variant-1/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Nginx: лимиты на wp-login и xmlrpc без отстрела нормальных пользователей</title>
		<link>https://remadmin.com/blog/vebmasteru/nginx-limity-na-wp-login-i-xmlrpc-bez-otstrela-normalnyh-polzovatelej/</link>
					<comments>https://remadmin.com/blog/vebmasteru/nginx-limity-na-wp-login-i-xmlrpc-bez-otstrela-normalnyh-polzovatelej/#respond</comments>
		
		<dc:creator><![CDATA[Cursor Blog]]></dc:creator>
		<pubDate>Sun, 06 Sep 2026 12:57:32 +0000</pubDate>
				<category><![CDATA[Вебмастеру]]></category>
		<category><![CDATA[Nginx]]></category>
		<category><![CDATA[Wordpress]]></category>
		<category><![CDATA[wp-login]]></category>
		<category><![CDATA[xmlrpc]]></category>
		<category><![CDATA[безопасность]]></category>
		<category><![CDATA[спам]]></category>
		<guid isPermaLink="false">https://remadmin.com/?p=7407</guid>

					<description><![CDATA[Как я режу брутфорс на wp-login.php и xmlrpc.php через nginx limit_req, не закрывая офис за NAT и не превращая отказ в 503 по всему PHP. Отдельные зоны, POST-only, real_ip и почему system.multicall лимитер не видит.]]></description>
										<content:encoded><![CDATA[<p>Типичная заявка: «сайт лежит, nginx отдаёт 503, в access.log сплошной wp-login.php». Открываю лог — не DDoS на главную, а брутфорс. Сотни POST в минуту на <code>wp-login.php</code> и <code>xmlrpc.php</code> с пары десятков адресов. Человек уже воткнул <code>limit_req</code> на весь <code>location ~ \.php$</code> «на всякий случай». Через час звонит редактор из офиса: не логинится, превью в админке крутится, мобильное приложение WordPress отвалилось.</p>
<p>Лимит сработал. Просто он отстрелил своих вместе с ботами. Офис сидит за одним белым IP, xmlrpc дергает приложение каждые несколько секунд, а общий php-location обслуживает и логин, и <code>admin-ajax.php</code>, и оформление заказа.</p>
<p>Ниже — как я режу именно логин и xmlrpc, оставляя сайт живым, и где <code>limit_req</code> бесполезен, сколько его ни крути. Без «поставьте плагин лимита логинов» и без слепого <code>deny all</code> на xmlrpc, пока не ясно, кто им пользуется.</p>
<h5>Что бьют на самом деле, и почему это две разные двери</h5>
<p>На типичном WordPress без WAF смотрят три URL:</p>
<ul>
<li><code>/wp-login.php</code> — форма входа. GET рисует форму, POST проверяет пароль.</li>
<li><code>/xmlrpc.php</code> — старый API. С него удобно перебирать пароли пачками через <code>system.multicall</code> и дёргать pingback.</li>
<li>Иногда <code>/wp-admin/</code> и REST <code>/wp-json/</code> — это уже другие истории, их в ту же зону, что логин, я не кладу.</li>
</ul>
<p>GET на логин — не атака. Человек открыл форму, браузер подтянул редирект, плагин 2FA нарисовал второй шаг. Если лимитировать любой метод, первый же заход «съедает» burst, а POST с паролем уже ловит 429. Поэтому ключ зоны я завязываю на POST, не на URL целиком.</p>
<p>xmlrpc почти всегда POST. Закрывать его наглухо можно, если им никто не пользуется: нет официального приложения WP, нет Jetpack, нет старых клиентов публикации. Если приложение нужно — <code>return 403</code> хуже брутфорса: свои отвалятся сразу, боты просто перейдут на <code>wp-login.php</code>.</p>
<h5>Почему общий limit_req на php убивает нормальных людей</h5>
<p><code>admin-ajax.php</code> на живом сайте стучит постоянно: пульс админки, автосохранение записи, корзина, фильтры каталога. Один редактор за минуту легко делает десятки POST. Посадили его в зону <code>1r/s burst=3</code> вместе с логином — получите «сайт тормозит», хотя CPU спокойный, а в логе 503 на ajax.</p>
<p>Вторая ловушка — общий IP. Офис, коворкинг, 4G-оператор с CGNAT, гостиница. Двадцать человек за одним адресом. Лимит «один запрос в секунду на IP» для брутфорса с ботнета ещё терпим, для офиса это коллективный бан после трёх неудачных паролей подряд.</p>
<p>Третья — статус по умолчанию. Nginx на превышение отдаёт 503. Мониторинг орёт «сайт мёртв», человек лезет в php-fpm и диск. Это не падение бэкенда, это отказ лимитера. Ставлю <code>limit_req_status 429</code> и в логах сразу видно, что именно отсекли, а не «бэкенд не отвечает».</p>
<h5>Две зоны, не одна</h5>
<p>Зоны объявляю в <code>http</code>, не в <code>server</code>. Размер <code>10m</code> для <code>$binary_remote_addr</code> — десятки тысяч IP, на обычный сайт с головой. Rate считаю отдельно:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="nginx"># http { ... }
limit_req_zone $limit_key_wplogin zone=wplogin:10m rate=6r/m;
limit_req_zone $limit_key_xmlrpc  zone=xmlrpc:10m  rate=1r/s;
limit_req_status 429;
limit_req_log_level warn;</pre>
<p>Шесть POST в минуту на логин — человек с кривым паролем ещё влезет, словарь на тысячи попыток уже нет. xmlrpc чуть свободнее по частоте HTTP-запросов: живое приложение иногда стучит пачкой. Это не дыра — дыра в xmlrpc не частота, а толстое тело, про это ниже.</p>
<p>Ключ пустой строкой означает «этого запроса в зоне нет». Так я выключаю лимит для GET и для белого списка.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="nginx">map $request_method $limit_key_wplogin {
    POST $binary_remote_addr;
    default "";
}

# xmlrpc почти весь POST; GET на него мне не жалко тоже резать
map $request_method $limit_key_xmlrpc {
    default $binary_remote_addr;
}</pre>
<p>Если офис сидит на постоянном адресе, вычитаю его из ключа через <code>geo</code>. В примере — документационный диапазон, не чей-то боевой:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="nginx">geo $offnet {
    default 0;
    203.0.113.0/24 1;   # офис, подставьте свой
}

map $offnet $limit_key_wplogin {
    1      "";
    default $limit_wplogin_ip;
}

map $request_method $limit_wplogin_ip {
    POST $binary_remote_addr;
    default "";
}</pre>
<p>Два <code>map</code> на одну переменную nginx не даст — ключ собираю в одном месте. Рабочий вариант без сюрпризов:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="nginx">geo $is_office {
    default 0;
    203.0.113.0/24 1;
}

map "$is_office:$request_method" $limit_key_wplogin {
    "1:POST" "";
    "0:POST" $binary_remote_addr;
    default  "";
}

map $is_office $limit_key_xmlrpc {
    1       "";
    default $binary_remote_addr;
}</pre>
<p>Белый список — не «интернет офиса целиком». Только тот префикс, с которого реально заходят в админку. Домашний динамический IP туда не тащу: через месяц он уедет соседу.</p>
<h5>location: только эти два файла, тот же php, что у остальных</h5>
<p><code>location =</code> с точным именем бьёт точнее regex. Обработчик php не выдумываю — копирую то, что уже работает для <code>*.php</code>. На Debian это часто <code>snippets/fastcgi-php.conf</code> плюс ваш <code>fastcgi_pass</code>. Сокет не копируйте из чужой статьи: у вас может быть другой пул.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="nginx">server {
    # ...

    location = /wp-login.php {
        limit_req zone=wplogin burst=4 nodelay;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php-fpm.sock;  # как у вас в общем php
    }

    location = /xmlrpc.php {
        limit_req zone=xmlrpc burst=8 nodelay;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }

    # общий php — БЕЗ limit_req этих зон
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }
}</pre>
<p><code>burst=4 nodelay</code> на логине: четыре лишних POST проходят сразу, дальше 429, без очереди. Без <code>nodelay</code> nginx начинает задерживать запросы — форма «думает» секундами, человек жмёт ещё раз, вы сами себе множите POST. Для логина я предпочитаю жёсткий отказ, не задержку.</p>
<p>На xmlrpc burst больше: приложение может открыть несколько соединений подряд при синхронизации. Если xmlrpc вам не нужен совсем — не лимитируйте, закройте:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="nginx">location = /xmlrpc.php {
    return 403;
}</pre>
<p>Проверяю необходимость просто: <code>grep xmlrpc access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head</code> и user-agent. Если неделю стучат только словари и <code>Hello world</code> — закрываю. Если есть <code>wp-iphone</code> / <code>wp-android</code> с адресов редакции — оставляю зону.</p>
<h5>Настоящий IP, иначе режете Cloudflare, а не бота</h5>
<p><code>$binary_remote_addr</code> — это TCP-пир. Стоит сайт за Cloudflare, nginx-прокси или балансировщиком хостера — пир один на всех. Лимит превращается в глобальный рубильник: первый бот выжигает burst, посетители с того же эджа ловят 429.</p>
<p>Смотрю, откуда приходит запрос:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">log_format debugip '$remote_addr cf="$http_cf_connecting_ip" xff="$http_x_forwarded_for"';</pre>
<p>Дальше <code>set_real_ip_from</code> на префиксы своего прокси и <code>real_ip_header</code> — тот заголовок, которому вы доверяете. Не доверяю <code>X-Forwarded-For</code> с интернета без фильтра: его подставляет кто угодно. Для Cloudflare — их диапазоны и <code>CF-Connecting-IP</code>. Для своего nginx-прокси — только его адрес в <code>set_real_ip_from</code>.</p>
<p>Пока real_ip не настроен, <code>limit_req</code> на логин я не включаю в боевой <code>server</code>. Иначе «защита» — это лотерея, кто первый постучал в этот воркер.</p>
<h5>Где лимит бесполезен: system.multicall</h5>
<p>Брутфорс по xmlrpc часто укладывается в <em>один</em> HTTP POST. Внутри <code>system.multicall</code> — сотни <code>wp.getUsersBlogs</code> с разными паролями. Для nginx это один запрос. Зона <code>1r/s</code> его пропускает, php-fpm честно пережёвывает пачку, в <code>auth.log</code> WordPress — сотни неудачных логинов, в access.log — одна строка 200.</p>
<p>Поэтому «я поставил rate limit, xmlrpc больше не брутфорсят» — самообман, если multicall жив. Nginx <code>$request_body</code> в <code>if</code> на практике пустой: тело ещё не прочитано в той фазе, конфиг из гугла с <code>if ($request_body ~ multicall)</code> просто не срабатывает. Я это не рисую как рабочую схему.</p>
<p>Что работает без магии:</p>
<ul>
<li>xmlrpc не нужен — <code>return 403</code> на location, плюс фильтр в WordPress на всякий случай;</li>
<li>нужен только pingback — всё равно спорно, pingback сам по себе источник шума; чаще его выключают;</li>
<li>нужно приложение — оставляю xmlrpc под <code>limit_req</code> и отдельно отключаю multicall на стороне PHP (му-плагин на <code>xmlrpc_methods</code> выкидывает <code>system.multicall</code>), либо режу на WAF, который тело реально видит;</li>
<li>Application Passwords и REST — другая дверь, xmlrpc для них не обязателен. Если мобильный вход уже через REST, xmlrpc я закрываю, не лимитирую.</li>
</ul>
<p>Лимит по запросам и разбор методов xmlrpc — два разных слоя. Первый без второго на этой двери недоделан.</p>
<h5>Как проверить, что своих не задело</h5>
<p>Не тестирую с того же ноутбука, с которого админю. Иначе сам себя выкину и буду чинить по аварийной консоли. С другого адреса, лучше с маленького VPS:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic"># форма должна открываться сколько угодно
for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-login.php; done

# POST — должны пойти 429 после burst
for i in $(seq 1 20); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST \
    -d "log=nosuchuser&amp;pwd=wrong&amp;wp-submit=Log+In" \
    https://example.com/wp-login.php
done

# ajax и главная не в этой зоне
for i in $(seq 1 30); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST https://example.com/wp-admin/admin-ajax.php
done</pre>
<p>Ожидание: GET логина — сплошные 200 (или 302, если уже есть редирект). POST — несколько 200/302, потом 429, не 503. ajax — без 429. Главная и карточки товара — без 429.</p>
<p>Потом то же с офисного IP, если он в <code>geo</code>: POST не должен упираться в 429, иначе белый список не попал в ключ. Частая ошибка — <code>geo</code> по <code>$remote_addr</code> до <code>real_ip</code>, и офис виден как адрес Cloudflare.</p>
<p>В логе ищу не «много 404», а явно статус и зону. С <code>limit_req_log_level warn</code> в error.log будет <code>limiting requests, excess: ... zone=wplogin</code>. Если зоны в логе нет, а 503 есть — вы смотрите не на лимитер, а на php-fpm/upstream.</p>
<h5>Что я не кладу в эти зоны</h5>
<ul>
<li><code>admin-ajax.php</code>, <code>async-upload.php</code>, cron <code>wp-cron.php</code> — это не брутфорс логина.</li>
<li>REST <code>/wp-json/</code> целиком. Там и легитимный редактор, и Application Passwords. Если резать — отдельные location на конкретные маршруты, не оптом.</li>
<li><code>wp-admin/</code> GET статики и <code>admin-post.php</code> с авторизованной сессией.</li>
<li>Статику и <code>wp-content/uploads</code> — к логину отношения не имеет.</li>
</ul>
<p>Отдельно: плагины «кастомной страницы входа» часто POST-ят не на <code>/wp-login.php</code>, а на <code>/</code> или на <code>/my-account/</code>. Тогда мой location логин не защищает, зато WooCommerce-чекаут внезапно оказывается под лимитом, если кто-то прикрутил зону на <code>location /</code>. Смотрю в access.log реальный URI при неудачном входе, не название плагина на коробке.</p>
<h5>fail2ban рядом, не вместо</h5>
<p>Nginx лимитер не помнит «этот IP вчера перебирал пароли». Он держит короткое окно. fail2ban по error.log с <code>zone=wplogin</code> или по auth-логу WordPress закрывает упёртых на часы. Но jail, который банит после трёх 429, снова отстрелит офис. Либо офис в ignoreip, либо порог заметно выше человеческого «забыл пароль, нажал пять раз».</p>
<p>Я не баняю 429 с ajax и не баняю 404 на <code>xmlrpc.php</code>, если сами его закрыли 403: боты будут стучать вечно, jail раздует ipset, а толку ноль — они и так получают отказ.</p>
<h5>Что оставляю на обычном сайте</h5>
<p>Рабочий минимум, которым я заканчиваю, если это один WordPress на VPS без приложения:</p>
<ul>
<li>real_ip настроен, в логе виден клиент, не эдж;</li>
<li><code>wp-login.php</code> — POST-only зона ~6r/m, burst 4 nodelay, статус 429;</li>
<li><code>xmlrpc.php</code> — 403, если за неделю нет своих клиентов;</li>
<li>если клиент есть — зона 1r/s + выключенный <code>system.multicall</code> в PHP;</li>
<li>офисный префикс в <code>geo</code>, не «весь провайдер»;</li>
<li>общий php-location без этих зон;</li>
<li>проверка циклом curl с чужого IP и глазами error.log, что падает зона, а не 503 от fpm.</li>
</ul>
<p>Плагин лимита логинов в WordPress я не считаю заменой: он срабатывает уже внутри PHP, после того как fpm взял воркер. Под словарём это дороже, чем отказ на nginx. Плагин имеет смысл как второй слой (капча, временная блокировка учётки), не как единственный.</p>
<p>Если после включения 429 сыпятся на живых редакторов — не поднимаю rate «пока не пройдёт». Сначала смотрю, какой URI и какой IP. Почти всегда это либо общий php-location, либо NAT офиса, либо xmlrpc приложения, которое я забыл, что оно существует.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://remadmin.com/blog/vebmasteru/nginx-limity-na-wp-login-i-xmlrpc-bez-otstrela-normalnyh-polzovatelej/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>