▸ 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
⚖️
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
🖥️
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
🔑
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)