Hybrid SSO Architecture

Azure Primary · On-Premises Failover · Cloudflare Edge · JuiceFS + Backblaze B2

Azure Primary HA Active On-Prem Failover JuiceFS + B2
Active / Primary
Standby / Failover
HTTPS / Auth
VPN / AD Replication
JuiceFS Data Path
Backblaze B2
GitLab CI/CD
NAT Gateway
Bastion (Admin)
🌐 Internet — Users & Cloudflare Edge
👤
End Users
HTTPS · sso.zth.com
app.zth.com · Port 443
External443
🔶
Cloudflare
DNS Proxy · WAF · DDoS
Load Balancer (Health Checks)
Pool A: Azure Tunnel (Primary)
Pool B: On-Prem HAProxy (Failover)
ProxyWAFLB
🛡️
Cloudflare Tunnel
Outbound from Azure DMZ
No inbound ports needed
TLS end-to-end to HAProxy
Zero public IP exposure
Zero TrustTLS
▼ HTTPS Primary Path
☁️ Azure — Primary (Active) VNET: 10.10.0.0/16

▸ DMZ — Ingress from Cloudflare (dmz-subnet: 10.10.0.0/26)

🔶
cloudflared Connector
Debian 12 VM
Tunnel — outbound only
Forwards → HAProxy VIP 10.10.1.10
10.10.0.4
TunnelZero ExposureOutbound Only
🔁
Keepalived VIP
Floating VIP: 10.10.1.10
Runs ON HAProxy Node 1 & 2
VRRP — not a separate VM
~2s auto-failover
VRRPVIP

▸ HAProxy Layer — SSL Termination + Load Balancing (haproxy-subnet: 10.10.1.0/24)

⚙️ HAProxy Nodes — Debian 12
⚖️
HAProxy Node 1
Debian 12 · Keepalived
SSL Termination (443)
web_backend → WS 1&2 (8080)
cas_backend → CAS 1&2 (8443)
10.10.1.4
SSL TermActive/VIPLB
HA
⚖️
HAProxy Node 2
Debian 12 · Keepalived
SSL Termination (443)
web_backend → WS 1&2 (8080)
cas_backend → CAS 1&2 (8443)
10.10.1.5
SSL TermStandbyLB
HAProxy Config: Binds 443, terminates TLS. web_backend → WS 1&2 port 8080 (round-robin). cas_backend → CAS 1&2 port 8443 (round-robin + cookie affinity).

▸ Egress & Admin Access

🔀
NAT Gateway
Static Public IP (egress only)
Covers web, cas, shared subnets
nat-subnet: 10.10.5.0/27
EgressStatic IP
🔐
Azure Bastion
Browser-based SSH / RDP
No VM public IPs required
AzureBastionSubnet: 10.10.6.0/26
SSH/RDPTLS Only
🔒
VPN Gateway
S2S IKEv2 → FortiGate
AD replication path
GatewaySubnet: 10.10.255.0/27
IPsecBGP
Zero Exposure: All VMs have zero public IPs. Inbound only via cloudflared tunnel → HAProxy VIP. Admin only via Azure Bastion. Egress via NAT Gateway static IP.

▸ Compute Subnets (Separate NSGs per subnet)

🌐 Web / App Subnet — 10.10.2.0/24
🖥️
Web Server 1
Debian 12
Apache2 / PHP / Tomcat
Banner Web Apps
/var/www ← JuiceFS mount
10.10.2.4
Active8080JuiceFS
HA
🖥️
Web Server 2
Debian 12
Apache2 / PHP / Tomcat
Banner Web Apps
/var/www ← JuiceFS mount
10.10.2.5
Active8080JuiceFS
🔐 CAS / SSO Subnet — 10.10.3.0/24
🔑
CAS Node 1
Debian 12 · Apereo CAS 7.x
← HAProxy cas_backend (8443)
AD / LDAP Auth
Redis DB 0 (Ticket Registry)
10.10.3.4
SSOActiveLDAP
HA
🔑
CAS Node 2
Debian 12 · Apereo CAS 7.x
← HAProxy cas_backend (8443)
AD / LDAP Auth
Redis DB 0 (Ticket Registry)
10.10.3.5
SSOActiveLDAP
🗄️ Shared Services — 10.10.4.0/24
Redis VM
Debian 12 · redis-server
DB 0 → CAS Ticket Registry
DB 1 → JuiceFS Metadata Engine
/mnt/webcontent (LVM sdb)
10.10.4.10
RedisCAS HAJuiceFS Meta
🏢
Azure AD DS / DC
Read-Only DC or AD DS
LDAP for CAS Auth
AD Sync from On-Prem
10.10.4.20
ADLDAP

▸ JuiceFS Storage Layer — Content at /var/www (shared-subnet: 10.10.4.0/24)

🗂️ Redis VM — JuiceFS Volume + LVM Disks · 10.10.4.10 · Debian 12
🍊
JuiceFS Volume
Formatted once — format cmd
Metadata → Redis DB 1 (local)
Data chunks → Backblaze B2
Mount URI: redis://10.10.4.10:6379/1
Web servers mount /var/www
POSIX FSDistributedHA
💾
LVM Disk Layout (4 Disks)
/dev/sdb
vg_webcontent
lv_webcontent
/mnt/webcontent
Redis data dir
Cache: Read-Only
/dev/sdc
vg_logs
lv_logs
/var/log/webapps
App log store
Cache: None
/dev/sdd
vg_backup
lv_backup
/mnt/backup
JuiceFS snapshots
Cache: None
/dev/sde
vg_staging
lv_staging
/mnt/staging
GitLab rsync zone
Cache: Read/Write
🦊
GitLab CI/CD
vldgitlab.zth.com (on-prem)
Pipeline on push to main
Runner inside Azure
rsync → /mnt/staging
promote → /var/www (JuiceFS)
PipelineAuto DeployRunner in Azure
JuiceFS Publish Flow: Author commits → GitLab pipeline → Runner rsyncs to /mnt/staging → promoted to /var/www (JuiceFS) → chunks written to Backblaze B2 → Web Server 1 & 2 see updated content instantly via JuiceFS mounts. Future (VPN live): DFS → Azure File Sync → JuiceFS replaces GitLab deploy step.

▸ NSG Rules (Key)

dmz-subnet NSG
Inbound: None (cloudflared dials out)
Outbound: 443 → Cloudflare IPs
Outbound: 443 → haproxy-subnet VIP
Inbound: 22 from AzureBastionSubnet
haproxy-subnet NSG
Inbound: 443 from dmz-subnet only
Inbound: 22 from AzureBastionSubnet
Outbound: 8080 → web-subnet
Outbound: 8443 → cas-subnet
web-subnet NSG
Inbound: 8080 from haproxy-subnet only
Inbound: 22 from AzureBastionSubnet
Outbound: 6379 → shared-subnet (JuiceFS)
Outbound: Internet via NAT GW (B2)
cas-subnet NSG
Inbound: 8443 from haproxy-subnet only
Inbound: 22 from AzureBastionSubnet
Outbound: 636 → shared-subnet (LDAP)
Outbound: 6379 → shared-subnet (Redis)
shared-subnet NSG
Redis 6379: Inbound from web+cas subnets
LDAP 636: Inbound from cas-subnet only
Inbound: 22 from AzureBastionSubnet
Outbound: Internet → Backblaze B2 (HTTPS)
☁ JuiceFS data chunks → Backblaze B2 · HTTPS S3-compatible API
🔥 Backblaze B2 — Object Storage Backend (External SaaS · S3-Compatible) $6/TB/month
🪣
B2 Bucket — zth-webcontent
s3.us-west-004.backblazeb2.com
JuiceFS data chunks
~$6/TB/month · 75% vs Azure Blob
Free egress → Cloudflare (Alliance)
Object StoreS3 APIRedundant
📥
Read Path
WS → Redis DB 1 (metadata)
WS → B2 (fetch chunks)
Local JuiceFS cache per WS
Hot content served from cache
JuiceFSCached Reads
📤
Write Path
GitLab Runner → /var/www
JuiceFS splits → chunks to B2
Metadata updated → Redis DB 1
Both WS see change instantly
JuiceFSGitLabInstant Sync
💰
Cost Profile
Storage: ~$6/TB/month
Download: $0.01/GB
Free egress → Cloudflare (Alliance)
No egress fees inside Azure
Low CostB2CF Alliance
🔒 Site-to-Site VPN (IPsec/IKEv2) · AD Replication · LDAP Fallback
🏢 On-Premises — Secondary / Failover STANDBY
⚖️
HAProxy (On-Prem LB)
Active only on Azure failure
Cloudflare auto-failover
Debian 12
Standby LBFailover
🖥️
On-Prem Web Servers
Debian 12 · Banner Apps
/var/www ← DFSR replica (now)
/var/www ← JuiceFS (future VPN)
Passive — Cloudflare failover
PassiveDFSR NowJuiceFS Future
📁
DFS Namespace + DFSR
\\domain\webcontent
DFSR → On-Prem WS 1&2
File Sync Agent (future VPN)
Windows File Server
SMB/UNCAuto ReplicateSync Agent Ready
🔑
On-Prem CAS
Apereo CAS 7.x (same version)
Local Redis ticket registry
On-prem AD / LDAP
Passive — Cloudflare failover
PassiveCAS
🏢
Active Directory (Primary)
Primary AD Forest
Syncs → Azure AD DS
Direct LDAP for on-prem CAS
Kerberos / SSSD
Primary ADLDAP
🔥
FortiGate
S2S VPN to Azure
IKEv2 / IPsec · BGP
DMZ + Internal
FirewallVPN GW

⚠ Failover Behaviour

Cloudflare health checks Azure cloudflared tunnel (HTTPS /cas/login every 30s). 2 consecutive failures → all traffic routes to on-prem HAProxy automatically. On-prem web servers read /var/www from DFSR local replica — content available independently of Azure and Backblaze. On-prem CAS uses local Redis — users re-authenticate on failover (acceptable). Once S2S VPN is live: on-prem web servers mount JuiceFS directly over VPN, achieving full content parity with Azure at all times.

▸ Normal Traffic Flow — Auth (Azure Primary)

01 · UserHTTPS Request
sso.zth.com
02 · CloudflareWAF / DDoS
Routes → Tunnel
03 · cloudflaredTunnel → VIP
dmz-subnet
04 · HAProxySSL Termination
LB → Web Server
05 · Web ServerBanner App
/var/www ← JuiceFS
06 · HAProxycas_backend
LB → CAS Node
07 · CAS NodeLDAP Auth → AD
Issues TGT / ST
08 · Redis DB 0CAS Ticket stored
Shared registry
09 · AppAuthenticated ✓
Session active

▸ Content Publish Flow — JuiceFS + Backblaze B2

01 · Authorgit push
vldgitlab main
02 · GitLab PipelineTriggers CI/CD
Runner in Azure
03 · Runnerrsync →
/mnt/staging (LVM)
04 · Promotestaging →
/var/www (JuiceFS)
05 · JuiceFS ClientSplits into chunks
→ Backblaze B2
06 · Redis DB 1Metadata updated
Inode / directory
07 · WS 1 & 2See change ✓
Instantly via mount

Azure Subnet Plan

SubnetCIDRPurpose
dmz-subnet10.10.0.0/26cloudflared Connector VM
haproxy-subnet10.10.1.0/24HAProxy Node 1 & 2 (Deb12)
web-subnet10.10.2.0/24Web Server 1 & 2 + JuiceFS client
cas-subnet10.10.3.0/24CAS Node 1 & 2
shared-subnet10.10.4.0/24Redis VM (CAS+JuiceFS) + AD DS
nat-subnet10.10.5.0/27NAT Gateway
AzureBastionSubnet10.10.6.0/26Azure Bastion (required name)
GatewaySubnet10.10.255.0/27VPN Gateway (required name)

JuiceFS + Backblaze B2

  • Format bucket once: juicefs format --storage s3 --bucket B2_ENDPOINT redis://10.10.4.10:6379/1 webcontent
  • Mount on each web server: juicefs mount redis://10.10.4.10:6379/1 /var/www
  • Redis DB 0 = CAS ticket registry (separate from JuiceFS)
  • Redis DB 1 = JuiceFS metadata engine
  • Both web servers see identical /var/www instantly
  • Local JuiceFS cache per web server reduces B2 read latency
  • No NFS server required — JuiceFS replaces it entirely
  • Free B2 egress via Cloudflare Bandwidth Alliance

LVM Disk Layout (Redis VM)

  • /dev/sdb → vg_webcontent → lv_webcontent → /mnt/webcontent — Redis data dir (Cache: RO)
  • /dev/sdc → vg_logs → lv_logs → /var/log/webapps — App logs (Cache: None)
  • /dev/sdd → vg_backup → lv_backup → /mnt/backup — Snapshots (Cache: None)
  • /dev/sde → vg_staging → lv_staging → /mnt/staging — GitLab rsync zone (Cache: RW)
  • pvcreate on raw disk — no partitioning needed
  • wipefs -a /dev/sdX before pvcreate if Azure pre-partitioned

CAS HA Requirements

  • Redis VM DB 0 as shared CAS ticket registry
  • Both CAS nodes point to redis://10.10.4.10:6379/0
  • Sticky sessions on HAProxy cas_backend (cookie-based)
  • Shared service registry (JSON in shared storage)
  • Identical keystore / signing certs on both nodes
  • NTP sync across all nodes

Key Design Decisions

  • JuiceFS + B2 replaces NFS/Azure Files — POSIX, distributed, cheap
  • Single Redis VM serves CAS tickets (DB 0) + JuiceFS metadata (DB 1)
  • 4 dedicated LVM disks — one VG per disk, wipefs before pvcreate
  • Cloudflare Tunnel = zero inbound exposure, no Azure public IPs
  • Keepalived VIP floats between HAProxy nodes — not a separate VM
  • GitLab Runner in Azure: rsync to /mnt/staging → promote to /var/www
  • On-prem failover reads DFSR; future VPN mounts JuiceFS on-prem too
  • All VMs Debian 12 — consistent OS across entire environment
Presented by Ola Oruku  ·  ZTH Hybrid Infrastructure  ·  Debian 12 · Apereo CAS 7.x · JuiceFS · Backblaze B2 · Cloudflare