Michael J Wright Archive Documentation

Zero Trust Security Deployment Guide

Quick Start (Step-by-Step)

Phase 1: Secure the VM (Do This First! 🔥)

On Your Local Machine (Windows):

  1. Commit current changes:

    git add .
    git commit -m "Security: Implement zero-trust architecture with localhost-only bindings"
    git push origin main
    
  2. Configure Azure NSG (Optional - can also do via Azure Portal):

    cd scripts
    .\configure-azure-nsg.ps1
    

    This will automatically:

    • Block all inbound ports except SSH from your IP
    • Explicitly deny ports 8080, 5432, 9090, 3000

On Azure VM:

  1. SSH into your VM:

    ssh youruser@YOUR_VM_IP
    
  2. Pull latest changes:

    cd ~/fedoraDockerCloudflare  # or wherever your repo is
    git pull origin main
    
  3. Run VM security script:

    chmod +x scripts/secure-vm.sh
    sudo ./scripts/secure-vm.sh
    

    This will:

    • Install UFW firewall
    • Block all incoming except SSH from your IP
    • Allow Docker internal networking
  4. Restart Docker with new localhost-only bindings:

    sudo docker-compose down
    sudo docker-compose up -d
    
  5. Verify security:

    chmod +x scripts/verify-security.sh
    ./scripts/verify-security.sh
    
  6. Test 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

  1. 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)
    
  2. Copy these values (you'll need them):

    • Application (client) ID: ________________
    • Directory (tenant) ID: ________________
  3. Certificates & secrets → New client secret

    • Description: Cloudflare Access
    • Expires: 24 months
    • Copy the VALUE immediately (you can't see it again!)
    • Client Secret: ________________
  4. API permissions → Add permission:

    • Microsoft Graph → Delegated permissions
    • Select: User.Read, email, openid, profile
    • Click Grant admin consent for [Your Org]
  5. Token configuration → Add optional claim:

    • Token type: ID
    • Claims: email, preferred_username

Step 2: Configure Cloudflare Zero Trust

  1. 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]
    
  2. 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.au
    

    Add 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]
    
  3. 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

  1. Visit https://data.michaeljwright.com.au
  2. Should redirect to Azure AD login
  3. After login, should see the archive frontend
  4. 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:

  1. Get your Cloudflare team name:

    • Zero Trust → Settings → Custom Pages
    • Your team domain is shown at the top (e.g., mjw-archive.cloudflareaccess.com)
  2. 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 };
  }
}
  1. 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...
  }
}
  1. Deploy updated workers:
cd workers/submit-ingest
npx wrangler deploy

cd ../manage
npx wrangler deploy

Security Verification Checklist

✅ VM Security

✅ Cloudflare Access (if implemented)

✅ General Security


Monitoring & Maintenance

Check Security Status Weekly

On VM:

./scripts/verify-security.sh

In Cloudflare Dashboard:

In Azure Portal:

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:


Rollback Plan (If Something Goes Wrong)

If locked out of VM:

  1. Use Azure Serial Console:

    • Azure Portal → VM → Serial console
    • Login with VM credentials
    • Disable firewall: sudo ufw disable
  2. Fix configuration

  3. Re-enable: sudo ufw enable

If Cloudflare Tunnel stops working:

  1. SSH into VM
  2. Check tunnel status: sudo systemctl status cloudflared
  3. Restart: sudo systemctl restart cloudflared
  4. 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:

Paid Options (Optional):


Support & Documentation


Questions or issues? Check SECURITY_IMPLEMENTATION.md for detailed technical information.