Multi-Realm Architecture Patterns
Multi-realm architecture in Keycloak enables true multi-tenancy with complete isolation between tenants. This tutorial explores patterns, implementation strategies, and best practices for building scalable multi-tenant applications.
Understanding Multi-Realm Architecture
What Are Realms?
Realms in Keycloak are:
- Complete isolation boundaries - Users, clients, and settings are separate
- Independent security domains - Each realm has its own configuration
- Scalable units - Can be distributed across clusters
- Perfect for multi-tenancy - Each tenant gets their own realm
When to Use Multiple Realms
Use multiple realms when you need:
- Complete tenant isolation - No data sharing between tenants
- Different authentication flows - Per-tenant customization
- Separate branding - Each tenant’s look and feel
- Independent user bases - No shared users
- Compliance requirements - Data residency per tenant
Organizations in Keycloak
New Alternative: Keycloak Organizations
Keycloak now provides built-in organization management features that can simplify multi-tenancy without requiring separate realms:
Organizations allow you to:
- Manage multiple tenants in a single realm - Reduces overhead and complexity
- Group users by organization - Automatic user segmentation
- Delegate administration - Organization-specific admins
- Share realm configuration - Consistent settings across tenants
- Implement organization-specific branding - Per-org customization
When to use Organizations instead of multiple realms:
- When tenants can share the same authentication flows
- When you need easier cross-tenant features
- When resource efficiency is important
- When tenants don’t require complete isolation
Learn more: Keycloak Organizations Documentation
Architecture Patterns
Pattern 1: Realm per Tenant
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Tenant A │ │ Tenant B │ │ Tenant C │
│ Realm │ │ Realm │ │ Realm │
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
│ • Users │ │ • Users │ │ • Users │
│ • Clients │ │ • Clients │ │ • Clients │
│ • Roles │ │ • Roles │ │ • Roles │
│ • Settings │ │ • Settings │ │ • Settings │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Pros:
- Complete isolation
- Per-tenant customization
- Independent scaling
- Clear security boundaries
Cons:
- More resources required
- Complex cross-tenant features
- Realm management overhead
Pattern 2: Shared Services Realm
┌─────────────────────────────────────┐
│ Shared Services Realm │
│ (Authentication, Common Services) │
└─────────────────────────────────────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Tenant A│ │ Tenant B│ │ Tenant C│
│ Realm │ │ Realm │ │ Realm │
└─────────┘ └─────────┘ └─────────┘
Use for:
- Shared administrative users
- Common services authentication
- Cross-tenant features
Pattern 3: Hierarchical Realms
┌─────────────────────┐
│ Master Realm │
├─────────────────────┤
│ Global Settings │
│ Realm Templates │
└──────────┬──────────┘
│
┌──────┴──────┐
▼ ▼
┌─────────┐ ┌─────────┐
│Regional │ │Regional │
│ Realm 1 │ │ Realm 2 │
└────┬────┘ └────┬────┘
│ │
┌──┴──┐ ┌──┴──┐
▼ ▼ ▼ ▼
┌────┐┌────┐ ┌────┐┌────┐
│Ten1││Ten2│ │Ten3││Ten4│
└────┘└────┘ └────┘└────┘
Implementation Guide
Step 1: Realm Management Service
@Service
public class RealmManagementService {
private final Keycloak keycloakAdmin;
private final RealmTemplateService templateService;
public RealmManagementService(KeycloakProperties props) {
this.keycloakAdmin = KeycloakBuilder.builder()
.serverUrl(props.getServerUrl())
.realm("master")
.clientId("admin-cli")
.clientSecret(props.getAdminSecret())
.build();
}
public RealmRepresentation createTenantRealm(TenantConfig config) {
try {
// Generate unique realm name
String realmName = generateRealmName(config.getTenantId());
// Get base template
RealmRepresentation realm = templateService.getTemplate(config.getPlan());
// Customize for tenant
realm.setRealm(realmName);
realm.setDisplayName(config.getCompanyName());
realm.setEnabled(true);
// Set realm-specific settings
customizeRealmSettings(realm, config);
// Create realm
keycloakAdmin.realms().create(realm);
// Post-creation setup
setupRealmDefaults(realmName, config);
return realm;
} catch (Exception e) {
throw new RealmCreationException("Failed to create realm", e);
}
}
private void customizeRealmSettings(RealmRepresentation realm, TenantConfig config) {
// Security settings
realm.setPasswordPolicy(getPasswordPolicyForPlan(config.getPlan()));
realm.setBruteForceProtected(true);
realm.setPermanentLockout(false);
realm.setMaxFailureWaitSeconds(900);
realm.setMaxDeltaTimeSeconds(43200);
// Token settings
realm.setAccessTokenLifespan(config.getTokenLifespan());
realm.setSsoSessionMaxLifespan(config.getSessionLifespan());
// Feature flags
Map<String, String> attributes = new HashMap<>();
attributes.put("plan", config.getPlan());
attributes.put("features", String.join(",", config.getEnabledFeatures()));
realm.setAttributes(attributes);
// Branding
if (config.getTheme() != null) {
realm.setLoginTheme(config.getTheme());
realm.setEmailTheme(config.getTheme());
}
}
private void setupRealmDefaults(String realmName, TenantConfig config) {
RealmResource realm = keycloakAdmin.realm(realmName);
// Create default roles
createDefaultRoles(realm);
// Create default groups
createDefaultGroups(realm);
// Create service accounts
createServiceAccounts(realm, config);
// Configure identity providers
if (config.getIdentityProviders() != null) {
configureIdentityProviders(realm, config.getIdentityProviders());
}
// Set up event listeners
configureEventListeners(realm);
}
private void createDefaultRoles(RealmResource realm) {
List<RoleRepresentation> defaultRoles = Arrays.asList(
createRole("tenant-admin", "Tenant Administrator"),
createRole("tenant-user", "Regular User"),
createRole("tenant-viewer", "Read-only User")
);
defaultRoles.forEach(role ->
realm.roles().create(role)
);
}
private RoleRepresentation createRole(String name, String description) {
RoleRepresentation role = new RoleRepresentation();
role.setName(name);
role.setDescription(description);
role.setComposite(false);
return role;
}
}Step 2: Dynamic Realm Routing
@Component
public class MultiRealmAuthenticationFilter extends OncePerRequestFilter {
private final RealmResolver realmResolver;
private final TokenValidator tokenValidator;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// Resolve realm from request
String realm = realmResolver.resolveRealm(request);
if (realm == null) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unable to determine realm");
return;
}
// Set realm context
RealmContext.setCurrentRealm(realm);
// Validate token for specific realm
String token = extractToken(request);
if (token != null) {
DecodedJWT jwt = tokenValidator.validateTokenForRealm(token, realm);
SecurityContext context = createSecurityContext(jwt, realm);
SecurityContextHolder.setContext(context);
}
filterChain.doFilter(request, response);
} finally {
// Clean up
RealmContext.clear();
SecurityContextHolder.clearContext();
}
}
}
@Component
public class RealmResolver {
public String resolveRealm(HttpServletRequest request) {
// Strategy 1: Subdomain-based
String host = request.getServerName();
if (host.endsWith(".myapp.com")) {
return host.substring(0, host.indexOf(".myapp.com"));
}
// Strategy 2: Path-based
String path = request.getRequestURI();
if (path.startsWith("/tenants/")) {
String[] parts = path.split("/");
if (parts.length > 2) {
return parts[2];
}
}
// Strategy 3: Header-based
String realmHeader = request.getHeader("X-Tenant-Realm");
if (realmHeader != null) {
return realmHeader;
}
// Strategy 4: JWT claim
String token = extractToken(request);
if (token != null) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("realm").asString();
} catch (Exception e) {
// Invalid token, try other strategies
}
}
return null;
}
}Step 3: Cross-Realm Communication
@Service
public class CrossRealmService {
private final Map<String, Keycloak> realmClients = new ConcurrentHashMap<>();
public void executeInRealm(String realm, Consumer<RealmResource> action) {
Keycloak client = getOrCreateClient(realm);
RealmResource realmResource = client.realm(realm);
try {
action.accept(realmResource);
} catch (Exception e) {
throw new CrossRealmException("Failed to execute in realm: " + realm, e);
}
}
public <T> T queryAcrossRealms(List<String> realms,
Function<RealmResource, T> query,
BinaryOperator<T> combiner) {
return realms.parallelStream()
.map(realm -> {
try {
Keycloak client = getOrCreateClient(realm);
return query.apply(client.realm(realm));
} catch (Exception e) {
logger.error("Failed to query realm: " + realm, e);
return null;
}
})
.filter(Objects::nonNull)
.reduce(combiner)
.orElse(null);
}
// Example: Count users across all realms
public long getTotalUserCount(List<String> realms) {
return queryAcrossRealms(
realms,
realm -> (long) realm.users().count(),
Long::sum
);
}
// Example: Search users across realms
public List<UserRepresentation> searchUsersAcrossRealms(String search) {
List<String> realms = getAllTenantRealms();
return realms.parallelStream()
.flatMap(realm -> {
try {
return getOrCreateClient(realm)
.realm(realm)
.users()
.search(search, 0, 10)
.stream()
.map(user -> {
user.setAttributes(
Map.of("source_realm", realm)
);
return user;
});
} catch (Exception e) {
return Stream.empty();
}
})
.collect(Collectors.toList());
}
}Step 4: Realm Templates
@Component
public class RealmTemplateService {
private final Map<String, RealmTemplate> templates = new HashMap<>();
@PostConstruct
public void initializeTemplates() {
templates.put("starter", createStarterTemplate());
templates.put("professional", createProfessionalTemplate());
templates.put("enterprise", createEnterpriseTemplate());
}
private RealmTemplate createStarterTemplate() {
return RealmTemplate.builder()
.name("Starter Plan Template")
.passwordPolicy("length(8) and specialChars(1) and digits(1)")
.maxUsers(1000)
.maxClients(5)
.features(Arrays.asList("basic-auth", "social-login"))
.tokenLifespan(300) // 5 minutes
.sessionLifespan(1800) // 30 minutes
.build();
}
private RealmTemplate createEnterpriseTemplate() {
return RealmTemplate.builder()
.name("Enterprise Plan Template")
.passwordPolicy("length(12) and specialChars(2) and digits(2) and upperCase(1)")
.maxUsers(-1) // Unlimited
.maxClients(-1) // Unlimited
.features(Arrays.asList(
"basic-auth",
"social-login",
"mfa",
"user-federation",
"custom-flows",
"advanced-analytics"
))
.tokenLifespan(900) // 15 minutes
.sessionLifespan(28800) // 8 hours
.identityProviders(Arrays.asList(
createSAMLProvider(),
createOIDCProvider()
))
.customAuthFlows(Arrays.asList(
"progressive-profiling",
"risk-based-auth"
))
.build();
}
public RealmRepresentation applyTemplate(String templateName, String realmName) {
RealmTemplate template = templates.get(templateName);
if (template == null) {
throw new IllegalArgumentException("Unknown template: " + templateName);
}
RealmRepresentation realm = new RealmRepresentation();
realm.setRealm(realmName);
realm.setEnabled(true);
// Apply template settings
realm.setPasswordPolicy(template.getPasswordPolicy());
realm.setAccessTokenLifespan(template.getTokenLifespan());
realm.setSsoSessionMaxLifespan(template.getSessionLifespan());
// Set attributes for feature flags
Map<String, String> attributes = new HashMap<>();
attributes.put("max_users", String.valueOf(template.getMaxUsers()));
attributes.put("max_clients", String.valueOf(template.getMaxClients()));
attributes.put("enabled_features", String.join(",", template.getFeatures()));
attributes.put("template", templateName);
realm.setAttributes(attributes);
return realm;
}
}Step 5: Monitoring and Management
@RestController
@RequestMapping("/api/admin/realms")
public class RealmManagementController {
private final RealmManagementService realmService;
private final RealmMetricsService metricsService;
@GetMapping
public ResponseEntity<List<RealmInfo>> listRealms(
@RequestParam(required = false) String status,
@RequestParam(required = false) String plan) {
List<RealmInfo> realms = realmService.getAllRealms().stream()
.filter(realm -> status == null || realm.getStatus().equals(status))
.filter(realm -> plan == null || realm.getPlan().equals(plan))
.map(this::enrichWithMetrics)
.collect(Collectors.toList());
return ResponseEntity.ok(realms);
}
@GetMapping("/{realmId}/metrics")
public ResponseEntity<RealmMetrics> getRealmMetrics(@PathVariable String realmId) {
RealmMetrics metrics = metricsService.getMetrics(realmId);
return ResponseEntity.ok(metrics);
}
@PostMapping("/{realmId}/suspend")
public ResponseEntity<Void> suspendRealm(@PathVariable String realmId) {
realmService.suspendRealm(realmId);
return ResponseEntity.noContent().build();
}
@DeleteMapping("/{realmId}")
public ResponseEntity<Void> deleteRealm(
@PathVariable String realmId,
@RequestParam(defaultValue = "false") boolean immediate) {
if (immediate) {
realmService.deleteRealmImmediately(realmId);
} else {
realmService.scheduleRealmDeletion(realmId, 30); // 30 days
}
return ResponseEntity.noContent().build();
}
private RealmInfo enrichWithMetrics(RealmInfo realm) {
try {
RealmMetrics metrics = metricsService.getQuickMetrics(realm.getId());
realm.setUserCount(metrics.getUserCount());
realm.setActiveUsers(metrics.getActiveUsers());
realm.setStorageUsed(metrics.getStorageUsed());
} catch (Exception e) {
logger.warn("Failed to get metrics for realm: " + realm.getId(), e);
}
return realm;
}
}
@Service
public class RealmMetricsService {
private final MeterRegistry meterRegistry;
private final Cache<String, RealmMetrics> metricsCache;
@Scheduled(fixedDelay = 60000) // Every minute
public void collectMetrics() {
List<String> realms = getAllRealmIds();
realms.parallelStream().forEach(realmId -> {
try {
RealmMetrics metrics = collectRealmMetrics(realmId);
metricsCache.put(realmId, metrics);
// Update Prometheus metrics
updatePrometheusMetrics(realmId, metrics);
} catch (Exception e) {
logger.error("Failed to collect metrics for realm: " + realmId, e);
}
});
}
private void updatePrometheusMetrics(String realmId, RealmMetrics metrics) {
meterRegistry.gauge("keycloak_realm_users",
Tags.of("realm", realmId), metrics.getUserCount());
meterRegistry.gauge("keycloak_realm_active_sessions",
Tags.of("realm", realmId), metrics.getActiveSessions());
meterRegistry.gauge("keycloak_realm_storage_bytes",
Tags.of("realm", realmId), metrics.getStorageUsed());
}
}Managing Branding Across Multiple Realms
Consistent Branding Strategy
When managing multiple realms, maintaining consistent branding is crucial for user experience:
Manual Approach (Traditional):
- Configure each realm individually
- Maintain separate theme deployments
- Risk of inconsistencies
- Time-consuming updates
Automated Approach (Skycloak): Skycloak provides the “Apply to All Realms” feature for efficient branding management:
-
Configure Primary Realm:
- Set up branding on your main realm
- Test and verify appearance
- Ensure all settings are correct
- Important: Click “Apply Branding” to save changes
-
Apply to All Realms:
- The button is only enabled after changes are saved
- Click “Apply to All Realms” button
- Select components to copy:
- Visual branding (logos, colors)
- Custom themes
- Login settings
- Exclude specific realms if needed
Workflow Requirement: You must apply pending changes to the current realm before using “Apply to All Realms”. The button will be disabled and show a tooltip if you have unsaved changes. This ensures the source realm has the latest configuration before propagating to other realms.
-
How It Works:
- Reads theme names from source realm’s Keycloak configuration
- Applies exact theme names to target realms
- Copies database configuration for UI consistency
- No redeployment needed - themes already exist in container
Benefits:
- Consistency: All realms have identical branding
- Efficiency: Update multiple realms in one operation
- Reliability: Direct theme name copying prevents errors
- Speed: No theme file deployments required
Branding Patterns for Multi-Realm
Pattern 1: Uniform Branding All realms share the same branding:
Main Realm (configured) → Apply to All → All realms identical
Use when: Single brand across all tenants
Pattern 2: Tiered Branding Different branding per tier:
Premium Realm → Apply to Premium Group
Standard Realm → Apply to Standard Group
Use when: Different service tiers need distinction
Pattern 3: White-Label Branding Each realm has unique branding:
Configure each realm individually
No "Apply to All" usage
Use when: Each tenant needs complete customization
Best Practices
- Test First: Always test branding on a single realm before applying to all
- Use Exclusions: Exclude development/test realms from bulk updates
- Version Control: Keep theme files in version control for rollback
- Document Changes: Track which realms have which branding version
- Monitor Application: Check logs during bulk application for any failures
Performance Optimization
Realm Caching Strategy
@Configuration
public class RealmCacheConfig {
@Bean
public CacheManager realmCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(5))
.recordStats());
return cacheManager;
}
@Bean
public RealmCache realmCache() {
return new RealmCache() {
private final LoadingCache<String, RealmData> cache = Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(2, TimeUnit.MINUTES)
.build(this::loadRealm);
private RealmData loadRealm(String realmId) {
// Load from Keycloak
return keycloakService.getRealmData(realmId);
}
@Override
public RealmData get(String realmId) {
return cache.get(realmId);
}
@Override
public void invalidate(String realmId) {
cache.invalidate(realmId);
}
};
}
}Connection Pooling
@Configuration
public class KeycloakConnectionConfig {
@Bean
public KeycloakConnectionPool connectionPool() {
return new KeycloakConnectionPool(
PoolConfig.builder()
.maxTotal(100)
.maxPerRoute(10)
.connectionTimeout(Duration.ofSeconds(5))
.socketTimeout(Duration.ofSeconds(30))
.build()
);
}
@Bean
public KeycloakClientFactory clientFactory(KeycloakConnectionPool pool) {
return new KeycloakClientFactory() {
private final Map<String, Keycloak> clients = new ConcurrentHashMap<>();
@Override
public Keycloak getClient(String realm) {
return clients.computeIfAbsent(realm, r ->
createPooledClient(r, pool)
);
}
private Keycloak createPooledClient(String realm, KeycloakConnectionPool pool) {
return KeycloakBuilder.builder()
.serverUrl(keycloakUrl)
.realm(realm)
.clientId(clientId)
.clientSecret(clientSecret)
.resteasyClient(pool.getClient())
.build();
}
};
}
}Security Considerations
Realm Isolation Enforcement
@Aspect
@Component
public class RealmIsolationAspect {
@Around("@annotation(RealmIsolated)")
public Object enforceRealmIsolation(ProceedingJoinPoint joinPoint) throws Throwable {
String currentRealm = RealmContext.getCurrentRealm();
if (currentRealm == null) {
throw new SecurityException("No realm context set");
}
// Extract realm from method arguments
Object[] args = joinPoint.getArgs();
String targetRealm = extractTargetRealm(args);
if (targetRealm != null && !targetRealm.equals(currentRealm)) {
throw new SecurityException(
"Access denied: Cannot access realm " + targetRealm +
" from realm " + currentRealm
);
}
return joinPoint.proceed();
}
}
// Usage
@Service
public class UserService {
@RealmIsolated
public User getUser(String realmId, String userId) {
// Method can only access users from current realm
return userRepository.findByRealmAndId(realmId, userId);
}
}Audit Logging
@Component
public class RealmAuditLogger {
@EventListener
public void handleRealmEvent(RealmEvent event) {
AuditEntry entry = AuditEntry.builder()
.timestamp(Instant.now())
.eventType(event.getType())
.realmId(event.getRealmId())
.userId(event.getUserId())
.details(event.getDetails())
.ipAddress(event.getIpAddress())
.userAgent(event.getUserAgent())
.build();
// Store in realm-specific audit log
auditRepository.save(entry);
// Alert on suspicious activity
if (isSuspicious(event)) {
alertService.sendSecurityAlert(entry);
}
}
private boolean isSuspicious(RealmEvent event) {
return event.getType() == EventType.REALM_DELETED ||
event.getType() == EventType.MASS_USER_DELETION ||
event.getType() == EventType.ADMIN_PERMISSION_CHANGED;
}
}Best Practices
1. Realm Naming Convention
public class RealmNamingStrategy {
public String generateRealmName(TenantInfo tenant) {
// Format: environment-region-tenantId
return String.format("%s-%s-%s",
tenant.getEnvironment().toLowerCase(),
tenant.getRegion().toLowerCase(),
tenant.getId().toLowerCase()
);
}
// Examples:
// prod-us-acme-corp
// staging-eu-test-company
// dev-asia-demo-tenant
}2. Realm Limits and Quotas
@Component
public class RealmQuotaEnforcer {
@Before("@annotation(RequiresQuotaCheck)")
public void enforceQuota(JoinPoint joinPoint) {
String realm = RealmContext.getCurrentRealm();
RealmQuota quota = quotaService.getQuota(realm);
// Check various limits
if (isCreatingUser(joinPoint) && quota.isUserLimitReached()) {
throw new QuotaExceededException("User limit reached for realm");
}
if (isCreatingClient(joinPoint) && quota.isClientLimitReached()) {
throw new QuotaExceededException("Client limit reached for realm");
}
}
}3. Backup and Recovery
@Service
public class RealmBackupService {
@Scheduled(cron = "0 0 2 * * *") // 2 AM daily
public void backupAllRealms() {
List<String> realms = realmService.getAllRealmIds();
realms.parallelStream().forEach(realmId -> {
try {
RealmRepresentation export = exportRealm(realmId);
String backupPath = storeBackup(realmId, export);
logger.info("Backed up realm {} to {}", realmId, backupPath);
} catch (Exception e) {
logger.error("Failed to backup realm: " + realmId, e);
}
});
}
public void restoreRealm(String realmId, String backupId) {
RealmRepresentation backup = loadBackup(realmId, backupId);
// Delete existing realm
keycloak.realm(realmId).remove();
// Recreate from backup
keycloak.realms().create(backup);
logger.info("Restored realm {} from backup {}", realmId, backupId);
}
}Testing Multi-Realm Applications
@SpringBootTest
public class MultiRealmIntegrationTest {
@Autowired
private RealmManagementService realmService;
private List<String> testRealms = new ArrayList<>();
@BeforeEach
public void setupTestRealms() {
// Create test realms
for (int i = 0; i < 3; i++) {
String realmId = "test-realm-" + i;
realmService.createRealm(realmId);
testRealms.add(realmId);
}
}
@AfterEach
public void cleanupTestRealms() {
testRealms.forEach(realm -> {
try {
realmService.deleteRealm(realm);
} catch (Exception e) {
// Ignore
}
});
}
@Test
public void testCrossRealmIsolation() {
// Create user in realm 1
String userId = createUser(testRealms.get(0), "user1");
// Try to access from realm 2 - should fail
RealmContext.setCurrentRealm(testRealms.get(1));
assertThrows(SecurityException.class, () -> {
userService.getUser(testRealms.get(0), userId);
});
}
@Test
public void testRealmSpecificConfiguration() {
// Configure different settings per realm
testRealms.forEach(realm -> {
RealmSettings settings = new RealmSettings();
settings.setPasswordPolicy("length(" + (8 + testRealms.indexOf(realm)) + ")");
realmService.updateSettings(realm, settings);
});
// Verify each realm has its own settings
for (int i = 0; i < testRealms.size(); i++) {
RealmSettings settings = realmService.getSettings(testRealms.get(i));
assertEquals("length(" + (8 + i) + ")", settings.getPasswordPolicy());
}
}
}Next Steps
- Cluster Management - Manage your Keycloak clusters
- Insights & Analytics - Monitor your realms
- Security Features - Protect your authentication