Overview
NeoSC implements a Zero Trust security architecture where no user or service is trusted by default, regardless of network location. Every request is authenticated and authorized based on identity, context, and policy.
Zero Trust Principles
Core Tenets
Never Trust, Always Verify Every request must be authenticated and authorized, even from internal networks
Least Privilege Access Users and services receive minimum permissions required for their role
Assume Breach Architecture designed assuming attackers may already be inside the network
Identity-Centric Security Security decisions based on verified identity, not network location
Architecture Components
Identity Provider (Zitadel)
Role : Source of truth for user identity and authentication
Issues OIDC tokens with user claims and roles
Manages user lifecycle and multi-factor authentication
Provides centralized identity management
Supports JIT (Just-In-Time) provisioning for demo sessions
Key Claims in Token :
{
"sub" : "user-id-12345" ,
"email" : "user@company.com" ,
"name" : "John Doe" ,
"urn:zitadel:iam:org:project:{PROJECT_ID}:roles" : {
"admin" : {},
"neosc" : {}
}
}
Zero Trust Proxy (Pomerium)
Role : Policy enforcement point for all application access
Pomerium sits between users and applications, enforcing access policies:
Intercepts all requests to protected applications
Validates session cookies and OIDC tokens
Enforces access policies based on identity and context
Injects identity headers to upstream applications
Logs all access attempts for audit
# Example Pomerium policy
policy :
- from : https://portal.kappa4.com
to : http://frontend:3000
# Only authenticated users
allow_any_authenticated_user : false
# Require specific roles
allowed_idp_claims :
urn:zitadel:iam:org:project:roles :
- admin
- neosc
- user
# Inject identity headers
pass_identity_headers : true
set_request_headers :
X-Pomerium-Email : "${pomerium.email}"
X-Pomerium-User : "${pomerium.user}"
X-Pomerium-Groups : "${pomerium.groups}"
Protected Applications
Role : Trust identity headers from Pomerium, no direct authentication
Applications never see user credentials - they only receive verified identity information via HTTP headers:
# Backend FastAPI example
async def verify_pomerium_headers (
x_pomerium_email : Optional[ str ] = Header( None ),
x_pomerium_user : Optional[ str ] = Header( None )
):
if not x_pomerium_email:
raise HTTPException( status_code = 401 , detail = "Unauthorized" )
return {
"email" : x_pomerium_email,
"user_id" : x_pomerium_user
}
@app.get ( "/api/user/profile" )
async def get_profile ( user : dict = Depends(verify_pomerium_headers)):
# user is already authenticated by Pomerium
return await fetch_user_profile(user[ "email" ])
Authentication Flow
Initial Access Request
User Pomerium Zitadel Application
│ │ │ │
│─────GET /────────────▶│ │ │
│ │ [No valid session] │ │
│ │ │ │
│◀──302 to gate────────│ │ │
│ │ │ │
│─GET /oauth2/sign_in──▶│ │ │
│ │ │ │
│ │──302 to Zitadel────▶│ │
│◀──────────────────────────────302──────────│ │
│ │ │ │
│──Login Form──────────────────────────────▶│ │
│◀─────────────────────────────────────────│ │
│ │ │ │
│──Credentials─────────────────────────────▶│ │
│ │ │ [Validate] │
│ │◀──code─────────────│ │
│◀──302 callback───────│ │ │
│ │ │ │
│─GET /callback?code───▶│ │ │
│ │──Exchange code─────▶│ │
│ │◀──tokens────────────│ │
│ │ [Create session] │ │
│ │ [Set cookie] │ │
│◀──302 to app─────────│ │ │
│ │ │ │
│─GET / [+ cookie]─────▶│ [Validate session] │ │
│ │ [Check policy] │ │
│ │ [Inject headers] │ │
│ │─────────────────────────────────────────▶│
│ │ │ │
│◀──────────────────────────────────────────────────200 OK───────│
Session Cookie
Pomerium sets a session cookie _pomerium on domain .kappa4.com:
Domain : .kappa4.com (valid for all subdomains)
Duration : 8 hours
Flags : HttpOnly, Secure, SameSite=Lax
Contents : Encrypted session token
This provides Single Sign-On (SSO) across all NeoSC services:
portal.kappa4.com ← Same session cookie
api.portal.kappa4.com ← Same session cookie
admin.portal.kappa4.com ← Same session cookie
workspace.portal.kappa4.com ← Same session cookie
Access Control Policies
Role-Based Access Control (RBAC)
Pomerium enforces access based on roles defined in Zitadel:
Role Access Level Routes adminFull access All routes including admin panel neoscStandard user Portal, API, workspace viewer userBasic access Portal, API (read-only) demo-userTemporary Workspace viewer only
Policy Examples
Portal Access
Admin Panel
Workspace Viewer
# portal.kappa4.com - Standard users
- from : https://portal.kappa4.com
to : http://frontend:3000
allowed_idp_claims :
urn:zitadel:iam:org:project:roles :
- admin
- neosc
- user
pass_identity_headers : true
cors_allow_preflight : true
allow_websockets : true
# admin.portal.kappa4.com - Admin only
- from : https://admin.portal.kappa4.com
to : http://backend:8001
allowed_idp_claims :
urn:zitadel:iam:org:project:roles :
- admin
pass_identity_headers : true
timeout : 30s
# workspace.portal.kappa4.com - Active demo session required
- from : https://workspace.portal.kappa4.com
to : http://kasm-proxy:6900
# Require demo session claim
allowed_idp_claims :
neosc:demo_session_id :
- "*" # Any valid session ID
set_request_headers :
X-Session-ID : "${pomerium.claims.neosc:demo_session_id}"
X-User-Email : "${pomerium.email}"
X-Workspace-Type : "${pomerium.claims.neosc:workspace_type}"
allow_websockets : true
allow_spdy : true
timeout : 0s # No timeout for streaming
Context-Aware Policies
Pomerium can enforce policies based on request context:
# Advanced policy with context
policy :
- from : https://portal.kappa4.com
to : http://frontend:3000
policy :
# Check token hasn't expired
- allow :
and :
- claim/neosc:demo_expires_at :
is : after_now
# Rate limiting per user
- allow :
and :
- user :
is : "{{ .User.Email }}"
rate_limit :
requests_per_second : 10
burst : 20
# Time-based access (business hours only)
- allow :
and :
- day_of_week :
is_in : [ "monday" , "tuesday" , "wednesday" , "thursday" , "friday" ]
- time :
after : "08:00:00"
before : "20:00:00"
timezone : "America/Mexico_City"
Network Segmentation
Docker Networks
Services are isolated using Docker networks:
networks :
proxy :
# Public network - Pomerium communicates with external world
driver : bridge
internal :
# Private network - services NOT exposed to external
driver : bridge
internal : true # No external connectivity
Network Topology :
┌─────────────────────────────────────────────────────────┐
│ External Network │
└───────────────────────┬─────────────────────────────────┘
│
┌────────────▼──────────────┐
│ Pomerium │ ◄── On both networks
│ (proxy + internal) │
└────────────┬──────────────┘
│
┌───────────────────────▼──────────────────────────────────┐
│ Internal Network (isolated) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Frontend │ │ Backend │ │ MongoDB │ │
│ │ :3000 │ │ :8001 │ │ :27017 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ NO EXTERNAL ACCESS - Only via Pomerium │
└──────────────────────────────────────────────────────────┘
Key Security Features
No Direct Access
Frontend, backend, and database are never exposed directly to the internet. All access goes through Pomerium.
Mutual TLS
Pomerium can enforce mutual TLS (mTLS) for service-to-service communication within the internal network.
Service Isolation
Each service runs in its own container with resource limits and no unnecessary privileges.
Just-In-Time (JIT) Access
For demo sessions, NeoSC implements JIT provisioning:
JIT Workflow
User requests demo access
Zitadel Action creates temporary role (e.g., demo-user-12345)
Role expires after 30 minutes
Custom claims added to token:
{
"neosc:demo_session_id" : "demo-1234567890-abc" ,
"neosc:demo_expires_at" : "2026-03-05T12:30:00Z" ,
"neosc:workspace_type" : "linux" ,
"neosc:max_duration" : 1800
}
Backend provisions isolated workspace
After expiration, role is automatically revoked
Automatic Cleanup
# Backend schedules cleanup when session expires
async def schedule_session_cleanup ( session_id : str , expires_at : datetime):
delay = (expires_at - datetime.now(timezone.utc)).total_seconds()
await asyncio.sleep(delay)
# Cleanup resources
await delete_demo_container(session_id)
await revoke_netbird_peer(session_id)
await remove_jit_role(session_id)
# Update session status
await db.demo_sessions.update_one(
{ "session_id" : session_id},
{ "$set" : { "status" : "expired" , "ended_at" : datetime.now(timezone.utc)}}
)
Security Boundaries
Trust Boundaries
╔═══════════════════════════════════════════════════════╗
║ UNTRUSTED (Internet) ║
╚════════════════════╦══════════════════════════════════╝
║
▼
┌─────────────────────────┐
│ Pomerium │ ◄── Trust boundary enforced here
│ (Authentication + │
│ Authorization) │
└─────────────────────────┘
║
▼
╔════════════════════════════════════════════════════════╗
║ TRUSTED (Internal Network) ║
║ Applications trust Pomerium-injected headers ║
╚════════════════════════════════════════════════════════╝
Security Assumptions
Critical Security Requirement : Applications in the internal network MUST only accept requests from Pomerium, never directly from users.
Backend validates X-Pomerium-* headers are present
If TRUST_POMERIUM_HEADERS=false, backend performs additional JWT validation
Network policies prevent direct access to internal services
Monitoring & Audit
Access Logs
Pomerium logs all access decisions:
{
"level" : "info" ,
"timestamp" : "2026-03-05T10:15:30Z" ,
"user" : "user@company.com" ,
"email" : "user@company.com" ,
"method" : "GET" ,
"path" : "/api/workspace/12345" ,
"host" : "api.portal.kappa4.com" ,
"decision" : "allow" ,
"reason" : "matched_policy" ,
"duration_ms" : 45 ,
"status" : 200
}
Audit Trail
All authentication and authorization events are logged for compliance:
User login/logout
Policy evaluation results
Access grants and denials
Session creation and expiration
Role assignments and revocations
Metrics
Key metrics to monitor:
# Authentication success rate
rate(pomerium_authenticate_success_total[5m]) /
rate(pomerium_authenticate_total[5m])
# Authorization denials (potential attacks)
rate(pomerium_authorize_deny_total[5m])
# Active sessions
pomerium_active_sessions
# Average policy evaluation time
rate(pomerium_policy_evaluation_duration_seconds_sum[5m]) /
rate(pomerium_policy_evaluation_duration_seconds_count[5m])
Best Practices
Minimize Token Lifetime Keep access tokens short-lived (30 min) and refresh tokens long-lived for better security.
Principle of Least Privilege Grant users minimum roles required. Use JIT provisioning for temporary access.
Defense in Depth Layer multiple security controls: network isolation, authentication, authorization, and audit.
Regular Audits Review access logs and policies regularly to detect anomalies and refine controls.
Next Steps
Pomerium Configuration Deep dive into Pomerium policies and routes
NetBird Security Learn about network-level isolation with NetBird