Highclere Castle Ticketing & Commerce · Disaster Recovery Programme
View:
Viewing in Plain English mode. Switch to Technical & Engineering in the top bar at any time to view database schemas, code specifications, and architecture diagrams.
Component 2 · Infrastructure & Disaster Recovery

Automated Server Blueprint & Instant Recovery Declarative Infrastructure & Automated Recovery (debian-ansible)

Built & Verified · Ready for Staging

Before opening new public ticket sales, we fundamentally changed how Highclere Castle's servers are built, secured, and recovered. Instead of relying on a single fragile server maintained by hand over 15 years, every Castle server is now generated automatically from a master digital blueprint that can rebuild a replacement server from scratch in minutes.

Component 2 replaces manual, undocumented server administration with debian-ansible: a declarative, idempotent infrastructure-as-code blueprint that provisions, hardens, and orchestrates disposable Debian 12/13 hosts, our Traefik v3 + CrowdSec Edge Bastion, our NetBird WireGuard mesh, and the Git-backed highclere-tickets Docker Compose stack from a single Host-Centric Inventory.

How Automated Infrastructure Protects Highclere Castle

Component 2 eliminates the hidden operational risks of traditional, hand-maintained servers:

  • Dismantling the "Jenga Tower" of IT: Traditional servers accumulate years of manual tweaks until nobody dares touch them. Our servers are built 100% from a master blueprint so there are zero hidden surprises.
  • Rebuild, Don't Repair: If a server ever suffers a hardware fault or software glitch on a busy Castle opening morning, we do not waste hours troubleshooting it by hand. We launch a clean server and let the blueprint rebuild it automatically in minutes.
  • Identical Testing & Live Servers: Our pre-launch Staging server and Live Production server are built from the exact same blueprint, guaranteeing that every feature works identically when promoted to the public.
  • Automatic Customer Email Lock on Test Servers: The blueprint automatically detects whether a server is running in Staging or Live mode, locking test servers so they can never accidentally email real customers.
  • Dedicated Outer Security Shield: An outer security gateway blocks automated cyber attacks and manages website security padlocks before visitor traffic ever reaches the ticketing database.
1 · Philosophy · Dismantling the IT Jenga Tower

"Rebuild, Don't Repair": Why Infrastructure Should Be Disposable

Most traditional IT systems are an accidental game of Jenga. Over months and years, every manual setting change, quick fix, or software update pulls another brick from the tower. The server keeps working for a while, which creates a powerful incentive never to touch it: until the day it suddenly falls.

The Old Way · Digital Archaeology

The Fragile "Snowflake Server"

When a server is configured by hand over 15 years, it becomes a one-of-a-kind Snowflake Server:

  • Nobody remembers which undocumented change three years ago keeps a critical script running.
  • Applying security updates becomes terrifying because nobody knows what else might quietly break.
  • When an outage happens, engineers are not fixing the system: they are performing slow "digital archaeology" under pressure while customers wait.
Something breaks on a busy morning
        ↓
Log into the old server by hand
        ↓
Spend hours guessing what changed
        ↓
Apply a manual patch & hope nothing else breaks
The New Way · Master Blueprint

Disposable, Reproducible Appliances

We solved this by moving the Castle's configuration outside the server into a permanent, version-controlled master blueprint:

  • Never Edit a Live Server by Hand: Every security rule, user account, and website setting is written in the blueprint first.
  • Nothing Precious Trapped on the Web Server: Because bookings live safely in the database and code lives in version control, the web server itself is completely replaceable.
  • Calm, Predictable Recovery: When a server misbehaves, replacement is a normal 5-minute routine rather than a weekend-ruining disaster.
Something misbehaves
        ↓
Switch off the degraded server
        ↓
Run the Master Blueprint on a fresh machine
        ↓
Verified & back online in minutes

As articulated in our engineering philosophy (Rebuild, Don't Repair and the Modulix concept model), imperative infrastructure records history (a fragile sequence of manual SSH edits, package pins, and ad-hoc cron jobs), whereas declarative infrastructure records desired state.

Legacy Anti-Pattern · Snowflake Pets

Why In-Place Repair Turns Into Archaeology

  • Accumulated Hidden State: The legacy PHP 7.4 server accumulated 15 years of unversioned edits in /var/www, manual crontab entries, and temporary .gd2 barcode files written into the webroot.
  • Fear of Upgrades: Because no staging environment or provisioning script existed, OS and PHP upgrades were frozen for years out of fear of collapsing the Jenga tower.
  • Unbounded MTTR: Recovering from hardware failure or compromise required reconstructing undocumented server history from memory.
Component 2 Standard · Cattle, Not Pets

Strict Idempotency & Zero Configuration Drift

  • Desired State Outside the Machine: Every OS package, sysctl/SSH drop-in, UFW rule, Docker daemon flag, Traefik route, and systemd timer is declared in debian-ansible.
  • Idempotent Convergence (changed=0): Playbooks run unattended (`-i inventory.yaml`). Running `site.yaml` a second time against a converged fleet produces zero mutations.
  • Stateless Compute vs Persistent Data: Application code is cloned cleanly from Gitea; only PostgreSQL relational state (`postgres_data`) persists across rebuilds.
2 · Capability Compilation · Host-Centric Inventory

Declare Intent Once. Compile the Entire Server Stack.

Software developers stopped writing raw machine code decades ago: they write high-level instructions and let a compiler do the heavy lifting. Component 2 brings that same principle to Highclere Castle's servers.

Before Component 2

14 Manual Steps Per Server

  1. Create virtual server by hand
  2. Set up administrator accounts and passwords
  3. Configure remote login security settings
  4. Write firewall rules manually (easy to mistype)
  5. Configure automatic operating system security updates
  6. Install container engine and runtime tools
  7. Install private office VPN software and keys
  8. Install brute-force login protection
  9. Download website code onto the server
  10. Hand-type secret keys and database passwords into config files
  11. Build and start the website and database containers
  12. Run database table updates by hand
  13. Configure background sync and nightly privacy cleanup jobs
  14. Test everything manually and hope Staging matches Live
With Component 2 Blueprint

Simple Capability Switches

Instead of following a 14-step manual checklist, we declare the server once in our master Host-Centric Inventory and flip on the capabilities it needs:

hs-ct-apps-p-01:
  ansible_host: 10.10.30.61
  highclere_tickets: true
  git_branch: "main"
  fail2ban: true
  netbird: true

Automatically compiled from those 6 lines:

  • Hardened Debian OS + automatic security updates
  • Private SDN firewall rules + brute-force IP blocking
  • Encrypted NetBird office network connection
  • Verified website code, PostgreSQL 16 database, 60-second office sync, and 3:15 AM GDPR cleanup timer

Following our ADR-0002 (Host-Centric Inventory and Dynamic Classification) and the Modulix Capability Translation model, every server is declared exactly once under all.hosts in inventory.yaml. Operators attach Composable Capability Flags rather than maintaining brittle, duplicated inventory groups.

Composable Capability Flag Implied Prerequisites Dynamic Target Group Provisioned Infrastructure & Runtime Scope
Universal Baseline (Implicit) None all roles/common: Unprivileged mad user, admin_users SSH keys, visudo -cf validation, sshd -t block/rescue hardening, UFW default-deny (allowing 22/tcp), and systemd apt-daily-upgrade.timer drop-in override.
docker: true None docker_nodes roles/docker: Official Docker CE GPG keyring (`/etc/apt/keyrings/docker.asc`), DEB822 repository, docker-ce + docker-compose-plugin, and mad group membership.
bastion: true docker: true bastion roles/bastion: Standalone Compose stack (`/home/mad/docker/bastion`) running Traefik v3 (`v3.7`) + CrowdSec (`v1.8.1`), DNS-01 wildcard ACME TLS, dynamic Cloudflare `trustedIPs`, and declarative Cloudflare DNS `A` records.
fail2ban: true None fail2ban_nodes apps/fail2ban: Hardened `/etc/fail2ban/jail.local` (`backend = systemd`, `bantime = 1h`, `maxretry = 5`) consuming the shared security_whitelist_ips list.
netbird: true None netbird_nodes apps/netbird: Official DEB822 NetBird client package and idempotent WireGuard mesh enrollment against https://netbird.highclere.tv.
highclere_tickets: true docker: true highclere_tickets_nodes apps/highclere_tickets: UFW rule allowing `3000/tcp` strictly from Bastion SDN (`10.10.30.0/24`), token-authenticated Gitea clone, branch-driven `.env` synthesis (`0600`), `docker-compose.prod.yaml` deployment, Drizzle migrations, and phased systemd timers (`highclere-etl-sync.timer`, `highclere-retention-prune.timer`).

Play 1 Pre-Flight Validation Gate (Fail-Fast Before Opening SSH)

Before opening a single remote SSH connection, Play 1 of site.yaml (gather_facts: false, tags: [always]) executes local ansible.builtin.assert checks across all hosts. If a host enables highclere_tickets: true with a missing Gitea token, a highclere_tickets_session_secret shorter than 32 characters, or an empty highclere_tickets_legacy_mariadb_url, execution halts immediately with an explicit remediation message.

3 · Defense-in-Depth Topology

Fasthosts Proxmox Fleet & Zero-Trust Network Isolation

Rather than placing the website, database, and staff administration tools on a single exposed machine, Component 2 organises the Castle's digital estate into four distinct, protected zones:

1 · Outer Security Gateway (hs-ct-bast-p-02)

The Castle's Digital Gatehouse

All public internet visitors arrive at our dedicated Edge Bastion server first. It verifies encrypted security certificates and runs an active security shield (CrowdSec) that automatically blocks known hackers, ticket scalping bots, and password guessers at the perimeter.

2 · Live Ticketing Server (hs-ct-apps-p-01)

Protected Internal Application & Database

The live ticketing website and its modern database sit behind a strict internal firewall. They cannot be reached directly from the public internet: they only accept verified visitor traffic forwarded by the outer Gatehouse over a private internal network.

3 · Pre-Launch Staging Server (hs-ct-apps-s-01)

Identical Dress-Rehearsal Environment

Built from the exact same blueprint as the live server and loaded with a full copy of historical bookings. Allows new features (such as Component 3 Ticket Checkout) to be tested safely before going live.

4 · Quarantined Legacy Server (Private VPN Only)

Isolated Office Administration

The old ticketing server is completely disconnected from the public internet. Office staff connect to it exclusively through our encrypted NetBird private office network while the new system copies updates every 60 seconds.

┌──────────────────────────────────────────────────────────────────────────────────────────┐
│                         PUBLIC INTERNET & CLOUDFLARE EDGE WAF                            │
│                    (highclerecastleshop.co.uk / *.highclerecastle.tv)                    │
└────────────────────────────────────────────┬─────────────────────────────────────────────┘
                                             │ HTTPS (80/tcp, 443/tcp on bastion_public_interface)
                                             ▼
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│  EDGE BASTION HOST: hs-ct-bast-p-02 (10.10.30.50 · roles/common + roles/bastion)         │
│  • Standalone Compose (/home/mad/docker/bastion): Traefik v3.7 + CrowdSec v1.8.1         │
│  • File-Provider-Only Routing (Zero /var/run/docker.sock exposure)                       │
│  • Let's Encrypt DNS-01 Wildcard TLS (acme.json 0600) + Cloudflare trustedIPs            │
└──────────────────────┬────────────────────────────────────────────┬──────────────────────┘
                       │ Internal SDN (10.10.30.0/24)               │ Internal SDN (10.10.30.0/24)
                       │ UFW: Allow 3000/tcp ONLY from SDN          │ UFW: Allow 3000/tcp ONLY from SDN
                       ▼                                            ▼
┌─────────────────────────────────────────────┐  ┌─────────────────────────────────────────┐
│  PRODUCTION NODE: hs-ct-apps-p-01           │  │  STAGING NODE: hs-ct-apps-s-01          │
│  (10.10.30.61 · git_branch: "main")         │  │  (10.10.30.60 · git_branch: "staging")  │
│  • apps/highclere_tickets (Docker Compose)  │  │  • apps/highclere_tickets (Compose)     │
│  • Astro SSR (:3000) + PostgreSQL 16        │  │  • Astro SSR (:3000) + PostgreSQL 16    │
│  •NODE_ENV=production                       │  │  • NODE_ENV=staging                     │
│  • EMAIL_ALLOWLIST="" (Live Postmark)       │  │  • EMAIL_ALLOWLIST=staff@... (Locked)   │
│  • highclere-etl-sync.timer (Every 60s)     │  │  • Mirrored 506 MB PostgreSQL dataset   │
│  • highclere-retention-prune.timer (03:15)  │  │                                         │
└──────────────────────┬──────────────────────┘  └─────────────────────────────────────────┘
                       │
                       │ Outbound MySQL (3306/tcp over Internal SDN / NetBird wt0)
                       │ Read-Only Incremental Batch Poll (Every 60s)
                       ▼
┌──────────────────────────────────────────────────────────────────────────────────────────┐
│  QUARANTINED LEGACY LAMP VM (10.10.30.20 · Zero Public Ingress)                          │
│  • Legacy PHP 7.4 (/admin) reachable ONLY by Castle Staff over NetBird WireGuard Mesh    │
│  • Legacy MariaDB 10.11 read by hs-ct-apps-p-01 ETL sync until Component 4 cutover       │
└──────────────────────────────────────────────────────────────────────────────────────────┘
4 · Environment Parity & Safety Guarantees

Single-Variable Branch Synthesis & Automatic Email Safety Locks

One of the biggest risks when testing a new ticketing system with real historical customer records is accidentally sending test emails to real visitors. Component 2 makes that mistake impossible by tying email safety directly to the server's role in the blueprint:

Setting Staging Server (hs-ct-apps-s-01) Live Production Server (hs-ct-apps-p-01)
Blueprint Branch Switch staging (Pre-launch testing version) main (Verified public version)
Website Address Private staging web address Official public website address
Customer Database Full 906,406-record copy for realistic testing Live 906,406-record database synced every 60s
Outbound Email Safety Lock Locked to Castle Staff Only
Automatically blocks emails to the public; only approved Castle staff & family addresses can receive sign-in codes.
Live Customer Delivery
Delivers instant 6-digit sign-in codes and ticket confirmations to all customers via Postmark.

In apps/highclere_tickets, the host's git_branch variable (defaulting to "main") controls both the Gitea repository checkout and the Jinja2 synthesis of /home/mad/docker/highclere-tickets/.env (mode '0600', owned by mad:mad, rendered with no_log: true):

# apps/highclere_tickets/templates/env.j2
APP_BIND_IP={{ highclere_tickets_bind_ip }}
{% if git_branch == 'main' %}
NODE_ENV=production
PUBLIC_SITE_URL=https://{{ bastion_domain }}
{% elif git_branch == 'staging' %}
NODE_ENV=staging
PUBLIC_SITE_URL=https://staging.{{ bastion_domain }}
{% else %}
NODE_ENV=development
PUBLIC_SITE_URL=https://{{ git_branch }}.{{ bastion_domain }}
{% endif %}
PORT={{ highclere_tickets_port }}
SESSION_SECRET={{ highclere_tickets_session_secret }}
POSTGRES_USER={{ highclere_tickets_postgres_user }}
POSTGRES_PASSWORD={{ highclere_tickets_postgres_password }}
POSTGRES_DB={{ highclere_tickets_postgres_db }}
DATABASE_URL=postgresql://{{ highclere_tickets_postgres_user }}:{{ highclere_tickets_postgres_password }}@postgres:5432/{{ highclere_tickets_postgres_db }}
LEGACY_MARIADB_URL={{ highclere_tickets_legacy_mariadb_url }}
EMAIL_MODE={{ highclere_tickets_email_mode }}
POSTMARK_SERVER_TOKEN={{ highclere_tickets_postmark_server_token }}
EMAIL_FROM={{ highclere_tickets_email_from }}
EMAIL_ALLOWLIST={{ highclere_tickets_email_allowlist if git_branch != 'main' else '' }}
CONTACT_EMAIL_DEFAULT={{ highclere_tickets_contact_email_default }}
COMPANY_VAT_NUMBER={{ highclere_tickets_company_vat_number }}
Structural Email Safety Guarantee

Because EMAIL_ALLOWLIST is populated automatically whenever git_branch != 'main', and enforced inside PostmarkEmailTransport at the application layer, a staging or development node can never send live emails to any of the 226,104 migrated customer addresses even if configured with a live Postmark token.

5 · Operational Runbook

The 4-Step Disaster Recovery Procedure

If a server ever suffers a critical hardware failure or software corruption, recovery follows a calm, repeatable 4-step procedure that requires zero guesswork:

Step 1 · Fresh Server (1 Minute)

Start a Clean Virtual Machine

Instead of trying to repair the broken server, we switch it off and launch a fresh, empty Debian server on our cloud hosting platform.

Step 2 · Run the Blueprint (3 Minutes)

Execute One Automated Command

We run our single master blueprint command. It automatically configures all security firewalls, installs the private VPN, downloads the latest verified website code, and starts the database.

Step 3 · Restore Bookings (2 Minutes)

Reload Customer & Ticket Data

The database is instantly populated either from our encrypted daily backup or by running the automatic synchronization bridge from the office database.

Step 4 · Automated Health Check (30 Seconds)

Verify 6 Safety Checks

Six built-in verification checks confirm that the firewall is locked, the website is responding, and the 60-second background sync timer is ticking normally.

To rebuild a degraded or destroyed application node (hs-ct-apps-s-01 or hs-ct-apps-p-01) from a bare Debian 12/13 installation:

# 1. Bootstrap clean Debian VM from Ansible controller (initial run as root creates 'mad' & hardens SSH)
ansible-playbook site.yaml -i inventory.yaml --limit hs-ct-apps-p-01 -e "ansible_user=root"

# 2. Subsequent runs connect seamlessly as unprivileged 'mad' user with zero drift (changed=0)
ansible-playbook site.yaml -i inventory.yaml --limit hs-ct-apps-p-01

# 3. Restore PostgreSQL state (either from compressed pg_dump or full idempotent MariaDB ETL import)
gunzip -c highclere_tickets_backup.sql.gz | \
  docker compose -f /home/mad/docker/highclere-tickets/docker-compose.prod.yaml exec -T postgres \
  psql -U highclere highclere_tickets
# OR reconstruct directly from legacy MariaDB:
docker compose -f /home/mad/docker/highclere-tickets/docker-compose.prod.yaml exec -T app \
  npm run db:sync-legacy -- --full

# 4. Run the 6-Point Post-Install Verification Checklist on the target host:
git -C /home/mad/docker/highclere-tickets status
sudo ls -l /home/mad/docker/highclere-tickets/.env                 # Must be -rw------- (0600) mad:mad
sudo ufw status verbose | grep 3000                                # Allowed ONLY from 10.10.30.0/24
docker compose -f /home/mad/docker/highclere-tickets/docker-compose.prod.yaml ps
curl -sI http://127.0.0.1:3000/api/health                          # HTTP/1.1 200 OK
systemctl list-timers 'highclere*'                                 # ETL (60s) & Prune (03:15 UTC) active
6 · Governance & Safety Gates

How We Prevent Blueprint Mistakes & Administrator Lockouts

When a single blueprint controls every server, a mistake in the blueprint itself could be dangerous if left unchecked. We protect the blueprint with strict automated safety locks and a 3-stage review process:

Stage 1 · Document First

Agree the Specification

Before any server automation code is written, the exact purpose, security rules, and recovery steps are written down in our 7-section operational guide.

Stage 2 · Build with Safety Locks

Automatic Rollback Guards

Every security change includes a built-in safety net. For example, remote login settings are tested automatically before being applied so administrators can never be accidentally locked out.

Stage 3 · Independent Audit

10-Point Security Verification

An independent verification check audits every change to confirm secret keys are hidden from logs, firewalls are tight, and running the blueprint twice produces zero unintended side effects.

Mandatory 3-Stage Agent Pipeline

All changes to debian-ansible execute sequentially across isolated specialist sub-agents (preceded by Stage 0: Researcher when auditing upstream releases):

  1. Stage 1 (Documenter): Updates CONTEXT.md, ADRs, and the mandatory 7-section guide in docs/roles/ or docs/apps/ first.
  2. Stage 2 (Generator): Implements tasks using strict FQCN modules (ansible.builtin.*, community.docker.*, community.general.*), explicit ansible_facts['...'] syntax, and atomic commits.
  3. Stage 3 (Validator): Verifies --syntax-check, yamllint, no_log: true secret hygiene, and two-run idempotency (changed=0).

Anti-Lockout & Secret Protection Guards

  • SSH Block/Rescue Validation: roles/common deploys 00-hardening.conf inside an Ansible block/rescue construct verified by sshd -t; if validation fails, the drop-in is automatically removed before restarting ssh.
  • Sudoers Syntax Check: All /etc/sudoers.d/ files enforce mode: '0440' and validate: 'visudo -cf %s'.
  • Unified Security Whitelist: Administrative origins in security_whitelist_ips are shared across Fail2ban and Bastion CrowdSec/Traefik allowlists.
  • Zero Secret Leakage: Every task handling password hashes, Gitea tokens, NetBird setup keys, Cloudflare API tokens, or .env templates enforces no_log: true.