web/docs/deployment/edut-dev-infra-cutover-checklist.md
Joshua 113da4df4e
Some checks are pending
check / secretapi (push) Waiting to run
chore: cut over repo tooling to git.edut.dev
2026-02-19 09:46:43 -08:00

9.8 KiB

EDUT Dev Infra Cutover Checklist

This checklist migrates EDUT infrastructure from git.workvsg.com to git.edut.dev with deterministic validation gates.

Server target:

  • Host: edut-prod
  • IP: 5.78.148.229
  • OS: Ubuntu 24.04 LTS

Preconditions

  1. DNS records exist and point to the new server:
    • edut.dev
    • www.edut.dev
    • api.edut.dev
    • git.edut.dev
  2. SSH key access as root is available.
  3. Cloudflare proxy mode and SSL mode are configured to allow origin TLS.
  4. Local private keys for joshua, claude-code, codex are available for validation.

Phase 1 - Server Setup

1. Baseline inventory

ssh -i ~/.ssh/edut_codex root@5.78.148.229 \
  "hostnamectl --static; cat /etc/os-release | head -n 6; ufw status verbose; docker --version || true"

Gate:

  1. Host is reachable.
  2. OS is Ubuntu 24.04.
  3. State is understood before changes.

2. Create Linux users and key-only SSH

Create users:

ssh -i ~/.ssh/edut_codex root@5.78.148.229 \
  "useradd -m -s /bin/bash joshua || true; useradd -m -s /bin/bash claude-code || true; useradd -m -s /bin/bash codex || true"

Install authorized keys:

ssh -i ~/.ssh/edut_codex root@5.78.148.229 "install -d -m 700 /home/joshua/.ssh /home/claude-code/.ssh /home/codex/.ssh"
ssh -i ~/.ssh/edut_codex root@5.78.148.229 "printf '%s\n' '<JOSHUA_PUBKEY>' > /home/joshua/.ssh/authorized_keys"
ssh -i ~/.ssh/edut_codex root@5.78.148.229 "printf '%s\n' '<CLAUDE_CODE_PUBKEY>' > /home/claude-code/.ssh/authorized_keys"
ssh -i ~/.ssh/edut_codex root@5.78.148.229 "printf '%s\n' '<CODEX_PUBKEY>' > /home/codex/.ssh/authorized_keys"
ssh -i ~/.ssh/edut_codex root@5.78.148.229 \
  "chmod 600 /home/*/.ssh/authorized_keys; chown -R joshua:joshua /home/joshua/.ssh; chown -R claude-code:claude-code /home/claude-code/.ssh; chown -R codex:codex /home/codex/.ssh"

Gate:

  1. All three users exist.
  2. Authorized keys are present with correct perms.

3. Sudo policy

ssh -i ~/.ssh/edut_codex root@5.78.148.229 \
  "usermod -aG sudo joshua; usermod -aG sudo claude-code; usermod -aG sudo codex"
ssh -i ~/.ssh/edut_codex root@5.78.148.229 \
  "printf '%s\n' 'claude-code ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-claude-code"
ssh -i ~/.ssh/edut_codex root@5.78.148.229 \
  "printf '%s\n' 'codex ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-codex"
ssh -i ~/.ssh/edut_codex root@5.78.148.229 "chmod 440 /etc/sudoers.d/90-claude-code /etc/sudoers.d/90-codex"
ssh -i ~/.ssh/edut_codex root@5.78.148.229 "visudo -cf /etc/sudoers"

Gate:

  1. visudo -cf passes.
  2. claude-code and codex have passwordless sudo.
  3. joshua remains standard sudo (password required).

4. SSH hardening + disable root login

ssh -i ~/.ssh/edut_codex root@5.78.148.229 "cat >/etc/ssh/sshd_config.d/99-edut-hardening.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
EOF"
ssh -i ~/.ssh/edut_codex root@5.78.148.229 "sshd -t && systemctl restart ssh"

Validation:

ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo -n true && echo codex-sudo-ok"
ssh -i ~/.ssh/edut_joshua joshua@5.78.148.229 "id"
ssh -i ~/.ssh/edut_codex -o BatchMode=yes root@5.78.148.229 "echo should-fail"

Gate:

  1. codex login works and sudo works.
  2. joshua key login works.
  3. root login is denied.

5. Firewall, fail2ban, unattended upgrades

ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo apt-get update"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo apt-get install -y ufw fail2ban unattended-upgrades apt-listchanges"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo ufw default deny incoming && sudo ufw default allow outgoing"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo ufw allow OpenSSH && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo ufw --force enable"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo systemctl enable --now fail2ban"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo dpkg-reconfigure -f noninteractive unattended-upgrades"

Gate:

  1. ufw status verbose shows only 22, 80, 443.
  2. fail2ban active.
  3. unattended upgrades enabled.

6. Install Docker + Compose plugin

ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo apt-get install -y ca-certificates curl gnupg"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo install -m 0755 -d /etc/apt/keyrings"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo chmod a+r /etc/apt/keyrings/docker.gpg"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "echo \"deb [arch=\$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \$(. /etc/os-release && echo \$VERSION_CODENAME) stable\" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo systemctl enable --now docker"

Gate:

  1. docker --version returns success.
  2. docker compose version returns success.

7. Deploy Gitea + TLS on git.edut.dev

Provision directories:

ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo install -d -m 755 /opt/edut/gitea/{gitea,postgres,caddy}"

Deploy stack (Gitea + Postgres + Caddy reverse proxy with auto TLS):

ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo tee /opt/edut/gitea/docker-compose.yml >/dev/null <<'EOF'
services:
  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: gitea
      POSTGRES_USER: gitea
      POSTGRES_PASSWORD: CHANGE_ME_DB_PASSWORD
    volumes:
      - /opt/edut/gitea/postgres:/var/lib/postgresql/data
    networks: [gitea_net]

  gitea:
    image: gitea/gitea:1.22
    restart: unless-stopped
    environment:
      USER_UID: "1000"
      USER_GID: "1000"
      GITEA__database__DB_TYPE: postgres
      GITEA__database__HOST: db:5432
      GITEA__database__NAME: gitea
      GITEA__database__USER: gitea
      GITEA__database__PASSWD: CHANGE_ME_DB_PASSWORD
      GITEA__server__ROOT_URL: https://git.edut.dev/
      GITEA__server__DOMAIN: git.edut.dev
      GITEA__server__HTTP_ADDR: 0.0.0.0
      GITEA__server__HTTP_PORT: 3000
      GITEA__security__INSTALL_LOCK: "true"
      GITEA__service__DISABLE_REGISTRATION: "true"
    volumes:
      - /opt/edut/gitea/gitea:/data
    depends_on: [db]
    networks: [gitea_net]

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /opt/edut/gitea/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - /opt/edut/gitea/caddy/data:/data
      - /opt/edut/gitea/caddy/config:/config
    depends_on: [gitea]
    networks: [gitea_net]

networks:
  gitea_net:
EOF"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo tee /opt/edut/gitea/caddy/Caddyfile >/dev/null <<'EOF'
git.edut.dev {
  reverse_proxy gitea:3000
}
EOF"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "cd /opt/edut/gitea && sudo docker compose up -d"

Gate:

  1. docker compose ps shows healthy containers.
  2. curl -I https://git.edut.dev returns 200/302.
  3. Browser shows valid TLS for git.edut.dev.

8. Bootstrap org and repos

Create admin and org:

ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo docker exec -u git gitea-gitea-1 gitea admin user create --username joshua --password 'CHANGE_ME_ADMIN_PASSWORD' --email j@edut.dev --admin --must-change-password=false"
ssh -i ~/.ssh/edut_codex codex@5.78.148.229 "sudo docker exec -u git gitea-gitea-1 gitea admin org create --name edut --username joshua || true"

Create repos:

for r in web launcher contracts governance kernel platform-docs; do
  curl -u "joshua:CHANGE_ME_ADMIN_PASSWORD" \
    -H "Content-Type: application/json" \
    -X POST "https://git.edut.dev/api/v1/orgs/edut/repos" \
    -d "{\"name\":\"$r\",\"private\":true}"
done

Gate:

  1. https://git.edut.dev/edut lists all repos.

Phase 2 - Migration

9. Mirror push histories

for r in web launcher contracts governance kernel; do
  git -C "/Users/vsg/Documents/VSG Codex/$r" push --mirror "https://git.edut.dev/edut/$r.git"
done
git -C "/Users/vsg/Documents/VSG Codex/platform-docs" push --mirror "https://git.edut.dev/edut/platform-docs.git"

Gate:

  1. Branches/tags match source repos.

10. Update local remotes

for r in web launcher contracts governance kernel platform-docs; do
  git -C "/Users/vsg/Documents/VSG Codex/$r" remote set-url origin "https://git.edut.dev/edut/$r.git"
done

Gate:

  1. git remote -v points to git.edut.dev for all repos.

11. Update hardcoded host references

Search:

rg -n "git\\.workvsg\\.com" /Users/vsg/Documents/VSG\ Codex

Replace references in scripts/docs/default flags/manifests.

Gate:

  1. No remaining production references to git.workvsg.com.

12. Freeze old host read-only

Set old Gitea org/repo archival or remove write access.

Gate:

  1. No new writes possible on old host.

Phase 3 - Verification

13. Smoke checks

  1. Push test branch to each repo.
  2. Open PR in each repo.
  3. Verify Gitea workflow runs.
  4. Create and push lightweight tag.
  5. Validate clone/fetch from clean directory.

Gate:

  1. All checks pass with new host only.

14. Deploy path verification

  1. Ensure api.edut.dev deployment docs/scripts reference new Git host where needed.
  2. Validate deployment artifact retrieval paths.

Gate:

  1. No deployment path depends on old host.

Phase 4 - Semantic Sweep

Only after all previous gates pass:

  1. Execute naming/commercial migration sweep.
  2. Enforce drift checks in CI.
  3. Cut release and decommission old host after soak.