-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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
- User opens
https://obol.stack - Clicks "Sign in with Google"
- Browser initiates Google OAuth PKCE flow
- Google redirects to
https://obol.stack/api/auth/callback - Next.js API route (server-side) exchanges code for tokens
- API route writes ID token to Kubernetes secret
obol-oauth-token - eRPC pod reads token from secret (via environment variable)
- eRPC injects
X-Nodecore-Token: <id_token>on all upstream requests - 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.io3. 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: token4. 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 changesPros:
- 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 1Pros:
- 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-tokensecret (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/loginroute (PKCE flow initiation) - Implement
/api/auth/callbackroute (token exchange + secret write) - Implement
/api/auth/refreshroute (background refresh) - Add auth state UI (signed in/out indicator)
Phase 3: eRPC Configuration
- Update eRPC Helm values with
secretEnvforOBOL_OAUTH_TOKEN - Add
X-Nodecore-Tokenheader 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 │
│ │