# 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 ```bash 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: ```bash 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: ```bash 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' '' > /home/joshua/.ssh/authorized_keys" ssh -i ~/.ssh/edut_codex root@5.78.148.229 "printf '%s\n' '' > /home/claude-code/.ssh/authorized_keys" ssh -i ~/.ssh/edut_codex root@5.78.148.229 "printf '%s\n' '' > /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 ```bash 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 ```bash 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: ```bash 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 ```bash 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 ```bash 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: ```bash 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): ```bash 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" ``` ```bash 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" ``` ```bash 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: ```bash 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: ```bash 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 ```bash 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 ```bash 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: ```bash 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.