Почему в Kubernetes нет понятия «вход» — и как мы решили это для AuditRadar
Когда мы приступили к созданию страницы Logins для AuditRadar — проводника журналов аудита в реальном времени для OpenShift и Kubernetes — мы столкнулись с препятствием, которое заставило нас детально разобраться в том, как аутентификация реально устроена на каждой из платформ. То, что поначалу казалось задачей «просто добавить отслеживание входов», превратилось в настоящий урок об архитектурных различиях между OpenShift и ванильным Kubernetes.
Ниже — история того, что мы обнаружили и как в итоге построили определение сессий, работающее на обеих платформах.
Проблема: «Кто вошёл?» — не такой уж простой вопрос
AuditRadar собирает и отображает события аудита от API-сервера Kubernetes. Страница Events у нас уже работала и показывала в реальном времени каждое CREATE, DELETE, PATCH. Следующим логичным шагом было: показать, кто вошёл, откуда и когда.
На OpenShift это оказалось сравнительно просто. На Kubernetes же выяснилось, что задача в традиционном смысле архитектурно неразрешима — потому что в Kubernetes нет никакого понятия «вход» вообще.
OpenShift: настоящий OAuth-сервер
OpenShift поставляется со встроенным OAuth 2.0-сервером (oauth-openshift), работающим в пространстве имён (namespace) openshift-authentication. Все действия пользователя проходят через него:
-
Вы открываете веб-консоль → браузер перенаправляется на OAuth
-
Вы запускаете
oc login→ CLI выполняет обмен OAuth-токена -
Вы используете сервисный аккаунт (service account) → выдаётся токен SA
Этот OAuth-поток создаёт реальные, поддающиеся аудиту события. В частности, когда пользователь входит в систему, OAuth-сервер создаёт ресурс oauthaccesstoken. При выходе он удаляется. Эти события попадают в журнал аудита openshift-apiserver со всеми подробностями.
Вот как выглядит событие входа в сыром журнале аудита:
{
"verb": "create",
"requestURI": "/apis/oauth.openshift.io/v1/oauthaccesstokens",
"user": {
"username": "system:serviceaccount:openshift-authentication:oauth-openshift"
},
"requestObject": {
"userName": "kubeadmin",
"clientName": "console"
}
}
Поле clientName — настоящая находка: оно сообщает ровно то, каким способом пользователь прошёл аутентификацию:
| clientName | Метод |
|---|---|
|
Веб-консоль (браузер) |
|
|
любое другое значение |
API-токен |
Проблема реального IP-адреса в OpenShift
Есть один подводный камень: стандартный Ingress OpenShift (HAProxy) работает в режиме TLS passthrough (be_tcp). Это означает, что HAProxy никогда не завершает TLS-соединение — он просто пробрасывает сырые TCP-пакеты к oauth-openshift. Результат: все события входа показывают внутренний IP-адрес HAProxy, а не реальный IP клиента.
Чтобы исправить это, мы настроили Nginx в качестве L7 reverse proxy перед OAuth-эндпоинтом с добавлением заголовка X-Forwarded-For и сконфигурировали IngressController с параметром forwardedHeaderPolicy: Append. Затем мы добавили в коллектор XFF-кэш, который сопоставляет записи журнала доступа HAProxy (где реальный IP есть) с событиями OAuth по временной метке.
Что мы фиксируем в OpenShift
✅ События входа (веб-консоль / CLI / API-токен)
✅ События выхода
✅ Неудачные попытки входа
✅ Реальный IP клиента (при наличии Nginx reverse proxy)
✅ Длительность сессии (вход → выход)
Kubernetes: понятия «вход» нет вовсе
В ванильном Kubernetes нет встроенного OAuth-сервера. Нет команды login, нет обмена токенами, нет понятия сессии. Аутентификация в Kubernetes работает через:
-
X.509-сертификаты клиента —
system:adminв kubeconfig использует сертификат -
Токены сервисных аккаунтов — поды используют JWT-токены, монтируемые в рантайме
-
OIDC — опциональная внешняя интеграция
-
Файлы статических токенов — устаревший механизм, применяется редко
Когда system:admin выполняет kubectl get pods, никакого события «входа» не возникает. Сертификат просто предъявляется с каждым запросом — как ключ, который вы держите в руке. Нет момента «входа в кластер»: вы либо постоянно предъявляете действительный сертификат, либо нет.
# Это НЕ вход — это просто API-вызов, аутентифицированный сертификатом
kubectl get pods -n audit-vision
В журнале аудита Kubernetes нет ни oauthaccesstokens, ни ресурса login, ни какого-либо понятия сессии.
Открытие: credential-id
Копаясь в журналах аудита k3s в попытке понять, как идентифицировать «сессии», мы обнаружили кое-что интересное в поле user.extra:
{
"user": {
"username": "system:admin",
"groups": ["system:masters", "system:authenticated"],
"extra": {
"authentication.kubernetes.io/credential-id": [
"X509SHA256=7095e7ebb6c0f08fb8c1a1151246adfd32bf243bcac72eddcd5e67ebd0ee33dd"
]
}
}
}
Поле credential-id — это SHA256-отпечаток X.509-сертификата, которым был аутентифицирован запрос. Он уникален для каждого сертификата, а значит — уникален для каждого контекста kubeconfig.
Это дало нам эвристику «входа»: первый API-запрос с новым credential-id = начало сессии.
Реализация
Мы добавили Extra map[string][]string в нашу структуру AuditUser, извлекаем credential-id в нормализаторе и строим in-memory кэш с ключом actor + ":" + credentialID:
func extractK8sSessionEvent(ne model.NormalizedEvent) (model.AuthEvent, bool) {
if ne.ActorType != "human" {
return model.AuthEvent{}, false
}
credID := ne.Annotations["authentication.kubernetes.io/credential-id"]
if credID == "" {
return model.AuthEvent{}, false
}
cacheKey := ne.Actor + ":" + credID
// Видели этот credential за последние 8 часов? Это не новая сессия.
if v, found := credentialCache.Load(cacheKey); found {
if time.Since(v.(time.Time)) < 8*time.Hour {
return model.AuthEvent{}, false
}
}
credentialCache.Store(cacheKey, time.Now())
return model.AuthEvent{
Actor: ne.Actor,
Method: detectLoginMethod(ne.UserAgent, ne.Source),
SourceIP: ne.SourceIP,
EventType: "login",
Success: true,
}, true
}
Функция detectLoginMethod разбирает User-Agent:
func detectLoginMethod(userAgent, source string) string {
ua := strings.ToLower(userAgent)
switch {
case strings.Contains(ua, "kubectl/"):
return "oc-cli" // kubectl
case strings.Contains(ua, "mozilla/") || strings.Contains(ua, "chrome/"):
return "web-console" // браузер (k8s dashboard)
default:
return "api-token" // скрипты, CI/CD
}
}
Что мы фиксируем в Kubernetes
✅ «Начало сессии» (первый запрос с новым credential-id)
✅ Реальный IP клиента (kube-apiserver видит его напрямую — прокси не нужен!)
✅ Метод аутентификации (kubectl / браузер / API)
❌ Нет событий выхода (сертификаты не истекают в середине сессии)
❌ Нет неудачных попыток входа (неверные сертификаты отклоняются на уровне TCP до стадии аудита)
Примечательно, что в Kubernetes реальный IP-адрес доступен бесплатно — потому что kubectl подключается напрямую к kube-apiserver:6443, минуя какой-либо OAuth-прокси. Никакого Nginx, никакого HAProxy, никаких манипуляций с XFF не требуется.
Сравнение: фундаментальное различие
| OpenShift | Kubernetes | |
|---|---|---|
Механизм аутентификации |
Встроенный OAuth 2.0-сервер |
X.509-сертификаты / токены SA / OIDC |
Событие входа |
|
Аналога нет |
Событие выхода |
|
Аналога нет |
Понятие сессии |
Нативное |
Эвристическое (credential-id) |
Отслеживание реального IP |
Требует L7-прокси + XFF |
Бесплатно (прямой TCP до apiserver) |
Неудачная аутентификация видна в аудите |
Да (через лог oauth-server) |
Нет (TLS отклоняет до аудита) |
Определение метода |
Поле |
Разбор User-Agent |
Определение платформы в интерфейсе
Поскольку AuditRadar поддерживает обе платформы, мы добавили переменную окружения PLATFORM, которую читает UI для переключения тем и подписей. На развёртываниях OpenShift в заголовке отображается красный значок OCP; на Kubernetes — синий K8S. CSS-переменные переключаются соответственно:
/* По умолчанию: красный OpenShift */
:root { --accent: #ee0000; }
/* Kubernetes: синий */
body.k8s { --accent: #326CE5; --bg: #08101a; }
Это задаётся через значения Helm в соответствующих чартах:
# Helm/audit-radar-k8s/values.yaml
env:
- name: PLATFORM
value: kubernetes
Что это означает для команд безопасности
Если вы проводите аудит соответствия (SOC2, PCI-DSS) и вам нужно ответить на вопрос «кто входил в кластер и когда»:
На OpenShift — вы получаете чёткий журнал входов и выходов с временными метками, методом и (при правильной настройке прокси) реальными IP-адресами. Это напрямую соответствует традиционным требованиям к журналам доступа.
На Kubernetes — вам придётся принять тот факт, что «вход» здесь — это условность, а не реальное событие. Подход на основе credential-id даёт осмысленные границы сессий, однако событий неудачной аутентификации и сигналов о выходе нет. Для полноценного отслеживания пользователей отраслевой ответ — интеграция OIDC с внешним провайдером (Dex, Keycloak, Okta), но это отдельная настройка, которой большинство самоуправляемых кластеров из коробки не имеют.
Попробуйте сами
AuditRadar — проект с открытым исходным кодом: github.com/vsenatorov/auditvision
В репозитории включены Helm-чарты как для OpenShift, так и для Kubernetes. Страница Logins работает на обеих платформах — просто означает немного разные вещи на каждой из них.
Если вы работаете с k3s, k8s или OpenShift и хотите видеть в реальном времени, кто и что делает в вашем кластере — попробуйте.
Виктор Сенаторов — старший архитектор в Red Hat. Разрабатывает инструменты безопасности с открытым исходным кодом для Kubernetes и OpenShift.