Security Overview
This document describes Shoehorn’s security model: how authentication, authorization, tenant isolation, and secret handling work end to end.
Authentication
Section titled “Authentication”Shoehorn supports two authentication methods:
Session-Based (Browser)
Section titled “Session-Based (Browser)”For interactive users accessing the web UI:
- User clicks Sign In
- Redirected to OIDC provider (Zitadel, Okta, Entra ID)
- After authentication, redirected back with tokens
- Session stored in encrypted HTTP-only cookie
- Tokens are automatically refreshed before expiration
Cookie properties:
HttpOnly- Not accessible via JavaScriptSameSite=Lax- CSRF protectionSecure- HTTPS only (production)- Encrypted with
AUTH_ENCRYPTION_KEY
API Key (Programmatic)
Section titled “API Key (Programmatic)”For automation, CI/CD, and machine-to-machine access:
curl https://shoehorn.example.com/api/v1/entities \ -H "Authorization: Bearer shp_svc_xxxxxxxxxxxx"See API Keys for details.
Authorization
Section titled “Authorization”Session Users: Role-Based Access Control (RBAC)
Section titled “Session Users: Role-Based Access Control (RBAC)”Session-authenticated users are authorized via Cerbos RBAC:
- User’s roles are loaded from the database
- Each API request is evaluated against Cerbos policies
- Policies check: user roles, resource type, action, and tenant
See Roles and RBAC for the full role hierarchy.
API Keys: Scope-Based Access
Section titled “API Keys: Scope-Based Access”API key requests are authorized via scopes:
- Key’s scopes are loaded from the database
- Each request checks if the required scope is present
- Scope hierarchy allows higher scopes to grant lower access
See API Keys for scope details.
Fail-Closed
Section titled “Fail-Closed”- Users with no roles: 403 Forbidden (no implicit permissions)
- API keys with insufficient scopes: 403 Forbidden
- Unknown environment: CORS denies all origins
Multi-Tenant Isolation
Section titled “Multi-Tenant Isolation”Shoehorn uses PostgreSQL Row-Level Security (RLS) for database-level tenant isolation:
- Every data table has a
tenant_idcolumn - RLS policies filter all queries by the authenticated tenant
- The runtime database user (
app_user) hasNOBYPASSRLS - Tenant ID is extracted from JWT claims, never from client input
See Multi-Tenant Security for details.
Authentication Flow Diagram
Section titled “Authentication Flow Diagram”Browser User API Client │ │ ▼ ▼OIDC Provider API Key (Bearer token) │ │ ▼ ▼Session Cookie Middleware validates key │ (bcrypt hash check) ▼ │JWT Verification ▼ │ Scope extraction ▼ │Role Lookup (DB) ▼ │ Scope authorization ▼ │Cerbos RBAC Check ▼ │ │ ▼ ▼ SET app.current_tenant_id = <tenant> │ ▼ Query with RLSSecurity Features
Section titled “Security Features”| Feature | Description |
|---|---|
| OIDC authentication | Standards-based identity via Zitadel, Okta, Entra ID |
| Cerbos RBAC | Policy-as-code authorization |
| API key scopes | Fine-grained programmatic access |
| Row-Level Security | Database-level tenant isolation |
| Encrypted sessions | AES-encrypted session cookies |
| Rate limiting | Per-IP and per-key rate limits on auth endpoints |
| CORS protection | Configurable allowed origins, fail-closed default |
| Webhook verification | HMAC-SHA256 signature validation |
| Structured logging | JSON audit logs with user context |
| Enumeration prevention | Identical error messages for all auth failures |
K8s Agent Security
Section titled “K8s Agent Security”The Shoehorn K8s Agent connects to the API using Bearer token authentication (bcrypt-hashed on the server). The agent applies several defense-in-depth measures:
| Feature | Description |
|---|---|
| Token redaction | API tokens are wrapped in a RedactedString type that prevents accidental logging via fmt, zap, or JSON serialization |
| TLS enforcement | HTTPS is strongly recommended. The agent warns on http:// endpoints. TLS 1.2 minimum is enforced on all HTTP clients |
| No redirect following | HTTP clients reject redirects to prevent Bearer token leakage to unintended hosts |
| URL validation | API endpoint URLs are validated at startup — only http:// and https:// schemes are accepted, embedded credentials are rejected |
| Error sanitization | API error response bodies are sanitized (Authorization/Bearer lines stripped) before inclusion in log messages |
| Annotation filtering | Sensitive K8s annotations (kubectl.kubernetes.io/last-applied-configuration) are stripped before pushing to the API to prevent leaking environment variable secrets |
| Minimal RBAC | Read-only ClusterRole (get, list, watch) with no access to Secrets or ConfigMaps |
| Non-root container | Runs as UID 1000 with all capabilities dropped, read-only root filesystem, and seccomp RuntimeDefault |
| Response limits | Error responses capped at 512 bytes, success response drain capped at 1MB |
Security Checklist
Section titled “Security Checklist”Before deploying to production, verify:
-
AUTH_ENCRYPTION_KEYis set (32-byte hex) -
ENVIRONMENTis set toproduction -
ALLOWED_ORIGINSis configured - OIDC provider is properly configured
-
CERBOS_ENABLED=true - Database uses 2-user RLS model (shoehorn_user + app_user)
- All secrets are in Kubernetes Secrets (not env vars)
- TLS is enabled on ingress
- Rate limiting thresholds are configured