Skip to content

Security Overview

This document describes Shoehorn’s security model: how authentication, authorization, tenant isolation, and secret handling work end to end.

Shoehorn supports two authentication methods:

For interactive users accessing the web UI:

  1. User clicks Sign In
  2. Redirected to OIDC provider (Zitadel, Okta, Entra ID)
  3. After authentication, redirected back with tokens
  4. Session stored in encrypted HTTP-only cookie
  5. Tokens are automatically refreshed before expiration

Cookie properties:

  • HttpOnly - Not accessible via JavaScript
  • SameSite=Lax - CSRF protection
  • Secure - HTTPS only (production)
  • Encrypted with AUTH_ENCRYPTION_KEY

For automation, CI/CD, and machine-to-machine access:

Terminal window
curl https://shoehorn.example.com/api/v1/entities \
-H "Authorization: Bearer shp_svc_xxxxxxxxxxxx"

See API Keys for details.

Session Users: Role-Based Access Control (RBAC)

Section titled “Session Users: Role-Based Access Control (RBAC)”

Session-authenticated users are authorized via Cerbos RBAC:

  1. User’s roles are loaded from the database
  2. Each API request is evaluated against Cerbos policies
  3. Policies check: user roles, resource type, action, and tenant

See Roles and RBAC for the full role hierarchy.

API key requests are authorized via scopes:

  1. Key’s scopes are loaded from the database
  2. Each request checks if the required scope is present
  3. Scope hierarchy allows higher scopes to grant lower access

See API Keys for scope details.

  • Users with no roles: 403 Forbidden (no implicit permissions)
  • API keys with insufficient scopes: 403 Forbidden
  • Unknown environment: CORS denies all origins

Shoehorn uses PostgreSQL Row-Level Security (RLS) for database-level tenant isolation:

  • Every data table has a tenant_id column
  • RLS policies filter all queries by the authenticated tenant
  • The runtime database user (app_user) has NOBYPASSRLS
  • Tenant ID is extracted from JWT claims, never from client input

See Multi-Tenant Security for details.

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 RLS
FeatureDescription
OIDC authenticationStandards-based identity via Zitadel, Okta, Entra ID
Cerbos RBACPolicy-as-code authorization
API key scopesFine-grained programmatic access
Row-Level SecurityDatabase-level tenant isolation
Encrypted sessionsAES-encrypted session cookies
Rate limitingPer-IP and per-key rate limits on auth endpoints
CORS protectionConfigurable allowed origins, fail-closed default
Webhook verificationHMAC-SHA256 signature validation
Structured loggingJSON audit logs with user context
Enumeration preventionIdentical error messages for all auth failures

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:

FeatureDescription
Token redactionAPI tokens are wrapped in a RedactedString type that prevents accidental logging via fmt, zap, or JSON serialization
TLS enforcementHTTPS is strongly recommended. The agent warns on http:// endpoints. TLS 1.2 minimum is enforced on all HTTP clients
No redirect followingHTTP clients reject redirects to prevent Bearer token leakage to unintended hosts
URL validationAPI endpoint URLs are validated at startup — only http:// and https:// schemes are accepted, embedded credentials are rejected
Error sanitizationAPI error response bodies are sanitized (Authorization/Bearer lines stripped) before inclusion in log messages
Annotation filteringSensitive K8s annotations (kubectl.kubernetes.io/last-applied-configuration) are stripped before pushing to the API to prevent leaking environment variable secrets
Minimal RBACRead-only ClusterRole (get, list, watch) with no access to Secrets or ConfigMaps
Non-root containerRuns as UID 1000 with all capabilities dropped, read-only root filesystem, and seccomp RuntimeDefault
Response limitsError responses capped at 512 bytes, success response drain capped at 1MB

Before deploying to production, verify:

  • AUTH_ENCRYPTION_KEY is set (32-byte hex)
  • ENVIRONMENT is set to production
  • ALLOWED_ORIGINS is 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