Skip to main content

Security Architecture

Overview

Centralize Management System menerapkan security secara berlapis:

  1. Authentication - Keycloak SSO + JWT
  2. Authorization - RBAC (Role-Based Access Control)
  3. Network Security - Docker network isolation
  4. Data Security - Encryption at rest & in transit
  5. Audit Trail - Immutable audit logging

Authentication Flow

┌──────────┐     ┌──────────┐     ┌──────────┐
│ Frontend │────▶│ Backend │────▶│ Keycloak │
│ │ │ │ │ │
│ Login │ │ Password │ │ Validate │
│ Form │ │ Grant │ │ Credentials│
└──────────┘ └──────────┘ └──────────┘
│ │
│◀───────────JWT Token─────────────│
│ │
│─────Bearer Token────────────────▶│
│ │
│◀─────────User Info───────────────│

Login Process

  1. User enters credentials di Frontend
  2. Frontend mengirim request ke Backend /api/login
  3. Backend melakukan Password Grant ke Keycloak
  4. Keycloak return access_token + refresh_token
  5. Backend return token ke Frontend
  6. Frontend menyimpan token di localStorage
  7. Semua subsequent requests menggunakan Authorization: Bearer <token>

Token Validation

// Backend JWT validation
func (s *Server) withAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := bearerToken(r.Header.Get("Authorization"))
if token == "" {
writeError(w, http.StatusUnauthorized, "missing bearer token")
return
}

claims, err := s.verifier.Validate(token)
if err != nil {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}

// Extract tenant_id from claims
tenantID := s.cfg.DefaultTenant
if tid, ok := claims["tenant_id"].(string); ok && tid != "" {
tenantID = tid
}

ctx := contextWithClaims(r.Context(), claims)
ctx = context.WithValue(ctx, "tenant_id", tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

Authorization (RBAC)

Role-Based Access Control

Sistem menggunakan 7 predefined roles:

RoleDescriptionDefault Permissions
executiveExecutive managementdashboard:read, analytics:read
it-opsIT Operationslogs:read, dashboard:read, alerts:read
network-infraNetwork & Infrastructurelogs:read, dashboard:read, infra:read
securitySecurity teamalerts:read, alerts:write, audit:read
complianceCompliance teamaudit:read, audit:export, compliance:read
helpdeskHelp desk supportlogs:read, alerts:read
adminSystem administratorFull access

Permission Model

Permissions diorganisasi per resource dan action:

-- Permission structure
resource:action

-- Examples
logs:read -- Read logs
logs:write -- Write logs (if applicable)
alerts:read -- Read alerts
alerts:write -- Create/update alerts
audit:read -- Read audit logs
audit:export -- Export audit logs
dashboard:read -- Read dashboard data
analytics:read -- Read analytics
tenants:manage -- Manage tenants
users:manage -- Manage users
roles:manage -- Manage roles

Middleware Stack

// Permission check middleware
func (s *Server) requirePermission(appSlug, resource, action string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := tenantFromContext(r.Context())
username := usernameFromContext(r.Context())

allowed, err := s.db.CheckPermission(username, tenantID, appSlug, resource, action)
if err != nil {
writeError(w, http.StatusInternalServerError, "permission check failed")
return
}
if !allowed {
writeError(w, http.StatusForbidden, "permission denied")
return
}

next.ServeHTTP(w, r)
})
}
}

Route Permission Mapping

RoutePermissionDescription
GET /api/v1/logs/*logs:readLog viewing
GET /api/v1/alerts/*alerts:readAlert listing
POST /api/v1/alerts/*alerts:writeAlert management
GET /api/v1/log-audit/*audit:readAudit log viewing
GET /api/v1/log-audit/export/*audit:exportAudit log export
GET /api/v1/dashboard/*dashboard:readDashboard data
GET /api/v1/analytics/*analytics:readAnalytics data
POST /api/tenantstenants:manageTenant CRUD
POST /api/usersusers:manageUser CRUD
POST /api/rolesroles:manageRole CRUD

Network Security

Docker Network Isolation

networks:
monitoring:
name: monitoring
driver: bridge
  • Semua services berada di internal network monitoring
  • Hanya ports yang diperlukan yang di-expose ke host
  • Inter-service communication menggunakan Docker DNS

TLS Configuration

# OpenSearch HTTPS
OPENSEARCH_SSL_VERIFICATIONMODE: none # Self-signed certs untuk PoC

CORS Configuration

func (s *Server) withCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if s.originAllowed(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Tenant-ID")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
}

if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}

next.ServeHTTP(w, r)
})
}

Audit Trail

Immutable Audit Logging

Semua operasi penting dicatat ke OpenSearch audit-log-* index:

{
"@timestamp": "2026-07-06T10:30:00Z",
"actor": "admin",
"action": "login",
"target": "system",
"result": "success",
"ip": "10.8.0.48",
"details": {
"method": "password"
},
"source_type": "audit",
"app_slug": "clm-backend",
"tenant_id": "bjb-syariah"
}

Audit Events

EventDescriptionTrigger
loginUser loginPOST /api/login
searchLog searchPOST /api/v1/logs/list/paginate
exportData exportGET /api/v1/log-audit/export/*
createResource creationPOST /api/*
updateResource updatePUT /api/*
deleteResource deletionDELETE /api/*

Multi-Tenant Security

Data Isolation

// Tenant filtering in OpenSearch queries
func (s *Server) handleLogPaginate(w http.ResponseWriter, r *http.Request) {
tenantSlug := tenantSlugFromContext(r.Context())

// Try to get index patterns from database aliases first
var indices []string
if s.db != nil && tenantSlug != "" {
aliases, err := s.db.GetTenantAliases(tenantSlug)
if err == nil && len(aliases) > 0 {
for _, alias := range aliases {
if strings.HasPrefix(alias.AliasName, "clm-") {
indices = append(indices, alias.IndexPattern)
}
}
}
}

// Always pass tenantID for filtering
tenantID := tenantSlug
result, err := s.search.SearchLogs(r.Context(), indices, req, "@timestamp", tenantID)
}

OpenSearch Alias Isolation

// Create tenant-specific alias with filter
filter := map[string]any{
"term": map[string]any{
"tenant_id.keyword": body.Slug,
},
}
_ = s.search.CreateAlias(r.Context(), "clm-"+body.Slug, "clm-v2-logs-*", filter)

API Key Authentication

Untuk service-to-service communication:

func (s *Server) withAPIKey(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
if key == "" || key != s.cfg.N8NAPIKey {
writeError(w, http.StatusUnauthorized, "invalid API key")
return
}
next.ServeHTTP(w, r)
})
}

Rate Limiting

func (s *Server) withRateLimit(next http.Handler) http.Handler {
rateLimit := 100 // requests per minute
window := time.Minute

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
ip = strings.Split(forwarded, ",")[0]
}

// Check rate limit per IP
if v.count > rateLimit {
w.Header().Set("Retry-After", "60")
writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
return
}

next.ServeHTTP(w, r)
})
}

Security Best Practices

Production Recommendations

  1. TLS Termination - Use reverse proxy (nginx/traefik) for TLS
  2. Secret Management - Use Vault or AWS Secrets Manager
  3. Network Policies - Implement Kubernetes NetworkPolicy
  4. Audit Log Retention - Set appropriate retention policies
  5. Regular Key Rotation - Rotate JWT signing keys periodically
  6. MFA - Enable Multi-Factor Authentication in Keycloak
  7. WAF - Deploy Web Application Firewall

Environment Variables Security

# Use .env files (not committed to git)
# Prefix sensitive values
KEYCLOAK_CLIENT_SECRET=<secret>
OPENSEARCH_PASSWORD=<secret>
DB_PASSWORD=<secret>
N8N_API_KEY=<secret>

Next Steps