Why Kubernetes Has No Login — And How We Solved It for AuditRadar
When we set out to build the Logins page for AuditRadar — a real-time audit log explorer for OpenShift and Kubernetes — we hit a wall that forced us to deeply understand how authentication actually works on each platform. What started as "just add a login tracking feature" turned into a fundamental lesson about the architectural differences between OpenShift and vanilla Kubernetes.
This is the story of what we found, and how we built session detection that works on both platforms.
The Problem: "Who logged in?" is not a simple question
AuditRadar collects and displays audit events from the Kubernetes API server. We already had the Events page showing every CREATE, DELETE, PATCH in real time. The next natural feature was: show who logged in, from where, and when.
On OpenShift, this was relatively straightforward. On Kubernetes, it turned out to be architecturally impossible in the traditional sense — because Kubernetes has no concept of "login" at all.
OpenShift: A Real OAuth Server
OpenShift ships with a built-in OAuth 2.0 server (oauth-openshift) running in the openshift-authentication namespace. Every user interaction goes through it:
You open the web console → browser redirects to OAuth
You run
oc login→ CLI performs OAuth token exchangeYou use a service account → SA token is issued
This OAuth flow creates real, auditable events. Specifically, when a user logs in, the OAuth server creates an oauthaccesstoken resource. When they log out, it gets deleted. These events appear in the openshift-apiserver audit log with full details.
Here's what a login event looks like in the raw audit log:
{
"verb": "create",
"requestURI": "/apis/oauth.openshift.io/v1/oauthaccesstokens",
"user": {
"username": "system:serviceaccount:openshift-authentication:oauth-openshift"
},
"requestObject": {
"userName": "kubeadmin",
"clientName": "console"
}
}
The clientName field is gold — it tells you exactly how the user authenticated:
| clientName | Method |
|---|---|
console |
Web Console (browser) |
openshift-challenging-client |
oc login (CLI) |
| anything else | API token |
The Real IP Problem on OpenShift
One catch: OpenShift's default ingress (HAProxy) operates in TLS passthrough mode (be_tcp). This means HAProxy never terminates the TLS connection — it just forwards raw TCP packets to oauth-openshift. The result: all login events show the internal HAProxy IP, not the real client IP.
To fix this, we set up an Nginx L7 reverse proxy in front of the OAuth endpoint with X-Forwarded-For injection, and configured the IngressController with forwardedHeaderPolicy: Append. We then added an XFF cache in the collector that correlates HAProxy access log entries (which do have the real IP) with OAuth events by timestamp.
What We Capture on OpenShift
✅ Login events (web console vs CLI vs API token)
✅ Logout events
✅ Failed login attempts
✅ Real client IP (with Nginx reverse proxy)
✅ Session duration (login → logout)
Kubernetes: No Login Concept at All
Vanilla Kubernetes has no built-in OAuth server. It has no login command, no token exchange, no session concept. Authentication in Kubernetes works through:
X.509 client certificates —
system:adminin kubeconfig uses a certService Account tokens — pods use JWT tokens mounted at runtime
OIDC — optional external integration
Static token files — legacy, rarely used
When system:admin runs kubectl get pods, there is no "login" event. The certificate is simply presented with every request, like a key you hold in your hand. There is no moment of "entering the cluster" — you are always either presenting a valid cert or you're not.
# This is NOT a login — it's just an API call authenticated with a certificate
kubectl get pods -n audit-vision
The Kubernetes audit log has no oauthaccesstokens, no login resource, no session concept whatsoever.
The Discovery: credential-id
While digging through k3s audit logs trying to figure out how to identify "sessions", we found something interesting in user.extra:
{
"user": {
"username": "system:admin",
"groups": ["system:masters", "system:authenticated"],
"extra": {
"authentication.kubernetes.io/credential-id": [
"X509SHA256=7095e7ebb6c0f08fb8c1a1151246adfd32bf243bcac72eddcd5e67ebd0ee33dd"
]
}
}
}
The credential-id field is a SHA256 fingerprint of the X.509 certificate used to authenticate the request. This is unique per certificate — meaning it's unique per kubeconfig context.
This gave us our "login" heuristic: the first API request from a new credential-id = session start.
Implementation
We added Extra map[string][]string to our AuditUser struct, extracted the credential-id in the normalizer, and built an in-memory cache keyed by 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
// Seen this credential in the last 8 hours? Not a new session.
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
}
The detectLoginMethod function parses the 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" // browser (k8s dashboard)
default:
return "api-token" // scripts, CI/CD
}
}
What We Capture on Kubernetes
✅ "Session start" (first request per credential-id)
✅ Real client IP (kube-apiserver sees it directly — no proxy needed!)
✅ Authentication method (kubectl vs browser vs API)
❌ No logout events (certificates don't expire mid-session)
❌ No failed login attempts (invalid certs get TCP-rejected before audit)
Interestingly, on Kubernetes the real IP is available for free — because kubectl connects directly to kube-apiserver:6443 without going through any OAuth proxy. No nginx, no HAProxy, no XFF magic needed.
Side by Side: The Fundamental Difference
| OpenShift | Kubernetes | |
|---|---|---|
| Auth mechanism | Built-in OAuth 2.0 server | X.509 certs / SA tokens / OIDC |
| Login event | oauthaccesstokens CREATE |
No equivalent |
| Logout event | oauthaccesstokens DELETE |
No equivalent |
| Session concept | Native | Heuristic (credential-id) |
| Real IP tracking | Requires L7 proxy + XFF | Free (direct TCP to apiserver) |
| Failed auth visible in audit | Yes (via oauth-server log) | No (TLS rejected before audit) |
| Method detection | clientName field |
User-Agent parsing |
Platform Detection in the UI
Since AuditRadar supports both platforms, we added a PLATFORM environment variable that the UI reads to switch themes and labels. On OpenShift deployments, the header shows a red OCP badge; on Kubernetes it shows a blue K8S badge. The CSS vars switch accordingly:
/* Default: OpenShift red */
:root { --accent: #ee0000; }
/* Kubernetes: blue */
body.k8s { --accent: #326CE5; --bg: #08101a; }
This is set via Helm values in the respective charts:
# Helm/audit-radar-k8s/values.yaml
env:
- name: PLATFORM
value: kubernetes
What This Means for Security Teams
If you're running a compliance audit (SOC2, PCI-DSS) and you need to answer "who logged into the cluster and when":
On OpenShift — you get a clean login/logout trail with timestamps, method, and (with proper proxy setup) real IPs. This maps directly to traditional access log requirements.
On Kubernetes — you need to accept that "login" is a construct, not a fact. The credential-id approach gives you meaningful session boundaries, but there are no failed auth events and no logout signals. For real user tracking, the industry answer is OIDC integration with an external provider (Dex, Keycloak, Okta) — but that's a separate setup that most self-managed clusters don't have out of the box.
Try It Yourself
AuditRadar is open source: github.com/vsenatorov/auditvision
Helm charts for both OpenShift and Kubernetes are included. The Logins page works on both platforms — it just means something slightly different on each.
If you're running k3s, k8s, or OpenShift and want real-time visibility into who's doing what in your cluster, give it a try.
Viktor Senatorov — Senior Architect at Red Hat. Building open-source security tools for Kubernetes and OpenShift.