1. Риск лишней видимости в Actuator
Когда мы впервые включаем env и configprops, кажется, что это просто удобная отладка: посмотрел — и понял, что именно загрузилось из YAML, что перетёрлось переменной окружения, а что прилетело из аргументов запуска. Проблема в том, что Actuator — это не “лог для разработчика”, а HTTP‑поверхность. А HTTP, как известно, имеет вредную привычку становиться доступным не только нам.
Представьте, что у вас дома есть “щиток” с автоматами, и вы решили сделать к нему дверцу прозрачной, чтобы “быстрее понимать, что где”. Пока это внутри квартиры — окей. Но если кто-то вытащит этот щиток в подъезд, или вы сами случайно откроете туда доступ, прозрачность внезапно превратится в подарок “всем желающим”.
С Actuator ровно так же. Endpoint’ы env и configprops могут показывать:
- какие property sources вообще есть в приложении, а значит — где можно искать секреты;
- какие значения победили, а значит — что именно сейчас используется;
- откуда они пришли, а значит — где это лежит: файл, env vars, системные свойства и т.д.
И даже если вы не открывали эти endpoint’ы “наружу”, остаются практические сценарии, где это становится проблемой: общий dev‑стенд, публичный тестовый сервер “на минутку”, проброшенный порт, случайный reverse‑proxy, “да я просто накинул include: "*", чтобы не мучаться”. У Actuator нет встроенного чувства стыда, он честно ответит. Поэтому мы и добавляем “ремень безопасности” в виде sanitization и аккуратного management port.
2. Что такое sensitive values в контексте backend‑сервиса
Новички часто думают, что “чувствительное” — это строго пароль и токен, а всё остальное можно показывать. На практике чувствительность — это шире: это любые данные, которые помогают атаковать систему, нарушают приватность или раскрывают внутреннюю кухню. И самое неприятное: иногда значение само по себе безобидное, но связка “ключ + источник + окружение” становится уже полезной информацией для злоумышленника (или просто для любопытного соседа по офисному Wi‑Fi).
Ниже небольшая “карта местности”, чтобы вы понимали, что именно может внезапно оказаться sensitive, даже в учебном catalog-service:
| Категория | Пример значения | Почему это неприятно показывать | Где часто всплывает |
|---|---|---|---|
| Секреты (очевидные) | пароли, API keys, токены, client secrets | прямой доступ к чужим сервисам/данным | env, configprops, иногда info (если вы туда положили) |
| Внутренние адреса | jdbc:..., internal hostnames, URLs внутренних API | помогает “прощупывать” инфраструктуру | env, configprops |
| Путь к файлам/директориям | /opt/app/config/..., C:\Users\... | помогает понимать окружение, иногда утечки пользовательских данных | configprops (origin/inputs), env |
| Бизнес‑конфиденциальное | цены, набор курсов, фичи, флаги “скоро запуск” | это не “секрет”, но может быть нежелательно публичным | configprops (ваши @ConfigurationProperties) |
| Персональные данные | email’ы, телефоны, user ids | приватность и комплаенс | чаще в логах и доменных endpoint’ах, но иногда и в конфиге |
Для нашего catalog-service особенно показателен “бизнес‑конфиденциальный” кусок: список курсов и цены мы держим в конфигурации. С технической точки зрения это не пароль. Но с продуктовой — это вполне может быть информация, которую вы не хотите отдавать любому, кто угадает URL /actuator/configprops. Поэтому главный принцип на сегодня будет такой: sanitization полезен, но не заменяет здравого смысла и ограничения exposure.
3. Sanitization: как Spring Boot маскирует значения
Когда вы впервые открываете env или configprops, вы можете заметить приятную штуку: некоторые значения уже не показываются “как есть”, вместо них приходит ******. Это и есть sanitization — маскирование потенциально чувствительных данных. Boot пытается быть вашим другом и не устраивать “угадай мой токен по логам” прямо из коробки.
Но важно понимать две разные ручки управления:
Первая ручка — показывать ли значения вообще. Для этого у многих endpoint’ов есть настройка в стиле show-values (в частности, для env и configprops). Вторая ручка — какие ключи всегда нужно маскировать, даже если значения в целом показывать разрешено. Это обычно настраивается через keys-to-sanitize.
Чтобы не держать это в голове как “абстракцию”, удобнее смотреть на show-values как на режим вывода:
| show-values | Что увидите в ответе | Кому подходит |
|---|---|---|
| never | значения всегда скрыты (маска вместо value) | безопасный baseline, особенно без Security |
| when_authorized | значения показываются только “авторизованному” | полезно, когда есть механизм авторизации (в нашем курсе его нет) |
| always | значения показываются, кроме тех, что попали под sanitization по ключам | локальная отладка “на своём ноутбуке”, и то временно |
И вот ключевой момент курса: пока у нас нет Security, режим when_authorized чаще всего превращается в “сюрприз: вы всё равно не авторизованы” или “сюрприз: вы всё равно всё показываете, потому что защита не настроена так, как вы думаете”. Поэтому в catalog-service на уровне курса разумнее опираться на never как безопасный default, и только точечно включать always, когда вы осознанно в локальной среде и понимаете последствия.
Для ощущения, как это выглядит, вот типичный фрагмент из ответа, где значение замаскировано:
{
"property": {
"source": "systemEnvironment",
"value": "******"
}
}
Важно: даже если value скрыто, сам факт наличия свойства, его имя и источник уже могут быть полезной информацией. Поэтому “маска” — это не пропуск в мир “можно открывать всем”.
4. Настраиваем show-values для env и configprops в catalog-service
Когда вы диагностируете env и configprops, есть соблазн держать их в режиме открытой лупы: так проще увидеть, какое значение победило и во что оно связалось. Но такой режим хорош только как временная local‑диагностика. Для постоянного режима catalog-service безопаснее, чтобы endpoint’ы могли быть доступны, а значения по умолчанию оставались скрыты.
Мы уже настроили exposure policy: в local/dev открываем больше диагностических endpoint’ов, а в prod оставляем минимум. Теперь добавим поверх этого ещё одно правило: даже если endpoint доступен в local, значения по умолчанию всё равно скрываем.
Начнем с application-local.yaml. Здесь мы предполагаем, что в local мы хотим видеть env и configprops, но без “вскрытия” значений:
# src/main/resources/application-local.yaml
management:
endpoints:
web:
exposure:
# В local можно включить расширенную диагностику...
include: "health,info,env,configprops"
endpoint:
env:
# ...но значения по умолчанию не показываем (только факт и источник).
show-values: never
configprops:
# То же самое для @ConfigurationProperties.
show-values: never
С точки зрения поведения это означает следующее. Endpoint’ы доступны по HTTP, потому что мы их включили в include, но значения будут замаскированы. Это хороший компромисс для новичка: вы можете проверить, что свойство существует и откуда оно пришло, но не превращаете Actuator в “витрину переменных окружения”.
Для prod мы держим “узкую” поверхность и не открываем env/configprops вообще. Тогда show-values в проде становится второстепенным, потому что endpoint’ов просто нет снаружи:
# src/main/resources/application-prod.yaml
management:
endpoints:
web:
exposure:
# В prod оставляем только базовые endpoint'ы.
include: "health,info"
Теперь важная практическая мысль. Иногда вам в local правда нужно увидеть значение. Например, вы отлаживаете, почему app.catalog.title не тот, или почему max-featured-count вдруг стал 0. В этот момент самый безопасный путь — не менять файл, а временно переопределить настройку через аргумент запуска или переменную окружения.
Например, локально можно запустить с временными override’ами:
--management.endpoint.env.show-values=always
--management.endpoint.configprops.show-values=always
Если нужен только один endpoint, включайте только его.
Смысл в том, что вы не фиксируете “опасный режим” в репозитории, а используете его как одноразовую лупу. В мире реальных проектов это очень помогает не устроить себе “а почему на dev‑стенде всем показываются токены?” через две недели после “временной отладки”.
5. keys-to-sanitize: когда маскировать нужно даже “не совсем секреты”
Очень человеческая ошибка — считать, что достаточно поставить show-values: always в локальном профиле, а потом “ну мы же всё равно маскируем секреты”. Проблема в том, что секреты маскируются по шаблонам ключей. А люди, включая нас, иногда называют ключи так, будто специально хотят обмануть систему: app.catalog.magicString, app.catalog.superValue, app.catalog.pleaseDontLookHere. И угадайте, что сделает sanitization? Правильно: вежливо покажет всё как есть.
Поэтому keys-to-sanitize — это способ сказать Boot: “Вот эти паттерны ключей всегда скрывай, даже если я включил показ значений”. Для env и configprops это можно задать отдельно. В учебном проекте мы обычно не храним настоящие секреты, но окружение (особенно на рабочем ноутбуке) может быть богато на переменные типа *_TOKEN и *_KEY.
Пример базовой настройки в local, которая делает поведение более предсказуемым:
# src/main/resources/application-local.yaml
management:
endpoint:
env:
# Маскируем типовые "опасные" ключи, даже если включили показ значений.
keys-to-sanitize: "password,secret,token,key"
configprops:
# То же самое для configprops (ваших @ConfigurationProperties).
keys-to-sanitize: "password,secret,token,key"
Заметьте тонкость: keys-to-sanitize не решает проблему exposure. Он только помогает не выдать значение “как есть”. Сами ключи, их наличие и источники будут видны, если endpoint открыт. Поэтому этот механизм — про “минимизировать ущерб”, а не про “сделать безопасно вообще”.
И ещё один забавный, но полезный вывод: если вы всё-таки заводите секретные параметры в конфиге, то лучше называть их так, чтобы они попадали под sanitization. Токен должен быть токеном, а не “хрустальной строкой судьбы”.
6. Отдельный management port
Даже при аккуратном include/exclude и скрытых значениях остаётся практическая боль: Actuator endpoint’ы живут рядом с обычными URL вашего приложения. Для разработчика это не страшно, но для организации окружений, и для привычки “не смешивать продуктовые запросы и диагностику”, отдельный management port бывает очень удобным.
Идея простая: основной сервис отвечает на одном порту, например 8080, а служебные endpoint’ы Actuator — на другом, например 8081. Визуально это выглядит примерно так:
flowchart LR U["Браузер / curl / Postman"] -->|"http://localhost:8080"| A["Product API + landing page"] U -->|"http://localhost:8081"| M["Actuator endpoints"]
В Spring Boot это делается одной настройкой:
# src/main/resources/application-local.yaml
management:
server:
# Отдельный порт только для /actuator/** (удобство, не безопасность).
port: 8081
Важно: это optional local/dev ветка, а не новый основной вариант проекта. Базовый сценарий всё ещё проще: приложение и Actuator живут на одном порту, а вы управляете тем, что видно, через exposure policy. Отдельный port имеет смысл, когда хочется физически развести продуктовые запросы и диагностику, но он не обязателен.
Если смотреть грубо и без погружения в кишки, Boot поднимает отдельный web‑сервер для management endpoints. Ваши обычные controller’ы продолжают жить на основном порту, а /actuator/** переезжает на management port.
Что это меняет. Разделяется трафик: вы можете проверять health, info и конфигурацию, не смешивая это с обычными запросами каталога. Для некоторых окружений это упрощает жизнь: можно “по умолчанию” работать с приложением на 8080, а диагностику держать на 8081.
Что это не меняет. Это не “встроенная безопасность”. Если management port доступен кому-то извне, то endpoint’ы доступны так же, как если бы они были на основном порту. Это как вынести запасные ключи под коврик у соседней двери: формально дверь другая, но идея защиты… спорная.
Поэтому в рамках курса мы воспринимаем отдельный port как организационную опцию для local/dev, а не как обязательный и не как “замену продуманной exposure policy”.
Ссылки и привычки с отдельным портом
Как только вы выносите Actuator на отдельный порт, всплывает бытовая деталь, которую легко не заметить: ссылки в landing page вида <a href="/actuator/health"> ведут на тот же порт, что и сама страница. То есть на основном порту они начнут давать 404, потому что /actuator/** теперь слушает 8081.
Это не ошибка Spring Boot — это мы поменяли физическую “адресацию” management endpoints. Поэтому нужно заранее договориться, как вы это оформляете для команды, или для себя будущего, который через месяц откроет проект и скажет: “почему ссылка умерла?”.
Здесь важно не держать сломанное состояние как норму. Пока Actuator живёт на том же порту, ссылки вида <a href="/actuator/health"> и <a href="/actuator/info"> на landing page удобны и действительно ускоряют проверку сервиса. Если вы временно включили отдельный management port, эти относительные ссылки на основной странице перестают быть полезными: они всё равно поведут на основной порт, где Actuator уже не слушает.
Поэтому separate port лучше воспринимать как отдельную ветку local/dev‑настройки. В этой ветке обычные ссылки на landing page лучше заменить нейтральной подсказкой про путь /actuator и отдельно открыть правильный порт, чем оставлять на странице красивый, но мёртвый 404.
Если вы включили такую ветку и всё равно хотите оставить подсказку на странице, можно добавить нейтральную заметку без привязки к конкретному порту, чтобы не сломать prod:
<!-- src/main/resources/static/index.html -->
<p>
<!-- Подсказка без привязки к порту: порт может отличаться в разных средах. -->
Actuator endpoints доступны по пути <code>/actuator</code>.
Если в среде настроен отдельный management port, откройте Actuator на этом порту.
</p>
Эта маленькая фраза экономит кучу времени новичкам: они перестают подозревать, что “Actuator не работает”, и начинают проверять, куда именно он был вынесен.
Ещё одна привычка, которая помогает: когда вы включаете management.server.port, сразу после старта приложения смотрите в startup logs. Там обычно видно, на каком порту поднялся основной сервер и где стартовал management server. Если вы этого не делаете, вы будете тратить минуты, а иногда и часы, на “почему у меня 404”, хотя ответ уже был в логах.
7. Sanitization не спасает от утечек в логах
Очень коварный момент: sanitization относится к ответам Actuator endpoint’ов, но не имеет никакой власти над тем, что вы логируете сами. Если вы однажды сделаете log.info("CatalogProperties={}", catalogProperties), то в лог улетит всё: список курсов, цены, флаги и так далее. А логи, в отличие от local‑Actuator, обычно живут долго и иногда оказываются в местах, где вы их не планировали видеть.
Чтобы связать это с нашим catalog-service, покажу два коротких фрагмента. Первый — “плохая, но очень соблазнительная идея”:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// Предполагается, что у вас есть logger (например, через Lombok @Slf4j или вручную).
log.info("Catalog properties: {}", catalogProperties); // Печатает весь объект конфигурации целиком.
Так вы печатаете весь объект, а значит — всё содержимое конфигурации. Для учебного проекта это может быть просто шум, а для реального — утечка.
Второй — более зрелый стиль: логировать только агрегаты и безопасные флаги, которые действительно помогают понять состояние сервиса, не превращая лог в дамп:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// Вместо полного дампа — логируем только то, что реально помогает диагностике.
log.info("Catalog loaded: {} courses, featured limit={}",
catalogProperties.courses().size(), // Сколько курсов загрузилось (агрегат)
catalogProperties.maxFeaturedCount()); // Ограничение, влияющее на поведение сервиса
Разница кажется “мелкой”, но именно такие мелочи обычно отделяют сервис, который удобно диагностировать, от сервиса, который удобно “случайно сдать на экзамене по утечкам”.
Если объединить это с темой сегодняшней лекции, правило получается простым: Actuator помогает увидеть, а sanitization помогает не увидеть лишнее в ответе. Но за то, что вы пишете в логах, отвечаете вы сами, без подсказок от Boot. Именно в таком виде Actuator полезен: не как открытая витрина настроек, а как аккуратный инструмент точечной диагностики — от условий auto-configuration до карты маршрутов, старта приложения и собственных health‑проверок.
8. Типичные ошибки при настройке Actuator
Ошибка №1: открыть env и configprops “временно”, а потом забыть про это.
Это самая частая история. Сервис удобно диагностируется, всё классно… пока вы не выкатываете тот же конфиг на общий dev‑сервер или не оставляете его в репозитории. Избежать этого помогает профильная дисциплина: расширенные endpoint’ы только в local/dev, а в prod — минимальный набор. И ещё лучше — временные overrides через аргументы запуска вместо правки YAML.
Ошибка №2: воспринимать sanitization как “раз можно открыть endpoint, значит всё безопасно”.
Маска ****** скрывает значение, но не скрывает сам ключ, его наличие и источник. Иногда уже этого достаточно, чтобы понять, что у вас есть, например, переменные с токенами, откуда они берутся и как называются. Sanitization — страховка на случай ошибки, а не лицензия на бездумное раскрытие диагностики.
Ошибка №3: перепутать “exposure” и show-values.
include/exclude отвечает на вопрос “endpoint вообще доступен по HTTP?”, а show-values — “что он показывает внутри ответа?”. Если endpoint открыт, но значения скрыты, это всё равно endpoint. И наоборот: если вы не открыли endpoint в prod, то show-values там почти не играет роли, потому что никто его не увидит по HTTP.
Ошибка №4: надеяться, что отдельный management.server.port сам по себе решит вопрос безопасности.
Отдельный порт — это удобное разделение, но не защита. Если порт доступен, endpoint доступен. В учебном проекте мы не строим сетевые правила и не подключаем Security, поэтому воспринимайте management port как “удобнее организовать” и “меньше путаться”, а не как “теперь всё защищено”.
Ошибка №5: логировать целиком @ConfigurationProperties или весь Environment ради отладки.
Это родная привычка после System.out.println: “давайте выведем всё”. В Boot‑сервисе это быстро превращается в риск утечки и в тонну шума. Гораздо полезнее логировать только то, что реально нужно для диагностики: количества, активные профили, режимы, ключевые флаги. Всё остальное для просмотра “как есть” у вас уже есть через Actuator — причём с возможностью маскировать значения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ