<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>152-Фз on DevOps Way - Практические гайды</title>
    <link>https://devopsway.ru/tags/152-%D1%84%D0%B7/</link>
    <description>Recent content in 152-Фз on DevOps Way - Практические гайды</description>
    <image>
      <title>DevOps Way - Практические гайды</title>
      <url>https://devopsway.ru/images/devopsway-og.png</url>
      <link>https://devopsway.ru/images/devopsway-og.png</link>
    </image>
    <generator>Hugo -- 0.166.0</generator>
    <language>ru</language>
    <lastBuildDate>Thu, 17 Sep 2026 09:48:23 -0400</lastBuildDate>
    <atom:link href="https://devopsway.ru/tags/152-%D1%84%D0%B7/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Единый вход на зоопарк внутренних тулов: Zitadel &#43; Pomerium</title>
      <link>https://devopsway.ru/posts/beyondcorp-pomerium-zitadel/</link>
      <pubDate>Thu, 17 Sep 2026 12:00:00 +0300</pubDate>
      <guid>https://devopsway.ru/posts/beyondcorp-pomerium-zitadel/</guid>
      <description>Как закрыть зоопарк внутренних тулов одним входом: почему не VPN, не nginx и не Keycloak, и как склеить Zitadel (единый IdP) с Pomerium (identity-aware шлюз). Deny-by-default, конфиг в гите, человек против машины и честный счёт от продакшена.</description>
      <content:encoded><![CDATA[<p>Достался в наследство продукт на Битриксе и вокруг него – зоопарк доступов, каждый кривой по-своему. Сам Битрикс с его авторизацией и админкой торчит в интернет. GitLab, арендованный где-то на стороне, светит страницей входа наружу. На серверы заходят по паролю – без ключей, без сертов, а сами доступы раздаются руками, &quot;скинь пароль в личку&quot;. Картина ясна: со входом надо наводить порядок. Один раз и на всех.</p>
<p>Чинить этот зоопарк на месте смысла не было. Мы переезжали – весь продукт целиком уезжал в облако, в чистый контур, закрытый от мира с самого начала. А развернуть в нём предстояло немало: мониторинг, многочисленные контуры для сборки и тестирования, бэкофис и прочее по мелочи – и у каждого свой веб-интерфейс, который иначе торчал бы наружу своим логином и портом. Городить перед каждым отдельный костыль – путь в никуда: нужен один общий вход на весь этот растущий зоопарк. Старьё на дешёвом хостинге я не трогал сверх необходимого: подлатал только то, без чего оно не доживёт до переезда, – месяц-полтора на подпорках, а дальше баста. Поэтому и вход строился не заплаткой на старом, а начисто: deny-by-default с нулевого дня.</p>
<p>Порядок этот – на двух фронтах. Первый – команда: внутренние тулы и серверы, доступ для своих. Второй – пользователи: регистрация и вход на самом сайте, те самые двадцать с лишним тысяч живых аккаунтов на битриксовой авторизации. Фронты разные, решаются по-разному. Эта статья – про первый; про увод пользователей с Битрикса будет отдельно.</p>
<p>Оговорюсь сразу: серверный SSH – своя история (ключи и сертификаты вместо паролей, бастион), и её я закрывал параллельно. Ниже – про браузерные тулы, к которым ходят люди.</p>
<h2 id="первый-инстинкт-бастион-и-vpn">Первый инстинкт: бастион и VPN</h2>
<p>Контур закрыт, тулы внутри. Вопрос – как впускать своих. Первое, что приходит в голову – бастион и VPN. И это честно работает: наружу торчит только вход, зашёл по VPN – ходи. WireGuard сегодня ставится за вечер.</p>
<p>Ломается это об людей. Сотрудников энцать, и половина – не инженеры. Маркетолог не будет ставить VPN-клиент, крутить профиль и запускать этот блудняк каждый раз, когда надо глянуть карточку в админке. С телефона – тем более. А ещё VPN даёт сетевое доверие: ты внутри – значит видишь всё, что в сети. Мне же надо наоборот – чтобы человек видел ровно те тулы, что положены его роли, и ни портом больше.</p>
<p>Значит нужен простой вход: открыл браузер, залогинился как в гугл – и ты внутри. Без клиента, без профилей, без звонков в поддержку.</p>
<h2 id="два-вопроса-которые-все-схлопывают-в-слово-sso">Два вопроса, которые все схлопывают в слово &quot;SSO&quot;</h2>
<p>Тут и прячется развилка. На самом деле вопроса два, и решаются они разными штуками:</p>
<ul>
<li><strong>Кто ты?</strong> – это про identity, единый логин. Тут нужен IdP.</li>
<li><strong>Кто стоит у двери?</strong> – это про то, чтобы аноним вообще не долетел до тулы. Тут нужен access-proxy.</li>
</ul>
<p>IdP отвечает на первый вопрос и ни разу – на второй. На этом спотыкаются чаще всего, поэтому разберу отдельно.</p>
<h2 id="вопрос-1--простой-sso-почему-не-keycloak-почему-zitadel">Вопрос 1 – простой SSO: почему не Keycloak, почему Zitadel</h2>
<p>Искал self-hosted IdP. Облачные Auth0/Okta отпадают сразу: данные и решения о входе уезжают за границу, а у меня 152-ФЗ и внутренний контур. Плюс плата за место.</p>
<p>Из self-hosted первым под руку лезет Keycloak – дедушка жанра, умеет всё. Откинул: тяжёлый JVM-комбайн. Память, долгий старт, тюнинг ради тюнинга, а UX как из нулевых.</p>
<p>Взял Zitadel. Он на Go – лёгкий бинарь, быстрый старт, скромный по памяти. OIDC из коробки, организации, Actions (кастомная логика на входе), нормальная консоль. Поднялся, стал единым стором личностей: один аккаунт на все тулы, 2FA под рукой (TOTP, ключи, passkeys), аудит входов в одном месте.</p>
<h2 id="вопрос-2--а-как-закрыться-от-мира-почему-pomerium">Вопрос 2 – а как закрыться от мира: почему Pomerium</h2>
<p>Zitadel теперь знает, кто я. Но он – паспортный стол, а не вахтёр. Он не стоит перед Grafana и не разворачивает анонима на входе: Grafana как торчала в интернет, так и торчит, просто теперь умеет пускать по OIDC. Между &quot;тула умеет OIDC&quot; и &quot;к туле не подойти без входа&quot; – пропасть.</p>
<p>Значит нужен слой перед тулами – identity-aware proxy. Что смотрел:</p>
<ul>
<li><strong>Cloudflare Access / Google IAP</strong> – удобно, но data-path и решения о доступе снова уезжают за рубеж. 152-ФЗ, мимо.</li>
<li><strong>Authentik</strong> – хочет быть ещё и IdP. У меня уже есть Zitadel, второй identity-стор – это раскол и лишний вес. Мимо.</li>
</ul>
<p>Критерий, который всё решил, – <strong>конфиг как код</strong>. Хочу, чтобы политики доступа лежали в гите, ревьюились в MR и раскатывались пайплайном, а не тыкались мышкой в чужой админке. Под это лёг <strong>Pomerium в редакции Core</strong> – open source, self-hosted, без лимитов (не путать с Pomerium Zero, где control plane висит у них в облаке – опять иностранный контур).</p>
<p>На чём он сам? Pomerium – Go для control plane, а собственно трафик тащит встроенный <strong>Envoy</strong> (тот самый, из k8s-мира). То есть policy, OIDC и сессии – лёгкий Go-бинарь, а проксирование – проверенный боем Envoy под капотом. Zitadel тоже Go. Итого весь мой вход – два Go-сервиса вместо JVM-фермы.</p>
<h2 id="как-они-склеиваются">Как они склеиваются</h2>
<p>Схема простая:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">браузер → grafana.company.ru → Pomerium → (нет сессии) Zitadel (логин) → назад → апстрим
</span></span></code></pre></div><p>Первый раз тебя редиректит в Zitadel, логин (и второй фактор, если включил), дальше сессия живёт и по остальным тулам вход молчаливый – та же сессия, пароль второй раз не спрашивают.</p>
<p>Работает это в два слоя, и это осознанно:</p>
<ul>
<li><strong>Pomerium – грубый сетевой гейт.</strong> Deny-by-default, пускает по claim <code>groups</code> из токена. Не в нужной группе – 403, до тулы даже не долетел.</li>
<li><strong>Нативный OIDC самой тулы – тонкий RBAC внутри.</strong> Pomerium сказал &quot;этот человек – ops&quot;, Grafana на основании той же сессии выдала ему роль Editor. Без второго пароля.</li>
</ul>
<p>Прокси решает &quot;пускать ли к двери вообще&quot;, тула – &quot;что тебе можно внутри&quot;.</p>
<h2 id="вопрос-из-зала">Вопрос из зала</h2>
<p><strong>– А почему не nginx? У меня уже стоит.</strong></p>
<p>Можно: <code>nginx + auth_request + oauth2-proxy</code> реально закрывает пару аппов. Но:</p>
<ul>
<li>oauth2-proxy отвечает по сути &quot;залогинен / нет&quot; – это грубо. Авторизация по роли на конкретный роут (&quot;в Grafana пускаем ops и admins, в биллинг – только billing&quot;) собирается вручную через <code>map</code>, lua и заголовки. У Pomerium это политика на роут из коробки.</li>
<li>Политика живёт россыпью по nginx-конфигам императивно, а не одним декларативным файлом под GitOps. На зоопарке из десятка тулов это расходится.</li>
<li>Pomerium отдаёт апстриму подписанный identity-JWT (проверяется по JWKS) – апп доверяет не &quot;какому-то заголовку&quot;, а криптоподписи шлюза.</li>
<li>Auto-TLS на роут, сессии, рефреш и логаут по всему зоопарку – централизованно.</li>
</ul>
<p>Честно: на 1–2 аппа <code>nginx + oauth2-proxy</code> легче и нормально. Pomerium отбивает себя, когда тулов зоопарк и нужна authz-как-код плюс единый identity-контракт к апстримам. Нет зоопарка – не тащи Pomerium.</p>
<p><strong>– Так у Grafana и GitLab уже есть свой SSO. Зачем прокси вообще?</strong></p>
<p>Нативный OIDC даёт RBAC внутри аппа, но не разворачивает анонима у двери – тула по-прежнему торчит в интернет: страница логина, необновлённая CVE, дебаг-эндпоинты доступны всем. Прокси – единый deny-by-default перед всем, включая тулы без нормального SSO (или с платным – &quot;SSO tax&quot;). Поэтому берём оба слоя: прокси (грубый гейт) плюс нативный OIDC (тонкий RBAC).</p>
<p><strong>– Ну поставьте WireGuard, он же теперь простой.</strong></p>
<p>Тот же клиент на каждом устройстве плюс телефоны плюс не-техи. И главное – VPN даёт сетевое доверие: ты в сети, значит видишь всё. Zero-trust – наоборот: нахождение в сети не даёт ничего, каждый запрос проверяется по личности и роли. Плюс ноль per-app аудита.</p>
<p><strong>– Basic Auth не проще?</strong></p>
<p>Общий <code>htpasswd</code> – ни 2FA, ни единого входа, ни отзыва по одному человеку, пароли в конфигах. Это ровно та &quot;раздача доступов руками&quot;, от которой бежим.</p>
<p><strong>– А это не single point of failure?</strong></p>
<p>Да, шлюз становится критпутём – поэтому он в отдельном ops-сегменте (не делит отказ со стейджем), с задокументированным break-glass, а машинный трафик (CI, registry, ingest) идёт мимо него. Об этом – следующий пункт.</p>
<h2 id="человек-против-машины--то-на-чём-спотыкаются">Человек против машины – то, на чём спотыкаются</h2>
<p>Тут легко начудить и потом потратить время на разборы: завернуть за интерактивный SSO вообще всё. Прокси редиректит на страницу логина, а git, CI, реестр образов и приём телеметрии браузер не открывают. Они приходят с токеном, получают 302 на логин-страницу и давятся HTML-ом.</p>
<p>Поэтому машинный трафик – мимо шлюза:</p>
<ul>
<li><strong>GitLab</strong> повесил на нативный OIDC (кнопка &quot;войти через Zitadel&quot; для людей), а git/CI/API/registry ходят как ходили, по своим токенам, не через Pomerium.</li>
<li><strong>Приём ошибок в трекер</strong> (ingest по DSN) пустил отдельным маршрутом в обход SSO – сам DSN и есть аутентификация. Это классический паттерн Sentry с отдельным ingest-хостом.</li>
</ul>
<p>Правило: за редирект-прокси – только браузерный трафик человека. Всё, что ходит по токену, – рядом, не через него.</p>
<h2 id="конфиг-как-код">Конфиг как код</h2>
<p>Политики – в гите. Роут выглядит примерно так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="l">https://grafana.company.ru</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">to</span><span class="p">:</span><span class="w"> </span><span class="l">http://grafana-internal:3000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">policy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">allow</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">or</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">claim/groups</span><span class="p">:</span><span class="w"> </span><span class="l">ops</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">claim/groups</span><span class="p">:</span><span class="w"> </span><span class="l">admins</span><span class="w">
</span></span></span></code></pre></div><p>Роли, юзеры и гранты в Zitadel – тоже в Terraform. Саморегистрацию в IdP выключил: новый человек не заводит себе аккаунт сам. Онбординг = дай email и ФИО, я добавляю ресурс в код, MR, apply – и у человека вход во все положенные тулы. Убрать доступ – убрать строку. Кто, когда и куда ходил – в access-логах шлюза.</p>
<p>Это и есть то, ради чего всё затевалось: доступы перестали быть паролями в личке и стали кодом с историей.</p>
<h2 id="счёт-от-продакшена">Счёт от продакшена</h2>
<p>Гладко было на бумаге. Что откусило вечера:</p>
<ul>
<li><strong>Хардкод root-url в compose.</strong> У Grafana <code>ROOT_URL</code> был прописан прямо в <code>docker-compose</code> в <code>environment</code>, а он перебивает <code>env_file</code>. Итог – <code>redirect_uri</code> уезжал в localhost, Zitadel такой вход отбивал. Урок: в compose <code>environment</code> сильнее <code>env_file</code>, не держи там то, что задаёшь снаружи.</li>
<li><strong>redirect_uri за прокси приходит как http:// и с внутренним хостом.</strong> Классика reverse-proxy: апп за Pomerium не знает, что снаружи https и другой домен. Лечится передачей <code>X-Forwarded-Proto</code> (апп должен ему верить) и сохранением исходного <code>Host</code>. Пока не настроишь – IdP отбивает &quot;invalid redirect&quot;.</li>
<li><strong>CSRF за прокси.</strong> Тот же корень – апп видит origin не тот, что у пользователя. Добавляешь внешний домен в доверенные origin – отпускает.</li>
<li><strong>&quot;No data&quot; во всех панелях Grafana.</strong> Отдельная засада: вход прошёл, дашборды открылись, а данных нет. Прокси резал POST-запросы к данным по CSRF. Поймал только когда воспроизвёл ровно как юзер – через cookie-сессию и шлюз, а не своим admin-шорткатом напрямую. Мораль: user-facing баг воспроизводи путём юзера, иначе видишь не ту картину.</li>
<li><strong>Сертификат не выписался после смены DNS.</strong> Если A-запись тулы переключаешь уже ПОСЛЕ того, как накатил маршрут, Pomerium залипает на старой неудачной попытке ACME и сам не перевыпускает – отдаёт чужой дефолтный серт, клиент видит TLS-mismatch. Лечится рестартом контейнера шлюза, перевыпуск за секунды.</li>
<li><strong>Автозаведённый юзер в блоке – это фича.</strong> Первый вход нового человека создал аккаунт в статусе &quot;ждёт подтверждения&quot; и не пустил. Секунду думал, что сломал – оказалось наоборот: fail-safe сработал, чужой по OIDC сам себе доступ не выпишет. Подтверждаешь и линкуешь вручную, осознанно.</li>
</ul>
<h2 id="что-в-итоге">Что в итоге</h2>
<p>На выходе: один вход на весь зоопарк (2FA доступен – TOTP, ключи, passkeys), deny-by-default перед каждой тулой, онбординг и оффбординг через MR, аудит в одном месте. Маркетолог заходит из браузера и не знает слов &quot;VPN-профиль&quot;. Инженер логинится один раз на все тулы.</p>
<p>И да – это закрыло только внутренний контур, для команды. Пользовательская регистрация всё ещё висела пуповиной на Битриксе. Как я уводил вход двадцати с лишним тысяч живых пользователей с битриксовой авторизации на GoTrue – в следующий раз.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
