9.8 KiB
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
- DNS records exist and point to the new server:
edut.devwww.edut.devapi.edut.devgit.edut.dev
- SSH key access as root is available.
- Cloudflare proxy mode and SSL mode are configured to allow origin TLS.
- Local private keys for
joshua,claude-code,codexare 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:
- Host is reachable.
- OS is Ubuntu 24.04.
- 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:
- All three users exist.
- 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:
visudo -cfpasses.claude-codeandcodexhave passwordless sudo.joshuaremains 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:
codexlogin works and sudo works.joshuakey login works.- 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:
ufw status verboseshows only22,80,443.fail2banactive.- 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:
docker --versionreturns success.docker compose versionreturns 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:
docker compose psshows healthy containers.curl -I https://git.edut.devreturns200/302.- 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:
https://git.edut.dev/edutlists 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:
- 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:
git remote -vpoints togit.edut.devfor 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:
- 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:
- No new writes possible on old host.
Phase 3 - Verification
13. Smoke checks
- Push test branch to each repo.
- Open PR in each repo.
- Verify Gitea workflow runs.
- Create and push lightweight tag.
- Validate clone/fetch from clean directory.
Gate:
- All checks pass with new host only.
14. Deploy path verification
- Ensure
api.edut.devdeployment docs/scripts reference new Git host where needed. - Validate deployment artifact retrieval paths.
Gate:
- No deployment path depends on old host.
Phase 4 - Semantic Sweep
Only after all previous gates pass:
- Execute naming/commercial migration sweep.
- Enforce drift checks in CI.
- Cut release and decommission old host after soak.