Skip to content

feat: Google Sign-In → Secreted Token → Free Nodecore RPC #124

@bussyjd

Description

@bussyjd

Summary

Enable authenticated access to Nodecore RPC through the local Obol Stack (obol.stack). Users authenticate via Google OAuth, and their ID token is stored in a Kubernetes secret. eRPC injects this token into upstream requests, allowing Nodecore to bypass rate limits for authenticated users.

Goals

  • Single-user authentication for local development stack
  • API key management via Kubernetes secrets
  • Auditing/logging user identity at Nodecore
  • Rate limit bypass for authenticated requests

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                           https://obol.stack                            │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────┐         ┌─────────────────┐                          │
│  │   Browser    │────────▶│   Traefik       │                          │
│  │              │         │   (TLS/mkcert)  │                          │
│  └──────────────┘         └────────┬────────┘                          │
│                                    │                                    │
│                    ┌───────────────┼───────────────┐                   │
│                    │               │               │                   │
│                    ▼               ▼               ▼                   │
│  ┌─────────────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │   Next.js Frontend  │  │    eRPC      │  │   Other      │          │
│  │   (obol-frontend)   │  │   /rpc/*     │  │   Services   │          │
│  └──────────┬──────────┘  └──────┬───────┘  └──────────────┘          │
│             │                    │                                     │
│             │ writes             │ reads env                           │
│             ▼                    ▼                                     │
│  ┌─────────────────────────────────────────────────────────────────┐  │
│  │         Kubernetes Secret: obol-oauth-token (namespace: erpc)   │  │
│  └─────────────────────────────────────────────────────────────────┘  │
│                                                                        │
│             eRPC injects header: X-Nodecore-Token                      │
│                                    │                                   │
│                                    ▼                                   │
│                          ┌──────────────────┐                         │
│                          │    Nodecore      │                         │
│                          │  (validates JWT) │                         │
│                          └──────────────────┘                         │
└─────────────────────────────────────────────────────────────────────────┘

Authentication Flow

Step-by-Step

  1. User opens https://obol.stack
  2. Clicks "Sign in with Google"
  3. Browser initiates Google OAuth PKCE flow
  4. Google redirects to https://obol.stack/api/auth/callback
  5. Next.js API route (server-side) exchanges code for tokens
  6. API route writes ID token to Kubernetes secret obol-oauth-token
  7. eRPC pod reads token from secret (via environment variable)
  8. eRPC injects X-Nodecore-Token: <id_token> on all upstream requests
  9. Nodecore validates JWT and bypasses rate limits

Important Clarification

The browser does NOT write directly to Kubernetes.
The Next.js API route (running server-side in the cluster) has Kubernetes API access and writes the secret.


Component Specifications

1. Ingress (Traefik)

Requirement Implementation
HTTPS mkcert for locally-trusted TLS certificate
Domain obol.stack (via /etc/hosts)
OAuth Redirect https://obol.stack/api/auth/callback

2. Frontend (Next.js)

New API Routes Required:

Route Purpose
GET /api/auth/login Initiates Google OAuth PKCE flow
GET /api/auth/callback Handles OAuth callback, writes token to k8s secret
POST /api/auth/refresh Refreshes token and updates secret
POST /api/auth/logout Clears token from secret

Token Management:

  • Frontend must refresh token every 45 minutes
  • Uses Google refresh token to obtain new ID token
  • Updates Kubernetes secret after each refresh

Kubernetes RBAC Required:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: oauth-token-writer
  namespace: erpc
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["obol-oauth-token"]
    verbs: ["get", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: frontend-oauth-token-writer
  namespace: erpc
subjects:
  - kind: ServiceAccount
    name: obol-frontend
    namespace: obol-frontend
roleRef:
  kind: Role
  name: oauth-token-writer
  apiGroup: rbac.authorization.k8s.io

3. eRPC Configuration

Header Injection (Native Support Confirmed):

eRPC supports custom headers on upstream requests via jsonRpc.headers with environment variable expansion using ${VAR} syntax.

# erpc.yaml
projects:
  - id: main
    upstreams:
      - id: nodecore
        endpoint: https://rpc.nodecore.io
        jsonRpc:
          headers:
            X-Nodecore-Token: "${OBOL_OAUTH_TOKEN}"

Helm Values:

# values/erpc.yaml
secretEnv:
  OBOL_OAUTH_TOKEN:
    secretKeyRef:
      name: obol-oauth-token
      key: token

4. Token Rotation

Decision Required: eRPC Reload Mechanism

Current Limitation:
eRPC expands environment variables once at startup (os.ExpandEnv()). There is no SIGHUP handler or config hot-reload support.

Options:

Option Latency Downtime Complexity Notes
A. Stakater Reloader ~30s Zero (rolling) Low Recommended for simplicity
B. Contribute SIGHUP to eRPC Immediate Zero Medium Requires upstream contribution
C. Short pod TTL Variable Zero (rolling) Low Wasteful, not recommended
D. Sidecar token injector Immediate Zero High Custom development required

Option A: Stakater Reloader (Will cause rolling restart))

apiVersion: apps/v1
kind: Deployment
metadata:
  name: erpc
  namespace: erpc
  annotations:
    secret.reloader.stakater.com/reload: "obol-oauth-token"
spec:
  # ... triggers rolling restart when secret changes

Pros:

  • No code changes to eRPC
  • Battle-tested solution
  • Zero downtime with readiness probes

Cons:

  • ~30 second propagation delay
  • Pod restart on every token refresh (every 45 min)

Option B: Contribute SIGHUP to eRPC

Add signal handler to eRPC upstream:

// Proposed change to cmd/erpc/main.go
sighup := make(chan os.Signal, 1)
signal.Notify(sighup, syscall.SIGHUP)
go func() {
    for range sighup {
        logger.Info().Msg("SIGHUP received, reloading config...")
        cfg, _ = common.LoadConfig(fs, configPath, opts)
        // Reinitialize upstreams with new config
    }
}()

Then trigger via:

kubectl exec -n erpc deploy/erpc -- kill -HUP 1

Pros:

  • Immediate token update
  • No pod restart
  • Benefits entire eRPC community

Cons:

  • Requires upstream PR and approval
  • Development and testing effort

5. Nodecore Requirements

JWT Validation:

Field Expected Value
iss (issuer) https://accounts.google.com
aud (audience) Google OAuth Client ID
exp (expiry) Must be in the future
Signature Validated against Google JWKS

JWKS Endpoint: https://www.googleapis.com/oauth2/v3/certs

Rate Limit Behavior:

  • Valid JWT → Skip rate limiting
  • Invalid/missing JWT → Apply standard rate limits

6. Kubernetes Secret Schema

apiVersion: v1
kind: Secret
metadata:
  name: obol-oauth-token
  namespace: erpc
type: Opaque
data:
  token: <base64-encoded-google-id-token>

Security Considerations

Concern Mitigation
Token exposure in logs eRPC does not log header values at INFO level
Cross-namespace access RBAC limits frontend to single secret
Token theft from secret k8s secrets encrypted at rest (k3s default)
Man-in-the-middle TLS required for all traffic
Token expiry 45-minute refresh cycle

Implementation Phases

Phase 1: Infrastructure

  • Configure mkcert TLS certificate for Traefik
  • Create obol-oauth-token secret (empty placeholder)
  • Add RBAC for cross-namespace secret access
  • Deploy Stakater Reloader (if Option A chosen)

Phase 2: Frontend OAuth

  • Configure Google OAuth Client (redirect URI: https://obol.stack/api/auth/callback)
  • Implement /api/auth/login route (PKCE flow initiation)
  • Implement /api/auth/callback route (token exchange + secret write)
  • Implement /api/auth/refresh route (background refresh)
  • Add auth state UI (signed in/out indicator)

Phase 3: eRPC Configuration

  • Update eRPC Helm values with secretEnv for OBOL_OAUTH_TOKEN
  • Add X-Nodecore-Token header to Nodecore upstream config
  • Add Reloader annotation to eRPC deployment
  • Test header injection with mock upstream

Phase 4: Nodecore Integration

  • Implement JWT validation middleware
  • Configure rate limit bypass for valid tokens
  • Add audit logging for authenticated requests

Open Decisions

# Decision Options Owner Due
1 eRPC reload mechanism A: Reloader, B: SIGHUP contribution Team TBD
2 Google OAuth Client ID source Environment var vs ConfigMap Team TBD
3 Token refresh location Frontend background task vs dedicated service Team TBD

Appendix: eRPC Header Injection Evidence

From erpc/clients/http_json_rpc_client.go:790-793:

// Add custom headers if provided
for k, v := range c.headers {
    httpReq.Header.Set(k, v)
}

From erpc/common/config.go:68:

expandedData := []byte(os.ExpandEnv(string(data)))

Environment variables using ${VAR} syntax are expanded at config load time.


Appendix: Google OAuth PKCE Flow

┌──────────┐                              ┌──────────────┐
│  Browser │                              │    Google    │
└────┬─────┘                              └──────┬───────┘
     │                                           │
     │ 1. GET /api/auth/login                    │
     │──────────────────────────────────────────▶│
     │                                           │
     │ 2. Redirect to Google (code_challenge)   │
     │◀──────────────────────────────────────────│
     │                                           │
     │ 3. User authenticates                     │
     │──────────────────────────────────────────▶│
     │                                           │
     │ 4. Redirect to callback (code)           │
     │◀──────────────────────────────────────────│
     │                                           │
     │ 5. POST /api/auth/callback               │
     │───────┐                                   │
     │       │ Exchange code for tokens          │
     │       │ (with code_verifier)              │
     │◀──────┘                                   │
     │                                           │
     │ 6. Write token to k8s secret             │
     │                                           │
     │ 7. Set session cookie, redirect to app   │
     │                                           │

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions