Skip to main content
Version: 1.0

Security

How KubeOpera actually authenticates and authorizes requests today — not a generic checklist. Every claim below traces to real code, not aspirational config.

Authentication Model​

There's no NextAuth.js, no third-party session library — auth-service is a custom Go identity provider that issues HMAC-signed JWTs. Every backend service that requires auth validates against the same shared signing secret (AUTH_JWT_ACCESS_SECRET, distributed as the shared-jwt-access-secret Kubernetes secret) — services check the signature locally rather than calling back into auth-service per request. auth-service also exposes GET /api/v1/auth/introspect for callers (like the frontend) that don't hold the signing secret directly and need auth-service's own opinion on a token.

OAuth2 login is implemented for Google, alongside email/password with optional TOTP MFA. GitHub and Keycloak are declared as supported providers in the domain model but have no working implementation — only "google" is handled in OAuthService. The Google flow itself is a plain OAuth2 authorize/token-exchange/userinfo-REST-call sequence (golang.org/x/oauth2) — it does not verify an ID token via JWKS, since Google's REST userinfo endpoint is trusted directly instead. Worth knowing before extending this to a generic customer OIDC provider for SSO: a real OIDC integration needs ID token verification, which this scaffolding doesn't have yet.

Per-Service Authorization​

As of this writing, every backend service with a mutating (POST/PUT/PATCH/DELETE) route requires authentication. This wasn't always true — a security audit found 25 of 33 backend services had no inbound authentication of any kind, and closing that gap was the bulk of the platform's POC→MVP security remediation work.

The JWT-checking middleware (pkg/authmiddleware in the o-apps monorepo) is shared source, not independently maintained per service — each service is its own Go module with an isolated CI build context, so a real cross-module import isn't available without larger infra changes. Instead, one canonical copy is propagated via a sync script, and each consuming service's CI diffs its copy against the canonical source and fails the build if they've drifted. As of this writing, 12 services consume it: action-agent-srv, nodes-manager, agent-runtime, app-advisor-srv, course-service, customer-service, predictive-scaler, kubeopera-ai, analysis-agent-srv, observability-agent-srv, rca-engine, recommendation-agent-srv.

A handful of services intentionally remain open — they're genuinely read-only telemetry endpoints (no mutating route exists at all), and gating them uniformly was explicitly rejected in favor of prioritizing write-capable services first.

Two services use a different scheme​

security-api and cicd-gateway validate a static shared API_KEY (via X-Api-Key header) instead of a user JWT — they're called by service-to-service integrations (the security scanning pipeline, CI/CD) rather than end users. security-collector and security-cron use the same API-key pattern for their own inbound routes.

Service-to-service calls​

agent-runtime (the AI tool-calling backend behind every agentic feature) has no per-tenant context of its own, so when it calls a JWT-protected service it mints a short-lived (5-minute) token for a platform-wide "service" identity — empty tenant_id, matching how kubeopera-api's tenant-scoping SQL treats an empty tenant as "cluster-wide." This is a deliberate interim choice (local minting, reusing the existing shared secret) over the architecturally cleaner but heavier long-term option — a proper OAuth2 client_credentials grant against auth-service, which doesn't exist yet.

Tenant Isolation​

Multi-tenant data access (deployments, apps, and similar tenant-scoped resources in kubeopera-api) is enforced at the SQL layer: AND ($n = '' OR tenant_id = $n). An empty tenant parameter means "no filter" — deliberately, for platform-wide/admin callers — so every handler that accepts a tenant-scoped resource ID must extract and pass the caller's real tenant ID, not leave it empty. A cross-tenant IDOR audit found and fixed the two spots this wasn't happening; a follow-up audit of the rest of kubeopera-api's single-record lookups found no further gaps.

Rate Limiting​

Login and registration attempts are rate-limited via a sliding-window counter (IncrementWithExpiry, TTL resets on every call). auth-service doesn't talk to Redis directly for this — it calls cache-service over HTTP, which owns the actual Redis-or-in-memory decision. This keeps no service tightly coupled to a specific cache backend.

SSRF Protection (Frontend)​

Every backend call the Next.js frontend makes goes through a server-side proxy layer (app/api/_lib/upstream.ts, proxy.ts) that validates the target host against an explicit allowlist (ALLOWED_UPSTREAM_EXACT / ALLOWED_UPSTREAM_SUFFIXES) before the request ever leaves the server. A misconfigured or compromised environment variable falls back to localhost rather than letting a proxy route reach an arbitrary host. New backend integrations must add their hostname to this allowlist before their proxy route works at all.

The one route that doesn't go through the generic proxy — app/api/ai/chat/route.ts, which fans out to roughly 15 different backend services directly since it's not a simple 1:1 proxy — resolves each upstream through the same allowlist function individually, and forwards either the caller's own verified JWT or a service API key depending on which auth scheme the target service uses.

Known Gaps​

Documented here deliberately, not hidden — these are real, current limitations, not solved problems:

  • mTLS is not implemented. The security posture pipeline (security-cron → security-collector → security-api) authenticates via the shared API_KEY scheme above, not mutual TLS. A prior "mTLS" code path in auth-service exists but has never been functional — its certificate validation doesn't actually check against a CA — and none of the three security services have any TLS server code to build on. cert-manager is available as cluster infrastructure if this is picked up later, but its current issuers are public-facing (Let's Encrypt), not suited to internal mTLS without additional setup.
  • SSO (OIDC + SAML 2.0) is not yet built. auth-service's existing OAuth2 scaffolding (Google/GitHub/Keycloak) extends naturally to a generic OIDC identity provider; SAML 2.0 has no existing scaffolding to build on and is net-new work.
  • A handful of backend services have no auth at all, by design — they expose only read-only telemetry with no mutating route. This is a deliberate prioritization choice (write-capable services first), not an oversight, but it means those endpoints are readable by anyone who can reach them on the network.
  • agent-runtime's own inbound API had no auth until this was found and fixed — it's mentioned here because it's the kind of gap this list exists to surface early, not because it's still open today.

Next Steps​