Zero Trust Security Deployment Guide
Quick Start (Step-by-Step)
Phase 1: Secure the VM (Do This First! 🔥)
On Your Local Machine (Windows):
Commit current changes:
git add . git commit -m "Security: Implement zero-trust architecture with localhost-only bindings" git push origin mainConfigure Azure NSG (Optional - can also do via Azure Portal):
cd scripts .\configure-azure-nsg.ps1This will automatically:
- Block all inbound ports except SSH from your IP
- Explicitly deny ports 8080, 5432, 9090, 3000
On Azure VM:
SSH into your VM:
ssh youruser@YOUR_VM_IPPull latest changes:
cd ~/fedoraDockerCloudflare # or wherever your repo is git pull origin mainRun VM security script:
chmod +x scripts/secure-vm.sh sudo ./scripts/secure-vm.shThis will:
- Install UFW firewall
- Block all incoming except SSH from your IP
- Allow Docker internal networking
Restart Docker with new localhost-only bindings:
sudo docker-compose down sudo docker-compose up -dVerify security:
chmod +x scripts/verify-security.sh ./scripts/verify-security.shTest from external network:
# From your local machine or another network: # These should all TIMEOUT (blocked): Test-NetConnection -ComputerName YOUR_VM_IP -Port 8080 Test-NetConnection -ComputerName YOUR_VM_IP -Port 5432 Test-NetConnection -ComputerName YOUR_VM_IP -Port 9090 Test-NetConnection -ComputerName YOUR_VM_IP -Port 3000 # This should work (tunnel): curl https://fcrepo.michaeljwright.com.au/fcrepo/rest # Expected: 401 Unauthorized (good - means tunnel works, just needs auth)
Phase 2: Implement Cloudflare Access (Azure SSO)
Step 1: Create Azure AD App Registration
Azure Portal → Azure Active Directory → App registrations → New registration
Name: MJW Archive Access Redirect URI: https://mjw-archive.cloudflareaccess.com/cdn-cgi/access/callback (or use your Cloudflare team name instead of mjw-archive)Copy these values (you'll need them):
- Application (client) ID:
________________ - Directory (tenant) ID:
________________
- Application (client) ID:
Certificates & secrets → New client secret
- Description:
Cloudflare Access - Expires: 24 months
- Copy the VALUE immediately (you can't see it again!)
- Client Secret:
________________
- Description:
API permissions → Add permission:
- Microsoft Graph → Delegated permissions
- Select:
User.Read,email,openid,profile - Click Grant admin consent for [Your Org]
Token configuration → Add optional claim:
- Token type: ID
- Claims:
email,preferred_username
Step 2: Configure Cloudflare Zero Trust
Cloudflare Dashboard → Zero Trust → Settings → Authentication
Add an identity provider:
Type: Azure AD Name: Azure AD SSO App ID: [paste Application (client) ID] Client secret: [paste secret value] Directory ID: [paste tenant ID]Create Access Applications:
Application 1: Frontend (data.michaeljwright.com.au)
Zero Trust → Access → Applications → Add an application
Type: Self-hosted Application name: MJW Archive Frontend Session Duration: 24 hours Application domain: data.michaeljwright.com.auAdd Policy:
Policy name: Authenticated Users Action: Allow Include: Emails ending in @yourdomain.com (or "Everyone" if you want to allow anyone with Azure AD account)Application 2: Submit Portal
Application name: MJW Submit Portal Application domain: submit.michaeljwright.com.au [Same policy]Application 3: Manage Portal
Application name: MJW Manage Portal Application domain: manage.michaeljwright.com.au [Same policy - or create "Curators Only" policy]Optional: Create Curator Role Policy
For manage.michaeljwright.com.au:
Policy name: Curators Only Action: Allow Include: Emails in list - curator1@yourdomain.com - curator2@yourdomain.com - admin@yourdomain.com
Step 3: Test SSO Login
- Visit https://data.michaeljwright.com.au
- Should redirect to Azure AD login
- After login, should see the archive frontend
- Repeat for submit and manage portals
Phase 3: Add Authentication Validation to Workers (Optional but Recommended)
This prevents direct API access bypassing Cloudflare Access.
Update Each Worker:
Get your Cloudflare team name:
- Zero Trust → Settings → Custom Pages
- Your team domain is shown at the top (e.g.,
mjw-archive.cloudflareaccess.com)
Add JWT validation to workers:
Create workers/shared/auth.js:
export async function validateCloudflareAccess(request, teamDomain) {
const token = request.headers.get('Cf-Access-Jwt-Assertion');
if (!token) {
return { error: 'No Access Token', status: 401 };
}
try {
// Get Cloudflare's public keys
const certsUrl = `https://${teamDomain}.cloudflareaccess.com/cdn-cgi/access/certs`;
const certsResponse = await fetch(certsUrl);
const { keys } = await certsResponse.json();
// Decode JWT payload
const [header, payload, signature] = token.split('.');
const decodedPayload = JSON.parse(atob(payload));
// Check expiration
if (decodedPayload.exp < Date.now() / 1000) {
return { error: 'Token Expired', status: 401 };
}
// Return user info
return {
user: {
email: decodedPayload.email,
sub: decodedPayload.sub,
name: decodedPayload.name,
}
};
} catch (error) {
return { error: 'Invalid Token', status: 401 };
}
}
- Update workers to use validation:
In each worker's src/index.js:
import { validateCloudflareAccess } from '../shared/auth.js';
export default {
async fetch(request, env, ctx) {
const TEAM_DOMAIN = 'mjw-archive'; // Your team name
// Validate authentication
const authResult = await validateCloudflareAccess(request, TEAM_DOMAIN);
if (authResult.error) {
return new Response(authResult.error, { status: authResult.status });
}
const user = authResult.user;
console.log(`Authenticated user: ${user.email}`);
// Continue with normal request handling...
}
}
- Deploy updated workers:
cd workers/submit-ingest
npx wrangler deploy
cd ../manage
npx wrangler deploy
Security Verification Checklist
✅ VM Security
- UFW firewall enabled and active
- All Docker ports bound to 127.0.0.1 (not 0.0.0.0)
- Azure NSG blocks all inbound except SSH from your IP
- Ports 8080, 5432, 9090, 3000 timeout from external network
- Cloudflare Tunnel still accessible (curl fcrepo.michaeljwright.com.au returns 401)
✅ Cloudflare Access (if implemented)
- Azure AD app registration created
- Cloudflare Access applications configured
- Login redirects to Azure AD
- Can access frontend after authentication
- Can access submit portal after authentication
- Can access manage portal after authentication
- JWT tokens validated by workers (optional)
✅ General Security
- All secrets stored in Cloudflare Workers secrets (not in code)
- HTTPS enforced on all domains
- Fedora credentials using strong passwords
- No sensitive data in git repository
Monitoring & Maintenance
Check Security Status Weekly
On VM:
./scripts/verify-security.sh
In Cloudflare Dashboard:
- Zero Trust → Logs → Access
- Review authentication attempts
- Check for suspicious activity
In Azure Portal:
- Monitor → Network → NSG Flow Logs
- Check for blocked connection attempts
Update SSH Allowed IP
If your IP changes:
On VM:
sudo ufw delete allow from OLD_IP to any port 22
sudo ufw allow from NEW_IP to any port 22
sudo ufw reload
In Azure Portal:
- NSG → Inbound security rules → AllowSSHFromMyIP
- Update source IP to new IP
Rollback Plan (If Something Goes Wrong)
If locked out of VM:
Use Azure Serial Console:
- Azure Portal → VM → Serial console
- Login with VM credentials
- Disable firewall:
sudo ufw disable
Fix configuration
Re-enable:
sudo ufw enable
If Cloudflare Tunnel stops working:
- SSH into VM
- Check tunnel status:
sudo systemctl status cloudflared - Restart:
sudo systemctl restart cloudflared - Check logs:
sudo journalctl -u cloudflared -f
If need to allow public access temporarily:
# On VM - allow Fedora from anywhere (emergency only!)
sudo ufw allow 8080
sudo docker-compose down
# Edit docker-compose.yml - change 127.0.0.1:8080:8080 to 8080:8080
sudo docker-compose up -d
# When fixed:
sudo ufw delete allow 8080
# Restore localhost binding
sudo docker-compose down && sudo docker-compose up -d
Cost Considerations
Free Tier Sufficient:
- Cloudflare Zero Trust: 50 users free
- Azure AD: Basic features free
- No additional infrastructure costs
Paid Options (Optional):
- Cloudflare Access: $3/user/month for advanced features
- Azure AD P1: $6/user/month for conditional access policies
- Cloudflare Access for Teams: $7/user/month includes WARP VPN
Support & Documentation
- Cloudflare Access Docs: https://developers.cloudflare.com/cloudflare-one/applications/
- Azure AD Integration: https://developers.cloudflare.com/cloudflare-one/identity/idp-integration/azuread/
- UFW Firewall Guide: https://help.ubuntu.com/community/UFW
- Azure NSG Documentation: https://docs.microsoft.com/azure/virtual-network/network-security-groups-overview
Questions or issues? Check SECURITY_IMPLEMENTATION.md for detailed technical information.